Skip to content

Commit 28974f6

Browse files
committed
🚀 Hand a prepared agent session to its native UI
`<Session.Launch>` prepares one durable coding-agent session from its rendered body and gives the provider's own interactive UI the terminal for that exact session, then continues the document after it exits. Backed by `Agent.launch(instructions, options)`, whose base handler fails: answering `prompt()` does not make a provider launch-capable. The body renders completely first, and only what it rendered crosses as the instruction layer — the prose around the element stays documentation. The launch performs no model turn. Phases are retained as `agent_session_launch` records under one expansion identity: `prepared` before ACP ownership is released, then `detached`, then `exited`. Separate records because a launch interrupted between detach and spawn has to resume the session it already prepared rather than create a replacement. The provider records each phase through the journal the invocation lends it, so structural data alone cannot author a launch. The ACPX provider installs the layer at session creation, refuses an agent whose native launcher is not advertised or that asserts no provider-native identity, releases ACP ownership before the spawn, and marks its handle stale so a later `<Prompt>` reattaches. Changing the layer on a session that has been prompted or handed to a native UI is refused: ACPX's cached transcript stays empty across native turns, so it cannot authorize discarding one. The native launcher is its own seam, not `exec`: it takes the run's one foreground-terminal lease before an agent is resolved, flushes what the document has produced, inherits the terminal to the child, and reaps a child that ignores the interrupt by pid — Deno's `ChildProcess.kill()` delivers only its first signal, so escalation through the handle silently does nothing. No real adapter is advertised. `claude` and `codex` have command shapes and contract tests; the opt-in Claude proof shows the ACP adapter creates no resumable native state and asserts no native identity, so it fails closed. Refs #517
1 parent a0210e1 commit 28974f6

33 files changed

Lines changed: 4608 additions & 28 deletions

‎architecture.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ Existing documents and code get aligned to this section retroactively.
7272
| host profile | a named production assembly a trusted host offers a testing harness — `run` today — reused after argument parsing rather than restated; a host that cannot offer one refuses rather than approximating it |
7373
| execution outcome | what one nested document execution ended as, as the harness publishes it: a settlement carrying the exact completion `Result`, or, for a workflow attempt, a durable suspension carrying the run and suspension identities. A suspension is neither success nor failure |
7474
| trusted host | the code that decides what an execution is for — a CLI entrypoint or a workflow runner — as distinct from the document, the components it expands, and the middleware packages composed around it |
75+
| native session launch | preparing one durable coding-agent session from a document and then handing its provider's own interactive UI the terminal for that exact session. It is session construction followed by an ownership handoff, not a chat frontend: XMD performs no model turn, proxies no native turn, and journals no native conversation |
76+
| provider-native session identity | the identity an agent asserts for its own durable session state, distinct from the logical XMD session key and from the ACP/ACPX record identity. Only an asserted one crosses a handoff; XMD never infers that a string a native CLI would accept is one |
77+
| instruction layer | the provider-native session, system or developer instructions a launch installs before the native UI accepts its first user turn. It is not a user message, and it is not conversation history |
78+
| foreground-terminal lease | the one exclusive claim on the run's terminal a native launch holds. A host with no terminal refuses it, and two launches cannot hold it at once even when they name different sessions |
79+
| native launcher | the host-owned seam that reserves that lease, flushes what the document has produced, spawns one native UI with the terminal inherited, and reports its terminal status and nothing else. It is not `exec`, whose children are piped, captured and journaled |
7580
| `JournalProvenance` | a non-operational, equality-only witness that a live publication stream descends from the exact journal backend a provider selected for one workflow run; it grants no append, read, execution, publication or reconciliation capability, and is meaningful only because the provider retains the witness it established and later requires exact equality |
7681

