refactor(acp): reduce bridge to automation protocol
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
Status: implemented
|
||||
|
||||
> Superseded by [ACP as an automation-only protocol](../simplification/2026-07-23-acp-automation-only-protocol.md). This note records the retired editor-facing bridge design.
|
||||
|
||||
## Problem
|
||||
|
||||
The harness originally exposed agents only through a readline loop. That surface could carry text, but it gave an editor no structured way to create or resume sessions, correlate prompt completion, stream reasoning and tool activity, render tool-specific UI, ask for permission, or cancel one conversation without disturbing another. ACP defines those interactions as JSON-RPC over stdio, and Zed is the target client used to make concrete compatibility decisions.
|
||||
@@ -10,7 +12,7 @@ The bridge must preserve the harness's existing ownership boundaries. It cannot
|
||||
|
||||
## Decision
|
||||
|
||||
`@deepseek-ai/dsh-acp` is a UI/client-driver plugin under `packages/ui/acp`. It uses `@agentclientprotocol/sdk`'s `AgentSideConnection` over stdin/stdout and programs only interface services: the agent create/resume factory, session persistence, tool registry, user interaction, and optional approval/bash capabilities. It does not change the agent loop and is not a capability-seam implementation.
|
||||
`@deepseek-ai/dsh-acp` was a UI/client-driver plugin in the former UI package group. It uses `@agentclientprotocol/sdk`'s `AgentSideConnection` over stdin/stdout and programs only interface services: the agent create/resume factory, session persistence, tool registry, user interaction, and optional approval/bash capabilities. It does not change the agent loop and is not a capability-seam implementation.
|
||||
|
||||
The bridge implements the following stable session path:
|
||||
|
||||
@@ -30,7 +32,7 @@ The bridge also provides the ACP-backed `UserInteractionProvider`: `ask_user_que
|
||||
|
||||
Lifecycle ownership is explicit. The bridge holds an `AgentHandle` per live session. Disconnect and Cordis disposal cancel pending prompts, dispose every handle in parallel, await loop quiescence and persistence flush, and then remove the records. Stream notification failures are contained so a vanished client cannot corrupt an agent turn. The ACP app composition loads no stdout logger; a test guards stdout as framed JSON-RPC only.
|
||||
|
||||
The precise supported and deferred protocol rows live in [`packages/ui/acp/acp-feature-support.md`](../../../../packages/ui/acp/acp-feature-support.md); the package README is the operational contract.
|
||||
The current protocol contract lives in the [`dsh-acp` package README](../../../../packages/acp/acp/README.md).
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -4,15 +4,15 @@ Status: implemented
|
||||
|
||||
## Problem
|
||||
|
||||
An ACP editor can keep several conversations alive over one agent subprocess. A single-active-session bridge would force extra processes and would not match Zed's client model, which tracks multiple session ids and concurrent loads. Multiplexing introduces isolation risks: events, prompt completion, cancellation, permission prompts, config selections, and predictable background-task ids must never cross session boundaries.
|
||||
An ACP automation client can keep several conversations alive over one agent subprocess. A single-active-session bridge would force extra processes and prevent one parent controller from driving independent children over one connection. Multiplexing introduces isolation risks: committed answers, prompt completion, cancellation, permission requests, and predictable background-task ids must never cross session boundaries.
|
||||
|
||||
## Decision
|
||||
|
||||
The ACP bridge stores live sessions in `Map<SessionId, SessionRecord>`. Agent-scoped callbacks use `ownedRecord`: look up `agent.session.id` in that forward map and accept the record only when it owns the exact agent object, so a foreign same-id object cannot claim the session. A record owns its agent handle, in-flight prompt, live tool-call presentation state, pending idle config switches, session cwd, and client capability snapshot. A separate loading-id set reserves each id before asynchronous resume so two pipelined loads cannot construct duplicate agents; distinct ids may load concurrently.
|
||||
The ACP bridge stores live sessions in `Map<SessionId, SessionRecord>`. Agent-scoped callbacks use `ownedRecord`: look up `agent.session.id` in that forward map and accept the record only when it owns the exact agent object, so a foreign same-id object cannot claim the session. A record owns its agent, exact disposer, and optional in-flight prompt with the durable turn number that eventually settles it. The session header owns its cwd; the bridge keeps no parallel workspace or client-capability state.
|
||||
|
||||
Every `session/event` and `agent/status` callback resolves the owning record before sending or settling anything. Each session permits one in-flight prompt independently. The prompt records a log watermark, captures its own `turn/start`, and settles only on the matching `turn/end`; a late end from a cancelled prior turn cannot resolve a newer prompt. `session/cancel` addresses one record and calls only that agent's queue-aware cancel path.
|
||||
Every `session/event` callback resolves the owning record before sending or settling anything. Each session permits one in-flight prompt independently. The prompt captures its own user-sourced message `turn/start` and settles only on the matching `turn/end`; injection turns, autonomous plugin or goal turns, and a late end from a cancelled prior turn cannot resolve it. `session/cancel` addresses one record and calls only that agent's queue-aware cancel path.
|
||||
|
||||
Permission ownership uses the same exact-agent check against the forward map. The ACP `approval/request` answerer prompts only the editor session that owns the requesting agent and delegates foreign requests. User-interaction elicitations likewise route by agent ownership. Per-session sandbox and approval config values fold only that session's events, with pending idle switches stored on that record until the next turn anchors them.
|
||||
Permission ownership uses the same exact-agent check against the forward map. The ACP `approval/request` answerer sends a one-shot machine-policy request only for the session that owns the requesting agent and delegates foreign or call-less requests. The bridge has no elicitation, config-selection, or other human-interaction state.
|
||||
|
||||
Background bash tasks carry an opaque owner token equal to the owning session id. `bash_output` and `bash_kill` compare the caller's token with the executor's task ownership before reading or killing; a predictable task id alone grants no access. Ownership is stored with the executor task, so a tool plugin reload does not erase it.
|
||||
|
||||
@@ -22,13 +22,13 @@ Connection teardown clears the live map, settles each pending prompt as cancelle
|
||||
|
||||
[ACP v1 expressly permits several concurrent sessions on one connection](https://github.com/agentclientprotocol/agent-client-protocol/blob/01beb5fb5eec60e9f516a80d85eb03594bac61e3/docs/get-started/architecture.mdx#L16-L24), and each new session carries its own primary `cwd`. This bridge implements that session-level multiplexing, including different primary workspaces as recorded by the [per-session cwd decision](../architecture/2026-07-02-fs-per-session-cwd.md); it does not create one agent subprocess per session.
|
||||
|
||||
A multi-root project inside one session is a separate optional capability: ACP defines the [effective roots as the primary `cwd` plus `additionalDirectories`](https://github.com/agentclientprotocol/agent-client-protocol/blob/01beb5fb5eec60e9f516a80d85eb03594bac61e3/docs/protocol/v1/session-setup.mdx#L313-L367). [Zed sends the remaining project work directories only when the agent advertises that capability](https://github.com/zed-industries/zed/blob/ea77ca2818f3e059a2b61ecc7e63b67e01e1cec5/crates/agent_servers/src/acp.rs#L1139-L1145), otherwise it [drops them from the session request](https://github.com/zed-industries/zed/blob/ea77ca2818f3e059a2b61ecc7e63b67e01e1cec5/crates/agent_servers/src/acp.rs#L1454-L1472). The bridge does not advertise this capability and rejects non-empty values, as recorded in its [known limitations](../../../../packages/ui/acp/README.md#known-limitations-and-deferred-work), so a current Zed multi-root project reaches it with only the first work directory.
|
||||
A multi-root project inside one session is a separate optional capability: ACP defines the [effective roots as the primary `cwd` plus `additionalDirectories`](https://github.com/agentclientprotocol/agent-client-protocol/blob/01beb5fb5eec60e9f516a80d85eb03594bac61e3/docs/protocol/v1/session-setup.mdx#L313-L367). The automation bridge advertises no multi-root capability and rejects non-empty `additionalDirectories`; each fresh session has exactly one workspace, as recorded in the [package contract](../../../../packages/acp/acp/README.md#protocol-contract).
|
||||
|
||||
[The standard transport is one editor-launched agent subprocess per stdio connection](https://github.com/agentclientprotocol/agent-client-protocol/blob/01beb5fb5eec60e9f516a80d85eb03594bac61e3/docs/protocol/v1/transports.mdx#L17-L42); multiple editor connections therefore require multiple subprocesses or a custom transport, while this decision guarantees multiple sessions within one connection. Within that connection, `ctx.sandboxPolicy` resolves every session's `cwd` as its own `workspace-write` root, so the shared bash and filesystem services can serve concurrent projects without granting cross-project writes. This does not add ACP `additionalDirectories`; it removes the process-wide root limit from the already-supported one-primary-root-per-session path.
|
||||
[The standard transport is one agent subprocess per stdio connection](https://github.com/agentclientprotocol/agent-client-protocol/blob/01beb5fb5eec60e9f516a80d85eb03594bac61e3/docs/protocol/v1/transports.mdx#L17-L42); multiple connections therefore require multiple subprocesses or a custom transport, while this decision guarantees multiple sessions within one connection. Within that connection, `ctx.sandboxPolicy` resolves every session's `cwd` as its own `workspace-write` root, so the shared bash and filesystem services can serve concurrent projects without granting cross-project writes. This does not add ACP `additionalDirectories`; it removes the process-wide root limit from the already-supported one-primary-root-per-session path.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**One live session per connection** — rejected. It adds process overhead and contradicts the target client's multi-session shape without removing multiplexing needs from the editor.
|
||||
**One live session per connection** — rejected. It adds process overhead and prevents a programmatic parent from multiplexing independently cancellable work.
|
||||
|
||||
**A per-session `ctx.extend()`** — rejected. A child context does not by itself create a child plugin fiber, so listeners would still belong to the bridge fiber. The implemented bridge instead uses global listeners with explicit O(1) demultiplexing and per-session owned records; agent lifecycle is owned by `AgentHandle`.
|
||||
|
||||
@@ -36,10 +36,10 @@ A multi-root project inside one session is a separate optional capability: ACP d
|
||||
|
||||
## Consequences
|
||||
|
||||
N sessions can stream, prompt, request permission, switch config, and run background tasks concurrently without interleaving or cross-settling. A cancel or dispose in one session does not affect its neighbors. The bridge pays for explicit maps and isolation tests, but it does not add one listener set per session and therefore avoids listener fan-out during long-lived connections.
|
||||
N sessions can return committed answers, prompt, request permission, and run background tasks concurrently without interleaving or cross-settling. A cancel in one session does not affect its neighbors. The bridge pays for explicit maps and isolation tests, but it does not add one listener set per session and therefore avoids listener fan-out during long-lived connections.
|
||||
|
||||
The bridge still exposes no protocol method to close one live session independently. Today records leave together on connection teardown; session close/resume lifecycle capabilities remain deferred in the ACP feature checklist.
|
||||
The bridge exposes no protocol method to close one live session independently. Records leave together on connection teardown; navigation and resume belong to host APIs rather than this automation protocol.
|
||||
|
||||
## Verification
|
||||
|
||||
The multi-session suite drives concurrent sessions through interleaved updates, independent in-flight prompts, targeted cancellation, same-id and distinct-id load races, permission routing, config isolation, and teardown. Tool-bash tests prove one session cannot read or kill another session's background task.
|
||||
The multi-session suite drives concurrent sessions through routed committed answers, independent in-flight prompts, targeted cancellation, permission routing, exact-agent rejection, and shared teardown. Tool-bash tests prove one session cannot read or kill another session's background task.
|
||||
|
||||
@@ -48,7 +48,7 @@ Under `'code'` and `'both'` the registry owns `run_code` as a reserved presentat
|
||||
|
||||
**Concurrency is serialized.** Each run owns a dispatch queue, so even `Promise.all` executes tool calls in submission order. Settlement abandons queued calls that have not started. Parallelism requires per-tool concurrency-safety metadata.
|
||||
|
||||
**Presentation.** `run_code`'s render intent is decided here per the [render-intent Agent Note](../architecture/2026-07-02-tool-render-intent-union.md): `presentCall` creates a `generic` card with `kind: 'execute'`, the program text as its title, and the same program text as `rawInput`; `run_code` intentionally declares no `presentResult`, so ACP and TUI complete that card through their generic raw-content fallback using the final durable `tool/result.content`, including captured logs plus the returned value, failure, or post-policy spill preview. The program is the title because ACP execute cards reliably render that field while some clients omit body and raw-input content. This is not a `terminal` card: that card's semantics are "a shell command in a working directory", which a program is not. See the [result-card completeness note](../bug-fix/2026-07-20-code-mode-result-card-completeness.md).
|
||||
**Presentation.** `run_code`'s render intent is decided here per the [render-intent Agent Note](../architecture/2026-07-02-tool-render-intent-union.md): `presentCall` creates a `generic` card with `kind: 'execute'`, the program text as its title, and the same program text as `rawInput`; `run_code` intentionally declares no `presentResult`, so TUI and JSON-RPC/Web complete that card through their generic raw-content fallback using the final durable `tool/result.content`, including captured logs plus the returned value, failure, or post-policy spill preview. This is not a `terminal` card: that card's semantics are "a shell command in a working directory", which a program is not. See the [result-card completeness note](../bug-fix/2026-07-20-code-mode-result-card-completeness.md).
|
||||
|
||||
### Observability: `tool/code-dispatch`
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Status: implemented
|
||||
|
||||
> Superseded for ACP by [ACP as an automation-only protocol](../simplification/2026-07-23-acp-automation-only-protocol.md). Tool render intents remain available to UI transports, but ACP no longer projects them into terminal cards.
|
||||
|
||||
## Problem
|
||||
|
||||
The ACP bridge lets each tool own its call rendering via `presentCall`/`presentResult` (see [tool-call UI presentation](2026-06-14-acp-agent-client-protocol.md) and `packages/core/tools`). For `bash` we surface the exact command as the `tool_call` title, the model's `description` as a content text block, `kind: 'execute'`, and the completed output wrapped in a fenced ` ```console ` text block.
|
||||
|
||||
@@ -16,7 +16,7 @@ The model-facing request vocabulary is deliberately aligned with the product-res
|
||||
|
||||
Providers return `{ answers: [{ id, selected, custom? }] }`. `selected` is always an array of selected option labels, so single-select and `multi_select` answers share one result shape. `custom` carries a free-text "Other" answer; optionless questions collect `custom` directly. When `custom` is present, it overrides any selected choices and `selected` is empty. A provider that supports partial completion represents a deliberately skipped item with the existing `{ id, selected: [] }` shape, preserving the other answers without extending the tool result vocabulary.
|
||||
|
||||
`UserInteractionError` extends `HarnessError`, so failures such as `NO_PROVIDER`, `ASK_ABORTED`, ACP cancellation, or missing session routing survive `ctx.tools.execute()` as machine-routable `{ name, code }` tool errors. This matches the structured-error taxonomy and lets the model or a wrapping plugin distinguish "user cancelled" from a generic thrown exception.
|
||||
`UserInteractionError` extends `HarnessError`, so failures such as `NO_PROVIDER`, `ASK_ABORTED`, or missing request ownership survive `ctx.tools.execute()` as machine-routable `{ name, code }` tool errors. This matches the structured-error taxonomy and lets the model or a wrapping plugin distinguish "user cancelled" from a generic thrown exception.
|
||||
|
||||
## UI mappings
|
||||
|
||||
@@ -26,9 +26,7 @@ The Web composer shows one question at a time while retaining every request in t
|
||||
|
||||
`dsh-tui` renders each question as a keyboard overlay, shows option descriptions, supports single- and multi-select choices plus free-form custom answers, and rejects pending questions on abort, provider disposal, or terminal shutdown. Batched and simultaneous requests are queued so one overlay owns keyboard focus at a time.
|
||||
|
||||
`dsh-acp` provides the same seam for ACP sessions. It resolves the calling `Agent` through `ownedRecord`, requiring the forward session-map record at `agent.session.id` to own that exact agent object, and calls ACP `unstable_createElicitation` with a session-scoped form for each question. Single-select options become a `choice` string enum; `multi_select` options become a `choice` array enum; optionless questions use a required `custom` text field. If the client returns both `choice` and non-empty `custom`, the custom answer wins. ACP `decline`/`cancel`, a missing answer, a missing session, and a client without elicitation support all become structured `UserInteractionError`s.
|
||||
|
||||
The ACP mapping deliberately uses elicitation, not `session/request_permission`. `request_permission` is still reserved for the separate permission gate: it is a yes/no-or-policy authorization protocol around tool execution. `ask_user_question` is a general information-gathering tool with optional free-form answers, so ACP form elicitation is the closer protocol fit. The bridge's session routing is shared with the future permission gate, but the user intent is different.
|
||||
The Web host exposes the same seam through its selected question provider. Stable request ids, whole-request cancellation, owner abort, per-item skips, and structured batch settlement keep browser interaction behind the provider-neutral service.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -36,18 +34,16 @@ The ACP mapping deliberately uses elicitation, not `session/request_permission`.
|
||||
|
||||
**Core-owned ask-user packages.** The first implementation split the seam and the model-facing tool across `packages/core` and `packages/ui`, but both names describe one UI-backed human-interaction affordance. The seam remains provider-neutral, but it is not providerless core infrastructure like sessions, tools, or the agent registry. Keeping `dsh-user-interaction` and `dsh-tool-ask-user` together under `packages/ui` makes the package map match the product boundary: apps and bridges provide the human-answer provider, and the stdio app opts into the model-facing tool.
|
||||
|
||||
**ACP `session/request_permission`.** Permission requests are authorization around tool execution; `ask_user_question` is information gathering with optional free-form answers. Using permission for general questions would collapse two different product concepts and make the future permission gate harder to reason about.
|
||||
**Use a permission request for general questions.** Permission requests authorize tool execution; `ask_user_question` gathers information with optional free-form answers. Reusing the permission channel would collapse two different product concepts.
|
||||
|
||||
**A loop-level pause primitive.** The agent loop already knows how to await a tool call and resume from a tool result. Adding a new loop special case would duplicate that async shape and make every loop implementation learn about a UI concern.
|
||||
|
||||
## Consequences
|
||||
|
||||
ACP elicitation is currently marked unstable in the SDK. The fallback is still structured: if a client does not implement it, the tool returns `ASK_FAILED` rather than hanging. A later ACP stabilization may rename or reshape the method; that migration should stay inside `dsh-acp` because the core `ctx.userInteraction` vocabulary is provider-neutral.
|
||||
|
||||
The feature gives the model a powerful pause primitive, so prompt guidance matters. The tool description tells the model to ask concise questions and use options when possible. Product policy can later wrap `tools/execute` to restrict when the tool is allowed, but the loop should not special-case it.
|
||||
|
||||
`dsh-user-interaction` and `dsh-tool-ask-user` both live in `packages/ui` because they form one product-facing human-interaction capability. `agent-core` does not load either the tool or a provider. `dsh-tui-demo` opts into the seam, TUI provider, and model-facing tool. `dsh web` boots the seam/provider in the host runtime and exposes the tool through the selected Web question plugin. `acp-agent` keeps only the `userInteraction` seam/provider by default: ACP elicitation support is still client-dependent, so an ACP leaf must opt into the model-facing tool deliberately once its client can complete elicitation requests.
|
||||
`dsh-user-interaction` and `dsh-tool-ask-user` both live in `packages/ui` because they form one product-facing human-interaction capability. `agent-core` does not load either the tool or a provider. `dsh-tui-demo` opts into the seam, TUI provider, and model-facing tool. `dsh web` boots the seam/provider in the host runtime and exposes the tool through the selected Web question plugin. The ACP automation app mounts neither the seam nor the tool.
|
||||
|
||||
## Testing
|
||||
|
||||
Unit coverage pins provider registration/disposal, duplicate-provider rejection, abort-before-provider, empty-question rejection, structured tool errors through `ctx.tools.execute()`, batched answers, multi-select answers, custom answers, explicit per-item skips, and the model schema including the removal of `value`, `recommended`, `allow_custom`, and `desc`. TUI tests cover option descriptions, queued requests, shutdown/abort cleanup, optionless free-form input, invalid choices, duplicate multi-select selections, and batched question flows. ACP bridge tests drive a real in-memory ACP connection with the real `ask_user_question` tool and verify selected-option, custom-overrides-choice, multi-select, and optionless free-form elicitation paths continue the agent loop. Web tests pin stable-id replay, response validation, first-wins settlement, duplicate and late responses, whole-request cancellation versus owner abort, single-select advance, IME-safe Enter submission, per-item skip preservation, composer takeover, structured batch submission, and restoration of the normal composer.
|
||||
Unit coverage pins provider registration/disposal, duplicate-provider rejection, abort-before-provider, empty-question rejection, structured tool errors through `ctx.tools.execute()`, batched answers, multi-select answers, custom answers, explicit per-item skips, and the model schema including the removal of `value`, `recommended`, `allow_custom`, and `desc`. TUI tests cover option descriptions, queued requests, shutdown/abort cleanup, optionless free-form input, invalid choices, duplicate multi-select selections, and batched question flows. Web tests pin stable-id replay, response validation, first-wins settlement, duplicate and late responses, whole-request cancellation versus owner abort, single-select advance, IME-safe Enter submission, per-item skip preservation, composer takeover, structured batch submission, and restoration of the normal composer.
|
||||
|
||||
@@ -4,31 +4,27 @@ Status: implemented
|
||||
|
||||
## Problem
|
||||
|
||||
The harness gives the model bash and subagent tools but no way to record a structured task list. A todo list serves two co-equal purposes: it steers the model to plan multi-step work and keep the active task unambiguous (at most one active, exactly one while work remains), and it gives the human a live progress checklist. The ACP protocol has a native `plan` sessionUpdate that editors (Zed) already render, but the bridge never emitted one. Every reference coding agent surveyed (claude-code, opencode, codex, oh-my-pi, pi) ships some form of this; the harness had nothing.
|
||||
The harness gives the model bash and subagent tools but no way to record a structured task list. A todo list serves two co-equal purposes: it steers the model to plan multi-step work and keep the active task unambiguous (at most one active, exactly one while work remains), and it gives an interactive host a live progress checklist. Every reference coding agent surveyed (claude-code, opencode, codex, oh-my-pi, pi) ships some form of this; the harness had nothing.
|
||||
|
||||
## Decision
|
||||
|
||||
Add a model-facing `todo_write(todos: [{ content, status }])` tool whose whole-list state lives on the event-sourced session log as a new `todo/write` `SessionEventMap` variant. Both the stdio UI and the ACP bridge render off the existing `session/event` — the ACP bridge maps the list to a `plan` sessionUpdate.
|
||||
Add a model-facing `todo_write(todos: [{ content, status }])` tool whose whole-list state lives on the event-sourced session log as a new `todo/write` `SessionEventMap` variant. Interactive hosts render from the durable event; the TUI folds it directly, while the [automation-only ACP bridge](../simplification/2026-07-23-acp-automation-only-protocol.md) deliberately omits todo presentation.
|
||||
|
||||
### Whole-list replace, three-state status
|
||||
|
||||
The model sends the ENTIRE list every call; the new list replaces the old (last-write-wins on replay). This is the shape claude-code V1, opencode, and codex `update_plan` all use, and the shape the model is most trained on — no per-item ids, no delta protocol. `status` is exactly `pending | in_progress | completed`: the same triple as codex `update_plan` and, crucially, **identical to the ACP `PlanEntryStatus`**, so the bridge maps it 1:1 with no lossy translation.
|
||||
The model sends the entire list every call; the new list replaces the old (last-write-wins on replay). This is the shape claude-code V1, opencode, and codex `update_plan` all use, and the shape the model is most trained on — no per-item ids, no delta protocol. `status` is exactly `pending | in_progress | completed`, the same triple as codex `update_plan`.
|
||||
|
||||
### State on the session log, not a service
|
||||
|
||||
The list is appended as a `todo/write` event carrying the full `{ todos }` snapshot. The harness is event-sourced — the LLM history, tool calls, and turn structure all live on the log — so the todo list lives there too. This buys durability, replay, and `session/load` reconstruction for free: a reopened session re-derives the current list (the last `todo/write`) and the ACP bridge re-emits the `plan` on load, with no separate persistence backend, no in-memory service to rehydrate, and no extra wiring. An in-memory `ctx.todos` service would have had to reinvent all of that.
|
||||
The list is appended as a `todo/write` event carrying the full `{ todos }` snapshot. The harness is event-sourced — the LLM history, tool calls, and turn structure all live on the log — so the todo list lives there too. This buys durability, replay, and resume reconstruction for free: a reopened session re-derives the current list from the latest `todo/write`, with no separate persistence backend, in-memory service to rehydrate, or extra wiring. An in-memory `ctx.todos` service would have to reinvent all of that.
|
||||
|
||||
### NOT a surface event
|
||||
|
||||
`todo/write` is deliberately excluded from `SurfaceEventType`. The surface is the projection that produces the LLM message history (`deriveMessages()`); a todo write produces no conversation message. So it carries no `surfaceOp`, never joins the ordered surface, and never reaches `deriveMessages()` — it is durable, replayable *UI* state that travels alongside the conversation without being part of it. (The dev-mode invariants still require it to sit inside an open turn, which it always does: it is appended mid-step during a tool call.)
|
||||
|
||||
### Priority synthesized only at the ACP boundary
|
||||
|
||||
ACP's `PlanEntry` requires `content` + `priority` + `status`, but a `TodoItem` has no priority — the model never reasons about it. Rather than burden the schema with a field the model must always supply, the bridge synthesizes a constant `priority: 'medium'` on every entry when it builds the `plan`. Priority is an ACP wire requirement, not a harness concept, so it lives at exactly the boundary that needs it.
|
||||
|
||||
### Dropped vs claude-code V1: `activeForm`, id, priority
|
||||
|
||||
claude-code V1's item is `{ content, status, activeForm }`; later (V2) it grew ids, dependencies, and ownership — but only to support agent *swarms* (disk-backed, lock-guarded, per-item mutation). This tool keeps the item at the minimum: `{ content, status }`. No `activeForm` (the present-continuous label) — the UI shows `content`; no id — whole-list replace needs no stable identity; no priority — see above. Each dropped field is one less thing the model must produce on every call.
|
||||
claude-code V1's item is `{ content, status, activeForm }`; later (V2) it grew ids, dependencies, and ownership — but only to support agent *swarms* (disk-backed, lock-guarded, per-item mutation). This tool keeps the item at the minimum: `{ content, status }`. No `activeForm` (the present-continuous label) — the UI shows `content`; no id — whole-list replace needs no stable identity; no priority — ordering is the only ranking the model controls. Each dropped field is one less thing the model must produce on every call.
|
||||
|
||||
### Single owner — no swarm machinery (YAGNI)
|
||||
|
||||
@@ -45,18 +41,18 @@ The schema enforces type/required/enum. Beyond that, `execute` rejects empty or
|
||||
## Testing
|
||||
|
||||
Four tiers, designed up front:
|
||||
- **Unit** — the session event (append/snapshot-clone/last-write-wins/not-on-surface); the tool (schema shape, arg validation via the real `ctx.tools.execute`, value validation, the event append + replacement, no-agent rejection, `presentCall`, HMR-safety); the ACP `todosToPlan` mapping; the stdio render arm.
|
||||
- **Unit** — the session event (append/snapshot-clone/last-write-wins/not-on-surface); the tool (schema shape, arg validation via the real `ctx.tools.execute`, value validation, the event append + replacement, no-agent rejection, `presentCall`, HMR-safety); and TUI folding.
|
||||
- **Real-Loader path** — the plugin run through `Loader.unwrapExports`, asserting the namespace export shape survives (it HAS `inject`, so a stray default would crash at load — postmortem/0001).
|
||||
- **Full-loop integration** — a scripted mock model calls `todo_write` through the real agent loop; the `todo/write` event lands and a second call replaces it.
|
||||
- **`session/load` replay** — a persisted `todo/write` re-emits the `plan` update when a fresh ACP bridge loads the session.
|
||||
- **With-key e2e + snapshot** — a real prompt induces a `todo_write`; the snapshot expected output gains the `plan` notification and the log event.
|
||||
- **Resume/replay** — a persisted `todo/write` folds back into the current task list.
|
||||
- **With-key e2e + snapshots** — a real prompt induces `todo_write`; assembled snapshots pin the log event and interactive rendering.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **In-memory `ctx.todos` service** — would reinvent durability, replay, and `session/load` reconstruction the log gives for free.
|
||||
- **In-memory `ctx.todos` service** — would reinvent durability, replay, and resume reconstruction the log gives for free.
|
||||
- **Per-item delta protocol** — only needed for a shared multi-owner list, which is out of scope; whole-list replace is simpler and matches the references.
|
||||
- **Tool in `core/`** — `todo_write` is an extension tool registering on `ctx.tools`, not part of the spine; it lives in its own `packages/todo/` group like other tool families.
|
||||
|
||||
## Consequences
|
||||
|
||||
The todo list is durable, replayable session state: a persisted `todo/write` re-emits the editor's `plan` update on `session/load`, and the log — not plugin memory — is the single source of truth. Whole-list replace means one tool call per update with last-write-wins; there is no delta protocol to reconcile. The event stays off the surface, so a todo update never perturbs the derived model history — the model sees only its own tool call and result.
|
||||
The todo list is durable, replayable session state: an interactive host re-derives it from the latest persisted `todo/write`, and the log — not plugin memory — is the single source of truth. Whole-list replace means one tool call per update with last-write-wins; there is no delta protocol to reconcile. The event stays off the model surface, so a todo update never perturbs derived model history — the model sees only its own tool call and result.
|
||||
|
||||
@@ -29,7 +29,7 @@ Each bridge maps the neutral `MergedHookOutcome` from the shared lib onto the se
|
||||
| `subagent/start` (emit) | additionalContext → inject into a live in-process child; a remote child has no local injection target | unsupported by this bridge |
|
||||
| `subagent/end` (emit) | observe-only | unsupported by this bridge |
|
||||
|
||||
The CC bridge's `ask` result is a real permission path, not a terminal bridge decision: `dsh-tools` resolves it through the optional [approval seam](2026-07-06-approval-seam.md). A composed ACP answerer prompts the owning editor session and `allowed-once` proceeds; without an ApprovalService or answerer, the call fails closed to `deny`.
|
||||
The CC bridge's `ask` result is a real permission path, not a terminal bridge decision: `dsh-tools` resolves it through the optional [approval seam](2026-07-06-approval-seam.md). An ACP automation client may answer the owning session's one-shot machine-policy request and `allowed-once` proceeds; without an ApprovalService or answerer, the call fails closed to `deny`.
|
||||
|
||||
### Context source is always the plugin (the mislabel guard)
|
||||
|
||||
@@ -53,7 +53,7 @@ Hooks run in the agent's session workspace, so relative paths target the user's
|
||||
|
||||
## Deferred compatibility gaps
|
||||
|
||||
- **Tool-input rewrite.** A CC/Codex `updatedInput` is logged + warned, not honored — input rewrite is a deferred consistency-design problem ([the pre-tool-input-rewrite Agent Note](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md)), because the pre-execution args are read by `tool/call` audit + `assistant/message` history + ACP/tool-bash presentation, so an honest rewrite is a design unit, not a field.
|
||||
- **Tool-input rewrite.** A CC/Codex `updatedInput` is logged + warned, not honored — input rewrite is a deferred consistency-design problem ([the pre-tool-input-rewrite Agent Note](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md)), because the pre-execution args are read by `tool/call` audit + `assistant/message` history + tool presentation, so an honest rewrite is a design unit, not a field.
|
||||
- **Stop loop-guard** (`TODO(stop-loop-guard)`). Claude Code supplies `stop_hook_active` and overrides a hook after eight consecutive blocks; Codex supplies `stop_hook_active` but documents no equivalent cap. Both bridges always report `false`, so a Stop hook that unconditionally blocks force-continues every step — a hook author must self-limit until state tracking lands.
|
||||
- **Hook `continue:false` (hard halt).** A hook can ask to halt the whole run (CC/Codex `continue:false`); the shared merge folds it into `MergedHookOutcome.stop`/`stopReason`, but no bridge acts on it (`TODO(hook-continue-false)`) — the interception seams have no "hard-halt the agent" primitive yet (a Decision blocks/steers a single point, not the run). Deferred with the loop-guard work; the halt request is recorded in the `hook/result` log, and the hook keeps its per-point effect (decision/context) meanwhile.
|
||||
- **Config discovery.** The path is explicit in `cordis.yml` and process-level (see above); the full multi-layer CC/Codex precedence walk, per-session project-local discovery, and the trust/hash model are not reimplemented (`TODO(per-session-hook-config)`).
|
||||
|
||||
@@ -43,7 +43,7 @@ Core dispatch and the tool body sit inside normalization boundaries, so tool, li
|
||||
|
||||
### Pre-tool input rewrite is a separate consistency decision
|
||||
|
||||
`PreToolDecision` cannot rewrite arguments. History and the audit call are logged before execution, and ACP presentation reads the same input, so the registry seals arguments before policy. A valid rewrite must update history, audit, presentation, and execution before identity is created; that contract belongs to the [input-rewrite proposal](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md).
|
||||
`PreToolDecision` cannot rewrite arguments. History and the audit call are logged before execution, and UI presentation reads the same input, so the registry seals arguments before policy. A valid rewrite must update history, audit, presentation, and execution before identity is created; that contract belongs to the [input-rewrite proposal](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md).
|
||||
|
||||
### Boundaries
|
||||
|
||||
|
||||
@@ -38,4 +38,4 @@ An empty prefix is forkable; any non-empty boundary must be a safe existing sequ
|
||||
|
||||
The public surface stays small and discoverable: live session branching is part of `ctx.sessions`, next to `create({ seed })`, rather than a standalone service or a two-step helper pair. Persistence continues to work through existing `session/created` and `session/flush` behavior: a forked child starts life with seeded events, so existing backends persist that seed once and preserve `parentSession` / `seedLength` in the header.
|
||||
|
||||
The v1 scope still excludes ACP `session/fork`, unloaded persisted-session forking, model-facing tools, and subagent refactors. If a future ACP method is added, it should advertise the capability only after it has transcript/snapshot coverage; this Agent Note adds no editor-facing updates, so no ACP snapshot is required now. Fork-child replay remains covered by the existing [seed-boundary testing Agent Note](../testing/2026-06-22-fork-child-replay-seed-boundary.md), while this API gets focused `dsh-session` unit tests plus JSONL persistence coverage.
|
||||
The v1 scope still excludes ACP `session/fork`, unloaded persisted-session forking, model-facing tools, and subagent refactors. If a future ACP method is added, it should advertise the capability only after it has protocol and snapshot coverage; this Agent Note adds no ACP wire behavior, so no ACP snapshot is required. Fork-child replay remains covered by the existing [seed-boundary testing Agent Note](../testing/2026-06-22-fork-child-replay-seed-boundary.md), while this API gets focused `dsh-session` unit tests plus JSONL persistence coverage.
|
||||
|
||||
@@ -58,7 +58,7 @@ Worker-side logic runs through an in-process `MessageChannel` so V8 coverage mea
|
||||
- **Nested `workflow()`**, **token `budget`**, and the `effort`/`isolation`/`agentType` agent options (each rejects loud with a message naming it deferred).
|
||||
- **An overall run wall-clock timeout** — cancellation always frees the caller (result settles within the grace), so a cap on total run time is a policy knob for the background redesign, not a correctness need here.
|
||||
- **Engine hardening beyond worker threads**: an isolated-vm or separate-process engine behind the same seam (actual sandboxing; memory limits).
|
||||
- **ACP progress UI** over the `workflow/*` events (a `/workflows`-style view); the events exist for it.
|
||||
- **Human-interface progress UI** over the `workflow/*` events (a `/workflows`-style view); the events exist for it.
|
||||
- **ACP-backend structured output** and **`toolFilter`** (both still capability-gated `false`).
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -10,7 +10,7 @@ DeepSeek Harness uses the same primitive so project-specific review, plugin-auth
|
||||
|
||||
## Decision
|
||||
|
||||
`@deepseek-ai/dsh-skill` is the pure provider registry (`ctx.skills`), `@deepseek-ai/dsh-skill-local` is the shipped local filesystem provider, and `@deepseek-ai/dsh-tool-skill` owns the session-prefix catalog and model-facing loader tool. `dsh-agent-spine-demo` loads the registry, local provider, and consumer by default so stdio and ACP apps get the same behavior while embedded or remote providers contribute skills without changing the registry or consumer. Its `skills` config forwards `registry`, `local`, and `tool` branches to those owners.
|
||||
`@deepseek-ai/dsh-skill` is the pure provider registry (`ctx.skills`), `@deepseek-ai/dsh-skill-local` is the shipped local filesystem provider, and `@deepseek-ai/dsh-tool-skill` owns the session-prefix catalog and model-facing loader tool. `dsh-agent-spine-demo` loads the registry, local provider, and consumer by default so TUI, headless, and ACP apps get the same behavior while embedded or remote providers contribute skills without changing the registry or consumer. Its `skills` config forwards `registry`, `local`, and `tool` branches to those owners.
|
||||
|
||||
Provider plugins register synchronously during `apply()`. Provider membership is direct effect-owned state: registration and disposal invalidate completed catalogs synchronously, and discovery reads the current provider map on demand rather than observing registry-change events. Provider catalogs return ranked candidates from awaited `list()` calls, where remote providers perform initialization, authentication, and discovery while honoring the lookup abort signal. The registry validates each candidate, resolves same-name skills first-wins by rank, provider registration order, and provider-local order, then sorts summaries by skill name for deterministic consumers. It caches only completed catalog snapshots and retries when a provider/runtime revision changes during discovery, so an unload cannot freeze a stale, unresolvable skill into a session prefix. Runtime `ctx.skills.register(...)` remains a convenience for embedded in-process skills and uses project-over-user priority; `runtime` is reserved as the registry-owned provider name.
|
||||
|
||||
@@ -30,7 +30,7 @@ The data structures and catalog/tool contract are documented in [skills.md](../.
|
||||
|
||||
**Inject full skill bodies into every system prompt.** Rejected because it destroys progressive disclosure and makes every request pay for instructions that may not apply.
|
||||
|
||||
**Expose skills only as slash commands.** Rejected because model-initiated loading is the core capability; slash/ACP command advertisement does not change discovery.
|
||||
**Expose skills only as slash commands.** Rejected because model-initiated loading is the core capability; human command advertisement does not change discovery.
|
||||
|
||||
**Put local filesystem scanning directly inside `ctx.skills`.** Rejected because coding agents, web agents, and future plugin ecosystems need different skill sources. A provider registry mirrors the subagent seam: the registry owns conflict resolution and consumers, while implementations own loading.
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ Status: implemented
|
||||
|
||||
## Problem
|
||||
|
||||
Two callers need to put one question — "may this specific action proceed?" — to a human: `tools/pre-execute`'s `ask` decision (including the Claude-Code hook bridge's `permissionDecision: ask`) and the [sandbox Agent Note](2026-07-06-sandbox.md)'s post-denial one-shot escalation retry. A shared seam keeps them from inventing separate outcome vocabularies, UI routing, cancellation, and audit trails, while guaranteeing that a deployment with no UI can never grant an unanswerable request.
|
||||
Two callers need one closed decision — "may this specific action proceed?": `tools/pre-execute`'s `ask` decision (including the Claude-Code hook bridge's `permissionDecision: ask`) and the [sandbox Agent Note](2026-07-06-sandbox.md)'s post-denial one-shot escalation retry. A shared seam keeps them from inventing separate outcome vocabularies, channel routing, cancellation, and audit trails, while guaranteeing that a deployment with no answerer can never grant an unanswerable request. The answerer may be an interactive host or an automated controller.
|
||||
|
||||
The routing problem is ownership: an approval prompt must reach the editor session that owns the asking agent (the ACP bridge multiplexes N sessions over one connection), fail closed for agents nobody owns (in-process subagents, tests), and stay out of deployments that compose no UI (headless, CI).
|
||||
The routing problem is ownership: a permission request must reach the channel that owns the asking agent, fail closed for agents nobody owns, and stay out of deployments that compose no answerer.
|
||||
|
||||
## Decision
|
||||
|
||||
One package, `dsh-user-approval` (`packages/ui/user-approval`), owning the vocabulary and the `ctx.approval` service — the MECHANISM. The POLICY — who answers, and whether a session is asked at all — lives outside it: answerers are `approval/request` waterfall listeners registered by the plugins that own the channel (the ACP bridge; future terminal UIs; test scripts), and a per-session policy tier can decide before any human is involved. Consumers (`dsh-tools`' ask routing, the sandbox escalation gate) resolve a question to a closed outcome and derive their own tool results from it. Deliberately ONE package, not the capability-seam three (see Alternatives).
|
||||
One package, `dsh-user-approval` (`packages/ui/user-approval`), owns the vocabulary and the `ctx.approval` service — the mechanism. The policy — who answers, and whether a session is asked at all — lives outside it: answerers are `approval/request` waterfall listeners registered by channel-owning plugins (the ACP bridge, host adapters, and test scripts), and a per-session policy tier can decide before a channel is involved. Consumers (`dsh-tools`' ask routing and the sandbox escalation gate) resolve a question to a closed outcome and derive their own tool results from it. This is deliberately one package, not the capability-seam three (see Alternatives).
|
||||
|
||||
### How a deployment uses it
|
||||
|
||||
@@ -23,11 +23,11 @@ One `cordis.yml` entry mounts the seam. Not loading it is the fail-closed opt-ou
|
||||
# policy: never # deployment default for sessions without an override; 'ask' when omitted
|
||||
```
|
||||
|
||||
The entry alone provides mechanism, not a channel: with no answerer composed, every ask resolves `unavailable` and the asking tool call denies — fail-closed needs no configuration. Composing the ACP app (`@deepseek-ai/dsh-acp-demo`, as in [the acp-agent example's default tree](../../../../examples/acp-agent/README.md)) completes the loop: its bridge registers an answerer that prompts the owning editor session via `session/request_permission`, so a hook's `ask` or an escalation request surfaces as a one-shot Allow/Reject prompt attached to the already-streamed tool call. `policy: never` is the unattended stance — every ask auto-rejects deterministically, stated in the system prompt, no human in the loop. `policy` is validated against the closed list at plugin load; anything else throws.
|
||||
The entry alone provides mechanism, not a channel: with no answerer composed, every ask resolves `unavailable` and the asking tool call denies — fail-closed needs no configuration. Composing the ACP app (`@deepseek-ai/dsh-acp-demo`, as in [the acp-agent example's default tree](../../../../examples/acp-agent/README.md)) completes the loop: its [automation-only bridge](../simplification/2026-07-23-acp-automation-only-protocol.md) registers an answerer that sends `session/request_permission` to the owning client with the exact tool-call id and one-shot allow/reject options. `policy: never` is the unattended stance — every ask auto-rejects deterministically and is stated in the system prompt. `policy` is validated against the closed list at plugin load; anything else throws.
|
||||
|
||||
What a composed deployment observes: `allowed-once` lets exactly that call proceed; rejection, dismissal, and channel absence deny with three distinct reasons the model can tell apart; a successful in-turn request lands a durable `approval/asked`/`approval/decided` pair on the asking agent's session log; nothing about a grant persists past the call that asked. An idle request or audit append failure rejects instead of returning an unaudited decision.
|
||||
|
||||
One ask under this composition, verbatim from the sandbox example's recorded `escalation-approved` scenario — the model requests a sandbox escalation, the gate asks, the bridge prompts the owning editor, the user clicks Allow once:
|
||||
One ask under this composition, from the sandbox example's recorded `escalation-approved` scenario — the model requests a sandbox escalation, the gate asks, and the automation client selects Allow once:
|
||||
|
||||
```
|
||||
tool/call bash {"command": "printf 'escalated\n' > escalated.txt && cat escalated.txt",
|
||||
@@ -38,12 +38,12 @@ approval/asked {"toolName": "bash", "callId": "call_00_…",
|
||||
→ session/request_permission {"toolCall": {"toolCallId": "call_00_…"},
|
||||
"options": [{"optionId": "allow-once", "name": "Allow once", "kind": "allow_once"},
|
||||
{"optionId": "reject-once", "name": "Reject", "kind": "reject_once"}]}
|
||||
← the user picks "Allow once" on the prompt the editor attaches to the streamed bash call
|
||||
← the client selects "Allow once"
|
||||
approval/decided {"outcome": "allowed-once"}
|
||||
tool/result "escalated" — this one call ran under the wider mode; the grant died with it
|
||||
```
|
||||
|
||||
The `escalation-rejected` twin ends in `{"outcome": "rejected"}` instead: nothing executes, and the model's result carries the asker's verbatim fail-closed text (`the user rejected escalating this command to "workspace-write"`). A hook's `permissionDecision: ask` rides the identical wire; only the asker and its deny texts differ (§ Ask routing in dsh-tools). Headless, the same request skips the prompt entirely and settles `unavailable`.
|
||||
The `escalation-rejected` twin ends in `{"outcome": "rejected"}` instead: nothing executes, and the model's result carries the asker's verbatim fail-closed text (`the user rejected escalating this command to "workspace-write"`). A hook's `permissionDecision: ask` rides the identical wire; only the asker and its deny texts differ (§ Ask routing in dsh-tools). Without an answerer, the same request settles `unavailable`.
|
||||
|
||||
### Design detail
|
||||
|
||||
@@ -53,11 +53,11 @@ After validation and a successful `approval/asked` append, the service resolves
|
||||
|
||||
Answerers are `approval/request` waterfall listeners. Zero listeners fall through to `unavailable`; a recognizing listener occupies the first-wins decision slot, while an unrecognized agent must delegate with `next()`. Listeners dispose with their fibers, so an unloaded channel fails closed. Because sibling registration order is not deterministic, a deployment composes one terminal answerer and reserves `prepend` for decide-or-delegate gates.
|
||||
|
||||
`ApprovalRequest` carries the asking `agent`, `toolName`, optional exact `callId`, human-readable `reason`, and optional `signal`. It uses the `CallId` brand without importing `dsh-tools`, which depends on this seam. Tool arguments stay on the already-streamed call that a UI references by `callId`.
|
||||
`ApprovalRequest` carries the asking `agent`, `toolName`, optional exact `callId`, human-readable `reason`, and optional `signal`. It uses the `CallId` brand without importing `dsh-tools`, which depends on this seam. Channel adapters correlate any richer call state by `callId`; the approval request does not duplicate tool arguments.
|
||||
|
||||
#### Ask routing in dsh-tools
|
||||
|
||||
`ToolRegistry.execute()` resolves `ask` before dispatch: `allowed-once` proceeds, while rejection, cancellation, and channel absence produce distinct deny reasons. Opportunistic `ctx.get('approval')` consumption lets an absent or unmounted service fail closed without gating the registry fiber. Agent-less execution also fails closed because it has neither an audit session nor a UI owner.
|
||||
`ToolRegistry.execute()` resolves `ask` before dispatch: `allowed-once` proceeds, while rejection, cancellation, and channel absence produce distinct deny reasons. Opportunistic `ctx.get('approval')` consumption lets an absent or unmounted service fail closed without gating the registry fiber. Agent-less execution also fails closed because it has neither an audit session nor a channel owner.
|
||||
|
||||
#### The per-session policy tier
|
||||
|
||||
@@ -65,9 +65,9 @@ The seam also owns the session-scoped `'ask' | 'never'` policy described by [the
|
||||
|
||||
#### The ACP answerer
|
||||
|
||||
The ACP bridge answers only for an exact agent object owned by its forward session map. It attaches `session/request_permission` to the existing `callId`, advertises one-shot allow/reject options, maps cancellation separately, and never grants an unknown option. Foreign or call-less requests delegate; a failed client RPC becomes `unavailable`. Hooks and `tools/pre-execute` decide whether a call asks at all.
|
||||
The ACP bridge answers only for an exact agent object owned by its session map. It sends `session/request_permission` with the existing `callId`, advertises one-shot allow/reject options, maps cancellation separately, and never grants an unknown option. Foreign or call-less requests delegate; a failed client RPC becomes `unavailable`. Hooks and `tools/pre-execute` decide whether a call asks at all. This channel is machine policy between an automated client and its agent, not ACP presentation.
|
||||
|
||||
The answerer routes through the bridge's exact-agent ownership check described by [the ACP support Agent Note](2026-06-14-acp-agent-client-protocol.md), implementing the per-session permission ownership required by [the multi-session Agent Note](2026-06-14-acp-multi-session.md).
|
||||
The answerer routes through the bridge's exact-agent ownership check described by [the ACP support Agent Note](2026-06-14-acp-agent-client-protocol.md), preserving the per-session permission ownership required by [the multi-session Agent Note](2026-06-14-acp-multi-session.md).
|
||||
|
||||
#### Audit, and what the model sees
|
||||
|
||||
@@ -86,13 +86,13 @@ Snapshots record allowed and rejected sandbox escalation through `session/reques
|
||||
## Deferred
|
||||
|
||||
- **`allow_always` grant storage** — honoring a persistent grant means designing storage, scope identity (call? path? prefix? session? time window?), and revocation; until designed, only the one-shot options are advertised ([the sandbox Agent Note](2026-07-06-sandbox.md) § Escalation records the open scope question).
|
||||
- **A recorded hook-driven `ask` through a composed answerer** — the human-prompt wire is recorded through the sandbox example's escalation branches. The hook matrix's `hook-cc-pretool-ask` pins the no-ApprovalService fallback denial, while the hook-producer-plus-answerer composition remains on the unit tier.
|
||||
- **Routing a child agent's approvals to the parent session** — `subagent-acp`'s child auto-answers its own `permission` requests; surfacing them to the parent's editor is its own design.
|
||||
- **A recorded hook-driven `ask` through a composed answerer** — the permission wire is recorded through the sandbox example's escalation branches. The hook matrix's `hook-cc-pretool-ask` pins the no-ApprovalService fallback denial, while the hook-producer-plus-answerer composition remains on the unit tier.
|
||||
- **Routing a child agent's approvals to the parent session** — `subagent-acp`'s child auto-answers its own permission requests; delegating them to the parent controller is its own design.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **A single registered provider instead of waterfall listeners** — rejected: a `registerProvider()` surface forces every composition question — allowlist pre-filters, external hook deciders, scripted test answers, a policy gate in front of a human — inside one provider implementation. The waterfall gets composition, fail-closed absence, and HMR disposal from machinery the runtime already has; the seam's JSDoc pins the single-decision-slot convention instead of inventing a provider registry.
|
||||
- **An inline `tools/pre-execute` permission gate in the ACP bridge** — rejected: prompting for every bridge-owned call hardwires the asking POLICY into the UI plugin, cannot serve a second asker (sandbox escalation happens after execution starts, with no pre-execute moment), and leaves hook-produced `ask` decisions without a shared mechanism.
|
||||
- **An inline `tools/pre-execute` permission gate in the ACP bridge** — rejected: prompting for every bridge-owned call hardwires the asking policy into the transport, cannot serve a second asker (sandbox escalation happens after execution starts, with no pre-execute moment), and leaves hook-produced `ask` decisions without a shared mechanism.
|
||||
- **The generic user-interaction seam (`ctx.userInteraction`)** — rejected as the approval mechanism: the two share a skeleton (route by agent, block for a human, handle absence), but approval's contract is narrower in every dimension that matters: a closed outcome vocabulary instead of free text, a protocol-native prompt attached to a tool call instead of a generic form, mandatory fail-closed absence, and audit events. Approval therefore does not ride the shipped `packages/ui/user-interaction` / `ask_user_question` elicitation path — an elicitation form is not a permission prompt, and a free-text answer is not a closed outcome; sharing provider plumbing stays open if the two ever converge.
|
||||
- **Static optional injection in `dsh-tools`** — rejected: the vendored cordis `Inject` type has no optional flag — the object form maps service names to intercept config, and a declared inject gates the fiber. `ctx.get('approval')` is the documented opportunistic-consumption pattern (the `tool-bash` owner-token lookup, the loop's persistence probe), reads presence per call, and degrades correctly across HMR without extra machinery.
|
||||
- **The capability-seam three-package split** — rejected: interface/implementation/consumer fits a seam whose implementation is swappable (bash-local vs bash-sandbox). Here the service body is fixed mechanism and the variable part is listeners that live with their owners — splitting would manufacture an implementation package with nothing in it ("don't split preemptively").
|
||||
@@ -105,13 +105,13 @@ The implemented contract is pinned by the suites in Testing:
|
||||
- `allowed-once` dispatches one action; every other outcome denies with a distinct reason, and `'never'` rejects before prompting.
|
||||
- Missing, foreign, agent-less, throwing, invalid, and disconnected answer paths fail closed.
|
||||
- Successful requests route by exact agent ownership and append one replayable, model-invisible audit pair; idle and pre-commit failures reject.
|
||||
- ACP ownership keeps prompts inside their session, while a deployment without the service emits no prompt or audit events.
|
||||
- ACP ownership keeps decisions inside their session, while a deployment without the service emits no request or audit events.
|
||||
|
||||
Costs and accepted limits:
|
||||
|
||||
- **Two decide-eager answerers race for the slot.** Sibling-plugin listener order is not deterministic, so the seam cannot referee competing terminal answerers — mitigated by convention (one terminal answerer per deployment; `prepend` only for decide-or-delegate gates) rather than a priority mechanism the event bus does not have.
|
||||
- **Production exercise rests on one composition.** `ask` has two producer families — the hook bridges through `tools/pre-execute`, and sandbox escalation through its own gate — with the wire recorded in the sandbox example's snapshot suite, so the seam's real-world coverage is that one composition until more deployments compose it.
|
||||
- **Ownership keys on `Agent` object identity.** The answerer resolves the forward session-map record at `agent.session.id`, then requires that record to own the exact agent object; every current path hands the same object through the loop and the seams, but a future boundary that clones or proxies agents would make the bridge delegate and fail closed — safe, but silently UI-less — and would need a different ownership contract.
|
||||
- **Ownership keys on `Agent` object identity.** The answerer resolves the session-map record at `agent.session.id`, then requires that record to own the exact agent object; every current path hands the same object through the loop and the seams, but a future boundary that clones or proxies agents would make the bridge delegate and fail closed, and would need a different ownership contract.
|
||||
|
||||
## FAQ
|
||||
|
||||
@@ -121,10 +121,10 @@ Costs and accepted limits:
|
||||
- **Who decides whether a call asks in the first place?** Policy producers: a hook returning `permissionDecision: ask`, any `tools/pre-execute` listener, or the sandbox escalation gate. The seam and the bridge only route and answer; neither injects its own judgment about what deserves a prompt.
|
||||
- **What happens when the user dismisses the prompt, or the turn aborts mid-ask?** Dismissal maps to `cancelled` with its own deny text. An already-aborted signal settles `cancelled` without dispatching; an abort during the ask discards the late answer. When both audit appends commit, either path records one pair, never two.
|
||||
- **What if the client answers with an option the harness never offered?** Any selection other than the offered `allow_once` maps to `rejected` — an unknown optionId from a non-conforming client can never grant.
|
||||
- **How do subagents' approvals route?** An agent no answerer owns delegates through the whole waterfall and fails closed — in-process subagents are deliberately unanswerable. `subagent-acp`'s child-side auto-answer is separate; routing a child's asks to the parent's editor is deferred (§ Deferred).
|
||||
- **How do subagents' approvals route?** An agent no answerer owns delegates through the whole waterfall and fails closed — in-process subagents are deliberately unanswerable. `subagent-acp`'s child-side auto-answer is separate; routing a child's asks to the parent controller is deferred (§ Deferred).
|
||||
- **What does `policy: 'never'` actually change at runtime?** The service resolves every ask for that session to `rejected` before dispatching any answerer (in-service, so no registration order can bypass it); the system prompt states the policy; switches are narrated at boundaries; each successful auto-rejection records the audit pair.
|
||||
- **What happens across a hot reload, or when the UI plugin unloads mid-session?** Answerers dispose with their owning fiber, so the next ask degrades to `unavailable` instead of hanging on a dead channel; remounting re-registers the answerer with no catch-up state.
|
||||
- **Where does the user see what they are approving?** On the tool call itself: the prompt attaches to the already-streamed call via `callId` — arguments included — and adds the asker's human-readable `reason`; the request carries no argument copy of its own.
|
||||
- **What happens across a hot reload, or when an answerer unloads mid-session?** Answerers dispose with their owning fiber, so the next ask degrades to `unavailable` instead of hanging on a dead channel; remounting re-registers the answerer with no catch-up state.
|
||||
- **Where does a client get approval context?** The request carries the exact `callId` and the asker's human-readable `reason`; channel adapters may correlate richer tool-call state without duplicating arguments in the approval seam.
|
||||
|
||||
## Prior art
|
||||
|
||||
@@ -133,5 +133,5 @@ In-repo precedents this design copies or contrasts with:
|
||||
- The `fs/write-intent` gate (`packages/fs/fs/`) — the documented single-occupancy decision-slot waterfall semantics (first answer wins, delegate via `next()`) the answerer contract reuses.
|
||||
- `hook/invoked`/`hook/result` — the log-only audit-pair precedent `approval/asked`/`approval/decided` follows; [the hook-bridges Agent Note](2026-06-30-hook-bridges.md) ships `permissionDecision: ask`, the first producer.
|
||||
- [The interception-seams Agent Note](2026-06-30-interception-seams.md) — the `tools/pre-execute` `allow`/`deny`/`ask` vocabulary whose `ask` this seam services.
|
||||
- [The ACP support Agent Note](2026-06-14-acp-agent-client-protocol.md) — the exact-agent ownership check against the forward session map that the answerer routes through; [the multi-session Agent Note](2026-06-14-acp-multi-session.md) — the per-session permission-ownership blocker this implements.
|
||||
- [The ACP support Agent Note](2026-06-14-acp-agent-client-protocol.md) — the exact-agent ownership check against the session map that the answerer routes through; [the multi-session Agent Note](2026-06-14-acp-multi-session.md) — the per-session permission-ownership blocker this implements.
|
||||
- The opportunistic `ctx.get()` consumption pattern (`tool-bash`'s owner-token lookup, the loop's persistence probe) — how `dsh-tools` consumes the seam without gating its fiber on it.
|
||||
|
||||
@@ -8,7 +8,7 @@ A coding agent needs this product path: bash subprocesses — and the hook comma
|
||||
|
||||
The harness is an SDK, so confinement must be a capability developers COMPOSE: whether to sandbox, and which backend per platform, belongs in the leaf `cordis.yml` as a first-class entry — not inside one executor's private machinery. And the first-choice runner, `bwrap`, is unusable on exactly the hosts a sandbox matters most (minimal containers, disabled unprivileged userns, LSMs that deny `mount`), so a fallback runner has to ship with the SDK rather than be assumed on the host.
|
||||
|
||||
Confinement alone leaves two gaps. A denial with no escalation path is terminal — the model can only give up, which pressure-cooks operators into configuring `workspace-write` or `danger-full-access` globally and defeats the sandbox. And the model-visible knobs (the sandbox mode, the approval policy) change over an agent's lifetime — an ACP user flips a per-session setting, an operator edits `cordis.yml` while the process is down — while the model must never act on a stale belief about them: what IS the state on every request, what changed while the agent lives, and what changed while nobody was watching all need answers.
|
||||
Confinement alone leaves two gaps. A denial with no escalation path is terminal — the model can only give up, which pressure-cooks operators into configuring `workspace-write` or `danger-full-access` globally and defeats the sandbox. The sandbox mode and approval policy can also change over an agent's lifetime through deployment config or an optional UI policy control; execution and model-visible policy must derive from the same logged state.
|
||||
|
||||
## Decision
|
||||
|
||||
@@ -38,13 +38,13 @@ The swap is invisible to every consumer of `ctx.bash`: the bash tools, hook comm
|
||||
|
||||
Misconfiguration fails loud: `mode` outside the closed vocabulary is rejected at plugin load, and a host with no usable backend throws the structured `SANDBOX_UNAVAILABLE` — at `confine()` before the command ever spawns — rather than degrading to unconfined execution. `runnerCommand` on `dsh-sandbox-local` is the operator's explicit assertion of a bwrap-compatible runner (chain and probes skipped); it doubles as the deterministic fake-runner seam for keyless tests.
|
||||
|
||||
Denied file effects return a `[sandbox: file access denied under <mode> mode]` marker and instructions not to work around the denial. A confining executor adds paired `sandbox_permissions` and `justification` fields for one approved retry that must be strictly wider than the session's effective mode. A grant widens only that retry; rejection executes nothing, returns `the user rejected escalating this command to "<mode>"`, and permits no re-ask. The prompt does not announce sandbox mode, avoiding preemptive refusal. When `dsh-permission` is composed, ACP exposes one `Permissions` select whose presets write both knob events; unmatched knobs appear as switch-away-only `custom`. Only a switch to the deterministic `'never'` approval policy is stated in the prompt and narrated.
|
||||
Denied file effects return a `[sandbox: file access denied under <mode> mode]` marker and instructions not to work around the denial. A confining executor adds paired `sandbox_permissions` and `justification` fields for one approved retry that must be strictly wider than the session's effective mode. A grant widens only that retry; rejection executes nothing, returns `the user rejected escalating this command to "<mode>"`, and permits no re-ask. The prompt does not announce sandbox mode, avoiding preemptive refusal. When `dsh-permission` is composed with a UI adapter, one preset selects both knob values; unmatched values fold to `custom`. The [ACP automation composition](../../../../examples/acp-agent/README.md) does not mount that UI service and selects its deployment mode explicitly.
|
||||
|
||||
### Design detail
|
||||
|
||||
#### Scope grounding
|
||||
|
||||
OS subprocess confinement applies to the bash executor, including hook commands, and later to ACP subagent children. Filesystem, web, and other tools execute in-process and require policy at their own seams; an argv wrapper cannot confine a function closing over `ctx`. The existing bash request/spec split carries per-call overrides, while `tools/pre-execute` and the approval seam own the human decision.
|
||||
OS subprocess confinement applies to the bash executor, including hook commands, and later to ACP subagent children. Filesystem, web, and other tools execute in-process and require policy at their own seams; an argv wrapper cannot confine a function closing over `ctx`. The existing bash request/spec split carries per-call overrides, while `tools/pre-execute` and the approval seam own the one-shot policy decision.
|
||||
|
||||
#### The seam: `ctx.sandbox`
|
||||
|
||||
@@ -90,7 +90,7 @@ Left open: what a durable grant's scope identity is beyond the sandbox mode —
|
||||
effective(session) = findLast(the session's own knob events)?.value ?? the composition-config default
|
||||
```
|
||||
|
||||
The default is composition config (`cordis.yml`) — operator-owned, process-wide. A runtime switch is a SESSION-SCOPED override recorded as one log-only event in that session's own log. Restart immunity (resuming a session replays its log, so overrides come back with zero catch-up machinery) and multi-session isolation (one editor tab's `workspace-write` cannot disturb another's `read-only`) both fall out by construction, and no external config store exists anywhere.
|
||||
The default is composition config (`cordis.yml`) — operator-owned, process-wide. A runtime switch is a session-scoped override recorded as one log-only event in that session's own log. Restart immunity (resuming a session replays its log, so overrides come back with zero catch-up machinery) and multi-session isolation both fall out by construction, and no external config store exists anywhere.
|
||||
|
||||
**One event per knob, owned by its domain** — the merge-extensible `SessionEventMap` idiom every existing event family already follows (`approval/*` in `dsh-user-approval`, `hook/*` in the hooks packages):
|
||||
|
||||
@@ -105,9 +105,9 @@ Each owner exports the same three-piece kit: the event declaration, a pure fold
|
||||
|
||||
Sandbox mode is not narrated in the prompt; denial results report the mode when it matters, avoiding preemptive refusal based on a standing label. Approval policy is different: only `'never'` is stated because automatic rejection otherwise looks like a user decision. Policy-change notices are coalesced and delivered by the next pre-step, with log-derived fallback after restart. The notice source is inferred from event position: a knob event after the last request header is user-driven; unlogged drift is operator or config driven.
|
||||
|
||||
**The editor surface** is protocol-native [Session Config Options](https://agentclientprotocol.com/protocol/session-config-options) — the spec's replacement for session modes (slated for removal in ACP v2), already SDK-typed. When `ctx.permission` is composed, the bridge advertises one `permission` select (category `mode`) in `session/new` and `session/load`; its options are the deployment's preset table, and its `currentValue` is `PermissionService.current()` over the session log plus composition defaults. The shipped `workspace-write` and `danger-full-access` presets each bundle a sandbox mode with an approval policy and write through to both domain setters; a knob combination outside the table is reported as switch-away-only `custom`. `session/set_config_option` validates and switches through the permission service, then returns the complete refreshed state (the spec contract).
|
||||
**The optional UI surface** is `PermissionService`: a deployment-defined preset table whose entries bundle one sandbox mode with one approval policy. The shipped `workspace-write` and `danger-full-access` presets write through to both domain setters; a knob combination outside the table is reported as `custom`. UI adapters may expose that table as a selector. The automation-only ACP transport advertises no configuration selector and mounts no permission-preset service.
|
||||
|
||||
**Turn enclosure is the commit boundary.** A switch during an open turn appends immediately. An idle switch remains pending on the bridge record and is appended at the next prompt submission, before assembly or execution; last write wins per knob. Openness comes from log boundaries rather than `agent.status`, and setters do not append from inside a `session/event` listener because that would reorder later observers. Until anchoring, responses overlay the pending value. A crash discards it, and reload returns the durable fold.
|
||||
**Turn enclosure is the commit boundary.** A runtime switch records its preset and changed knob events on the target session, and every later capability resolution folds the last values. Adapters must use a valid session append boundary; the ACP transport has no runtime switch path.
|
||||
|
||||
#### In-process tools
|
||||
|
||||
@@ -115,10 +115,10 @@ fs/web/todo execute in-process, so their sandbox semantics are policy at their s
|
||||
|
||||
### Testing
|
||||
|
||||
- **Unit:** pin platform selection and profiles, fail-closed runner classification, per-call mode/root resolution, per-process facts, escalation validation and outcomes, permission preset folding and write-through, narrator coalescing, ACP advertisement and validation, and turn-enclosed config writes.
|
||||
- **Keyless real-runner:** exercise bwrap, Landlock, and Seatbelt against real filesystem effects at provider and bash-consumer layers; one real Cordis context concurrently drives two project sessions through shipped bash and fs tools, proving own-root success and sibling-root denial. Packed-install coverage proves the registry launcher remains executable. The real ACP composition pins permission switching and rejects unknown presets. CI rejects a silent all-skip.
|
||||
- **Unit:** pin platform selection and profiles, fail-closed runner classification, per-call mode/root resolution, per-process facts, escalation validation and outcomes, permission preset folding and write-through, narrator coalescing, and turn-enclosed config writes.
|
||||
- **Keyless real-runner:** exercise bwrap, Landlock, and Seatbelt against real filesystem effects at provider and bash-consumer layers; one real Cordis context concurrently drives two project sessions through shipped bash and fs tools, proving own-root success and sibling-root denial. Packed-install coverage proves the registry launcher remains executable. CI rejects a silent all-skip.
|
||||
- **With-key:** start the real ACP composition in read-only mode, let a model-driven bash write hit the runner's denial marker, then drive the bridge answerer and disk effect through granted and rejected workspace-write retries; unavailable credentials or runners self-skip.
|
||||
- **Snapshot:** pin the permission config-option wire, preset and knob events, prompt deltas and notices, and both scripted approval branches. A real ACP example scenario places its session under the user home while the deployment fallback points at `/tmp`, then pins a successful workspace-write mutation; this distinguishes session-root resolution from the process fallback without depending on runner-specific denial text. Other snapshots start unconfined so unrelated fixtures remain platform-independent, and policy scenarios switch explicitly.
|
||||
- **Snapshot:** pin prompt deltas and notices plus both scripted approval branches. A real ACP example scenario places its session under the user home while the deployment fallback points at `/tmp`, then pins a successful deployment-selected workspace-write mutation; this distinguishes session-root resolution from the process fallback without depending on runner-specific denial text. Other snapshots start unconfined so unrelated fixtures remain platform-independent.
|
||||
|
||||
## Deferred phases
|
||||
|
||||
@@ -150,7 +150,7 @@ Each phase gets its full design when picked up, validated against the code at th
|
||||
- **Narrate via `agent/user-message` + a bus event** — rejected: it presupposes a turn-entry seam that does not exist (the real seam is `agent/prompt-submit`), and pre-step's position serves both the coalesced turn-entry notice and the mid-turn immediacy bound with one listener.
|
||||
- **A standing prompt statement of the sandbox mode (+ a switch narrator)** — shipped first, then removed on live evidence: with `Bash commands run under the "read-only" file sandbox.` in every request, the model refused to ATTEMPT denied-then-escalatable work (five of twelve turns in the first manual session ended with zero tool calls), turning the sandbox into a soft lockout. The denial marker names the mode at the moment it matters and the escalation fields carry the recovery; the approval knob keeps its statement because an auto-rejection is behaviorally indistinguishable from a human "no".
|
||||
- **Track "last told" with its own bookkeeping events** — rejected: the `request/header` fold already records the exact prompt the model saw; parsing the closed candidate sentences back replaces a second bookkeeping stream — events are needed only where they ARE the store.
|
||||
- **ACP session modes instead of config options** — rejected: the preset is already one deployment-defined config-option select, and modes are slated for removal in ACP v2.
|
||||
- **Independent sandbox and approval selectors** — rejected: one deployment-defined permission preset keeps the two policy knobs coherent for UI clients that expose runtime switching.
|
||||
|
||||
## Consequences
|
||||
|
||||
@@ -159,11 +159,11 @@ What shipped pins — the tiers in Testing hold each:
|
||||
- A denied command retried with `sandbox_permissions` + `justification` prompts the user through the composed answerer chain; a grant runs THAT call under the wider mode (result facts say so) while every other call keeps its own effective mode; every non-grant outcome produces its distinct error text and executes nothing.
|
||||
- The escalation fields exist exactly when the mounted executor confines; a request that is not strictly wider than the call's effective mode fails closed with its own text and prompts no one; a deployment with no ApprovalService fails escalating calls closed and leaves plain calls untouched.
|
||||
- The system prompt never states the sandbox mode (an approval `'never'` policy is the one stated knob), and the whole exchange — headers, knob events, notices, approvals, results — reconstructs from the session log alone, with no event types beyond the two knob events.
|
||||
- N idle-time flips produce at most one anchored event per knob (a net-zero sequence anchors none — a no-op push from a client echoing current selections records nothing); an approval-policy switch is narrated in at most one coalesced notice; a mid-turn sandbox switch is honored by the next call's stamp.
|
||||
- A resumed session's overrides apply and are reported to the editor with no special-casing; a default changed while the process was down is narrated before the session's first new request, attributed to the operator.
|
||||
- Two concurrent sessions never see each other's state, notices, or config options.
|
||||
- One preset selection records only changed knob values, while a no-op selection records nothing; an approval-policy switch is narrated in at most one coalesced notice, and a committed sandbox switch is honored by the next call's stamp.
|
||||
- A resumed session's overrides apply with no catch-up state; a default changed while the process was down is narrated before the session's first new request, attributed to the operator.
|
||||
- Two concurrent sessions never see each other's state or notices.
|
||||
- Two concurrent project sessions in one Cordis context resolve independent workspace roots; bash and fs writes succeed inside the calling session's cwd and fail against its neighbor's cwd.
|
||||
- `agent-loop` is untouched — everything rides `systemPrompt.section`, `SessionEventMap` merging, `agent.inject()`, `agent/pre-step`, `agent/prompt-submit`, and the ACP handler surface.
|
||||
- `agent-loop` is untouched — everything rides `systemPrompt.section`, `SessionEventMap` merging, `agent.inject()`, `agent/pre-step`, `agent/prompt-submit`, and capability-owned policy resolution.
|
||||
|
||||
Costs and accepted limits:
|
||||
|
||||
@@ -176,7 +176,6 @@ Costs and accepted limits:
|
||||
- **The model may over-ask.** Escalating without denial grounding, or picking `danger-full-access` where `workspace-write` suffices: the description steers and the enum forces the ladder, but the human prompt is the actual gate; the `approval/asked` reasons make over-asking auditable, and a `prepend` policy answerer can auto-reject patterns a deployment never wants.
|
||||
- **The advertised target set is static while the effective mode is per-session** (schemas are registry-global) — a session already at the widest mode is still offered the fields. Harmless by construction: the strict-wider check at execution, not the enum, is the safety boundary — a non-widening request fails with its own text and never prompts anyone.
|
||||
- **A granted escalation is not a working sandbox.** An unavailable backend still fails closed even for a granted escalation to a confining mode — at `confine()` when the platform has no chain or every probe fails, at execution when an unprobed sole runner refuses (classified as a sandbox failure, not a command failure) — while a granted `danger-full-access` run never touches the provider at all: there the grant, not the probe, is the authority.
|
||||
- **An idle switch lives in bridge memory until the next prompt submission anchors it.** A crash in that window reverts it (reported on `session/load`), and a session that never submits another prompt never persists it — accepted, with a loop-owned idle commit turn left as future work if durability becomes required.
|
||||
- **The approval narrator's restart baseline parses prompt prose.** The closed candidate sentence is owned by the writing module itself, so a wording change is a coordinated writer+parser edit in one file; a session whose headers predate the section silently adopts the current policy without a notice.
|
||||
- **The approval section is still a dynamic prompt surface** (a `'never'` switch breaks provider prompt-prefix caching for that session). Accepted: policy switches are rare, and a model acting on a stale `'never'` is worse. The sandbox knob no longer touches the prompt at all.
|
||||
- **The model may hold a stale belief about the sandbox mode** (nothing announces a switch). Accepted deliberately: the next attempt's marker or success corrects it, and the observed failure mode of announcing — preemptive refusal — is worse than one wasted retry.
|
||||
@@ -190,7 +189,7 @@ Costs and accepted limits:
|
||||
- **Does the sandbox restrict network or process visibility?** No — `SandboxMode` claims FILE effects only; the bwrap profile deliberately does not unshare pid, and no backend claims network. Whether network restriction becomes its own knob is left open in § The seam.
|
||||
- **Which tools actually run confined?** OS subprocesses through `ctx.bash` — the bash tools, and hook commands transitively — plus the filesystem tools (`read`/`write`/`edit`) through the sandboxed `ctx.fs` provider (the [cross-family fs sandbox RFC](2026-07-14-cross-family-fs-sandbox.md)): bash confines via the OS runner, fs via an in-process path fence, both keying off the same `ctx.sandboxPolicy` mode. web/todo stay in-process and unfenced (web's only effect is network, outside the file-effect mode vocabulary).
|
||||
- **Does a granted escalation persist?** No. The grant is consumed by the exact foreground or background call that asked; every neighboring call keeps its own effective mode. A later background denial surfaces through `task_output` and may ground a new exact-command retry.
|
||||
- **When does an editor's mode switch take effect?** Mid-turn: appended immediately, honored by the very next call's stamp. Idle: held on the bridge's session record, anchored at the next `agent/prompt-submit` inside its open turn, with N flips coalescing to at most one event (none if net-zero); a crash before anchoring reverts it and `session/load` reports the truth. The model is not told — its next command simply behaves under the new mode.
|
||||
- **When does a runtime mode switch take effect?** Once its session event commits, the very next capability resolution folds and stamps the new mode. The model is not told a standing mode; its next command simply behaves under the new policy, and any denial names that policy at the point of use.
|
||||
- **What survives a restart — and what if the operator changed the config default while the process was down?** Overrides replay from the session log (`effective = fold ?? config`), so a resumed session keeps its modes with zero catch-up machinery; a default that drifted offline changes behavior the same way a switch does (the approval policy, being stated, is additionally narrated with operator/config attribution).
|
||||
- **What does `enforcement: 'partial'` on a result mean?** The selected backend enforces the subset its kernel ABI governs — e.g. Landlock before ABI v3 does not govern path truncate — and says so structurally instead of refusing the host; the probe's report line distinguishes the cases. The bwrap and Seatbelt profiles govern every promised file effect by construction, so they always report `full`.
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ This server-qualified shape is the de-facto standard among multi-server agent cl
|
||||
1. On connect: drain `client.listTools()` pagination, derive every tool's `publicName`, then register each as a raw `ToolDefinition` via `ctx.tools.register()`. The MCP JSON Schema and description pass through unchanged (no `defineTool` DSL conversion); only the model-facing `name` is replaced.
|
||||
2. Listen for `notifications/tools/list_changed` → re-run the same sync (dispose previous generation, register new). Deterministic names mean unchanged tools keep their names across re-syncs.
|
||||
3. The executor closes over `rawName`; the public name is never sent to the server and never parsed to recover the raw name.
|
||||
4. No `presentCall`/`presentResult` — the ACP bridge's generic-card fallback handles rendering.
|
||||
4. No `presentCall`/`presentResult` — UI consumers use the provider-neutral generic-card fallback.
|
||||
5. Tools are transparent in the system prompt — no "[via MCP]" annotation beyond the name itself.
|
||||
|
||||
### Public name normalization
|
||||
@@ -199,7 +199,7 @@ Coverage is named per tier; each behavior lives at the cheapest tier that can ex
|
||||
|
||||
- **Unit** (`tests/mcp-client.spec.ts`, `tests/apply.spec.ts`, mocked MCP SDK): the `publicToolName` algorithm (clean, normalize, truncate-and-hash, determinism, distinct-identity separation), raw-vs-public wire discipline, cross-server and native-tool coexistence, duplicate-`serverName` load failure and reservation release, invalid-tool-list rejection, generation swap/rollback, failed-re-sync retention, result mapping, cancellation, config schema validation. 100% per-file coverage gates the package.
|
||||
- **E2E** (`tests/mcp-client.e2e.ts`, keyless): the real MCP protocol against the in-repo fixture server, `@modelcontextprotocol/server-everything`, and `@modelcontextprotocol/server-filesystem` over stdio, and against an in-process `StreamableHTTPServerTransport` server over Streamable HTTP — discovery under the namespace, dotted-name normalization end to end, execution round-trips, duplicate-`serverName` rejection, disposal.
|
||||
- **Snapshot**: deliberately none. MCP tools introduce no new transcript surface — they register as raw `ToolDefinition`s and render through the ACP bridge's generic-card fallback, which the bridge's unit suite already pins (`packages/ui/acp/tests/stream-update.spec.ts`). Adding an MCP server to the snapshot example's `cordis.yml` would mutate the pinned `text-turn` system-prompt fixture (forcing a with-key re-record of every recorded expected output) and make every replay depend on spawning an external MCP server process — for zero new rendering behavior. If a later change gives MCP tools their own render intent, that change names its snapshot coverage then.
|
||||
- **Snapshot**: deliberately none. MCP tools introduce no new presentation shape — they register as raw `ToolDefinition`s and UI consumers use the generic-card fallback already pinned by their presentation suites. Adding an MCP server to a runnable snapshot composition would mutate its pinned system-prompt fixture and make every replay depend on spawning an external MCP server process for no new behavior. If a later change gives MCP tools their own render intent, that change names its snapshot coverage then.
|
||||
|
||||
## Consequences
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ Status: implemented
|
||||
|
||||
> **Superseded vocabulary (2026-07-22):** [Collapse named session modes into plan mode](../simplification/2026-07-22-plan-specific-collaboration-state.md) replaces this note's generic `dsh-mode`, `mode/set`, definition map, and `ctx.modes` design with the current plan-specific `dsh-plan-mode`, `plan/mode`, `{ section }`, and `ctx.planMode` contract. The review, boundary, reconstructability, and sandbox-orthogonality decisions below remain in force; generic API examples are retained as the historical design this simplification removed.
|
||||
|
||||
> **Superseded ACP mapping:** [ACP as an automation-only protocol](../simplification/2026-07-23-acp-automation-only-protocol.md) removes the picker, config-option, and elicitation mappings described below. Plan mode remains available to human-facing interfaces.
|
||||
|
||||
## Problem
|
||||
|
||||
Before this change, the harness had no durable way to put one agent into a distinct working stance. Plan mode needs the agent to explore and design under planning guidance, produce a reviewable artifact, cross an explicit approval boundary, and restore that state across resume and fork without making the model-visible request diverge from the session log.
|
||||
@@ -118,7 +120,7 @@ No new cordis event is declared (`mode/set` rides `session/event`; the listeners
|
||||
|
||||
Each behind its own decision: subagent mode inheritance via a forwarded creation-time mode option (removed as unconsumed; it returns with its first consumer), preset modes beyond `plan` (read-only, accept-edits), the idle-record primitive if pending-intent loss proves real, and — the big one — **effects self-declaration on tool definitions**: a per-tool read-only/mutating classification (the MCP `ToolAnnotations` vocabulary — `readOnlyHint`/`destructiveHint` — is the natural template, with its untrusted-hint caveat implying trust tiers). That item is what a general per-mode tool policy waits on: this Agent Note first shipped an interim per-mode name allowlist and removed it before release — a hand-maintained list mislabels the effects question, must track every tool a deployment composes, and rots silently as tools arrive — so mode-scoped tool availability (and per-tool `ask` policies) returns as a CONSUMER of declared effects, which is its restart trigger.
|
||||
|
||||
The canonical [`examples/acp-agent`](../../../../examples/acp-agent/) composition mounts the mode and question-tool plugins on the full ACP coding server; plan mode is an additive session feature, not a second server profile. Its snapshot suite pins the plan-shaped initial header, a real read, scripted approval, stable tool schemas across the pure-removal header delta, a subsequent edit, rejection feedback, and the keyless mode wire. A self-skipping real-API smoke boots that same leaf, verifies the file before approving the review, and verifies the approved implementation afterward.
|
||||
The ACP automation composition does not mount plan mode or the question tool. Human-facing compositions own plan selection and review; focused plan-mode tests and interactive-interface snapshots pin its logged state, guidance, review, and stable tool schemas.
|
||||
|
||||
## FAQ
|
||||
|
||||
@@ -138,13 +140,13 @@ Behavioral clarifications of the chosen design; rejected designs live in [Altern
|
||||
|
||||
**How does plan mode relate to the sandbox's read-only mode?** They are separate axes that never touch: the mode is the collaboration stance (a `mode/set` fold), the sandbox mode is an enforcement knob (a `bash/sandbox-mode` fold, [the sandbox Agent Note](2026-07-06-sandbox.md)) — plan mode neither reads nor caps it, exactly as Codex keeps its Plan/Default presets separate from its sandbox and approval settings. A user who wants kernel-enforced read-only while planning sets both: flip the mode picker AND the sandbox-mode option, in either order; each switch changes only its own fold, so there is no interference and no restore step to crash out of. The log attributes each axis to its own event — the stance to `mode/set`, the confinement to `bash/sandbox-mode`.
|
||||
|
||||
**Why aren't sandbox mode, approval policy, or the model themselves modes?** They are individual environment knobs and belong to ACP's `session/set_config_option`; the division this proposal pins is picker-to-modes / knobs-to-config-options, recorded in [the feature matrix](../../../../packages/ui/acp/acp-feature-support.md) now that both this stack's picker and the sandbox stack's config options are landed. A mode definition may later bundle env facts (applied through `ctx.envState` where mounted) so a Codex-style preset stays a single mode; fusing approval policy into the mode CONCEPT itself is rejected in [Alternatives considered](#alternatives-considered).
|
||||
**Why aren't sandbox mode, approval policy, or the model themselves modes?** They are individual environment knobs independent of collaboration state. The retired ACP mapping is recorded by the [automation-only protocol decision](../simplification/2026-07-23-acp-automation-only-protocol.md). A mode definition may later bundle env facts (applied through `ctx.envState` where mounted) so a Codex-style preset stays a single mode; fusing approval policy into the mode CONCEPT itself is rejected in [Alternatives considered](#alternatives-considered).
|
||||
|
||||
## Prior art
|
||||
|
||||
A survey of shipped plan modes (Claude Code, Cursor, Copilot, OpenCode, Gemini CLI, Cline, Windsurf, Codex) shows the same five parts everywhere — the low-authority tool policy, plan artifact, approval moment, execution-state switch, and durable state that [Problem](#problem) builds on.
|
||||
|
||||
The mode surface is a LIST everywhere it is advertised, never a boolean: Claude Code's picker offers `plan` beside `acceptEdits` (plus an auto-mode entry into plan), and Codex exposes `Plan` beside `Default` as collaboration-mode presets while keeping approval and sandbox settings separate. This is the surface [the ACP feature matrix](../../../../packages/ui/acp/acp-feature-support.md) records as the gap, and what sizes the vocabulary as named modes rather than a flag.
|
||||
The mode surface is a LIST everywhere it is advertised, never a boolean: Claude Code's picker offers `plan` beside `acceptEdits` (plus an auto-mode entry into plan), and Codex exposes `Plan` beside `Default` as collaboration-mode presets while keeping approval and sandbox settings separate. The ACP transport does not advertise this human-facing control.
|
||||
|
||||
The deployment-owned example prompt borrows the instrumental behavior, not product-specific mechanics. From Codex: remain in plan mode despite imperative implementation language, explore before asking, distinguish repository facts from user-owned choices, and make the plan decision-complete across APIs, data flow, failures, tests, and assumptions. From Claude Code: prohibit mutations and commits, prefer existing patterns, use questions only for requirements or approach choices, and finish through the exit tool rather than a prose approval request. It deliberately omits Codex protocol tags and Claude's plan-file or phased-subagent machinery because those belong to their runtimes, not this plugin contract.
|
||||
|
||||
@@ -186,7 +188,7 @@ What holds now, pinned by the unit, protocol, snapshot, and real-API tiers:
|
||||
- Native tool schemas and Code Mode's SDK stay byte-identical across default, plan, and custom-mode transitions; only the configured guidance section changes.
|
||||
- Plan mode changes nothing on the enforcement axes: the toolset, the sandbox mode, escalation, and the approval policy behave identically in plan and default — pairing the mode with the independent sandbox/approval knobs is how a deployment hardens planning.
|
||||
- Mode definitions are changeable from `cordis.yml` with no code edit; the complete plan instructions are required there, while missing plan config, malformed definitions, and unknown keys fail at load and unknown mode names fail at `set()`.
|
||||
- `exit_plan_mode` is always advertised, rejects outside plan, drops only plan guidance after approval, and carries keep-planning feedback in a corrective `isError`; ACP mode updates and each surface's user-interaction provider carry the human side.
|
||||
- `exit_plan_mode` is always advertised, rejects outside plan, drops only plan guidance after approval, and carries keep-planning feedback in a corrective `isError`; each human-facing surface's user-interaction provider carries the review.
|
||||
- The docs tail shipped with the landing: READMEs, regenerated catalogs (persistence log, config, cordis services, tools), the packages map and architecture rows, and the cookbook row.
|
||||
|
||||
The accepted costs: a pending user flip set while idle is lost if the process dies before the next turn (the UI re-applies; the idle-record primitive is the escape hatch if this bites in practice). A mode transition changes the system prompt at order 50, so the cache path from that point onward changes, but the tool schemas and Code Mode SDK no longer churn. **A mode restrains by guidance alone**: a model that ignores the section CAN mutate during plan — the review moment, the session log, and independent sandbox, approval, and filesystem policies are the containment surface. Hardening planning means setting those knobs, not widening the mode; the removed enforcement shapes and their effects-declaration restart trigger remain in [Alternatives considered](#alternatives-considered) and [Deferred](#deferred). The ACP mode surface carries the picker while sandbox, approval, and model selectors remain config options under the division pinned in the [FAQ](#faq) and [feature matrix](../../../../packages/ui/acp/acp-feature-support.md). If ACP removes session modes in favor of config options, the picker mapping can migrate without changing the logged mode state or model surface.
|
||||
The accepted costs: a pending user flip set while idle is lost if the process dies before the next turn (the UI re-applies; the idle-record primitive is the escape hatch if this bites in practice). A mode transition changes the system prompt at order 50, so the cache path from that point onward changes, but the tool schemas and Code Mode SDK no longer churn. **A mode restrains by guidance alone**: a model that ignores the section CAN mutate during plan — the review moment, the session log, and independent sandbox, approval, and filesystem policies are the containment surface. Hardening planning means setting those knobs, not widening the mode; the removed enforcement shapes and their effects-declaration restart trigger remain in [Alternatives considered](#alternatives-considered) and [Deferred](#deferred). Human-facing interfaces own the plan picker and review interaction; the ACP automation transport carries neither.
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-16-harness-level-loop.md: 9a9511b9dcea1b5fdc90f4fc716c4399f2346967
|
||||
2026-07-16-harness-level-loop.zh.md: 284e73051eaaa4633b9f56367de9096dadc8184e
|
||||
2026-07-16-harness-level-loop.md: 15b5ce7e20b7afc429f6ff7b8a4d2d69150c22a0
|
||||
2026-07-16-harness-level-loop.zh.md: a3fabe40d36c45c715f613ce8def35faa427d3bd
|
||||
|
||||
@@ -39,7 +39,7 @@ Time-based `/loop` or scheduled execution is a third policy and is not implement
|
||||
| `@deepseek-ai/dsh-tool-goal` | `packages/goal/tool-goal/`, model-facing consumer | Registers exclusive `get_goal`, `create_goal`, and `update_goal`; authenticates live turn provenance and narrows autonomous-round authority to completion or blocking reports with machine-routable reason codes. |
|
||||
| `@deepseek-ai/dsh-goal-session` | `packages/goal/goal-session/`, continuation policy | Reserves, fences, admits, attributes, settles, cancels, and quiescently drains same-session goal rounds without importing the concrete loop. |
|
||||
| `@deepseek-ai/dsh-commands` | `packages/ui/commands/`, UI registry | Owns `CommandDefinition`, discovery, scoped registration, direct dispatch, `CommandResult`, and request cancellation for human-only commands. |
|
||||
| `@deepseek-ai/dsh-command-goal` | `packages/goal/command-goal/`, human-command producer | Registers `/goal` status, creation, edit, pause, resume, and clear over the goal domain for TUI and ACP. |
|
||||
| `@deepseek-ai/dsh-command-goal` | `packages/goal/command-goal/`, human-command producer | Registers `/goal` status, creation, edit, pause, resume, and clear over the goal domain for TUI. |
|
||||
| `@deepseek-ai/dsh-tool-ralph` | `packages/workflow/tool-ralph/`, fixed workflow consumer | Registers `ralph({ objective, maxRounds? })`, validates the fresh structured provider and bounded `RalphRoundReport`, and returns `complete`, `blocked`, or `budget-limited`. |
|
||||
|
||||
The detailed contracts live in the [goal-domain](2026-07-19-persisted-same-session-goal-domain.md), [model goal-tools](2026-07-19-model-facing-goal-tools.md), [goal-round driver](2026-07-19-same-session-goal-round-driver.md), [command registry](2026-07-19-plugin-command-registration.md), [human goal-command](2026-07-19-human-goal-command.md), and [Ralph workflow-tool](2026-07-19-fresh-agent-ralph-workflow-tool.md) Agent Notes.
|
||||
@@ -70,7 +70,7 @@ The human UX follows the compact Codex shape in the [public OpenAI Codex TUI dis
|
||||
|
||||
The model receives only `get_goal`, `create_goal`, and `update_goal`. It may create a goal when a direct human request clearly asks for substantial multi-round work, and it may infer that intent in any language. It must not turn routine one-turn work into a goal. Direct-human provenance is enforced in code; semantic interpretation remains model judgment. An autonomous goal round may report `complete` or `blocked` for the exact current goal round but cannot edit, pause, resume, or replace the human objective.
|
||||
|
||||
TUI and ACP mount the shared command registry and complete goal stack by default and expose `/goal` through one producer. Every effective registered command is discoverable and invocable through every composed command adapter; a plugin incompatible with an application omits its command producer from that composition rather than relying on registry-level surface masks. The UI-less agent spine is opt-in so one-shot callers do not silently become multi-round operations. The headless CLI and JSON-RPC front doors do not consume the command plane; ordinary human text can still authorize model goal tools when that stack is composed.
|
||||
TUI mounts the shared command registry and complete goal stack by default and exposes `/goal` through one producer. ACP mounts the goal domain, model tools, and same-session driver but deliberately omits the human command plane. Every effective registered command is discoverable and invocable through every composed command adapter; a plugin incompatible with an application omits its command producer from that composition rather than relying on registry-level surface masks. The UI-less agent spine is opt-in so one-shot callers do not silently become multi-round operations. The headless CLI and JSON-RPC front doors do not consume the command plane; ordinary human text can still authorize model goal tools when that stack is composed.
|
||||
|
||||
### Fresh-agent Ralph execution
|
||||
|
||||
@@ -94,7 +94,7 @@ External products are comparators, not compatibility targets. The local source s
|
||||
|
||||
### Verification
|
||||
|
||||
The six owning Agent Notes record unit, integration, process, snapshot, cancellation, replay, and built-runtime coverage. The stack exercises strict goal-record folding, compare-and-set races, session fork inheritance, disarmed restoration, natural-language direct-human authority, configurable caps and blocked thresholds, exact goal-round attribution, adapter-wide command discovery, and transcript isolation. Shipped keyless snapshots cover model goal creation/inspection through the headless app, multi-round same-session lifecycle and cancellation through ACP, direct `/goal` status without a model turn, and two real Ralph rounds through the headless app. The Ralph snapshot boots the worker-thread engine, spawn provider, structured-output runtime, and agent loop, then inspects distinct unseeded child logs and exact one-way bounded handoff while pinning the parent stream. Focused real-stack tests additionally cover completion, blocker and round-limit outcomes, malformed and oversized reports, ordinary child failure with the last good handoff, one phase event, and cancellation to child quiescence. Package sources remain under the repository's per-file 100% coverage gate, and built-binary tests cover installed-artifact resolution. The implementation experience is recorded in the root testing policy: every non-trivial model- or human-visible change must carry a real-example keyless snapshot in the same PR rather than relying on package-only or mock-only fixture coverage.
|
||||
The six owning Agent Notes record unit, integration, process, snapshot, cancellation, replay, and built-runtime coverage. The stack exercises strict goal-record folding, compare-and-set races, session fork inheritance, disarmed restoration, natural-language direct-human authority, configurable caps and blocked thresholds, exact goal-round attribution, adapter-wide command discovery, and transcript isolation. Shipped keyless snapshots cover model goal creation/inspection through the headless app, multi-round same-session lifecycle and cancellation through ACP, and two real Ralph rounds through the headless app; focused command tests pin direct `/goal` status without a model turn. The Ralph snapshot boots the worker-thread engine, spawn provider, structured-output runtime, and agent loop, then inspects distinct unseeded child logs and exact one-way bounded handoff while pinning the parent stream. Focused real-stack tests additionally cover completion, blocker and round-limit outcomes, malformed and oversized reports, ordinary child failure with the last good handoff, one phase event, and cancellation to child quiescence. Package sources remain under the repository's per-file 100% coverage gate, and built-binary tests cover installed-artifact resolution. The implementation experience is recorded in the root testing policy: every non-trivial model- or human-visible change must carry a real-example keyless snapshot in the same PR rather than relying on package-only or mock-only fixture coverage.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -126,4 +126,4 @@ The six owning Agent Notes record unit, integration, process, snapshot, cancella
|
||||
- **No goal reflector** — concern events, automatic no-progress heuristics, goal revision by an independent reflector, stuck-pattern detection, and `loop_split` are not implemented. Humans can edit, pause, clear, or resume the goal directly.
|
||||
- **Ralph policy remains narrow** — one round creates one fresh child; within-round fan-out, evaluator/worker role separation, dynamic provider/model selection, and structural recursive-Ralph tool denial need separate policy surfaces. Prompt guidance is not enforcement.
|
||||
- **Ralph does not retry a failed child** — an ordinary failure preserves the failed round and last good handoff, while fatal workflow infrastructure failures can end before that state is available. Retry count, backoff, and richer failure transport need separate policy and seam design.
|
||||
- **Portable UI remains modest** — TUI and ACP render plain-text goal status and generic Ralph cards. There is no continuous status widget, reconnectable command output, modal goal editor, or command plane in the headless CLI or JSON-RPC front doors.
|
||||
- **Portable UI remains modest** — TUI renders plain-text goal status and generic Ralph cards. ACP carries only committed assistant text; there is no continuous status widget, reconnectable command output, modal goal editor, or command plane in ACP, the headless CLI, or JSON-RPC.
|
||||
|
||||
@@ -39,7 +39,7 @@ Status: implemented
|
||||
| `@deepseek-ai/dsh-tool-goal` | `packages/goal/tool-goal/`,面向模型消费者 | 注册互斥的 `get_goal`、`create_goal` 与 `update_goal`;认证实时 Turn 来源,并把自治 Round 权限收窄到带机器可路由原因代码的完成或阻塞报告。 |
|
||||
| `@deepseek-ai/dsh-goal-session` | `packages/goal/goal-session/`,续行策略 | 在不导入具体 loop 的情况下,预留、设围栏、接纳、归属、结算、取消并静止排空同会话目标回合。 |
|
||||
| `@deepseek-ai/dsh-commands` | `packages/ui/commands/`,UI 注册表 | 拥有面向人类专用命令的 `CommandDefinition`、发现、作用域注册、直接分发、`CommandResult` 与请求取消。 |
|
||||
| `@deepseek-ai/dsh-command-goal` | `packages/goal/command-goal/`,人类命令生产方 | 为 TUI 和 ACP 注册构建在目标领域之上的 `/goal` 状态、创建、编辑、暂停、恢复与清除。 |
|
||||
| `@deepseek-ai/dsh-command-goal` | `packages/goal/command-goal/`,人类命令生产方 | 为 TUI 注册构建在目标领域之上的 `/goal` 状态、创建、编辑、暂停、恢复与清除。 |
|
||||
| `@deepseek-ai/dsh-tool-ralph` | `packages/workflow/tool-ralph/`,固定工作流消费者 | 注册 `ralph({ objective, maxRounds? })`,验证全新结构化 provider 与有界 `RalphRoundReport`,并返回 `complete`、`blocked` 或 `budget-limited`。 |
|
||||
|
||||
详细契约分别由[目标领域](2026-07-19-persisted-same-session-goal-domain.md)、[模型目标工具](2026-07-19-model-facing-goal-tools.md)、[目标回合驱动器](2026-07-19-same-session-goal-round-driver.md)、[命令注册表](2026-07-19-plugin-command-registration.md)、[人类目标命令](2026-07-19-human-goal-command.md)与 [Ralph 工作流工具](2026-07-19-fresh-agent-ralph-workflow-tool.md) Agent Note 拥有。
|
||||
@@ -70,7 +70,7 @@ fork 会话会继承持久目标前缀,因为这是自然的重放结果。for
|
||||
|
||||
模型只接收 `get_goal`、`create_goal` 和 `update_goal`。当直接人类请求清楚要求大量多 Round 工作时,模型可以创建目标,并且可以从任何语言推断该意图。它不得把日常单 Turn 工作变成目标。直接人类来源由代码强制执行;语义解释仍是模型判断。自治目标 Round 可以为准确当前目标 Round 报告 `complete` 或 `blocked`,但不能编辑、暂停、恢复或替换人类目标。
|
||||
|
||||
TUI 与 ACP 默认挂载共享命令注册表和完整目标栈,并通过同一个生产方暴露 `/goal`。每条有效已注册命令都能被每个已组合的命令适配器发现和调用;若插件与某应用不兼容,该应用组合会省略其命令生产方,而不是依赖注册表层面的表面掩码。无 UI agent spine 要求显式选择加入,以免单次调用方静默变成多 Round 操作。无头 CLI 与 JSON-RPC 前端不消费命令平面;挂载目标栈后,普通人类文本仍可授权模型目标工具。
|
||||
TUI 默认挂载共享命令注册表和完整目标栈,并通过一个生产方暴露 `/goal`。ACP 挂载目标领域、模型工具和同会话驱动器,但有意省略人类命令平面。每条有效已注册命令都能被每个已组合的命令适配器发现和调用;若插件与某应用不兼容,该应用组合会省略其命令生产方,而不是依赖注册表层面的表面掩码。无 UI agent spine 要求显式选择加入,以免单次调用方静默变成多 Round 操作。无头 CLI 与 JSON-RPC 前端不消费命令平面;挂载目标栈后,普通人类文本仍可授权模型目标工具。
|
||||
|
||||
### 全新 agent Ralph 执行
|
||||
|
||||
@@ -94,7 +94,7 @@ Codex 提供了这里采用的最小可观察目标 UX:一个附着于聊天
|
||||
|
||||
### 验证
|
||||
|
||||
六份所属 Agent Note 记录了单元、集成、进程、快照、取消、重放与构建后运行时覆盖。该栈验证严格目标记录折叠、比较并交换竞争、会话 fork 继承、恢复后未激活、自然语言直接人类权限、可配置上限与阻塞阈值、准确目标回合归属、适配器范围的命令发现与转录隔离。已发布的无密钥快照覆盖通过无头应用创建/检查模型目标、通过 ACP 执行多 Round 同会话生命周期与取消、无需模型 Turn 的直接 `/goal` 状态,以及通过无头应用执行两个真实 Ralph Round。Ralph 快照会启动工作线程引擎、spawn provider、结构化输出运行时与 agent loop,随后检查互不相同且无种子的子日志和准确单向有界交接,同时固定父级事件流。聚焦的真实栈测试还覆盖完成、阻塞与 Round 上限结果、畸形及过大报告、保留上一份有效交接的普通子 agent 失败、单个阶段事件,以及取消后达到子 agent 静止状态。包源码继续受仓库逐文件 100% 覆盖率门禁约束,构建后二进制测试覆盖已安装产物解析。实现经验已记录进根测试策略:每项非平凡的模型或人类可见变更都必须在同一 PR 中携带真实示例无密钥快照,而不能依赖仅包级或仅模拟夹具的覆盖。
|
||||
六份所属 Agent Note 记录了单元、集成、进程、快照、取消、重放与构建后运行时覆盖。该栈验证严格目标记录折叠、比较并交换竞争、会话 fork 继承、恢复后未激活、自然语言直接人类权限、可配置上限与阻塞阈值、准确目标回合归属、适配器范围的命令发现与转录隔离。已发布的无密钥快照覆盖通过无头应用创建/检查模型目标、通过 ACP 执行多 Round 同会话生命周期与取消,以及通过无头应用执行两个真实 Ralph Round;聚焦的命令测试固定了无需模型 Turn 的直接 `/goal` 状态。Ralph 快照会启动工作线程引擎、spawn provider、结构化输出运行时与 agent loop,随后检查互不相同且无种子的子日志和准确单向有界交接,同时固定父级事件流。聚焦的真实栈测试还覆盖完成、阻塞与 Round 上限结果、畸形及过大报告、保留上一份有效交接的普通子 agent 失败、单个阶段事件,以及取消后达到子 agent 静止状态。包源码继续受仓库逐文件 100% 覆盖率门禁约束,构建后二进制测试覆盖已安装产物解析。实现经验已记录进根测试策略:每项非平凡的模型或人类可见变更都必须在同一 PR 中携带真实示例无密钥快照,而不能依赖仅包级或仅模拟夹具的覆盖。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -126,4 +126,4 @@ Codex 提供了这里采用的最小可观察目标 UX:一个附着于聊天
|
||||
- **没有目标反思器**——concern 事件、自动无进展启发式、由独立反思器执行的目标修订、卡住模式检测与 `loop_split` 均未实现。人类可以直接编辑、暂停、清除或恢复目标。
|
||||
- **Ralph 策略仍然狭窄**——一个 Round 创建一个全新子 agent;Round 内扇出、评估器/工作者角色分离、动态 provider/模型选择与结构化递归 Ralph 工具禁止都需要独立策略表面。提示词指导不是强制执行。
|
||||
- **Ralph 不会重试失败的子 agent**——普通失败会保留失败 Round 与上一份有效交接,而致命工作流基础设施错误可能在该状态可用前结束。重试次数、退避与更丰富的失败传输需要独立的策略与接缝设计。
|
||||
- **可移植 UI 仍较朴素**——TUI 与 ACP 渲染纯文本目标状态和通用 Ralph 卡片。系统没有持续状态组件、可重连命令输出、模态目标编辑器,无头 CLI 与 JSON-RPC 前端也没有命令平面。
|
||||
- **可移植 UI 仍较朴素**——TUI 渲染纯文本目标状态和通用 Ralph 卡片。ACP 只承载已提交的助手文本;系统没有持续状态组件、可重连命令输出、模态目标编辑器,ACP、无头 CLI 与 JSON-RPC 也没有命令平面。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-16-persistent-pty-sessions.md: b33993d36753d3195ec52d3b38dda62746a47bf3
|
||||
2026-07-16-persistent-pty-sessions.zh.md: 6ed330d75824a4e6fca9de0d82db61a7c6543322
|
||||
2026-07-16-persistent-pty-sessions.md: 148d4a2f47689e38a3ec83a7a41e4f75c4b73d95
|
||||
2026-07-16-persistent-pty-sessions.zh.md: 9a9d9cd4b0f61e8abaf011996ecd8739d13851f8
|
||||
|
||||
@@ -24,7 +24,7 @@ The implementation supports interactive shells and line-oriented REPLs on Linux
|
||||
|---|---|---|
|
||||
| `dsh-pty` | `PtyService`, branded `PtySessionId`, backend registry, owner-scoped session contract, and result types | `ctx.pty` |
|
||||
| `dsh-pty-local` | [`node-pty`](https://github.com/microsoft/node-pty)-based local backend, platform process inspection, bounded terminal buffer, sandbox resolution, and process-tree supervision | registers a backend on `ctx.pty` |
|
||||
| `dsh-tool-pty` | Six model-facing tools, task-runtime integration for background sends, guidance, and ACP render intents | registers on `ctx.tools` |
|
||||
| `dsh-tool-pty` | Six model-facing tools, task-runtime integration for background sends, guidance, and UI render intents | registers on `ctx.tools` |
|
||||
|
||||
Idle detection is backend behavior, not a second public seam. A remote or container backend may have authoritative readiness signals that do not resemble local `/proc` inspection; every `PtyBackend` therefore returns the common send result while owning its detection mechanism internally.
|
||||
|
||||
@@ -58,7 +58,7 @@ The implementation uses only public `node-pty` capabilities: child PID, `data` a
|
||||
| `terminal_close` | Close one session and await process-tree quiescence | `{ killed }` |
|
||||
| `terminal_list` | List the caller's live sessions | owner-scoped session summaries |
|
||||
|
||||
The ACP render contract is exact and location-free. `terminal_send` uses terminal call/result cards only for foreground sends; its background form is generic `execute`. `terminal_open`, `terminal_read`, `terminal_signal`, `terminal_close`, and `terminal_list` use generic `execute`, `read`, `execute`, `delete`, and `read` cards respectively. No PTY tool emits `locations`.
|
||||
The UI render contract is exact and location-free. `terminal_send` uses terminal call/result cards only for foreground sends; its background form is generic `execute`. `terminal_open`, `terminal_read`, `terminal_signal`, `terminal_close`, and `terminal_list` use generic `execute`, `read`, `execute`, `delete`, and `read` cards respectively. No PTY tool emits `locations`.
|
||||
|
||||
`terminal_send({ sessionId, text, submit?, run_in_background? })` treats `text` as UTF-8 bytes and resolves `submit` to `true` in the tool implementation. When `submit` is true it writes the platform Enter sequence after the text; when false it writes only the text, allowing control characters and REPL fragments without hidden content heuristics. `enableRunInBackground` defaults to true; false removes `run_in_background` from the schema and rejects the same undeclared argument if a caller forces it through execution.
|
||||
|
||||
@@ -155,7 +155,7 @@ The package ships concise tool guidance explaining persistent state, owner isola
|
||||
- Per-file coverage pins owner fencing, concurrent reservations, unpublished-spawn cancellation and awaited teardown, sandbox-mode change rejection, retriable lifecycle cleanup, readiness tiers, sanitizer carry state, complete UTF-8 bounds, task integration, schemas, and exact render intents.
|
||||
- Linux process fixtures cover non-leader and non-main-thread stdin waits, zombie quiescence, unreadable process state, supported syscall tables, unsupported architectures, and false-positive rejection; macOS inspector logic is injected into the same unit suite.
|
||||
- Real `node-pty` tests exercise shell state, shared sandbox policy, environment scrubbing, raw-mode foreground `SIGINT`, a TERM-ignoring descendant, and immediate post-disposal quiescence on supported hosts.
|
||||
- A Loader-driven `cordis.yml` test mounts the real three-package composition, while ACP and headless snapshots pin the six schemas, bounded results, error rendering, and terminal/generic cards through opt-in overlays.
|
||||
- A Loader-driven `cordis.yml` test mounts the real three-package composition. ACP and headless snapshots pin the six schemas, bounded results, and errors through opt-in overlays; TUI snapshots pin terminal and generic card presentation.
|
||||
- Package contracts, the architecture map, core data structures, generated catalogs, and the website API describe the same shipped surface.
|
||||
- The repository CI-equivalent sequence owns type, lint, coverage, snapshot, documentation, build, hygiene, demo, and built-entry verification.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ harness 可以运行前台与后台命令、编辑文件和委派工作,但无
|
||||
|---|---|---|
|
||||
| `dsh-pty` | `PtyService`、branded `PtySessionId`、后端注册表、按 owner 隔离的会话契约和结果类型 | `ctx.pty` |
|
||||
| `dsh-pty-local` | 基于 [`node-pty`](https://github.com/microsoft/node-pty) 的本地后端、平台进程检查、有界终端缓冲、沙箱解析和进程树监管 | 在 `ctx.pty` 上注册后端 |
|
||||
| `dsh-tool-pty` | 6 个面向模型的工具、后台发送的 task 运行时集成、使用指引和 ACP render intent | 注册到 `ctx.tools` |
|
||||
| `dsh-tool-pty` | 6 个面向模型的工具、后台发送的 task 运行时集成、使用指引和 UI 渲染意图 | 注册到 `ctx.tools` |
|
||||
|
||||
idle 检测属于后端行为,不是第二条公共 seam。远程或容器后端可能拥有完全不同于本地 `/proc` 检查的权威就绪信号;因此每个 `PtyBackend` 都返回统一的发送结果,同时在内部拥有自己的检测机制。
|
||||
|
||||
@@ -58,7 +58,7 @@ agent scope dispose 时先关闭注册,再等待全部所属 PTY 静默退出
|
||||
| `terminal_close` | 关闭一个会话并等待进程树静默退出 | `{ killed }` |
|
||||
| `terminal_list` | 列出调用方的活会话 | 按 owner 隔离的会话摘要 |
|
||||
|
||||
ACP 渲染契约精确且不携带位置信息。`terminal_send` 只为前台发送使用 terminal 调用卡片和结果卡片;后台形式使用通用 `execute` 卡片。`terminal_open`、`terminal_read`、`terminal_signal`、`terminal_close` 和 `terminal_list` 分别使用通用 `execute`、`read`、`execute`、`delete` 和 `read` 卡片。所有 PTY 工具都不发出 `locations`。
|
||||
UI 渲染契约精确且不携带位置信息。`terminal_send` 只为前台发送使用 terminal 调用卡片和结果卡片;后台形式使用通用 `execute` 卡片。`terminal_open`、`terminal_read`、`terminal_signal`、`terminal_close` 和 `terminal_list` 分别使用通用 `execute`、`read`、`execute`、`delete` 和 `read` 卡片。所有 PTY 工具都不发出 `locations`。
|
||||
|
||||
`terminal_send({ sessionId, text, submit?, run_in_background? })` 将 `text` 视为 UTF-8 字节,并由工具实现在解析阶段把 `submit` 默认成 `true`。`submit` 为 true 时先写入文本,再写入平台 Enter 序列;为 false 时只写文本,使控制字符和 REPL 片段无需隐藏的内容启发式即可发送。`enableRunInBackground` 默认为 true;设为 false 时,schema 中会移除 `run_in_background`,调用方即使强行把这个未声明参数传入执行流程,也会被拒绝。
|
||||
|
||||
@@ -122,7 +122,7 @@ plugins:
|
||||
maxResultBytes: 262144
|
||||
```
|
||||
|
||||
包提供简洁的工具指引,说明持久状态、owner 隔离、不确定的 idle 结果、清理,以及无需交互时优先使用现有一次性工具。已发布的基础示例不挂载 PTY:PTY 仅通过专用组合 opt-in,ACP 与 headless 快照 overlay 覆盖该组合。`dsh-tool-pty` 实例一旦启用,6 个工具和 `run_in_background` 就会默认启用;部署可通过配置仅禁用后台参数。
|
||||
包提供简洁的工具指引,说明持久状态、owner 隔离、不确定的 idle 结果、清理,以及无需交互时优先使用现有一次性工具。已发布的基础示例不挂载 PTY:PTY 仅通过专用组合 opt-in,ACP(Agent Client Protocol)与 headless 快照 overlay 覆盖该组合。`dsh-tool-pty` 实例一旦启用,6 个工具和 `run_in_background` 就会默认启用;部署可通过配置仅禁用后台参数。
|
||||
|
||||
### 推迟的工作
|
||||
|
||||
@@ -155,7 +155,7 @@ plugins:
|
||||
- 每文件覆盖率固定 owner 隔离、并发预留、未发布 spawn 的取消与等待式 teardown、沙箱模式变更拒绝、可重试的生命周期清理、就绪层级、sanitizer carry state、完整 UTF-8 结果上限、task 集成、schema 和精确 render intent。
|
||||
- Linux 进程 fixture 覆盖非 leader 与非主线程的 stdin 等待、僵尸进程静止性、不可读进程状态、受支持的 syscall 表、不支持的架构和误报拒绝;同一单元测试套件通过注入覆盖 macOS 检查器逻辑。
|
||||
- 真实 `node-pty` 测试在受支持宿主上覆盖 shell 状态、共享沙箱策略、环境清洗、raw mode 下的前台 `SIGINT`、忽略 `SIGTERM` 的子进程,以及 dispose 返回后立即静默。
|
||||
- Loader 驱动的 `cordis.yml` 测试挂载真实三包组合;ACP 与 headless 快照通过 opt-in overlay 固定 6 个 schema、有界结果、错误渲染和 terminal/generic card。
|
||||
- Loader 驱动的 `cordis.yml` 测试挂载真实三包组合。ACP 与 headless 快照通过 opt-in overlay 固定 6 个 schema、有界结果和错误;TUI 快照固定 terminal 与 generic 卡片展示。
|
||||
- 包契约、架构图、核心数据结构、生成目录和 website API 描述同一个已发布接口。
|
||||
- 仓库 CI 等价序列负责类型、lint、覆盖率、快照、文档、构建、hygiene、demo 和 built-entry 验证。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-17-dedicated-full-screen-tui-front-door.md: ecfda138593fc2b98ac42929acc586b11e437ee2
|
||||
2026-07-17-dedicated-full-screen-tui-front-door.zh.md: 6b8cc63f7657672a6da542e2033d765b54bd4f07
|
||||
2026-07-17-dedicated-full-screen-tui-front-door.md: aac67ffec89606d04d5abfd233d0469e2241b102
|
||||
2026-07-17-dedicated-full-screen-tui-front-door.zh.md: 0f64e2ce14b18d315f75913b82c731e77758e377
|
||||
|
||||
@@ -14,7 +14,7 @@ The interactive channel must remain a Cordis plugin over the same agent, session
|
||||
|
||||
DeepSeek Harness ships [`@deepseek-ai/dsh-tui`](../../../../packages/ui/tui/README.md) as a dedicated Cordis plugin. It owns terminal input and presentation only; agent lifecycle, session persistence, tool execution, and the model-facing question tool remain separate composition entries. The plugin requires both stdin and stdout to be TTYs and fails instead of silently changing to line-oriented behavior.
|
||||
|
||||
The app layer has one terminal front door. `@deepseek-ai/dsh-tui-demo` mounts the TUI before the configured agent, and `examples/tui-agent` owns the interactive coding composition and Code Mode overlay directly. Non-interactive tasks use `@deepseek-ai/dsh-cli-demo`; ACP remains a separate editor protocol.
|
||||
The app layer has one terminal front door. `@deepseek-ai/dsh-tui-demo` mounts the TUI before the configured agent, and `examples/tui-agent` owns the interactive coding composition and Code Mode overlay directly. Non-interactive tasks use `@deepseek-ai/dsh-cli-demo`; ACP remains a separate automation protocol.
|
||||
|
||||
The selected front door receives the exact generated or resumed `SessionId` used by the pre-created agent. It mounts before the agent composition, waits for the matching root agent, and enters full-screen mode only after that agent exists. A matching `agent-loop/config-start-failed` event is therefore reported before screen takeover and exits with status 1.
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Status: implemented
|
||||
|
||||
DeepSeek Harness 将 [`@deepseek-ai/dsh-tui`](../../../../packages/ui/tui/README.md) 作为独立的 Cordis 插件交付。该插件只负责终端输入与呈现;agent 生命周期、会话持久化、工具执行以及模型可见的提问工具仍由不同组合项负责。插件要求 stdin 和 stdout 均为 TTY;条件不满足时会失败,不会静默切换为逐行输出。
|
||||
|
||||
应用组合层只有一个终端入口。`@deepseek-ai/dsh-tui-demo` 在已配置 agent 之前挂载 TUI,`examples/tui-agent` 直接拥有交互式 coding 组装及其 Code Mode overlay。非交互任务使用 `@deepseek-ai/dsh-cli-demo`;ACP 仍是独立的编辑器协议。
|
||||
应用组合层只有一个终端入口。`@deepseek-ai/dsh-tui-demo` 在已配置 agent 之前挂载 TUI,`examples/tui-agent` 直接拥有交互式 coding 组装及其 Code Mode overlay。非交互任务使用 `@deepseek-ai/dsh-cli-demo`;ACP 仍是独立的自动化协议。
|
||||
|
||||
所选入口接收预创建 agent 使用的同一个新建或恢复 `SessionId`。入口先于 agent 组合挂载,等待相符的根 agent 出现,然后才进入全屏模式。因此,相符的 `agent-loop/config-start-failed` 事件会在接管屏幕前报告,并以状态码 1 退出。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-19-fresh-agent-ralph-workflow-tool.md: c2db4d7dd30c27a25adecdfc425db261cc3dfeb5
|
||||
2026-07-19-fresh-agent-ralph-workflow-tool.zh.md: e33e9848d71c98c8f83494ebe8bf171ef10b9305
|
||||
2026-07-19-fresh-agent-ralph-workflow-tool.md: 6fe96587c49ef0316d1618fda2ee26b015b1ce87
|
||||
2026-07-19-fresh-agent-ralph-workflow-tool.zh.md: c615c16ac020a0c905f6c8b52d8dc487fbcfe8be
|
||||
|
||||
@@ -40,7 +40,7 @@ The workflow language maps a normally settled but unsuccessful child to `null`.
|
||||
|
||||
The model may supply only `objective` and optional `maxRounds`; provider selection, report schema, handoff cap, and script are deployment-owned. A fixed prompt section says to use `ralph` only when the direct human explicitly asks for Ralph or fresh-agent iteration, and distinguishes it from same-session goals, bounded delegation, and general fan-out workflows. This is guidance rather than a new goal UX state machine.
|
||||
|
||||
ACP and terminal presentation use a generic `ralph` card whose raw input is the objective. Successful completion and blocker envelopes say that a worker reported the outcome rather than presenting it as independent certification. The parent transcript retains the original tool call and one bounded successful terminal report or an error, not intermediate child messages. Shipped headless, TUI, and ACP compositions load the plugin beside the existing workflow engine; JSON-RPC remains unchanged because its default composition does not expose workflows.
|
||||
Human-facing presentation uses a generic `ralph` card whose raw input is the objective; ACP carries only the committed assistant text. Successful completion and blocker envelopes say that a worker reported the outcome rather than presenting it as independent certification. The parent transcript retains the original tool call and one bounded successful terminal report or an error, not intermediate child messages. Shipped headless, TUI, and ACP compositions load the plugin beside the existing workflow engine; JSON-RPC remains unchanged because its default composition does not expose workflows.
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ Ralph 插件的 `subagentProvider` 默认为 `spawn`。每次调用前,它要
|
||||
|
||||
模型只能提供 `objective` 和可选的 `maxRounds`;provider 选择、报告 schema、交接上限和脚本都由部署拥有。固定提示区段说明,只有直接人类明确要求 Ralph 或全新 agent 迭代时才使用 `ralph`,并将其与同会话目标、有界委派和通用扇出工作流区分开。这是指导,而不是新的目标 UX 状态机。
|
||||
|
||||
ACP 和终端展示使用通用 `ralph` 卡片,并把目标作为原始输入。成功完成与阻塞的外层文本会说明结果由工作者报告,而不会把它呈现为独立认证。父转录只保留原始工具调用,以及一份有界成功终止报告或一个错误,不包含中间子 agent 消息。发布的无头、TUI 与 ACP 组合会在现有工作流引擎旁加载该插件;JSON-RPC 保持不变,因为其默认组合不暴露工作流。
|
||||
面向人类的展示使用通用 `ralph` 卡片,并把目标作为原始输入;ACP 只承载已提交的助手文本。成功完成与阻塞的外层文本会说明结果由工作者报告,而不会把它呈现为独立认证。父转录只保留原始工具调用,以及一份有界成功终止报告或一个错误,不包含中间子 agent 消息。发布的无头、TUI 与 ACP 组合会在现有工作流引擎旁加载该插件;JSON-RPC 保持不变,因为其默认组合不暴露工作流。
|
||||
|
||||
## 测试
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-19-human-goal-command.md: a272206a3bfad50a01ce871c56c7e7bcf924684e
|
||||
2026-07-19-human-goal-command.zh.md: 370c9bc24510320c70e3d789926c492e543968b1
|
||||
2026-07-19-human-goal-command.md: ce5c37fd28f9432d8c9a8797cac32c632617e317
|
||||
2026-07-19-human-goal-command.zh.md: d5ce36bd75a6f1070ee1eaeb1ac6ee97778c246b
|
||||
|
||||
@@ -6,7 +6,7 @@ English | [中文](2026-07-19-human-goal-command.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The same-session goal domain and model tools provide the state machine and semantic natural-language path, but they are not a sufficient human UX. A user needs to inspect the exact current phase and round budget without asking the model, explicitly pause or clear work without spending a model turn, and rearm a restored active goal after the required post-resume human decision. Implementing those actions independently in TUI and ACP would duplicate parsing, let the surfaces drift, and risk routing an unknown or unavailable command into the model.
|
||||
The same-session goal domain and model tools provide the state machine and semantic natural-language path, but they are not a sufficient human UX. A user needs to inspect the exact current phase and round budget without asking the model, explicitly pause or clear work without spending a model turn, and rearm a restored active goal after the required post-resume human decision. Implementing those actions independently in each UI would duplicate parsing, let the surfaces drift, and risk routing an unknown or unavailable command into the model.
|
||||
|
||||
The command must also respect the goal design's two kinds of state. Durable phase, objective, revisions, and rounds come from the session log; process-local activation decides whether an active goal may continue automatically. Showing only “active” after a resume would be misleading when the restored goal is intentionally disarmed and waiting for human authorization.
|
||||
|
||||
@@ -22,7 +22,7 @@ The command follows the compact Codex shape in the [public OpenAI Codex TUI disp
|
||||
|
||||
`/goal <objective>` creates an active armed goal. A completed goal may be replaced, which creates a fresh goal identity through the existing domain rule. Any unfinished goal makes the command fail directly with instructions to use inline edit or explicit clear. The generic command service deliberately has no modal confirmation API, so silently clearing and creating two durable records would manufacture destructive consent and expose a non-atomic failure window.
|
||||
|
||||
`/goal edit <objective>` edits the current non-complete goal without changing phase or activation. On a completed goal it creates a fresh active goal because the domain does not permit completed state to resume and a new completion objective is a new goal identity. Bare `edit` is an error rather than an editor launch because ACP's shared unstructured command contract has no portable modal editor.
|
||||
`/goal edit <objective>` edits the current non-complete goal without changing phase or activation. On a completed goal it creates a fresh active goal because the domain does not permit completed state to resume and a new completion objective is a new goal identity. Bare `edit` is an error rather than an editor launch because the portable unstructured command contract has no modal editor.
|
||||
|
||||
`/goal pause`, `/goal resume`, and `/goal clear` call the matching compare-and-set domain verbs against the current view. Resume covers both stopped durable phases and an active-but-disarmed goal after session resume, fork, or driver replacement. Domain rules still reject exhausted round caps, redundant active/armed resume, invalid phase transitions, and stale identity. Clear removes the current pointer while the session log retains the revisioned tombstone and earlier snapshots.
|
||||
|
||||
@@ -40,16 +40,16 @@ Generic slash input, status text, and errors are not persisted. Successful goal
|
||||
|
||||
`agent-spine-demo` accepts an optional `goals` composition object containing the goal-domain and model-tool owner configs. Omission or `false` leaves the stack unmounted. This explicit opt-in is important for headless one-shot callers: their result API settles one correlated physical turn and must not silently become a long-running logical goal operation.
|
||||
|
||||
The interactive app bundles make the opposite product choice. ACP and TUI default `goals` to the owner defaults and mount the goal domain, model tools, same-session driver, command registry, and this producer. Both apps accept `goals: false` as one coherent stack opt-out. The Python SDK runtime closure ships this producer alongside ACP, commands, and the goal stack so an external `cordis.yml` can compose the same command.
|
||||
The TUI app bundle makes the opposite product choice. It defaults `goals` to the owner defaults and mounts the goal domain, model tools, same-session driver, command registry, and this producer; `goals: false` removes the stack coherently. The [ACP automation app](../simplification/2026-07-23-acp-automation-only-protocol.md) also defaults the goal domain and model tools but deliberately omits command services. The Python SDK runtime closure ships this producer, commands, and the goal stack so an external `cordis.yml` can compose the same command.
|
||||
|
||||
## Testing
|
||||
|
||||
The producer suite uses the real command registry, goal service, agent registry, and session log. It covers Loader-safe exports, registry discovery, disposal, empty status, objective parsing, unfinished replacement refusal, inline edit, completed replacement, all missing-state controls, pause/resume/clear, every durable phase, blocked code/explanation presentation, armed/disarmed presentation, sanitized domain errors, unexpected failures, and persisted mutation records. App composition tests cover explicit spine opt-in, TUI/ACP defaults, coherent opt-out, forwarded domain/tool config, command discovery, the packaged-runtime closure, and the expanded model-tool assembly. A keyless snapshot boots the shipped ACP application, observes its advertised `/goal` metadata, invokes `/goal` directly, and pins the no-model-turn result; the surrounding ACP snapshots also pin the goal tool schemas in that composition.
|
||||
The producer suite uses the real command registry, goal service, agent registry, and session log. It covers Loader-safe exports, registry discovery, disposal, empty status, objective parsing, unfinished replacement refusal, inline edit, completed replacement, all missing-state controls, pause/resume/clear, every durable phase, blocked code/explanation presentation, armed/disarmed presentation, sanitized domain errors, unexpected failures, and persisted mutation records. App composition tests cover explicit spine opt-in, TUI defaults, coherent opt-out, forwarded domain/tool config, command discovery, the packaged-runtime closure, and the expanded model-tool assembly. ACP backend snapshots continue to pin the goal tool schemas independently of this human command.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Let the model handle `/goal` as ordinary text** — rejected because status and direct lifecycle actions would cost a model turn, could be reinterpreted, and would not provide deterministic ACP discovery.
|
||||
- **Implement separate TUI and ACP handlers** — rejected because grammar, error behavior, and goal-state formatting would drift and optional deployments could not add or remove the capability as one effect.
|
||||
- **Let the model handle `/goal` as ordinary text** — rejected because status and direct lifecycle actions would cost a model turn, could be reinterpreted, and would not provide deterministic command discovery.
|
||||
- **Implement separate handlers in each UI** — rejected because grammar, error behavior, and goal-state formatting would drift and optional deployments could not add or remove the capability as one effect.
|
||||
- **Add modal editing and replacement confirmation to `ctx.commands`** — rejected because the existing cross-surface contract is unstructured input plus direct output; a general interaction protocol needs more than this one producer.
|
||||
- **Silently replace an unfinished goal** — rejected because it combines clear and create without atomicity or explicit destructive intent.
|
||||
- **Expose goal id and revision in human status** — rejected because human actions always target the exact current view inside one synchronous handler; those fields add implementation noise without preventing another race.
|
||||
@@ -57,7 +57,7 @@ The producer suite uses the real command registry, goal service, agent registry,
|
||||
|
||||
## Consequences
|
||||
|
||||
- TUI and ACP expose one Codex-shaped `/goal` command supplied by a removable plugin.
|
||||
- TUI exposes one Codex-shaped `/goal` command supplied by a removable plugin.
|
||||
- Human status distinguishes durable phase from live activation and reports the exact goal-round cap.
|
||||
- Direct pause, resume, clear, creation, and edit consume no model turn while their accepted mutations remain reconstructable from the session log.
|
||||
- Restored sessions wait for a human decision; `/goal resume` is the literal command path, while an ordinary prompt in any language may authorize the model tool path.
|
||||
@@ -67,6 +67,6 @@ The producer suite uses the real command registry, goal service, agent registry,
|
||||
|
||||
- The portable command contract has no modal editor or confirmation interaction; inline edit and explicit clear are intentional until a general cross-surface interaction primitive exists.
|
||||
- `/goal` does not accept a per-command round cap. Deployment config owns the default, and the authorized model tool can edit a cap after direct human instruction.
|
||||
- TUI and ACP render portable plain text rather than a continuously updated goal status widget. Reconnectable command output and adapter-specific status indicators are deferred.
|
||||
- The headless CLI and JSON-RPC front doors do not consume the command registry.
|
||||
- TUI renders portable plain text rather than a continuously updated goal status widget. Reconnectable command output and adapter-specific status indicators are deferred.
|
||||
- The ACP automation server, headless CLI, and JSON-RPC front doors do not consume the command registry.
|
||||
- The command observes and mutates state but does not certify completion or blockers. Evaluator-backed certification remains deferred to a separate policy layer with an explicit authority and isolation contract.
|
||||
|
||||
@@ -6,7 +6,7 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
同会话目标领域和模型工具提供了状态机与自然语言语义路径,但尚不足以构成面向人类的 UX。用户需要在不询问模型的情况下检查准确的当前阶段与回合预算,在不消耗模型轮次的情况下明确暂停或清除工作,并在会话恢复后经过必要的人类决策重新激活已恢复的活跃目标。若在 TUI 与 ACP 中分别实现这些操作,就会重复解析逻辑、导致两个表面发生偏差,还可能把未知或不可用的命令交给模型处理。
|
||||
同会话目标领域和模型工具提供了状态机与自然语言语义路径,但尚不足以构成面向人类的 UX。用户需要在不询问模型的情况下检查准确的当前阶段与回合预算,在不消耗模型轮次的情况下明确暂停或清除工作,并在会话恢复后经过必要的人类决策重新激活已恢复的活跃目标。若在各 UI 中分别实现这些操作,就会重复解析逻辑、导致各界面发生偏差,还可能把未知或不可用的命令交给模型处理。
|
||||
|
||||
该命令还必须遵守目标设计中的两类状态。持久阶段、目标描述、修订号与回合来自会话日志;进程本地激活态决定活跃目标能否自动继续。恢复后若只显示“活跃”,就会掩盖目标已被有意设为未激活、正在等待人类授权这一事实。
|
||||
|
||||
@@ -22,7 +22,7 @@ Status: implemented
|
||||
|
||||
`/goal <objective>` 创建活跃且已激活的目标。已完成目标可以被替换,此时通过现有领域规则创建新的目标身份。任何未完成目标都会让命令直接失败,并提示用户使用行内编辑或明确清除。通用命令服务有意不提供模态确认 API;若静默执行清除再创建两条持久记录,就等于凭空制造破坏性同意,并暴露一个非原子的失败窗口。
|
||||
|
||||
`/goal edit <objective>` 编辑当前未完成目标,但不改变其阶段或激活态。若目标已经完成,则创建一个新的活跃目标,因为领域不允许恢复已完成状态,而新的完成条件应拥有新的目标身份。单独使用 `edit` 会返回错误而不是启动编辑器,因为 ACP 共享的非结构化命令契约没有可移植的模态编辑器。
|
||||
`/goal edit <objective>` 编辑当前未完成目标,但不改变其阶段或激活态。若目标已经完成,则创建一个新的活跃目标,因为领域不允许恢复已完成状态,而新的完成条件应拥有新的目标身份。单独使用 `edit` 会返回错误而不是启动编辑器,因为可移植的非结构化命令契约没有模态编辑器。
|
||||
|
||||
`/goal pause`、`/goal resume` 与 `/goal clear` 使用当前视图调用相应的比较并交换领域动词。恢复既适用于停止的持久阶段,也适用于会话恢复、fork 或驱动器替换后处于活跃但未激活状态的目标。领域规则仍会拒绝已耗尽的回合上限、对已活跃且已激活目标的重复恢复、非法阶段转换与陈旧身份。清除会移除当前指针,而会话日志保留带修订号的墓碑和此前快照。
|
||||
|
||||
@@ -40,16 +40,16 @@ Status: implemented
|
||||
|
||||
`agent-spine-demo` 接受可选的 `goals` 组合对象,其中包含目标领域与模型工具的所有者配置。省略或设为 `false` 时不会挂载该栈。对无头单次调用方而言,明确选择加入非常重要:它们的结果 API 会在一个相关物理轮次后结束,不能静默变成长时间运行的逻辑目标操作。
|
||||
|
||||
交互式应用包作出相反的产品选择。ACP 与 TUI 默认让 `goals` 使用所有者默认值,并挂载目标领域、模型工具、同会话驱动器、命令注册表与本生产方。两个应用都接受 `goals: false` 作为一致的整体退出选项。Python SDK 运行时闭包把本生产方与 ACP、命令及目标栈一并交付,使外部 `cordis.yml` 能组合相同命令。
|
||||
TUI 应用包作出相反的产品选择。它默认让 `goals` 使用所有者默认值,并挂载目标领域、模型工具、同会话驱动器、命令注册表与本生产方;`goals: false` 会一致地移除整个栈。[ACP(Agent Client Protocol)自动化应用](../simplification/2026-07-23-acp-automation-only-protocol.md)也默认挂载目标领域与模型工具,但有意省略命令服务。Python SDK 运行时闭包交付本生产方、命令与目标栈,使外部 `cordis.yml` 能组合相同命令。
|
||||
|
||||
## 测试
|
||||
|
||||
生产方测试套件使用真实命令注册表、目标服务、agent 注册表与会话日志。它覆盖 Loader 安全导出、注册表发现、资源释放、空状态、目标描述解析、拒绝未完成目标替换、行内编辑、已完成目标替换、所有缺失状态控制、暂停/恢复/清除、每个持久阶段、阻塞代码/说明展示、已激活/未激活展示、经净化的领域错误、意外失败与持久变更记录。应用组合测试覆盖显式主干选择加入、TUI/ACP 默认值、一致退出、转发的领域/工具配置、命令发现、打包运行时闭包与扩展后的模型工具组装。一个无密钥快照会启动交付的 ACP 应用,观察其公布的 `/goal` 元数据,直接调用 `/goal`,并固定不经过模型轮次的结果;周边 ACP 快照还会固定该组合中的目标工具 schema。
|
||||
生产方测试套件使用真实命令注册表、目标服务、agent 注册表与会话日志。它覆盖 Loader 安全导出、注册表发现、资源释放、空状态、目标描述解析、拒绝未完成目标替换、行内编辑、已完成目标替换、所有缺失状态控制、暂停/恢复/清除、每个持久阶段、阻塞代码/说明展示、已激活/未激活展示、经净化的领域错误、意外失败与持久变更记录。应用组合测试覆盖显式主干选择加入、TUI 默认值、一致退出、转发的领域/工具配置、命令发现、打包运行时闭包与扩展后的模型工具组装。ACP 后端快照继续固定目标工具 schema,与这项面向人类的命令无关。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
- **让模型把 `/goal` 当作普通文本处理**——不予采纳,因为状态与直接生命周期操作会消耗模型轮次、可能被重新解释,也无法提供确定性的 ACP 发现。
|
||||
- **分别实现 TUI 和 ACP 处理器**——不予采纳,因为语法、错误行为与目标状态格式会发生偏差,可选部署也无法把该功能作为一个 effect 统一增删。
|
||||
- **让模型把 `/goal` 当作普通文本处理**——不予采纳,因为状态与直接生命周期操作会消耗模型轮次、可能被重新解释,也无法提供确定性的命令发现。
|
||||
- **在各 UI 中分别实现处理器**——不予采纳,因为语法、错误行为与目标状态格式会发生偏差,可选部署也无法把该功能作为一个 effect 统一增删。
|
||||
- **为 `ctx.commands` 添加模态编辑与替换确认**——不予采纳,因为现有跨表面契约是非结构化输入加直接输出;通用交互协议所需的设计远超这一个生产方。
|
||||
- **静默替换未完成目标**——不予采纳,因为这会在没有原子性或明确破坏性意图的情况下组合清除与创建。
|
||||
- **在人类状态中暴露目标 id 与修订号**——不予采纳,因为人类操作始终在一个同步处理器内针对准确当前视图;这些字段只会增加实现噪声,无法消除其他竞争。
|
||||
@@ -57,7 +57,7 @@ Status: implemented
|
||||
|
||||
## 后果
|
||||
|
||||
- TUI 与 ACP 暴露由可移除插件提供的同一个 Codex 形态 `/goal` 命令。
|
||||
- TUI 暴露由可移除插件提供的 Codex 形态 `/goal` 命令。
|
||||
- 人类状态会区分持久阶段与实时激活态,并报告准确的目标回合上限。
|
||||
- 直接暂停、恢复、清除、创建与编辑不消耗模型轮次,而其已接受变更仍可从会话日志重建。
|
||||
- 恢复后的会话等待人类决策;`/goal resume` 是字面命令路径,任何语言的普通提示词则可以授权模型工具路径。
|
||||
@@ -67,6 +67,6 @@ Status: implemented
|
||||
|
||||
- 可移植命令契约没有模态编辑器或确认交互;在出现通用跨表面交互原语之前,行内编辑与明确清除是有意选择。
|
||||
- `/goal` 不接受逐命令回合上限。部署配置拥有默认值;得到直接人类指示后,已授权模型工具可以编辑上限。
|
||||
- TUI 与 ACP 渲染可移植纯文本,而不是持续更新的目标状态组件。可重连命令输出和适配器专用状态指示器予以延期。
|
||||
- 无头 CLI 与 JSON-RPC 前端不消费命令注册表。
|
||||
- TUI 渲染可移植纯文本,而不是持续更新的目标状态组件。可重连命令输出和适配器专用状态指示器予以延期。
|
||||
- ACP 自动化服务器、无头 CLI 与 JSON-RPC 前端不消费命令注册表。
|
||||
- 该命令观察并改变状态,但不认证完成或阻塞。基于评估器的认证延期到具有明确权限与隔离契约的独立策略层。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-19-model-facing-goal-tools.md: 7cc3907d708115207e166455ea988120a03d768b
|
||||
2026-07-19-model-facing-goal-tools.zh.md: 1a381160354d6a2a24f957f41bc9e375c1ab01ca
|
||||
2026-07-19-model-facing-goal-tools.md: 6d8fb2a9a6cd48d9b0e1e3e6c3ccbbdfbe571e42
|
||||
2026-07-19-model-facing-goal-tools.zh.md: 377dd204357c8d830ec2096f375e4142f9a7d079
|
||||
|
||||
@@ -20,7 +20,7 @@ The surface also needs to preserve the separation between durable state and live
|
||||
|
||||
The prompt tells the model that it may infer goal intent from a direct human request in any wording or language, but should not convert routine single-turn work into a goal. It must read the current goal before updating and copy the exact id and revision. On a restored or forked active-but-disarmed goal, a semantic human request to continue is grounds for `resume`. Completion is reserved for an achieved objective, and difficulty or uncertainty alone is not a blocker; a block report must name the concrete condition.
|
||||
|
||||
All three tools use exclusive execution so a model-ordered batch observes prior mutations and their new revisions. Results are compact JSON. ACP presentation is a pure function of arguments and uses generic read or mutation cards; activation is reported only as live observation and is never written into replay state.
|
||||
All three tools use exclusive execution so a model-ordered batch observes prior mutations and their new revisions. Results are compact JSON. UI presentation is a pure function of arguments and uses generic read or mutation cards; activation is reported only as live observation and is never written into replay state.
|
||||
|
||||
An autonomous goal round that successfully reports completion or blocking contributes the existing terminal `agent/turn-stop` decision for that physical turn, preventing an unnecessary follow-up request. Direct-human mutations do not contribute a terminal stop: the assistant can acknowledge the change, and concurrent human steering remains available to ordinary continuation folding.
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ Status: implemented
|
||||
|
||||
提示词告诉模型:它可以从任何措辞或语言的直接人类请求中推断目标意图,但不应把常规单轮工作转换为目标。更新前必须读取当前目标,并复制准确的 id 和修订号。对于恢复或派生后处于活跃但未激活状态的目标,人类在语义上要求继续即可成为执行 `resume` 的依据。只有目标已经实现时才能标记完成,困难或不确定性本身不构成阻塞;阻塞报告必须说明具体条件。
|
||||
|
||||
三个工具都采用独占执行,使模型排序的批次可以观察此前变更及其新修订号。结果为紧凑 JSON。ACP 展示是参数的纯函数,使用通用读取或变更卡片;激活态仅作为实时观察返回,绝不会写入回放状态。
|
||||
三个工具都采用独占执行,使模型排序的批次可以观察此前变更及其新修订号。结果为紧凑 JSON。UI 展示是参数的纯函数,使用通用读取或变更卡片;激活态仅作为实时观察返回,绝不会写入回放状态。
|
||||
|
||||
自主目标回合成功报告完成或阻塞后,插件会为该物理轮次贡献现有的终止型 `agent/turn-stop` 决策,避免再发起一次不必要的模型请求。直接人类发起的变更不会贡献终止决策:智能体可以确认该变更,并且并发的人类 steering(转向)仍可参与普通的继续执行折叠。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-19-plugin-command-registration.md: bc3d33f9abf7cd87b78aac8f7d36ac9c021a7910
|
||||
2026-07-19-plugin-command-registration.zh.md: 054ab3a90eeecc8c5ddc2ff072b53112fd8e7845
|
||||
2026-07-19-plugin-command-registration.md: 119b1e6e545dca8ea8cb425407723c8d11c806ad
|
||||
2026-07-19-plugin-command-registration.zh.md: fed14ca6e3bdb0cd40dabac7641a359f4bc3b3f2
|
||||
|
||||
@@ -6,17 +6,17 @@ English | [中文](2026-07-19-plugin-command-registration.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The TUI owns seven slash commands, while ACP defines a standard command catalog and invocation shape. Keeping command names, help text, autocomplete, dispatch, and cancellation inside each adapter makes every new command an adapter edit, prevents optional plugins from contributing commands, and lets the two front doors drift. Treating slash input as an ordinary model prompt is also unsafe: a user-visible direct action can unexpectedly consume tokens or let the model reinterpret an unknown command.
|
||||
The TUI owns slash commands. Keeping command names, help text, autocomplete, dispatch, and cancellation inside the adapter makes every new command a TUI edit and prevents optional plugins from contributing commands. Treating slash input as an ordinary model prompt is also unsafe: a user-visible direct action can unexpectedly consume tokens or let the model reinterpret an unknown command.
|
||||
|
||||
A shared mechanism must remain a UI concern rather than a model tool or agent-loop branch. It also needs exact per-agent visibility, HMR-safe removal, per-session ACP discovery, direct result rendering, and request-scoped cancellation without automatically adding command text or output to model history.
|
||||
A shared mechanism must remain a UI concern rather than a model tool or agent-loop branch. It also needs exact per-agent visibility, HMR-safe removal, direct result rendering, and request-scoped cancellation without automatically adding command text or output to model history.
|
||||
|
||||
## Decision
|
||||
|
||||
`@deepseek-ai/dsh-commands` in `packages/ui/commands/` is the product command registry. The terminal and ACP app bundles mount it beside their consuming front door, and the SDK project helper emits the same service when scaffolding ACP directly; the executor-less, UI-less agent spine remains independent. TUI and ACP inject the service, while command producers depend only on the registry and any domain they operate.
|
||||
`@deepseek-ai/dsh-commands` in `packages/ui/commands/` is the product command registry. The TUI app bundle mounts it beside its consuming front door; the [automation-only ACP app](../simplification/2026-07-23-acp-automation-only-protocol.md) and the executor-less, UI-less agent spine omit it. TUI injects the service, while command producers depend only on the registry and any domain they operate.
|
||||
|
||||
### Registry contract
|
||||
|
||||
A `CommandDefinition` contains a lowercase name without `/`, a non-empty description, an optional unstructured-input hint, and an abortable handler. Registration validates and detaches the metadata, freezes the effective definition, and returns the exact Cordis effect disposer. Duplicate names fail within one layer. Every adapter consuming the registry sees every effective definition; a command plugin that cannot operate in a deployment omits its registration there instead of encoding adapter identities in the shared domain.
|
||||
A `CommandDefinition` contains a lowercase name without `/`, a non-empty description, an optional unstructured-input hint, and an abortable handler. Registration validates and detaches the metadata, freezes the effective definition, and returns the exact Cordis effect disposer. Duplicate names fail within one layer. Every consumer sees every effective definition; a command plugin that cannot operate in a deployment omits its registration there instead of encoding consumer identities in the shared domain.
|
||||
|
||||
`list(agent)` returns immutable name-sorted descriptors after scoped shadowing. `find(agent, name)` resolves the effective definition. `execute(agent, line, signal)` parses and runs a known definition, returning a detached `success` or `error` result; invalid syntax and unknown names return `undefined` so the adapter owns its direct error text.
|
||||
|
||||
@@ -36,46 +36,35 @@ Expected handler failures return `CommandResult.error`. Thrown or malformed resu
|
||||
|
||||
### TUI mapping
|
||||
|
||||
The TUI registers `help`, `clear`, `cancel`, `reasoning`, `tools`, `redraw`, and `exit` as agent-scoped command definitions instead of switching on strings. Its autocomplete and help view read the live catalog, so plugin commands appear and disappear with their effects. Any submitted line beginning with `/` stays in the command plane; unknown input produces a terminal warning rather than falling through to `Agent.send()` or `Agent.steer()`.
|
||||
The TUI registers its built-in slash commands as agent-scoped command definitions instead of switching on strings. Its autocomplete and help view read the live catalog, so plugin commands appear and disappear with their effects. Any submitted line beginning with `/` stays in the command plane; unknown input produces a terminal warning rather than falling through to `Agent.send()` or `Agent.steer()`.
|
||||
|
||||
Each submitted command owns an `AbortController`. TUI disposal aborts outstanding dispatches, removes the local definitions, and waits for the command-producing fiber before completing teardown.
|
||||
|
||||
### ACP mapping
|
||||
|
||||
The bridge follows the current [ACP v1 slash-command contract](https://agentclientprotocol.com/protocol/v1/slash-commands). `session/new` and `session/load` emit the exact agent's full `available_commands_update` snapshot; a new session's RPC response introduces its server-generated id before the snapshot is enqueued. Every registry change emits a replacement snapshot for each live session. Names, descriptions, and optional unstructured-input hints map directly to `AvailableCommand`.
|
||||
|
||||
ACP permits a command prompt to contain additional supported content blocks. The bridge applies its ordinary lossless `text` and `resource_link` flattening, then enters the command plane when the result starts with `/`. Unsupported prompt blocks are rejected by the existing capability boundary. Known commands execute directly; unknown or malformed slash input returns a direct error and never reaches the model. Successful text, expected errors, and thrown-failure diagnostics stream as live `agent_message_chunk` output and settle `end_turn`.
|
||||
|
||||
One model prompt or direct command may be in flight per ACP session, independently across sessions. `session/cancel` aborts the direct command when one owns the request; it calls `Agent.cancel()` only for an agent prompt, so cancelling a command cannot destroy unrelated queued or injected agent work. Connection teardown aborts commands and then disposes the owned agents.
|
||||
|
||||
## Testing
|
||||
|
||||
The registry suite covers syntax boundaries, immutable normalization, runtime metadata validation, deterministic sorting, global and scoped shadowing, duplicate rejection, exact disposal, contained change-notification failures, direct invocation, expected and malformed results, synchronous and asynchronous failure, and every abort timing edge at per-file 100% statement, branch, function, and line coverage.
|
||||
|
||||
TUI tests exercise all migrated built-ins, live plugin discovery, help/autocomplete refresh, direct results, unknown-command rejection, raw-input delivery, definition removal, startup rollback, and disposal cancellation. ACP tests use the real SDK connection, agent factory, loop, and JSONL persistence to verify create/load snapshots, dynamic updates, scoped multi-session catalogs, supported-block flattening, direct success/error/failure, unknown-command isolation, cancellation, and the absence of model requests or session messages. The SDK helper suite pins direct-ACP composition. Keyless ACP and terminal snapshots pin the new protocol and rendered transcript shapes.
|
||||
TUI tests exercise all migrated built-ins, live plugin discovery, help/autocomplete refresh, direct results, unknown-command rejection, raw-input delivery, definition removal, startup rollback, and disposal cancellation. Keyless terminal snapshots pin the rendered help, error, and command-result shapes.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Keep adapter-local switches** — rejected because optional plugins cannot contribute discovery and behavior without editing every front door.
|
||||
- **Keep adapter-local switches** — rejected because optional plugins cannot contribute discovery and behavior without editing the TUI.
|
||||
- **Represent human commands as model tools** — rejected because discovery and direct invocation are human UI behavior; routing through the model adds latency, token cost, and reinterpretation.
|
||||
- **Put the registry in the core agent spine** — rejected because headless and JSON-RPC agents do not consume it, while the two UI app bundles can compose it explicitly.
|
||||
- **Put the registry in the core agent spine** — rejected because UI-less front doors do not consume it, while TUI can compose it explicitly.
|
||||
- **Make `dsh-agent-loop` inject commands** — rejected because the loop does not execute or discover human commands. Agent-scoped producers declare the UI dependency in a child plugin instead.
|
||||
- **Attach adapter masks to each definition** — rejected because support is a composition fact, not command-domain state. Every composed adapter exposes a registered command; an incompatible plugin omits registration in that deployment.
|
||||
- **Send unknown slash input to the model** — rejected because typoed or unavailable direct actions must fail predictably rather than change execution planes.
|
||||
- **Persist generic command input and output** — rejected because adapter notices are not model-visible state. A handler that changes durable behavior calls the owning domain API, which records its own events.
|
||||
- **Restrict ACP commands to one text block** — rejected because ACP v1 permits accompanying content; the bridge already has a lossless accepted-block translation.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Command producers are ordinary removable plugins, and TUI/ACP share one validated catalog and dispatch contract.
|
||||
- Command producers are ordinary removable plugins, and TUI consumes their validated catalog and dispatch contract.
|
||||
- Agent-specific definitions retain existing flat scope and shadow semantics without a core-to-UI dependency.
|
||||
- Unknown slash input and command output are deterministic UI behavior with zero direct model tokens.
|
||||
- ACP clients receive current per-session snapshots after creation, load, registration, and HMR removal.
|
||||
- Direct command cancellation is isolated from model-turn cancellation.
|
||||
|
||||
## Known limitations and deferred work
|
||||
|
||||
- Input metadata is ACP's current unstructured text hint. Typed forms, argument schemas, and completion providers remain command-owned or require a later protocol extension.
|
||||
- Generic command output is live-only and is not reconstructed after TUI restart or ACP reconnect.
|
||||
- Input metadata is limited to an unstructured text hint. Typed forms, argument schemas, and completion providers remain command-owned or require a later registry or consumer extension.
|
||||
- Generic command output is live-only and is not reconstructed after TUI restart.
|
||||
- Registry cancellation stops awaiting immediately, but external work stops only when a handler cooperates with its signal.
|
||||
- The headless CLI and JSON-RPC SDK front doors do not expose the command plane; only TUI and ACP consume it.
|
||||
- The ACP automation server, headless CLI, and JSON-RPC SDK front doors do not expose the command plane; only TUI consumes it.
|
||||
|
||||
@@ -6,17 +6,17 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
TUI 拥有七个斜杠命令,而 ACP 定义了标准命令目录与调用形态。如果命令名、帮助文本、自动补全、分派和取消都留在各适配器内部,每个新命令都需要修改适配器,可选插件无法贡献命令,两个前端也会逐渐偏离。把斜杠输入当作普通模型提示同样不安全:用户可见的直接操作可能意外消耗 token,或让模型重新解释未知命令。
|
||||
TUI 拥有斜杠命令。如果命令名、帮助文本、自动补全、分派和取消都留在适配器内部,每个新命令都需要修改 TUI,可选插件也无法贡献命令。把斜杠输入当作普通模型提示同样不安全:用户可见的直接操作可能意外消耗 token,或让模型重新解释未知命令。
|
||||
|
||||
共享机制必须仍是 UI 关注点,而不是模型工具或智能体循环分支。它还需要精确的逐智能体可见性、可安全 HMR 移除、逐会话 ACP 发现、直接结果渲染和请求作用域取消,同时不会自动把命令文本或输出加入模型历史。
|
||||
共享机制必须仍是 UI 关注点,而不是模型工具或智能体循环分支。它还需要精确的逐智能体可见性、可安全 HMR 移除、直接结果渲染和请求作用域取消,同时不会自动把命令文本或输出加入模型历史。
|
||||
|
||||
## 决策
|
||||
|
||||
位于 `packages/ui/commands/` 的 `@deepseek-ai/dsh-commands` 是产品命令注册表。终端与 ACP 应用 bundle(组合包)把它挂载在消费该服务的前端旁,SDK 项目 helper(辅助器)在直接搭建 ACP 时也会生成同一服务;无执行器、无 UI 的智能体 spine(主干)保持独立。TUI 与 ACP 注入该服务,命令生产者只依赖注册表及其操作的领域。
|
||||
位于 `packages/ui/commands/` 的 `@deepseek-ai/dsh-commands` 是产品命令注册表。TUI 应用 bundle(组合包)把它挂载在消费该服务的前端旁;[仅面向自动化的 ACP(Agent Client Protocol)应用](../simplification/2026-07-23-acp-automation-only-protocol.md)和无执行器、无 UI 的智能体 spine(主干)都省略该服务。TUI 注入该服务,命令生产者只依赖注册表及其操作的领域。
|
||||
|
||||
### 注册表契约
|
||||
|
||||
`CommandDefinition` 包含不带 `/` 的小写名称、非空描述、可选的非结构化输入提示,以及可取消处理器。注册会校验并分离元数据、冻结有效定义,并返回准确的 Cordis effect disposer(副作用释放器)。同一层中的重复名称会失败。每个消费该注册表的适配器都能看到所有有效定义;若命令插件无法在某种部署中运行,它就不在该部署中注册,而不是把适配器身份编码进共享领域。
|
||||
`CommandDefinition` 包含不带 `/` 的小写名称、非空描述、可选的非结构化输入提示,以及可取消处理器。注册会校验并分离元数据、冻结有效定义,并返回准确的 Cordis effect disposer(副作用释放器)。同一层中的重复名称会失败。每个消费方都能看到所有有效定义;若命令插件无法在某种部署中运行,它就不在该部署中注册,而不是把消费方身份编码进共享领域。
|
||||
|
||||
`list(agent)` 在作用域遮蔽后返回不可变、按名称排序的描述符。`find(agent, name)` 解析有效定义。`execute(agent, line, signal)` 解析并运行已知定义,返回分离后的 `success` 或 `error` 结果;无效语法和未知名称返回 `undefined`,由适配器拥有直接错误文本。
|
||||
|
||||
@@ -36,46 +36,35 @@ TUI 拥有七个斜杠命令,而 ACP 定义了标准命令目录与调用形
|
||||
|
||||
### TUI 映射
|
||||
|
||||
TUI 把 `help`、`clear`、`cancel`、`reasoning`、`tools`、`redraw` 和 `exit` 注册为智能体作用域命令定义,不再对字符串执行 switch。自动补全与帮助视图读取实时目录,因此插件命令会随其副作用出现和消失。任何以 `/` 开头的提交行都留在命令平面;未知输入产生终端警告,不会落入 `Agent.send()` 或 `Agent.steer()`。
|
||||
TUI 把内置斜杠命令注册为智能体作用域命令定义,不再对字符串执行 switch。自动补全与帮助视图读取实时目录,因此插件命令会随其副作用出现和消失。任何以 `/` 开头的提交行都留在命令平面;未知输入产生终端警告,不会落入 `Agent.send()` 或 `Agent.steer()`。
|
||||
|
||||
每个提交的命令拥有一个 `AbortController`。TUI 释放会中止未完成的分派、移除本地定义,并等待命令生产者 fiber(纤程)后再完成清理。
|
||||
|
||||
### ACP 映射
|
||||
|
||||
桥接遵循当前的 [ACP v1 斜杠命令契约](https://agentclientprotocol.com/protocol/v1/slash-commands)。`session/new` 与 `session/load` 发出准确智能体的完整 `available_commands_update` 快照;新会话的 RPC 响应会先引入服务端生成的 id,随后快照才会入队。每次注册表变更都会为每个实时会话发出替换快照。名称、描述和可选非结构化输入提示直接映射到 `AvailableCommand`。
|
||||
|
||||
ACP 允许命令提示携带额外的受支持内容块。桥接应用普通的无损 `text` 与 `resource_link` 扁平化,然后在结果以 `/` 开头时进入命令平面。不支持的提示块由现有能力边界拒绝。已知命令直接执行;未知或格式错误的斜杠输入返回直接错误,绝不会到达模型。成功文本、预期错误和抛出失败的诊断作为实时 `agent_message_chunk` 输出流式发送,并以 `end_turn` 结束请求。
|
||||
|
||||
每个 ACP 会话同时只能有一个模型提示或直接命令进行中,各会话彼此独立。当直接命令拥有请求时,`session/cancel` 会中止它;只有智能体提示才调用 `Agent.cancel()`,因此取消命令不会销毁无关的排队或注入智能体工作。连接清理会先中止命令,再释放所拥有的智能体。
|
||||
|
||||
## 测试
|
||||
|
||||
注册表测试覆盖语法边界、不可变规范化、运行时元数据校验、确定性排序、全局与作用域遮蔽、重复拒绝、准确释放、变更通知失败隔离、直接调用、预期和格式错误结果、同步与异步失败,以及每种中止时序边沿;该源文件达到逐文件 100% 语句、分支、函数和行覆盖率。
|
||||
|
||||
TUI 测试覆盖全部迁移后的内置命令、实时插件发现、帮助与自动补全刷新、直接结果、未知命令拒绝、原始输入交付、定义移除、启动回滚和释放取消。ACP 测试使用真实 SDK 连接、智能体工厂、循环与 JSONL 持久化,验证创建/加载快照、动态更新、作用域多会话目录、受支持块扁平化、直接成功/错误/失败、未知命令隔离、取消,以及不存在模型请求或会话消息。SDK helper 测试固定直接 ACP 组合。无密钥 ACP 与终端快照固定新的协议和渲染记录形态。
|
||||
TUI 测试覆盖全部迁移后的内置命令、实时插件发现、帮助与自动补全刷新、直接结果、未知命令拒绝、原始输入交付、定义移除、启动回滚和释放取消。无密钥终端快照固定渲染后的帮助、错误与命令结果形态。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
- **保留适配器本地 switch**——不予采纳,因为可选插件无法贡献发现与行为,除非修改每个前端。
|
||||
- **保留适配器本地 switch**——不予采纳,因为可选插件无法贡献发现与行为,除非修改 TUI。
|
||||
- **把人类命令表示为模型工具**——不予采纳,因为发现与直接调用属于人类 UI 行为;经由模型路由会增加延迟、token 成本和重新解释。
|
||||
- **把注册表放入核心智能体主干**——不予采纳,因为无头和 JSON-RPC 智能体不消费它,而两个 UI 应用组合包可以显式组合它。
|
||||
- **把注册表放入核心智能体主干**——不予采纳,因为无 UI 前端不消费它,而 TUI 可以显式组合它。
|
||||
- **让 `dsh-agent-loop` 注入 commands**——不予采纳,因为循环不执行也不发现人类命令。智能体作用域生产者改为在子插件中声明 UI 依赖。
|
||||
- **为每个定义附加适配器掩码**——不予采纳,因为支持能力是组合事实,而不是命令领域状态。每个已组合适配器都暴露已注册命令;不兼容插件不会在该部署中注册。
|
||||
- **把未知斜杠输入发送给模型**——不予采纳,因为输入错误或不可用的直接操作必须可预测地失败,而不能改变执行平面。
|
||||
- **持久化通用命令输入与输出**——不予采纳,因为适配器提示不是模型可见状态。改变持久行为的处理器会调用拥有该状态的领域 API,由后者记录自己的事件。
|
||||
- **把 ACP 命令限制为单个文本块**——不予采纳,因为 ACP v1 允许附带内容,而桥接已有无损的已接纳块转换。
|
||||
|
||||
## 后果
|
||||
|
||||
- 命令生产者是普通的可移除插件,TUI 与 ACP 共享一个经过校验的目录和分派契约。
|
||||
- 命令生产者是普通的可移除插件,TUI 消费其经过校验的目录与分派契约。
|
||||
- 智能体特定定义保留现有扁平作用域与遮蔽语义,不引入核心到 UI 的依赖。
|
||||
- 未知斜杠输入与命令输出是确定性 UI 行为,直接模型 token 成本为零。
|
||||
- ACP 客户端在创建、加载、注册和 HMR 移除后收到当前的逐会话快照。
|
||||
- 直接命令取消与模型轮次取消彼此隔离。
|
||||
|
||||
## 已知限制与延期工作
|
||||
|
||||
- 输入元数据仅为 ACP 当前的非结构化文本提示。类型化表单、参数模式和补全提供器仍由命令拥有,或需要后续协议扩展。
|
||||
- 通用命令输出仅实时存在,TUI 重启或 ACP 重新连接后不会重建。
|
||||
- 输入元数据仅限非结构化文本提示。类型化表单、参数模式和补全提供器仍由命令拥有,或需要后续注册表或消费方扩展。
|
||||
- 通用命令输出仅实时存在,TUI 重启后不会重建。
|
||||
- 注册表取消会立即停止等待,但外部工作只有在处理器配合信号时才会停止。
|
||||
- 无头 CLI 与 JSON-RPC SDK 前端不暴露命令平面;只有 TUI 和 ACP 消费它。
|
||||
- ACP 自动化服务器、无头 CLI 与 JSON-RPC SDK 前端不暴露命令平面;只有 TUI 消费它。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-19-same-session-goal-round-driver.md: 34d59456b5a8b54c92aba581da0ff22ea045b626
|
||||
2026-07-19-same-session-goal-round-driver.zh.md: dc2afd1ce18a45964bc1db04121211a9958445f3
|
||||
2026-07-19-same-session-goal-round-driver.md: d23af9a9b05d60d2dccad095455524844f1185b9
|
||||
2026-07-19-same-session-goal-round-driver.zh.md: f4f0cd6fd575d14427025bdbd8d10bc90e25f780
|
||||
|
||||
@@ -70,7 +70,7 @@ An inbox acceptance can win the microtask race immediately before plugin unload
|
||||
|
||||
The unit suite uses the real agent loop and session service with only the model scripted. It covers exact sequential admission and cap enforcement, load/resume inertness, every outcome classification, rate limiting, request errors, max tokens, downstream prompt veto, pre-admission and in-flight cancellation, unrelated-human cancellation, failed-pause fallback, human-input ordering, queued and downstream revision races, forged goal attribution, failed mutation and turn checkpoints including a later one-shot injection, scheduler and custom-agent failures, session-start reset, exact lifecycle retirement, and queued/running plugin teardown. The new driver source has per-file 100% statement, branch, function, and line coverage.
|
||||
|
||||
A keyless ACP snapshot mounts the shipped editor app with the real goal domain, goal tools, goal driver, agent loop, persistence, and replay adapter through `cordis.yml`. One human turn creates and inspects a two-round goal, the first automatic turn stops normally, and ACP cancellation of a deliberately stalled second round records a durable pause. The normalized wire transcript and external JSONL assertions prove one session, round sources `1, 2`, the lifecycle mutation, and exact replay accounting without using `echo-agent` as an application surrogate.
|
||||
A keyless ACP snapshot mounts the shipped automation app with the real goal domain, goal tools, goal driver, agent loop, persistence, and replay adapter through `cordis.yml`. One human-originated turn creates and inspects a two-round goal, the first automatic turn stops normally, and ACP cancellation of a deliberately stalled second round records a durable pause. The normalized wire transcript and external JSONL assertions prove one session, round sources `1, 2`, the lifecycle mutation, and exact replay accounting without using `echo-agent` as an application surrogate.
|
||||
|
||||
The core cancellation test proves notification order and containment: observers run only for effective cancellation, can queue replacement work before the inbox clear, cannot veto later observers by throwing, and an idle call emits nothing.
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ Status: implemented
|
||||
|
||||
单元测试使用真实 agent loop 与会话服务,只对模型编写脚本。覆盖内容包括精确连续接纳和上限执行、加载与恢复的惰性、所有结果分类、限流、请求错误、最大 token、下游提示词否决、接纳前与执行中取消、无关人类工作取消、暂停失败回退、人类输入排序、排队时与下游修订竞争、伪造目标来源、变更与轮次检查点失败(包括后续一次性注入)、调度器与自定义 agent 失败、会话启动重置、精确生命周期退出,以及排队中和运行中的插件卸载。新驱动器源码达到逐文件 100% 语句、分支、函数和行覆盖率。
|
||||
|
||||
无密钥 ACP 快照通过 `cordis.yml` 挂载已发布的编辑器应用,以及真实目标领域、目标工具、目标驱动器、agent loop、持久化和回放适配器。一个人类轮次创建并检查一个两回合目标;第一个自动轮次正常停止,ACP 随后取消刻意停滞的第二个回合并记录持久暂停。规范化线协议和外部 JSONL 断言证明只有一个会话、回合来源依次为 `1, 2`、生命周期变更与回放计数精确,并且没有把 `echo-agent` 当作应用替身。
|
||||
无密钥 ACP 快照通过 `cordis.yml` 挂载已发布的自动化应用,以及真实目标领域、目标工具、目标驱动器、agent loop、持久化和回放适配器。一个源自人类的轮次创建并检查一个两回合目标;第一个自动轮次正常停止,ACP 随后取消刻意停滞的第二个回合并记录持久暂停。规范化线协议和外部 JSONL 断言证明只有一个会话、回合来源依次为 `1, 2`、生命周期变更与回放计数精确,并且没有把 `echo-agent` 当作应用替身。
|
||||
|
||||
核心取消测试固定通知顺序与隔离:只有有效取消才会通知;观察者可以在清空收件箱前排入替代工作;抛错不能阻止后续观察者;空闲调用不会发出事件。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-20-code-mode-typed-tool-returns.md: 29f139a7e965de3a374d195ecc205210e6ae7e93
|
||||
2026-07-20-code-mode-typed-tool-returns.zh.md: 431c0b1717c6783771255ce8291c241f8f92c30b
|
||||
2026-07-20-code-mode-typed-tool-returns.md: 1b5cbb9f4664c371a03cfefd079d5dc531711b51
|
||||
2026-07-20-code-mode-typed-tool-returns.zh.md: 70fd9ee72803c5cd2fa228a1d38ddf7ae47a4814
|
||||
|
||||
@@ -75,7 +75,7 @@ Dynamic Cordis mounting follows the same rule: `cordis_mount` returns `{ id, plu
|
||||
|
||||
Nested dispatch keeps the existing bounded `tool/code-dispatch.resultSummary` for diagnostics but does not persist canonical values. `tool/result` continues to persist only rendered content, error, and optional metadata. This is deliberately not a session-format change, so `SESSION_FORMAT_VERSION` remains unchanged and replay cannot recreate intermediate program values.
|
||||
|
||||
The opaque `exec.parent` token marks nested calls. Presentation metadata and generic or tool-owned spill projections skip those calls because they have no direct result card and their canonical values never enter context. The outer `run_code` call alone produces one card and may spill its final post-policy presentation; `run_code` intentionally declares neither a result presenter nor presentation metadata, so ACP and TUI complete the card through their generic raw-content fallback using durable `tool/result.content`.
|
||||
The opaque `exec.parent` token marks nested calls. Presentation metadata and generic or tool-owned spill projections skip those calls because they have no direct result card and their canonical values never enter context. The outer `run_code` call alone produces one card and may spill its final post-policy presentation; `run_code` intentionally declares neither a result presenter nor presentation metadata, so UI adapters complete the card through their generic raw-content fallback using durable `tool/result.content`.
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -95,7 +95,7 @@ Keyless real-worker integration tests pin the two handle workflows that prose re
|
||||
|
||||
## Consequences
|
||||
|
||||
Code programs can compose tools through stable values instead of reverse-engineering Native prose. Native and Both Mode retain their existing text and editor presentation, while Code Mode receives output-schema types and exact runtime JSON. Tool authors must treat the canonical value as their programmatic API and put display-only formatting in the renderer.
|
||||
Code programs can compose tools through stable values instead of reverse-engineering Native prose. Native and Both Mode retain their existing text and UI presentation, while Code Mode receives output-schema types and exact runtime JSON. Tool authors must treat the canonical value as their programmatic API and put display-only formatting in the renderer.
|
||||
|
||||
The worker performs bounded-depth flat-wire transport and lossless validation but does not make intermediate values cheap or durable. Outer overflow is an explicit failed run, and error handling remains intentionally human-guided rather than a versioned code union.
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ Code Mode 通过运行时请求中的 `{ name: "ToolCallError", memberNameProper
|
||||
|
||||
嵌套分发会为诊断保留既有的有界 `tool/code-dispatch.resultSummary`,但不会持久化规范值。`tool/result` 继续只持久化渲染后的内容、错误和可选元数据。这并非会话格式变更,因此 `SESSION_FORMAT_VERSION` 保持不变,回放也无法重建程序的中间值。
|
||||
|
||||
不透明的 `exec.parent` token 用于标识嵌套调用。由于这些调用没有直接对应的结果卡片,而且其规范值永远不会进入上下文,展示元数据以及通用或工具自有的输出落盘投影都会跳过它们。只有外层 `run_code` 调用会生成一张卡片,并且可能将 post-policy 处理后的最终展示写入落盘文件;`run_code` 有意既不声明结果展示器,也不声明展示元数据,因此 ACP 和 TUI 通过其通用的原始内容回退机制,使用持久化的 `tool/result.content` 补全该卡片。
|
||||
不透明的 `exec.parent` token 用于标识嵌套调用。由于这些调用没有直接对应的结果卡片,而且其规范值永远不会进入上下文,展示元数据以及通用或工具自有的输出落盘投影都会跳过它们。只有外层 `run_code` 调用会生成一张卡片,并且可能将 post-policy 处理后的最终展示写入落盘文件;`run_code` 有意既不声明结果展示器,也不声明展示元数据,因此 UI 适配器会通过通用的原始内容回退机制,使用持久化的 `tool/result.content` 补全该卡片。
|
||||
|
||||
## 测试
|
||||
|
||||
@@ -95,7 +95,7 @@ Code Mode 通过运行时请求中的 `{ name: "ToolCallError", memberNameProper
|
||||
|
||||
## 影响
|
||||
|
||||
Code Mode 程序可以通过稳定值组合工具,无需逆向解析 Native 自然语言。Native 和 Both Mode 保留现有文本与编辑器展示,Code Mode 则获得输出 schema 类型和精确的运行时 JSON。工具作者必须把规范值视为程序化 API,并将仅用于展示的格式化放入渲染器。
|
||||
Code Mode 程序可以通过稳定值组合工具,无需逆向解析 Native 自然语言。Native 和 Both Mode 保留现有文本与 UI 展示,Code Mode 则获得输出 schema 类型和精确的运行时 JSON。工具作者必须把规范值视为程序化 API,并将仅用于展示的格式化放入渲染器。
|
||||
|
||||
worker 会以嵌套深度有界的扁平协议格式传输数据并执行无损校验,但不会降低中间值的开销,也不会使其具备持久性。外层输出溢出会显式导致运行失败,错误处理则有意由人类引导,而不是依赖带版本的错误代码联合。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-21-cross-session-references.md: bfa015b24cda6c8651829b6a7f0800326da5b502
|
||||
2026-07-21-cross-session-references.zh.md: e8e99124f7e2ccfe9fbe97323c17143372017562
|
||||
2026-07-21-cross-session-references.md: fc084b36e7920a72efff0f363278d24eaebc4c69
|
||||
2026-07-21-cross-session-references.zh.md: fe4a876b5265fa7ad298adf3b829bcec70e878e8
|
||||
|
||||
@@ -6,13 +6,13 @@ English | [中文](2026-07-21-cross-session-references.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
TUI and ACP users need to bring relevant work from another conversation into one new message without resuming, forking, or granting the source transcript authority over the current session. The harness already exposes exact session enumeration and raw event inspection, but every host independently parsing logs would duplicate compaction folding, provenance filtering, size limits, error behavior, and persistence. Encoding host markup directly into the agent message contract would also bind the core loop to one UI syntax.
|
||||
TUI users need to bring relevant work from another conversation into one new message without resuming, forking, or granting the source transcript authority over the current session. The harness already exposes exact session enumeration and raw event inspection, but every host independently parsing logs would duplicate compaction folding, provenance filtering, size limits, error behavior, and persistence. Encoding host markup directly into the agent message contract would also bind the core loop to one UI syntax.
|
||||
|
||||
## Decision
|
||||
|
||||
`@deepseek-ai/dsh-session-reference` is one context consumer service at `ctx.sessionReferences`. Hosts normalize their protocol into `SessionReferenceInput[]`, call `prepare()` before enqueue, and pass the returned contexts through the generic `SendOptions.contexts` boundary. Core agent packages know only that one queued message may carry frozen `HookContext[]`; they do not parse session URIs or read another log.
|
||||
|
||||
`dsh-session:<base64url(JSON.stringify(sessionId))>` is the canonical host-independent identifier. JSON string encoding precedes base64url so quotes, slashes, backslashes, Unicode, newlines, and every other JavaScript string value round-trip without delimiter ambiguity. TUI renders that URI inside `@[label](uri)` and ACP uses standard `resource_link`; text-only clients may use the same inline mention. Explicit Markdown mentions and resource links reject malformed URIs. Bare text becomes a reference only for a non-empty base64url-shaped payload, whose decode must still be canonical; empty or punctuation-only uses remain ordinary discussion text.
|
||||
`dsh-session:<base64url(JSON.stringify(sessionId))>` is the canonical host-independent identifier. JSON string encoding precedes base64url so quotes, slashes, backslashes, Unicode, newlines, and every other JavaScript string value round-trip without delimiter ambiguity. TUI renders that URI inside `@[label](uri)`; text-only clients may use the same inline mention. Explicit Markdown mentions reject malformed URIs. Bare text becomes a reference only for a non-empty base64url-shaped payload, whose decode must still be canonical; empty or punctuation-only uses remain ordinary discussion text.
|
||||
|
||||
The service uses `ctx.sessionQuery.readSurface(sessionId)`, which loads one live-preferred corpus observation, folds it with the session package's canonical surface algorithm, and returns a detached header, capture seq, and current nodes. FTS is not a dependency: v1 discovery filters only id and cwd, and future title/body search can replace the candidate layer without changing reference identity or preparation.
|
||||
|
||||
@@ -28,13 +28,13 @@ One aggregated context is serialized as JSON beneath a fixed untrusted-backgroun
|
||||
|
||||
`send()` and `steer()` snapshot content, resolved source, and contexts together as one deeply frozen lossless-JSON inbox record. Synthetic `inject()` accepts source and model-hidden metadata but not attached contexts, which belong to inbox messages. A claimed ordinary message exposes its attached contexts as the default `agent/prompt-submit` additional contexts; a block writes neither user message nor contexts. The waterfall's returned allow is authoritative, so a listener wrapping `next()` preserves downstream content and contexts unless it intentionally replaces them. After admission, absent or `separate` placement writes an independent `context/message`, while `prompt-prefix` placement bakes context and the effective request into one prompt event. Drained steering bypasses `agent/prompt-submit` but applies the same placement split. Late steering retains the same record when converted to queued input, while cancellation, disposal, and terminal discard drop message and contexts together. `agent/queued` reports the frozen contexts so the observation event describes the complete retained item.
|
||||
|
||||
This preserves host driving semantics: TUI decides `send()` versus `steer()` from the agent state after preparation, so only its queued path dispatches UserPromptSubmit hooks; ACP continues to call `send()` once per `session/prompt`. Reference preparation is not a new steering protocol and does not create a turn by itself.
|
||||
This preserves host driving semantics: TUI decides `send()` versus `steer()` from the agent state after preparation, so only its queued path dispatches UserPromptSubmit hooks. Reference preparation is not a new steering protocol and does not create a turn by itself.
|
||||
|
||||
## Host adapters
|
||||
|
||||
TUI combines session candidates with the existing `@` file provider. Each candidate displays the latest folded session title and falls back to the session id; lookup follows the editor's cancellation signal, and session id, cwd, and mention labels escape external terminal controls while the canonical URI retains the original id. TUI prepares only submissions containing structured mentions, disables duplicate submit while awaiting snapshots, restores failed input, renders the prompt envelope's display content as the user message, and renders its session-reference metadata as a compact source list instead of exposing the complete JSON in the terminal.
|
||||
|
||||
ACP detects direct slash commands from ordinary prompt flattening before extracting `dsh-session:` resource links and canonical inline mentions, so URI-shaped command arguments remain opaque while ordinary resource-link rendering is preserved. Standard `session/list` exposes each loadable session's folded title and, when references are mounted, a canonical URI under `_meta["deepseek-harness/sessionReference"]`; a client can use `title ?? sessionId` as the resource-link name. A valid reference without the optional service returns a capability-unavailable RPC error, and preparation failure occurs before the in-flight turn slot and agent send. A preparation-specific abort owner makes `session/cancel` and bridge teardown stop pending reads. Picker UI remains an ACP client responsibility because ACP does not define a cross-session mention menu.
|
||||
The [automation-only ACP transport](../simplification/2026-07-23-acp-automation-only-protocol.md) deliberately does not mount session-query or session-reference services.
|
||||
|
||||
## Budget and retention
|
||||
|
||||
@@ -43,8 +43,8 @@ Each of at most three references is independently capped at 65,536 UTF-8 bytes b
|
||||
## Alternatives considered
|
||||
|
||||
- **Wait for SQLite FTS5** — rejected because snapshot correctness requires exact id reads and canonical surface folding, not content search. FTS improves discovery only.
|
||||
- **Put mention syntax in `Agent.send()`** — rejected because it would make the core protocol parse TUI/ACP presentation and prevent typed non-text hosts from sharing the semantic layer.
|
||||
- **Implement references inside TUI and ACP separately** — rejected because projection, security warning, retention, and persistence would drift across hosts.
|
||||
- **Put mention syntax in `Agent.send()`** — rejected because it would make the core protocol parse one host's presentation syntax and prevent typed non-text hosts from sharing the semantic layer.
|
||||
- **Implement references separately in each host** — rejected because projection, security warning, retention, and persistence would drift across hosts.
|
||||
- **Place a separate user-role context message beside the prompt** — rejected because two adjacent user messages weaken the prompt's deictic binding: in `@foo what does this session discuss?`, the model may resolve “this session” as the current conversation instead of the referenced snapshot.
|
||||
- **Bake the prefix host-side before `send()`** — rejected because `agent/prompt-submit` must inspect and rewrite only the direct prompt. The effective prompt and attached contexts meet only after admission in AgentLoop, which can apply an `allow.content` rewrite consistently to both combined model content and `envelope.displayContent`; earlier host assembly would expose snapshot bytes to the hook or let those two views diverge.
|
||||
- **Replay the raw source log or restore shadowed events** — rejected because compact defines the current model surface and may intentionally retire sensitive or expensive history.
|
||||
@@ -53,8 +53,8 @@ Each of at most three references is independently capped at 65,536 UTF-8 bytes b
|
||||
|
||||
## Verification
|
||||
|
||||
Unit and integration coverage pins URI round-trips and text-boundary punctuation, explicit malformed references, title-aware candidate ranking, terminal-control escaping, projection exclusions, non-recursive prompt-envelope projection, backend-independent compact checkpoints, tag-safe framing, deduplication, self-reference, count limits, all-or-nothing reads, prompt cancellation against a non-settling storage read, independent per-source byte retention, frozen message ownership, prompt blocking, send/steer placement, title isolation, missing capability, title-aware ACP session listing, ordinary ACP resource links, opaque ACP command arguments, and compact TUI/ACP replay. A keyless TUI snapshot runs the real agent loop: the source surface replaces old user/assistant history with a compact checkpoint, the target submits a mention, and the captured model request contains one user message ordered as snapshot, request delimiter, and current prompt, without either shadowed string.
|
||||
Unit and integration coverage pins URI round-trips and text-boundary punctuation, explicit malformed references, title-aware candidate ranking, terminal-control escaping, projection exclusions, non-recursive prompt-envelope projection, backend-independent compact checkpoints, tag-safe framing, deduplication, self-reference, count limits, all-or-nothing reads, prompt cancellation against a non-settling storage read, independent per-source byte retention, frozen message ownership, prompt blocking, send/steer placement, title isolation, missing capability, and compact TUI replay. A keyless TUI snapshot runs the real agent loop: the source surface replaces old user/assistant history with a compact checkpoint, the target submits a mention, and the captured model request contains one user message ordered as snapshot, request delimiter, and current prompt, without either shadowed string.
|
||||
|
||||
## Consequences
|
||||
|
||||
The new plugin is the stable semantic boundary and adds no persistence schema, event type, FTS dependency, source subscription, or compact shadow access. Standard TUI/ACP demo bundles mount it explicitly and expose its count and per-source byte limits in their own config; custom hosts remain unchanged until they mount the service and adapt their input. Reference contexts increase target history size within configured bounds and can later be summarized by ordinary target compaction, after which the source session is irrelevant.
|
||||
The new plugin is the stable semantic boundary and adds no persistence schema, event type, FTS dependency, source subscription, or compact shadow access. The standard TUI demo bundle mounts it explicitly and exposes its count and per-source byte limits in its config; custom hosts remain unchanged until they mount the service and adapt their input. Reference contexts increase target history size within configured bounds and can later be summarized by ordinary target compaction, after which the source session is irrelevant.
|
||||
|
||||
@@ -6,13 +6,13 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
TUI 与 ACP(Agent Client Protocol)用户需要把另一场对话中的相关工作带入一条新消息,但不恢复、不 fork,也不让源 transcript(文本记录)对当前会话拥有权威性。harness 已经提供准确的会话枚举与原始事件检查,但若每个宿主都独立解析日志,就会重复实现压缩(compaction)折叠、来源过滤、大小限制、错误行为和持久化。把宿主标记直接编码进 agent(智能体)消息契约,还会让核心循环绑定某一种 UI 语法。
|
||||
TUI 用户需要把另一场对话中的相关工作带入一条新消息,但不恢复、不 fork,也不让源 transcript(文本记录)对当前会话拥有权威性。harness 已经提供准确的会话枚举与原始事件检查,但若每个宿主都独立解析日志,就会重复实现压缩(compaction)折叠、来源过滤、大小限制、错误行为和持久化。把宿主标记直接编码进 agent(智能体)消息契约,还会让核心循环绑定某一种 UI 语法。
|
||||
|
||||
## 决策
|
||||
|
||||
`@deepseek-ai/dsh-session-reference` 是注册在 `ctx.sessionReferences` 上的单一上下文消费服务。宿主先把各自的协议规范化为 `SessionReferenceInput[]`,在入队前调用 `prepare()`,再通过通用的 `SendOptions.contexts` 边界传递返回的上下文。核心 agent 包只知道一条排队消息可以携带已冻结的 `HookContext[]`;它们既不解析会话 URI,也不读取其他日志。
|
||||
|
||||
`dsh-session:<base64url(JSON.stringify(sessionId))>` 是与宿主无关的规范标识符。系统先执行 JSON 字符串编码,再执行 base64url 编码,因此引号、正斜杠、反斜杠、Unicode、换行符以及其他任意 JavaScript 字符串值都能无损往返,不会因分隔符产生歧义。TUI 把该 URI 渲染到 `@[label](uri)` 中,ACP 使用标准 `resource_link`;纯文本客户端可以使用同一种行内提及标记。显式 Markdown 提及标记与资源链接会拒绝格式错误的 URI。裸文本只有在负载非空且形状符合 base64url 时才会成为引用,而且解码过程仍须通过规范性校验;空负载或只含标点符号的用法仍按普通讨论文本处理。
|
||||
`dsh-session:<base64url(JSON.stringify(sessionId))>` 是与宿主无关的规范标识符。系统先执行 JSON 字符串编码,再执行 base64url 编码,因此引号、正斜杠、反斜杠、Unicode、换行符以及其他任意 JavaScript 字符串值都能无损往返,不会因分隔符产生歧义。TUI 把该 URI 渲染到 `@[label](uri)` 中;纯文本客户端可以使用同一种行内提及标记。显式 Markdown 提及标记会拒绝格式错误的 URI。裸文本只有在负载非空且形状符合 base64url 时才会成为引用,而且解码过程仍须通过规范性校验;空负载或只含标点符号的用法仍按普通讨论文本处理。
|
||||
|
||||
该服务使用 `ctx.sessionQuery.readSurface(sessionId)`:它优先从实时会话加载一次语料观察结果,使用会话包的规范表层算法执行折叠,并返回与源数据分离的会话头、捕获序号和当前节点。FTS 不是功能依赖:v1 的候选发现只按 id 和 cwd 过滤;未来的标题或正文搜索可以替换候选层,而无需改变引用标识或准备过程。
|
||||
|
||||
@@ -28,13 +28,13 @@ TUI 与 ACP(Agent Client Protocol)用户需要把另一场对话中的相关
|
||||
|
||||
`send()` 与 `steer()` 会把内容、解析后的来源和上下文一起快照为一条深度冻结、无损 JSON 的收件箱记录。合成的 `inject()` 接受来源和模型不可见的元数据,但不接受附加上下文,因为上下文属于收件箱消息。普通消息被认领后,其附带的上下文会作为 `agent/prompt-submit` 的默认附加上下文公开;提示词被阻止时,系统既不写入用户消息,也不写入上下文。waterfall(瀑布式事件)返回的 allow 结果具有最终权威性,因此监听器包装 `next()` 时会保留下游内容与上下文,除非它有意替换这些值。消息被接纳后,未指定放置方式或指定为 `separate` 时会写入独立的 `context/message`;指定为 `prompt-prefix` 时则会把上下文与最终生效的请求合并写入同一个提示词事件。排空 steering 消息时会绕过 `agent/prompt-submit`,但采用相同的放置方式分流。延迟到达的 steering 转换为排队输入时保留同一条记录;取消、dispose(资源释放)和到达终止态后的丢弃则会同时丢弃消息与上下文。`agent/queued` 会报告已冻结的上下文,使观察事件能够描述完整的保留项。
|
||||
|
||||
这保留了宿主的驱动语义:TUI 在准备完成后根据 agent 状态决定调用 `send()` 还是 `steer()`,因此只有它的排队路径才会分派 UserPromptSubmit 钩子;ACP 则继续调用 `send()`,每个 `session/prompt` 调用一次。引用准备过程不是新的 steering 协议,本身也不会创建轮次。
|
||||
这保留了宿主的驱动语义:TUI 在准备完成后根据 agent 状态决定调用 `send()` 还是 `steer()`,因此只有它的排队路径才会分派 UserPromptSubmit 钩子。引用准备过程不是新的 steering 协议,本身也不会创建轮次。
|
||||
|
||||
## 宿主适配器
|
||||
|
||||
TUI 把会话候选与现有 `@` 文件提供方组合在一起。每个候选项显示最新折叠后的会话标题,没有标题时回退到 session id。候选查询遵循编辑器的取消信号;session id、cwd 和提及标签中的外部终端控制字符会被转义,但规范 URI 仍保留原始 id。TUI 只准备包含结构化提及标记的提交;等待快照时禁用重复提交;失败时恢复输入;它把提示词封套的显示内容渲染为用户消息,并把其中的会话引用元数据渲染为精简的来源列表,不在终端中暴露完整 JSON。
|
||||
|
||||
ACP 先从普通提示词扁平化结果中检测直接斜杠命令,再提取 `dsh-session:` 资源链接和规范的行内提及标记,因此形如 URI 的命令参数保持不透明,同时保留普通资源链接的渲染方式。标准 `session/list` 会公开每个可加载会话折叠后的标题;挂载会话引用功能时,还会在 `_meta["deepseek-harness/sessionReference"]` 下公开规范 URI。客户端可以使用 `title ?? sessionId` 作为资源链接名称。若引用有效但可选服务未挂载,系统会返回「功能不可用」RPC 错误;准备失败会发生在占用进行中轮次槽位并调用 agent send 之前。引用准备过程单独拥有中止控制权,因此 `session/cancel` 和桥接释放都能停止待处理的读取。选择器 UI 仍由 ACP 客户端负责,因为 ACP 未定义跨会话提及菜单。
|
||||
[仅面向自动化的 ACP(Agent Client Protocol)传输层](../simplification/2026-07-23-acp-automation-only-protocol.md)有意不挂载会话查询或会话引用服务。
|
||||
|
||||
## 预算与保留策略
|
||||
|
||||
@@ -43,8 +43,8 @@ ACP 先从普通提示词扁平化结果中检测直接斜杠命令,再提取
|
||||
## 考虑过的替代方案
|
||||
|
||||
- **等待 SQLite FTS5**:不予采纳,因为快照正确性依赖按准确 id 读取和规范表层折叠,而不是内容搜索。FTS 只改进候选发现。
|
||||
- **把提及标记语法放入 `Agent.send()`**:不予采纳,因为这会迫使核心协议解析 TUI/ACP 的表现层,并阻止带类型的非文本宿主复用同一语义层。
|
||||
- **在 TUI 和 ACP 中分别实现引用**:不予采纳,因为投影、安全警告、保留策略和持久化会在不同宿主之间逐渐偏离。
|
||||
- **把提及标记语法放入 `Agent.send()`**:不予采纳,因为这会迫使核心协议解析某个宿主的展示语法,并阻止带类型的非文本宿主复用同一语义层。
|
||||
- **在每个宿主中分别实现引用**:不予采纳,因为投影、安全警告、保留策略和持久化会在不同宿主之间逐渐偏离。
|
||||
- **在提示词旁放置单独的用户角色上下文消息**:不予采纳,因为相邻的两条用户消息会削弱提示词的指示语绑定:在 `@foo what does this session discuss?` 中,模型可能把「this session」解析为当前对话,而不是被引用的快照。
|
||||
- **在调用 `send()` 前由宿主合并前缀**:不予采纳,因为 `agent/prompt-submit` 必须只检查和改写直接提示词。最终生效的提示词与附加上下文只有在 AgentLoop 接纳后才汇合;此时 AgentLoop 可以把 `allow.content` 改写一致应用于合并后的模型内容和 `envelope.displayContent`。若由宿主更早组装,就会向该钩子暴露快照字节,或使这两个视图发生偏离。
|
||||
- **回放原始源日志或恢复被遮蔽的事件**:不予采纳,因为压缩定义了当前模型表层,并且可能有意淘汰敏感或开销高昂的历史内容。
|
||||
@@ -53,8 +53,8 @@ ACP 先从普通提示词扁平化结果中检测直接斜杠命令,再提取
|
||||
|
||||
## 验证
|
||||
|
||||
单元与集成测试覆盖 URI 无损往返与文本边界标点、显式格式错误的引用、会考虑标题的候选排序、终端控制字符转义、投影排除规则、提示词封套的非递归投影、与后端无关的压缩检查点、标签安全封套、去重、自引用、数量限制、读取的全有或全无、存储读取不结束时取消提示词、逐源独立字节保留、冻结的消息所有权、提示词阻止、send/steer 放置方式、标题隔离、功能缺失、包含标题信息的 ACP 会话列表、普通 ACP 资源链接、不透明的 ACP 命令参数,以及精简的 TUI/ACP 回放。无密钥 TUI 快照会运行真实的 agent loop(智能体循环):源表层用一个压缩检查点替换旧的用户/assistant 历史,目标会话提交一个提及标记,捕获到的模型请求只包含一条用户消息,其中依次为快照、请求分隔符和当前提示词,并且不包含任一被遮蔽的字符串。
|
||||
单元与集成测试覆盖 URI 无损往返与文本边界标点、显式格式错误的引用、会考虑标题的候选排序、终端控制字符转义、投影排除规则、提示词封套的非递归投影、与后端无关的压缩检查点、标签安全封套、去重、自引用、数量限制、读取的全有或全无、存储读取不结束时取消提示词、逐源独立字节保留、冻结的消息所有权、提示词阻止、send/steer 放置方式、标题隔离、功能缺失和精简的 TUI 回放。无密钥 TUI 快照会运行真实的 agent loop(智能体循环):源表层用一个压缩检查点替换旧的用户/assistant 历史,目标会话提交一个提及标记,捕获到的模型请求只包含一条用户消息,其中依次为快照、请求分隔符和当前提示词,并且不包含任一被遮蔽的字符串。
|
||||
|
||||
## 后果
|
||||
|
||||
新插件构成稳定的语义边界,不会新增持久化 schema、事件类型、FTS 依赖、源会话订阅或对压缩所遮蔽内容的访问。标准 TUI/ACP 演示组合包会显式挂载它,并在各自的配置中暴露引用数量和逐源字节上限;自定义宿主在挂载该服务并适配输入前保持不变。引用上下文会在配置的界限内增大目标历史,随后可由目标会话的普通压缩进行摘要;完成压缩后,源会话便不再相关。
|
||||
新插件构成稳定的语义边界,不会新增持久化 schema、事件类型、FTS 依赖、源会话订阅或对压缩所遮蔽内容的访问。标准 TUI 演示组合包会显式挂载它,并在自身配置中暴露引用数量和逐源字节上限;自定义宿主在挂载该服务并适配输入前保持不变。引用上下文会在配置的界限内增大目标历史,随后可由目标会话的普通压缩进行摘要;完成压缩后,源会话便不再相关。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-21-log-backed-session-titles.md: 183aa6909fecffdaf18c77c2a66fbc38c67c2d2c
|
||||
2026-07-21-log-backed-session-titles.zh.md: c6a0c2ce2ad4b2cddec2ada36f655fe55adb143b
|
||||
2026-07-21-log-backed-session-titles.md: 4cf238a278a4eec7b894a0fcfb0f2ac464325bb0
|
||||
2026-07-21-log-backed-session-titles.zh.md: 933cc14eb245581c128cb18055df726174f953ec
|
||||
|
||||
@@ -40,7 +40,7 @@ Automatic provider failures are nonfatal warnings and retain the latest title. E
|
||||
|
||||
A fork inherits seed title events unchanged, like the rest of its source log. The first-message provider does not automatically retitle a fork. The all-messages provider may append a child-owned revision after a later child prompt, using inherited and new eligible messages.
|
||||
|
||||
`ctx.sessionQuery.readTitle()` folds one live-preferred or persisted log without loading titles during `listSessions()`. ACP maps the event to `session_info_update` during both live streaming and load replay, using the event timestamp for `updatedAt`. The TUI uses the latest title as its header subtitle and sets the terminal window title to `<session title> — <configured product title>` after terminal-safe rendering. The Web host folds the same log state into a validated mux control frame after each attached-session subscription baseline and immediately after forwarding a live raw title event. The browser retains only newer title event seqs even when the control frame precedes list or session-instance creation; sidebar labels, search, breadcrumbs, and the browser title then react to the projected revision. `session.list` remains metadata-only, so a cold persisted session uses the cwd basename or id until opening or resuming it attaches the log. The browser title uses `<session title> — <existing HTML title>` only for a selected titled session and otherwise preserves the product title. A synthetic title turn remains a completed durability boundary for the metadata write; consumers reporting agent completion use the core `findLastMessageTurnEnd()` fold so a later title, injection, or other plugin-owned turn cannot replace the preceding message-triggered outcome.
|
||||
`ctx.sessionQuery.readTitle()` folds one live-preferred or persisted log without loading titles during `listSessions()`. The TUI uses the latest title as its header subtitle and sets the terminal window title to `<session title> — <configured product title>` after terminal-safe rendering. The Web host folds the same log state into a validated mux control frame after each attached-session subscription baseline and immediately after forwarding a live raw title event. The browser retains only newer title event seqs even when the control frame precedes list or session-instance creation; sidebar labels, search, breadcrumbs, and the browser title then react to the projected revision. `session.list` remains metadata-only, so a cold persisted session uses the cwd basename or id until opening or resuming it attaches the log. The browser title uses `<session title> — <existing HTML title>` only for a selected titled session and otherwise preserves the product title. A synthetic title turn remains a completed durability boundary for the metadata write; consumers reporting agent completion use the core `findLastMessageTurnEnd()` fold so a later title, injection, or other plugin-owned turn cannot replace the preceding message-triggered outcome.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -54,7 +54,7 @@ A fork inherits seed title events unchanged, like the rest of its source log. Th
|
||||
|
||||
## Consequences
|
||||
|
||||
- Titles survive JSONL and SQLite persistence, replay through ACP, and follow fork inheritance without a separate mutable record.
|
||||
- Titles survive JSONL and SQLite persistence, replay, and fork inheritance without a separate mutable record.
|
||||
- Web title delivery stays incremental and log-backed without a title index or persisted-list scan; cold list rows improve after attach.
|
||||
- A fallback appears immediately. Each fresh Web session adds one first-message auxiliary call; other compositions choose whether better titles justify model cost and whether later prompts should retitle a session.
|
||||
- Auxiliary request records and late accepted titles consume event seqs and may create balanced zero-step turns, so persistence exposes both attempted dispatches and accepted updates even though model history and KV-cache identity do not change.
|
||||
|
||||
@@ -40,7 +40,7 @@ Status: implemented
|
||||
|
||||
与源日志的其他部分相同,fork 会原样继承作为种子的标题事件。首消息提供方不会自动为 fork 重新生成标题。全部消息提供方可以在子会话出现后续提示词后追加一项归子会话所有的修订,并使用继承的合格消息和新增的合格消息。
|
||||
|
||||
`ctx.sessionQuery.readTitle()` 会折叠一份实时优先或已持久化的日志,而不会在 `listSessions()` 期间加载标题。ACP(Agent Client Protocol)会在实时流式输出和加载回放期间把该事件映射到 `session_info_update`,并使用事件时间戳作为 `updatedAt`。TUI 使用最新标题作为其标题栏副标题,并在完成终端安全渲染后,将终端窗口标题设置为 `<session title> — <configured product title>`。Web host 会在每个已附加会话的订阅基线之后,以及转发实时原始标题事件后立即,将同一份日志状态折叠为经过校验的 mux 控制帧。即使控制帧先于列表或会话实例创建抵达,浏览器也只保留标题事件 seq 较新的版本;侧边栏标签、搜索、面包屑和浏览器标题会随投影后的修订更新。`session.list` 仍只包含元数据,因此尚未打开的持久化会话会继续以 cwd 基名或 id 作为回退,直至打开或恢复会话时附加其日志。浏览器仅在选中已有标题的会话时将标题设置为 `<session title> — <existing HTML title>`,否则保留产品标题。合成标题轮次本身仍会完成,并作为元数据写入的持久性边界;报告 agent 完成情况的消费方使用核心的 `findLastMessageTurnEnd()` 折叠逻辑,因此后续的标题轮次、注入轮次或其他归插件所有的轮次无法取代此前由消息触发的结果。
|
||||
`ctx.sessionQuery.readTitle()` 会折叠一份实时优先或已持久化的日志,而不会在 `listSessions()` 期间加载标题。TUI 使用最新标题作为其标题栏副标题,并在完成终端安全渲染后,将终端窗口标题设置为 `<session title> — <configured product title>`。Web host 会在每个已附加会话的订阅基线之后,以及转发实时原始标题事件后立即,将同一份日志状态折叠为经过校验的 mux 控制帧。即使控制帧先于列表或会话实例创建抵达,浏览器也只保留标题事件 seq 较新的版本;侧边栏标签、搜索、面包屑和浏览器标题会随投影后的修订更新。`session.list` 仍只包含元数据,因此尚未打开的持久化会话会继续以 cwd 基名或 id 作为回退,直至打开或恢复会话时附加其日志。浏览器仅在选中已有标题的会话时将标题设置为 `<session title> — <existing HTML title>`,否则保留产品标题。合成标题轮次本身仍会完成,并作为元数据写入的持久性边界;报告 agent 完成情况的消费方使用核心的 `findLastMessageTurnEnd()` 折叠逻辑,因此后续的标题轮次、注入轮次或其他归插件所有的轮次无法取代此前由消息触发的结果。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -54,7 +54,7 @@ Status: implemented
|
||||
|
||||
## 后果
|
||||
|
||||
- 标题可以在 JSONL 和 SQLite 持久化中存续,通过 ACP 回放,并遵循 fork 继承语义,而无需单独的可变记录。
|
||||
- 标题可以在 JSONL 和 SQLite 持久化中存续、重放并遵循 fork 继承语义,而无需单独的可变记录。
|
||||
- Web 标题仍以增量方式从日志交付,无需标题索引或扫描持久化列表;冷启动列表项会在会话附加后改用标题。
|
||||
- 回退标题会立即出现。每个新建的 Web 会话都会增加一次针对首消息的辅助调用;其他组合可以自行决定更优标题是否值得模型成本,以及后续提示词是否需要重新生成会话标题。
|
||||
- 辅助请求记录和延迟接受的标题会占用事件 seq,并可能创建平衡的零步骤轮次,因此持久化会同时呈现尝试发起的调用与已接受的更新,尽管模型历史和 KV 缓存标识保持不变。
|
||||
|
||||
Reference in New Issue
Block a user