Skip to content

Commit c77a4b8

Browse files
authored
✨ Select the model and effort a <Session> runs under (#829)
* ✨ Add Session configuration contracts `<Session>` accepts `model` and `effort`, and the Agent Api carries them: `session()` takes the configuration the element authored, and `options()` answers with the model and effort choices an agent advertises, in provider-neutral terms. Both are exported from `@executablemd/core/api`, so a Plugin composes them without naming a provider's protocol. Core does not apply anything. What it owns is what the document asked for and what a provider reported: a prompt retains the requested and effective model and effort, a launch request carries the requested pair as facts, and the coordinator refuses a preparation that changed them. `configuration-refused` joins the durable launch failure classes. Every new durable member is optional and exact. A record written before this feature parses as a launch or prompt that asked for nothing, and a member that names no choice refuses the record rather than being read past. * ✨ Configure ACP sessions in place A `<Session>` that names a model or an effort level reaches the provider that issued it, and the conversation is put into that configuration before anything is said in it. The order is the contract: read, write the model, refresh for that model's effort choices, write the effort, verify — and only then does a turn start. A setting nobody authored is never written, and a session nobody configured reads no status at all. What ACP advertises is read strictly and translated once. Categories are preferred and exact ids are the fallback, groups flatten onto their members in the provider's own order, and a selector that does not read back fails the operation rather than degrading to "this agent offers you nothing". A write that may have landed and then failed is put back, model first, and verified. The original rejection is still what the caller is told. A restoration nobody can verify marks that exact session unusable for the rest of the run, and every later route to it refuses before a status read, a write, an ensure, a turn or a process. `Agent.options()` answers what an agent advertises through a conversation of its own: created, read, and given up however the operation ends, with no prompt, no placement, no host mapping and no route. * ✨ Verify configured native launches A launch beneath a configured `<Session>` puts the conversation under what the document asked for at the one moment it can: after the exact session exists and before anything is spent in it. The turn a launch may owe, the detach, and the native UI all follow a configuration this provider applied and verified, and the preparation retains what was asked and what the provider reported. An adapter whose session its own interface creates has no such moment. A configured launch on that route refuses before a route is published, before an identity is allocated, before a private instruction file exists, and before anything could be detached or spawned. Unconfigured launches on either route are unchanged, and read no configuration at all. A replay reaches its conversation again at whatever phase the journal stopped at — the detach for a launch that never handed over, the spawn for one that did. It reattaches by the exact retained identity, puts the retained configuration back into force, and relinquishes ACP again without authoring a second account of having done so. * ✨ Inspect Agent model and effort options Model and effort are settings a `<Session>` supplies, not inputs a handler composes. `Agent.session()` carries a name and nothing else, so middleware selects, reroutes or refuses whole conversations exactly as it always could and never receives a settings object it could edit. What an element asks of its conversation travels where the engine identity already travels: sealed inside the opaque placement, before anything routes. A handler holding that placement can transfer the whole route — which is what routing is for — and cannot read the settings, edit them, or attach them to a route of its own. A structural copy, a descriptor clone, an object built on the prototype and a request from another loaded copy carry no issuance, so they carry no settings either. The installed provider settles that placement through the coordinator delivered to its factory, saying which exact conversation it resolved and whether that conversation exists yet. A fresh placement applies nothing and stays inert until its first consumer builds it. An established one is configured in place, and the value sealed into the use is the canonical one the provider reports it verified — a provider that reports something else, or nothing at all, refuses rather than being recorded as having complied. Who settles is an ordinary closure inside one provider installation, and it is the authority for the uses it issues as well as for the placements it settles. It decides once per placement, holds one way of configuring each conversation, and closes immediately after that provider's own finalizers — dropping its registrations, so a placement settled afterwards and a use read afterwards both reach nothing. Two providers side by side in one document own different ones, so a conversation one of them configured is one only it can describe. Nothing points back to it: no context, no module table, no property on a session, no middleware value. What an element asks is settled when it opens its placement, either way: an element that authored nothing says so. By the time any handler holds the request there is nothing left to say, so there is no authored configuration to overwrite and no unconfigured placement to attach one to. A configured `<Session>` owns one frozen use, and nested operations select that same object. Where a handler routes is where the operation runs: an authentic use applies and verifies its own settings; another authentic use applies that other use's; a raw session, a name, no session at all, or a session a handler answered with itself is an ordinary unconfigured route that reads and writes nothing; and a value claiming to be configured that nobody issued refuses at the provider's first read. A launch follows the same rule through its own one-use request and never consults what a prompt is doing. A live operation applies the settings under that session's existing serialization and ownership: read, apply the model, refresh for that model's effort choices, apply the effort, verify exact equality. Only then does a turn begin or a native UI open. A value the agent does not advertise, a rejected write, a readback naming something else, or an agent that cannot report its configuration all fail before the turn — restoring model-first, reporting the original rejection, and taking a session nobody can describe out of use for the rest of the run. Omitted settings read and write nothing at all. One durable member carries it, on a prompt record and on a launch record alike, retained only for work that actually ran under it and read from the session the provider named as the one the turn ran in — so a record describes the conversation that ran rather than the element the operation was written inside. The members a released build wrote are read, validated and dropped: they configure no replay, they are never written again, and beside the canonical member they refuse the record. `xmd agent options` reports what an agent advertises, as text or version-1 JSON, through one scoped inspection session that sends no prompt and creates no document run, journal or durable session — which its help states before the command is run. A real `xmd` binary proves it against a real agent process: one session, one refresh, no turn, nothing left behind, and an answer it cannot read refused whole without repeating a word of what the agent sent. * 🐛 Recognize a placement the provider's own copy did not mint A provider is a package, and the host that installs it may have loaded a different copy of core than the one the provider resolved — a bundled plugin, a vendored build, two specifiers that do not dedupe. Asking its own copy whether a routed value is a placement answers no about a perfectly genuine one, and the conversation it names then runs as an ordinary session: unconfigured, and quietly, because nothing on that path fails. So the provider classifies only what every copy agrees about. A conversation somebody already resolved carries a session key and a working directory; a placement carries neither. Everything else in the routing union that is an object goes to the coordinator delivered to this factory, which is the only thing that can say whose placement it is. Its refusal is the answer — not caught, with no raw-session path behind it — and the descriptive name is read as the public member it is. Reading the public shape classifies the route and does nothing else: it says this is the kind of value a placement is, never that it is a live one and never whose. The exact object crosses unchanged, and an object that is neither a conversation nor the shape of a placement refuses rather than resolving some other conversation and saying nothing about it. Nothing new is published to reach that decision: no context, no registry, no authenticity marker, no shared symbol, and no readable configuration member. The evidence runs this repository's provider against a second, separately bundled copy of core, which is the same situation the other way round: every placement and the coordinator itself are foreign to the provider's own copy. An unconfigured placement is handed over exactly once and asks for nothing; a configured one has its engine identity read by the provider's placement integration and is put under exactly what it asked; a placement this installation did not issue refuses before anything is resolved or written; and a resolved session routed back in is still a session, carrying nothing to apply. Everything that crosses from the other copy is read by its members — requests, owners, handles and the sessions they settle — because asserting a shape would be claiming to recognize values from a copy this one cannot.
1 parent c00fa82 commit c77a4b8

45 files changed

Lines changed: 7837 additions & 254 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎architecture.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5186,6 +5186,7 @@ Status is measured against main.
51865186
| 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 |
51875187
| nested run-profile Agent and elicitation declarations | lets one `<Execution host="run">` declare one child-scoped `<TestAgent>` scenario set and one non-delegating `<Answers>` matcher set; only frozen test data crosses the harness request, the trusted host constructs both providers inside the isolated child, siblings share no session or provider state, ordinary component shadowing remains in force, and the child journal retains only the selected Prompt and Elicit components' ordinary results. A controlled `<Plan>` may author an exact scenario label that this host alone maps to Plan's derived conversation identity; declaration selection uses the label while runtime state stays keyed by the opaque identity and child, with no matcher or fallback added to ordinary TestAgent sessions | built on the #641 stack; controlled Plan routing added on the #728 stack |
51885188
| `Config` run deadline / exec default / Fetch default / verbosity | three independently owned contextual timeouts, absent unless configured, each read by exactly one consumer, and contextual verbosity — a boolean that is false unless configured, seeded by the command line and overridable for a lexical subtree, bounding nothing and deciding no permission | built on this stack |
5189+
| Agent session configuration (`<Session model effort>` / `Agent.options()`) | a `<Session>` says which model and effort level its conversation runs under, in exact provider ids, and no other element does. `Agent.session()` continues to route only a name or opaque placement; configuration is not a middleware argument. Each provider installation owns one private registration closure for its exact lifetime. An opaque placement carries an unreadable reference to its operation-local issuance, including optional frozen configuration and durable component identity; `with({ name })` preserves it, another placement substitutes its own, and a raw string or directly returned raw Session selects no configuration. The directly delivered provider coordinator accepts the final live placement once and completes it with the provider's exact Session plus either fresh state or one stable established-session configuration operation. Fresh completion performs no configuration work; established completion applies and verifies eagerly, and the returned canonical value is sealed into one opaque frozen session use selected lexically for nested prompts and launches. The owner refuses a different operation for an already registered exact Session, registration or lookup after teardown, and copied or foreign placements and uses; teardown drops its registrations after provider finalizers. No Context, module registry, public Agent operation, middleware-readable member, return value or Session property exposes the owner or operation, and the provider retains no desired-settings association. A string passed to `useConfiguredSession()` creates one operation-local placement with no durable component identity. Middleware may observe, refuse or reroute whole Sessions. The final routed value decides the operation: an authentic configured use applies and verifies its own settings; another authentic use applies its settings; a raw Session, name or absence is unconfigured; and a copied or foreign configured claim refuses before provider work. The journal records only the verified configuration the final route actually started under, never an earlier lexical request. Settings are applied under the Session's existing serialization and ownership — read, apply model, refresh for that model's effort choices, apply effort, verify exact equality — before a turn or native UI. Omission performs no configuration read or write. An unavailable option, unsupported capability, rejected write, mismatching readback or unverifiable restoration fails before the turn, restoring model first and reporting the original rejection; a restoration nobody can verify makes that exact Session unusable for the rest of the run. A prompt that started and a successful launch preparation retain one canonical `configuration`; a configuration refusal and an unconfigured operation retain none. Completed replay contacts no provider, and an incomplete configured launch reapplies that canonical value to the exact retained Session or fails. Released launch `requestedModel` and observational `model` strings are compatibility-reader inputs only: valid values are discarded, malformed values refuse, and neither can configure replay. A final configured provider-returned launch verifies before materialization, detach and native UI; a final configured client-allocated launch refuses before route, identity, private file or process, while middleware that routes an unconfigured Session receives the unchanged unconfigured contract. `Agent.options()` reports provider-ordered, grouped choices through one scoped unretained inspection session that sends no prompt and creates no XMD execution, journal or durable session, and `xmd agent options` renders them as text or version-1 JSON | built on the #828 stack; ACP translation lives entirely in `@executablemd/acp`, and no ACP or ACPX type crosses into core |
51895190
| native session launch (`<Session.Launch>` / `launchAgentSession()`) | 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 except an adapter's declared materialization turn, which runs at most once, only for a conversation the launch just created, and only with that adapter's fixed prompt; it takes the run's foreground-terminal lease before an agent is resolved. A host with no applicable terminal refuses without probing for an installed CLI. A session is constructed once, by one of two mechanisms, and its create-once construction route says which. Where the provider returns the identity, the ACPX provider creates the session, installs the layer at creation, releases ACP ownership before the spawn, and marks its handle stale so a later `<Prompt>` reattaches. Where the adapter names its own sessions, it allocates the identity inside ownership before any process exists, the native process creates the session under that name from a private mode-0600 instruction file, and ACP creates nothing — the instruction text reaches neither argv nor environment, and the file is removed on success, failure and cancellation alike while ownership is still held. Neither route converts into the other, and which one governs is chosen by the first operation that consumes the placement rather than by the `<Session>` that made it: a fresh `<Session>` publishes no route and establishes nothing, so a `<Session.Launch>` nested inside one constructs the session it placed, while a first subscribed `<Prompt>` publishes ACP-first before it ensures and keeps that account even if the turn that follows is never accepted. An established route is validated eagerly by a later `<Session>`, and a launch meeting a published ACP-first route refuses before an identity exists. A `<Session>` or `<Prompt>` meeting a bound client-allocated route attaches under the route's exact identity; a legacy unbound route or an unavailable attachment capability refuses before a turn and creates no substitute conversation. Phases are retained as `agent_session_launch` records under one expansion identity — `prepared` before ownership is released, then `materialized` where an adapter required a materialization turn, then `detached`, then `exited` — so a completed replay launches nothing, a replay holding only `prepared` proves the handoff never began and may still create under the retained identity, and one holding `detached` resumes and never falls back. The public route carries an opaque one-use launch request and answers nothing; the right to run and retain a phase is delivered to the installed provider directly, so neither a returned completion nor a rebuilt request authors a launch. Every operation that can act on an advertised session takes exclusive ownership under one natural key first, through a coordinator the host built and passed in; contention refuses instead of queueing, and an owner that never proved it stopped leaves a recovery tombstone. A host that cannot say who owns a session refuses every advertised operation, and one that cannot say how a session was constructed additionally refuses an agent that names its own — before any provider effect. Every private setup or child-creation failure is normalized to `process-creation-failed` with fixed provider-owned text, carrying no path, argv, environment or host message. No launch path discards persistent provider state. A session whose adapter pins a build is bound to that one build, whichever side named it: the build is observed inside ownership before an identity is allocated or a provider session is created, the binding is published with the bound route — V2 for client-native, V3 for ACP-first — and retained beside the prepared record, the native child runs the exact observed path in place of the launcher name, and every later create, resume, attachment and incomplete replay reobserves and compares before a process, an ensure or a turn. A `<Session>` or `<Prompt>` meeting a bound client-native route attaches to it: it reobserves the build, requires any retained provider arrangement to assert that same conversation, calls ensure with the route identity as `resumeSessionId`, and requires the provider to report that identity before a turn — refusing on missing capability, build drift, missing history or a differing assertion without creating a substitute conversation. ACP runtimes are partitioned by resolved agent command and binding, each handle is closed by the partition that created it, and a bound partition is torn down when its last handle closes. A legacy V1 client-native route keeps exactly the released native-only behavior and never attaches, and a legacy V1 ACP-first route refuses an agent whose adapter binds a build rather than gaining the current executable | built on the #517 stack, extended by the #519 and #561 stacks; Deno and the compiled binary assemble the host — coordinator, route store and executable observer — and Node and Bun keep the same advertised names while assembling none of it, so every advertised operation refuses before provider work; `claude` is advertised for native launch after passing the client-allocated gate at Claude Code 2.1.241 on macOS arm64 (#520) and separately for client-native attachment after passing the native-to-ACP marker gate (#561); `codex` is advertised for native launch after passing the provider-returned gate at `codex-cli 0.153.2` on macOS arm64 with the vendored `@agentclientprotocol/codex-acp` 1.6.2 snapshot that asserts the App Server thread on the response `_meta` (#755), which costs one fixed `codex-materialization.v1` turn because Codex writes the rollout `codex resume <id>` reads at a thread's first turn. That tuple is the compatibility point the advertisement stands on rather than an allowlist: admission accepts one canonical `codex-cli x.y.z` line, so another canonical version is admitted while bare, malformed, ambiguous or multi-line output refuses; exactness belongs to the session, whose accepted string becomes its retained executable binding and is reobserved and compared before every later create, resume, attachment and incomplete replay, so continuing a session across releases is not offered. Reading the ACP session id as the native identity stays forbidden; `Agent.AddDir` is unbuilt |
51905191
| `<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 |
51915192
| `API.Files` | routes every document filesystem operation to the installed provider, with no host default and structural failure data. Its mandatory semantic operations include `ensureDirectory`, which recursively creates or adopts one directory and returns Unit; separately loaded copies compose through the stable Api name | built on the #227 stack; directory ensure added by #643 |

0 commit comments

Comments
 (0)