7782
## Three axes
@@ -2439,6 +2444,7 @@ Status is measured against main.
24392444
| `exec as="name"` | binds one command's settled outcome — exit status, stdout and stderr — as an ordinary mutable binding; the block displays neither channel, renders nothing, and a nonzero status raises nothing. Only the built-in exec terminal and the built-in `timeout` may compose one, authorized by factory identity rather than by registered name, and asked for through a capability-backed request public middleware composes around but cannot issue, claim twice or answer | built on the #447 stack |
24402445
| testing harness (`<Execution>`) | runs another document as a real root under a production host profile, authorized by canonical `<Test>` alone: declarations installed before the root import, child output displayed progressively and collected only when asked, journal retention selected independently of observation, and the outcome published by the invocation's own terminal through a request public middleware composes around but cannot answer | built on the #454 stack for `host="run"`; the workflow profile and `<WorkflowRun>` are unbuilt, and a host that offers no workflow profile refuses them |
24412446
| `Config` run deadline / exec default / Fetch default | three independently owned contextual timeouts, absent unless configured, each read by exactly one consumer | built on main |
2447+
| native session launch (`<Session.Launch>` / `Agent.launch()`) | prepares one durable coding-agent session from the rendered body of `<Session.Launch>` and hands the provider's native UI the terminal for that exact session, then continues the document after it exits. The body renders completely first and only what it rendered crosses as the instruction layer; the launch performs no model turn; the run's one foreground-terminal lease is taken before an agent is resolved, so a host with no terminal refuses without probing for an installed CLI; the ACPX provider installs the layer at session creation, refuses an agent whose native launcher is not advertised or that asserts no provider-native identity, releases ACP ownership before the spawn, and marks its handle stale so a later `<Prompt>` reattaches. Phases are retained as `agent_session_launch` records under one expansion identity — `prepared` before ownership is released, then `detached`, then `exited` — so a completed replay launches nothing and an interrupted one resumes the retained identity instead of creating a replacement session; the provider records each phase through the journal the invocation lends it, so structural data alone cannot author a launch | built on the #517 stack; no real adapter is advertised, `Agent.AddDir` is unbuilt, and both fail closed |
24422448
| `<Fetch>` | performs one XMD-mediated HTTP read through contextual `API.Fetch`, admitting the whole request before transport, and retains the normalized request and the detached response as one `fetch` durable observation; capture decides whether a status is data or a failure, and the trusted host's destination ceiling sits below the component | built on the #456 stack; a generated fragment may name the pinned identity only for a request the trusted host stated exactly, on the #369 stack |
24432449
| `API.Files` | routes every document filesystem operation to the installed provider, with no host default and structural failure data | built on the #227 stack |
24442450
| host Files provider / `useHostFiles()` | resolves document paths in the caller's filesystem, containing them while the host namespace is stable; installed by all four CLI entrypoints | built on the #227 stack |

‎packages/acp/mod.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,16 @@ export type { AcpxProviderDependencies, SessionRouteContext } from "./src/provid
1919
/** The agent ACPX selects when nothing else is configured. */
2020
export { DEFAULT_AGENT_NAME } from "acpx/runtime";
2121

22+
/**
23+
* Native session launch: the adapters whose resume command shape this package
24+
* knows, and the (empty by default) set it is willing to hand a session to.
25+
*/
26+
export {
27+
ADVERTISED_NATIVE_LAUNCH,
28+
knownNativeAdapters,
29+
nativeAdapterFor,
30+
} from "./src/native-launch.ts";
31+
export type { NativeAdapter } from "./src/native-launch.ts";
32+
2233
export { useAcpxProvider } from "./src/provider.ts";
2334
export type { AcpxProvider, ProbeCapableRuntime } from "./src/provider.ts";

‎packages/acp/src/native-launch.ts‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Native launcher adapters (specs/native-agent-session-launch-spec.md
3+
* §Provider-native identity).
4+
*
5+
* An adapter knows one thing a document must never state: the argv that makes
6+
* a particular coding-agent CLI resume one exact provider-native session.
7+
* `claude --resume <id>` and `codex resume <id>` are adapter implementation
8+
* details, not authored values, and neither an executable path nor a session
9+
* id ever appears in a document.
10+
*
11+
* Knowing the command shape is not the same as being launch-capable.
12+
* Advertisement is separate and deliberately empty by default: an adapter is
13+
* advertised only once an integration test has proven, against the installed
14+
* CLI, that the session ACP created is the session the native UI resumes and
15+
* that the prepared instruction layer is in force on its first user turn.
16+
* Until then `<Session.Launch>` refuses that agent before releasing its ACP
17+
* session, which is the failure the contract asks for rather than a hopeful
18+
* spawn.
19+
*/
20+
21+
export interface NativeAdapter {
22+
/** Stable adapter identity — `claude`, `codex`. Never an executable path. */
23+
launcher: string;
24+
/** The argv that resumes this exact provider-native session. */
25+
resume(nativeSessionId: string): string[];
26+
}
27+
28+
const ADAPTERS: Readonly<Record<string, NativeAdapter>> = {
29+
claude: {
30+
launcher: "claude",
31+
resume: (nativeSessionId) => ["claude", "--resume", nativeSessionId],
32+
},
33+
codex: {
34+
launcher: "codex",
35+
resume: (nativeSessionId) => ["codex", "resume", nativeSessionId],
36+
},
37+
};
38+
39+
/**
40+
* The adapters whose native resume and instruction contracts have been proven
41+
* against the installed CLI.
42+
*
43+
* Empty on `main`. A host that has run the opt-in integration proof for an
44+
* adapter passes its name through `AcpxProviderDependencies.advertiseNativeLaunch`.
45+
*/
46+
export const ADVERTISED_NATIVE_LAUNCH: readonly string[] = [];
47+
48+
export function nativeAdapterFor(agentName: string): NativeAdapter | undefined {
49+
return Object.hasOwn(ADAPTERS, agentName) ? ADAPTERS[agentName] : undefined;
50+
}
51+
52+
/** Every adapter whose command shape this package knows, for diagnostics. */
53+
export function knownNativeAdapters(): string[] {
54+
return Object.keys(ADAPTERS).sort();
55+
}

0 commit comments

Comments
 (0)