Skip to content

Commit 47adc1f

Browse files
authored
Support standard ACP agents with explicit session guarantees (#1085)
1 parent bd17b46 commit 47adc1f

36 files changed

Lines changed: 1227 additions & 214 deletions

‎architecture.md‎

Lines changed: 52 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3562,7 +3562,11 @@ translation lives entirely in `@executablemd/acp`, and no ACP or ACPX type cross
35623562
A provider-native session identity is the identity an agent asserts for its own durable
35633563
session state, distinct from the logical XMD session key and from the ACP/ACPX record
35643564
identity. Only an asserted one crosses a handoff; XMD never infers that a string a
3565-
native CLI would accept is one.
3565+
native CLI would accept is one. A workflow may retain the distinctly tagged
3566+
`acpx.acpSessionId` loading target when no native assertion exists. An asserted
3567+
`acpx.agentSessionId` takes precedence; changed kind or value refuses
3568+
reattachment. The ACP target never becomes `Session.agentSessionId` or a native
3569+
CLI handoff identity.
35663570

35673571
#### Inert placement
35683572

@@ -3588,24 +3592,42 @@ constructed.
35883592

35893593
#### Materialization
35903594

3591-
Session materialization is the transition that makes a placement's chosen route and its
3592-
backend history resumable. The runtime is the integrity-pinned ACPX 0.21.1 source
3593-
closure. Its released child environment and response metadata coexist with the
3594-
local explicit-acceptance barrier: `promptStarted` acknowledges transport only.
3595-
Promotion and ordinary checkpoints share the released session owner's writer;
3596-
the asserting copy is saved before memory adopts it, and finalization joins
3597-
promotion before saving. ACP-first materialization happens only when the backend
3598-
reports that it accepted the session's first turn; client-native materialization is the
3599-
native launch's existing retained construction. Nothing else promotes a placement — not
3600-
a returning ensure, a first output, a terminal result, a checkpoint token, an error code
3601-
or a diagnostic.
3595+
An adapter declaring `acceptance: "first-turn-acceptance"` requires explicit
3596+
backend acceptance before a new ACP session exposes events. Codex and Claude
3597+
declare it. The integrity-pinned ACPX 0.21.1 runtime consumes the versioned
3598+
`executablemd.session-materialization/v1` marker. Its `promptStarted` is only
3599+
transport acknowledgement; text and `end_turn` prove no stronger acceptance.
3600+
Promotion shares the session owner's writer: the asserting copy is saved before
3601+
memory adopts it, and finalization joins promotion before saving. Host mapping
3602+
commits after that save and before events. Missing acceptance never selects the
3603+
standard path. Client-native materialization keeps the native launch's retained
3604+
construction.
3605+
3606+
A standard adapter uses ordinary ACP creation without this barrier. The provider
3607+
saves its record and the host retains its identity before configuration,
3608+
permission routing and turn execution. Events report streamed text and the
3609+
actual terminal outcome. Native identity exists only when the peer asserted it;
3610+
an ACP session id alone proves neither native identity nor durable recovery.
36023611

36033612
#### Established sessions
36043613

3605-
An established session is a placement whose immutable construction route and durable
3606-
provider or native identity both already exist, and which is therefore validated
3607-
eagerly: reattached, compared against its retained history, and refused when either is
3608-
missing or names another conversation.
3614+
A retained standard record is existing. Cold continuation loads or resumes the
3615+
exact stored ACP session, or refuses before another prompt. Missing load/resume
3616+
support refuses even an unused record. The released `-32603` recreation remains
3617+
only for a provably never-prompted record; used and uncertain conversations are
3618+
never replaced. Failed attempts may retain their user request and lifecycle
3619+
facts while preserving earlier messages, identity and configuration.
3620+
3621+
A held conversation's canonical history independently establishes use: a turn
3622+
with nonempty agent output, status other than failed, a stop reason, or a native
3623+
identity counts as used. Its private issuance requires existing construction. If
3624+
provider storage is gone, setup refuses before creation or sending, preserves the history and
3625+
keeps the conversation usable. A history containing only failed-before-answer
3626+
turns retains its creation retry. An outcome-less turn still refuses opening
3627+
and is never resent.
3628+
3629+
Established strong sessions retain their construction route, asserted native
3630+
identity and executable checks. Their continuation never silently downgrades.
36093631

36103632
#### Instruction placement
36113633

@@ -3749,7 +3771,9 @@ path; holding a partition grants work, never permission.
37493771

37503772
The [local-agent example](examples/agent-certification/README.md) uses ordinary
37513773
Agent/Session/Prompt routing and literal-command registration. Private comparisons
3752-
produce categorical capability evidence; expectations confer no execution,
3774+
report basic authentication, streaming and completion separately from acceptance,
3775+
asserted native identity, durability and cleanup. Missing stronger evidence
3776+
leaves demonstrated basic passes intact. Expectations confer no execution,
37533777
identity or certification. Reports finalize after the existing owner joins.
37543778
Native cleanup remains unverified at the production probe/release boundary,
37553779
so cancellation and durable certification remain non-pass. Successfully joined
@@ -3908,20 +3932,21 @@ identity. Cwd and provider placement are never compatibility inputs. What stays
39083932
beside the run on disk is the provider's own session store and one empty working
39093933
directory per session; both are disposable.
39103934

3911-
The order is placement, then the backend's acceptance of the session's first
3912-
turn, then the provider's canonical, tagged assertion of a durable identity,
3913-
then the mapping commit — and only then is anything that first turn produced
3914-
exposed to the document.
3935+
Strong adapters order placement, explicit first-turn acceptance, asserting
3936+
provider save, tagged native assertion and mapping commit before events.
3937+
Standard adapters order placement, ACP creation and provider save, tagged ACP
3938+
loading-target mapping, configuration and permission routing, then turn and
3939+
events. Native assertion takes precedence when available. Neither path derives
3940+
stronger acceptance from transport acknowledgement or output.
39153941

39163942
Placement is inert. A `<Session>` says where a session will live and which agent
39173943
it is compatible with; it creates no provider session, contacts no backend and
3918-
writes no row. The session comes into existence when the first subscribed
3919-
`<Prompt>` starts a turn the backend accepts, through the one handle that
3920-
ensured it, and acceptance is reported by the adapter rather than inferred from
3921-
what the turn then produced.
3944+
writes no row. The first subscribed `<Prompt>` constructs it through the handle that ensured
3945+
it. Strong construction additionally waits for the adapter's explicit acceptance
3946+
rather than inferring acceptance from what the turn produced.
39223947

39233948
Occupancy of a key in the provider's own store is not an assertion: it says
3924-
something is there, not what conversation it is. A record the provider holds for
3949+
something is there, not what conversation it is. A pending strong record the provider holds for
39253950
a first turn nobody accepted is exactly that — occupancy — so it asserts nothing,
39263951
no mapping is committed for it, and the next attempt creates rather than
39273952
resumes.
@@ -9575,7 +9600,7 @@ Status is measured against main.
95759600
| Agent session portability evidence in an XMD artifact | classifies every logical Agent session that contributed a retained Prompt as portable — with ordered provider checkpoint tokens and an opaque Agent session bundle — or as explicitly unavailable, as two content kinds inside the existing version-1 manifest and identity [Invariant](#portable-xmd-artifacts); [Interface and evidence](specs/xmd-artifact-spec.md#25-agent-session-portability-evidence). | specified by `specs/xmd-artifact-spec.md` §2.5; the closed union, both content kinds and the complete post-identity profile verifier are built on the #621 stack, Deno provider only. Provider bundle capture, Agent-aware export, intrinsic Agent-aware inspection and artifact-backed fork are unbuilt |
95769601
| historical authored source | retains an authored durable operation's normalized `SourcePosition` beside its identity, and history parses it or refuses the entry [Invariant](#read-only-lifecycle-inspection); [Interface and evidence](specs/workflow-workspace-spec.md#42-history). | built on the #367 stack |
95779602
| history fork | creates a new run from one compatible checkpoint and retained Workspace root, under a new immutable definition and normalized props [Invariant](#retained-effects-and-history-forks); [Interface and evidence](specs/workflow-workspace-spec.md#11-history-forks). | built on the #368 stack, Deno provider only |
9578-
| workflow Agent session | Uses inert placement and authenticated first-turn mapping under the run-owned no-native-tool profile. [Session mapping](#agent-permissions-and-generated-xmd). | built on the #302 stack, with the explicit ACP-only selection from #561; the portable proof that an adapter honours an empty tool set is tracked by #496 and does not widen the ceiling |
9603+
| workflow Agent session | Uses inert placement and capability-specific identity mapping under the run-owned no-native-tool profile. [Session mapping](#agent-permissions-and-generated-xmd). | built on the #302 stack, with the explicit ACP-only selection from #561; the portable proof that an adapter honours an empty tool set is tracked by #496 and does not widen the ceiling |
95799604
| generated-XMD admission | Preflights and retains one generated fragment under the exact selected identities and forms. [Admission and continuation](#agent-permissions-and-generated-xmd). | built on the #369 stack, continuation basis amended by #589, and the host's contribution narrowed to `Component.generated` by this stack; core owns the mechanics and no host wrapper remains |
95809605
| `<Evaluate>` | Runs untrusted fragment text or paired-produced text under the host's captured generated selection; whole-fragment admission, local data expressions and ordinary rendered output remain distinct from provider permission. [Admission and captured implementations](#agent-permissions-and-generated-xmd). | built on the #302 stack, extended by #369, and narrowed to a name-and-form selection by this stack |
95819606
| generated mutation proposals | Admits filesystem change proposals through the host's generated selection and ordinary component transactions. [Invariant](#agent-permissions-and-generated-xmd); [Interface and evidence](specs/workflow-workspace-spec.md#mutation-proposal-admission). | Built on #369/#567, with persistent directory creation added by #643; legacy Dir grants refuse, and Git/process/credential/external-write classes remain excluded. |

‎examples/agent-certification/README.md‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ has 30 seconds and the outer work deadline has 600 seconds. Expiry ends admissio
2424
the actual owner still joins. No uncertain turn is retried.
2525

2626
The runner computes setup dependencies before provider work. Continuity,
27-
separation, reconnect and resume require one accepted A seed. Only selected
27+
separation, reconnect and resume require one completed A seed. Only selected
2828
continuity adds A's second turn; separation adds independent B. Cancellation
2929
needs no marker scenario. Selecting reconnect or resume authorizes their seed
3030
setup, even when the setup capability rows are unselected.
3131

3232
The checked-in initial document keeps one provider and A Session alive for the
3333
seed and recall, then tests B. Reconnect opens a new same-process invocation.
3434
Cancellation owns a separate document; its five-second interval begins at actual
35-
accepted start and structured cancellation joins that document. Resume uses one
35+
transport-confirmed start and structured cancellation joins that document. Resume uses one
3636
separate CLI invocation and ordinary retained A history. Its private comparison
3737
file is neither provider history nor continuation authority.
3838

@@ -55,7 +55,7 @@ Ordinary CI exercises controlled agents without credentials or model calls.
5555

5656
## Read the report
5757

58-
The current JSON format is closed and unversioned. Each of eleven capability rows
58+
The current JSON format is closed and unversioned. Each of thirteen capability rows
5959
records selected and expected flags, an outcome, a fixed reason and bounded facts.
6060
`pass`, `failure`, `unsupported` and `not_run` are distinct. An absent selector is
6161
unsupported; a selection the target Session does not apply is a failure. Omitted
@@ -64,7 +64,12 @@ model/effort settings supply no explicit-selection evidence.
6464
`complete=false` includes missing provenance and selected evidence blocked by
6565
prerequisites. A measured unsupported capability is a non-pass. Overall success
6666
requires complete evidence and all selected/expected capabilities passing.
67-
Durable certification additionally requires continuity, separation, reconnect,
67+
Acceptance and native identity are separate rows: a standard command can pass
68+
basic authentication, streaming and completion while acceptance is
69+
`unsupported/acceptance-unavailable` and identity is unsupported. An actual native
70+
assertion can pass identity independently. Only a strong-declared adapter's gated
71+
started event proves acceptance; marker-like output from a literal command does
72+
not. Durable certification additionally requires acceptance, identity, continuity, separation, reconnect,
6873
separate-invocation resume and verified cleanup. Native cleanup remains
6974
`not_run/cleanup-unverified` after a successful owner join; actual observed cleanup
7075
failure is failure. Correct stop attribution alone cannot pass cancellation.
@@ -90,8 +95,10 @@ xmd run /checkout/examples/agent-certification/orchestrate.md --include /checkou
9095

9196
This is a separate explicitly requested model call, outside the qualification
9297
suite. Ordinary name/cwd retention is not workflow Session-site identity.
93-
The existing backend first-turn acceptance barrier remains mandatory; an
94-
unpatched adapter can be refused even after producing output.
98+
A literal command uses standard ACP without the private acceptance barrier.
99+
Sequential prompts share the live Session; later reopening loads the exact ACP
100+
identity if supported and refuses otherwise. Interrupted requests are never
101+
automatically resent. Strong-declared adapters retain their acceptance barrier.
95102

96103
Kimi's selected live scope uses `kimi acp`, provider-default model and effort,
97104
and no retries. A later useful live run requires changed-source review, exact fingerprints and
@@ -100,7 +107,7 @@ retained invocation evidence only; it establishes no capability battery. Gemini
100107
configuration syntax, not availability or certification evidence. This delivery
101108
authorizes zero Gemini live calls and installs no agent.
102109

103-
The terminal matrix always lists all eleven capabilities with selected/expected flags,
110+
The terminal matrix always lists all thirteen capabilities with selected/expected flags,
104111
outcomes and fixed reasons, plus observed Prompt/cancellation counts and overall
105112
flags. Failure preserves earlier independent rows and a meaningful nonzero status.
106113
Private markers, replies, Session identities and child fragments remain local.

‎examples/agent-certification/observe.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { nativeAdapterFor } from "@executablemd/acp";
12
import { Agent, Component, configurationOf, sessionOf } from "@executablemd/core";
23
import type { AgentPromptEvent } from "@executablemd/core";
34
import { scoped, Err, Ok, useScope, spawn, sleep } from "effection";
@@ -7,6 +8,7 @@ import {
78
recordConfigurationFailure,
89
recordConfigurationSuccess,
910
recordTurn,
11+
recordSessionEvidence,
1012
} from "./report.ts";
1113
import type { Report } from "./report.ts";
1214

@@ -39,6 +41,12 @@ export function* observeQualification(
3941
recordTurn(report, { accepted, completed, deltas }, !accepted);
4042
} else if (event.value.type === "started") {
4143
accepted = true;
44+
recordSessionEvidence(
45+
report,
46+
nativeAdapterFor(event.value.agent)?.acceptance === "first-turn-acceptance",
47+
(sessionOf(event.value.session) ?? event.value.session).agentSessionId !==
48+
undefined,
49+
);
4250
report.counts.prompts++;
4351
recordConfigurationSuccess(report, configurationOf(event.value.session));
4452
} else if (event.value.type === "text_delta") {

‎examples/agent-certification/report.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type { SessionConfiguration } from "@executablemd/core";
22
import { z } from "zod";
33

44
export const capability = z.enum([
5+
"acceptance",
6+
"identity",
57
"authentication",
68
"streaming",
79
"completion",
@@ -33,6 +35,7 @@ const reason = z.enum([
3335
"agent-unavailable",
3436
"authentication-refused",
3537
"acceptance-unavailable",
38+
"identity-unavailable",
3639
"selector-unavailable",
3740
"configuration-mismatch",
3841
"marker-mismatch",
@@ -92,6 +95,8 @@ export const reportSchema = z.strictObject({
9295
}),
9396
exitState: z.enum(["settled", "failed", "interrupted"]),
9497
capabilities: z.strictObject({
98+
acceptance: rowSchema,
99+
identity: rowSchema,
95100
authentication: rowSchema,
96101
streaming: rowSchema,
97102
completion: rowSchema,
@@ -143,6 +148,8 @@ export function initialReport(config: Configuration): Report {
143148
counts: { prompts: 0, cancellationRequests: 0 },
144149
exitState: "settled",
145150
capabilities: {
151+
acceptance: initialRow(config, "acceptance"),
152+
identity: initialRow(config, "identity"),
146153
authentication: initialRow(config, "authentication"),
147154
streaming: initialRow(config, "streaming"),
148155
completion: initialRow(config, "completion"),
@@ -331,6 +338,8 @@ export function finalizeReport(input: Report): Report {
331338
report.durableSessionCertified =
332339
report.selectedCapabilitiesPassed &&
333340
[
341+
report.capabilities.acceptance,
342+
report.capabilities.identity,
334343
report.capabilities.continuity,
335344
report.capabilities.separation,
336345
report.capabilities.reconnect,
@@ -339,3 +348,20 @@ export function finalizeReport(input: Report): Report {
339348
].every((row) => row.outcome === "pass");
340349
return reportSchema.parse(report);
341350
}
351+
352+
export function recordSessionEvidence(report: Report, strong: boolean, native: boolean): void {
353+
for (const [name, passed, unavailable] of [
354+
["acceptance", strong, "acceptance-unavailable"],
355+
["identity", native, "identity-unavailable"],
356+
] satisfies [
357+
"acceptance" | "identity",
358+
boolean,
359+
"acceptance-unavailable" | "identity-unavailable",
360+
][]) {
361+
const row = report.capabilities[name];
362+
if (row.selected && row.outcome !== "pass") {
363+
row.outcome = passed ? "pass" : "unsupported";
364+
row.reason = passed ? "completed" : unavailable;
365+
}
366+
}
367+
}

‎examples/agent-certification/stage-runner.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ function schedule(state: QualificationState) {
6666
const selected = (name: keyof Report["capabilities"]) => state.report.capabilities[name].selected;
6767
const seed =
6868
[
69+
"acceptance",
70+
"identity",
6971
"authentication",
7072
"streaming",
7173
"completion",

‎packages/acp/src/acpx-runtime.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* The ACP runtime this package executes.
33
*
44
* The released 0.21.1 source closure retains the explicit backend-acceptance
5-
* barrier required by session creation. Its transport acknowledgement does not
5+
* barrier used by strong-declared session creation. Its transport acknowledgement does not
66
* establish a durable conversation. See `../vendor/acpx/PROVENANCE.md`.
77
*/
88
// @ts-types="../vendor/acpx/generated/runtime.d.ts"

0 commit comments

Comments
 (0)