Merge remote-tracking branch 'origin/codex/package-readme-limitations-audit-20260712' into codex/model-experience-readmes-20260712

# Conflicts:
#	packages/core/tools/README.md
This commit is contained in:
Tianyi Cui
2026-07-13 13:48:43 +08:00
42 changed files with 170 additions and 502 deletions

View File

@@ -93,7 +93,7 @@ forever:
checkpoint persistence and notify idle/running status
```
Prompt assembly is single-path: the loop sends `renderPrompt(await assemble(assembleContextFor(agent)))`; the helper couples the explicit agent and scope. Plugins contribute ordered sections, tool schemas, and named variables interpolated as `{{name}}` at render — strictly, so an unknown or valueless reference fails the turn instead of shipping a hole. `dsh-system-prompt` owns the openers — the static `harness:identity` section (order 100) and the deployment's persona (order 0, its `persona` config, shared context-wide) — while the loop registers the `model`/`cwd` variables; prompt-fact ownership is pinned by the [prompt-variables RFC](rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md).
Prompt assembly is single-path: the loop sends `renderPrompt(await assemble(assembleContextFor(agent)))`; the helper couples the explicit agent and scope. Plugins contribute ordered sections, tool schemas, and named variables interpolated as `{{name}}` at render — strictly, so an unknown or valueless reference fails the turn instead of shipping a hole. `dsh-system-prompt` owns the openers — the static `harness:identity` section (order 100) and the deployment's global default persona (order 0, shadowable by a same-named agent-scoped section) — while the loop registers the `model`/`cwd` variables; prompt-fact ownership is pinned by the [prompt-variables RFC](rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md).
Post-tool context lands after all tool results so tool-call/result adjacency stays stable. Steering drains between steps; ordinary leftover steering after a turn is re-queued as input. A terminal `agent/turn-stop` is the explicit exception: it runs after ordinary continuation and steering folding, then remains authoritative through turn close and flush so steering from those later listeners is discarded rather than becoming another step or turn; ordinary queued prompts are preserved.

View File

@@ -777,7 +777,7 @@ export interface Config {
}
```
Source: [`packages/core/system-prompt/src/index.ts:257`](../packages/core/system-prompt/src/index.ts)
Source: [`packages/core/system-prompt/src/index.ts:225`](../packages/core/system-prompt/src/index.ts)
## `@deepseek-ai/dsh-tool-cordis`
@@ -958,7 +958,7 @@ export interface Config {
export type ToolPresentationMode = 'native' | 'code' | 'both'
```
Source: [`packages/core/tools/src/index.ts:407`](../packages/core/tools/src/index.ts)
Source: [`packages/core/tools/src/index.ts:401`](../packages/core/tools/src/index.ts)
## `@deepseek-ai/dsh-user-approval`

View File

@@ -89,19 +89,21 @@ Three complete examples load their plugin trees from `cordis.yml`: [`examples/ec
Every product feature maps to a listener on a documented extension seam — the microkernel claim made checkable ([microkernel RFC](../rfc/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md)). No row modifies the loop.
`system-prompt/assemble` is an expert cooperative whole-assembly transform: its returned assembly is authoritative, so listener authors own preserving active Code Mode and structured-output protocol contributions. Prefer `ctx.tools.restrict()` for tool filtering that must stay aligned across presentation, lookup, and execution.
| Product feature | Plugin mechanism |
|---|---|
| Hook system (user + project level) | listeners on `agent/session-start`, `agent/prompt-submit`, `agent/request`, `agent/step-result`, `tools/pre-execute`, `tools/post-execute`, `agent/turn-continuation` — each interception waterfall returns a typed Decision; the `dsh-hooks-claude` / `dsh-hooks-codex` bridges map hook config files onto these seams |
| `/goal` | force-continue via `agent/turn-continuation` + `steer()` reminders |
| `/loop` | on the `turn/end` session event, `send()` the next iteration; or force-continue |
| Dynamic workflow | `ctx.workflows` + the worker-thread engine + the `workflow` tool; structured in-process children enforce output with scoped owner-final prompt/tool contributions, a monotonic tool guard, final `tools/result` commit (including enclosing `run_code`), and terminal `agent/turn-stop` |
| Dynamic workflow | `ctx.workflows` + the worker-thread engine + the `workflow` tool; structured in-process children enforce output with scoped prompt/tool registrations, a monotonic tool guard, final `tools/result` commit (including enclosing `run_code`), and terminal `agent/turn-stop` |
| Queued + steering messages | core `Agent.send()` / `Agent.steer()` |
| Context compaction (auto + manual) | the `ctx.compact` seam + a backend (`dsh-compact-basic`) on the serial `agent/pre-step` seam; auto = token-pressure check before each step; a manual trigger invokes the same `ctx.compact` routine ([compaction RFC](../rfc/implemented/feature/2026-06-18-compaction-capability-seam.md) — the model-facing `/compact` consumer tool is deferred) |
| System prompt configurability | `ctx.systemPrompt.section()` with ordering; a protocol owner sets `ownerFinal: true` on the section or tool only when canonical presence is a correctness invariant |
| System prompt configurability | `ctx.systemPrompt.section()` with ordering and scope-local shadowing |
| AGENTS.md (root) | a section provider reading the file |
| AGENTS.md (subdir, on-touch) + file-change notices | `agent.inject()` from a watcher / tool-result listener |
| Built-in tools | `ctx.tools.register()`; schemas flow into the assembly automatically — the `dsh-tool-*` families (bash, fs, web, subagent, todo) are the shipped examples |
| ToolSearch / progressive disclosure | filter ordinary capabilities at `system-prompt/assemble` (the loop logs the result as the request header); owner-final transport and correctness contributions retain their canonical presence or absence |
| ToolSearch / progressive disclosure | replace a scoped `ctx.tools.restrict()` registration as the visible set changes; the registry keeps presentation, lookup, and execution aligned |
| Tool deadline / retry / metrics | wrap core dispatch with `tools/execute`; a wrapper may replace `exec.signal`, delegate, and inspect the normalized result in one lexical lifetime |
| Final tool-result metrics / audit / capture | observe immutable authoritative outcomes with `tools/result`; use `tools/post-execute` instead only when the plugin must transform the result or attach context |
| Monotonic terminal turn policy | return `{ action: 'stop' }` from serial `agent/turn-stop`, after continuation and steering have already been folded |

View File

@@ -355,11 +355,15 @@ Source: [`packages/subagent/subagent/src/index.ts:82`](../../packages/subagent/s
Waterfall around prompt assembly — mutate or extend the PromptAssembly (sections + tools + variables) before it is rendered. Bound to the SystemPrompt service; call `next()` to delegate.
Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): the carrier is keyed by `context.scope` — a listener registered through `agent.ctx` fires only for that agent's assemblies; a plain plugin listener fires for every assembly (scope-less ones included, dispatched subject-less).
The returned assembly is authoritative. This is an expert composition seam: a listener that removes or replaces another plugin's protocol contribution owns preserving that protocol's invariants.
```ts cordis-catalog
'system-prompt/assemble'(this: Scoped<SystemPrompt>, assembly: PromptAssembly, context: AssembleContext, next: () => Promise<PromptAssembly>): Promise<PromptAssembly>
```
Source: [`packages/core/system-prompt/src/index.ts:45`](../../packages/core/system-prompt/src/index.ts)
Source: [`packages/core/system-prompt/src/index.ts:49`](../../packages/core/system-prompt/src/index.ts)
### `system-prompt/change` — emit
@@ -369,7 +373,7 @@ A section, tool provider, or variable provider was registered or unregistered (t
'system-prompt/change'(): void
```
Source: [`packages/core/system-prompt/src/index.ts:55`](../../packages/core/system-prompt/src/index.ts)
Source: [`packages/core/system-prompt/src/index.ts:59`](../../packages/core/system-prompt/src/index.ts)
## `tools/*`

View File

@@ -218,7 +218,7 @@ list(): Session[]
fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session
```
Source: [`packages/core/session/src/index.ts:591`](../../packages/core/session/src/index.ts)
Source: [`packages/core/session/src/index.ts:590`](../../packages/core/session/src/index.ts)
## `ctx.skills` — `SkillService`
@@ -248,7 +248,7 @@ Source: [`packages/subagent/subagent/src/index.ts:123`](../../packages/subagent/
## `ctx.systemPrompt` — `SystemPrompt`
Registry service (`ctx.systemPrompt`): plugins contribute ordered text sections, tool-schema providers, named prompt variables, and owner-final contributions; the agent loop calls `assemble(context)` once per step. Registers the harness-owned `harness:identity` and `deployment:persona` sections itself (see Config.persona).
Registry service (`ctx.systemPrompt`): plugins contribute ordered text sections, tool-schema providers, and named prompt variables; the agent loop calls `assemble(context)` once per step. Registers the harness-owned `harness:identity` and `deployment:persona` sections itself (see Config.persona).
```ts cordis-catalog
section(section: PromptSection): () => void
@@ -257,13 +257,13 @@ variable(name: string, provider: (context: AssembleContext) => string | undefine
async assemble(context: AssembleContext = {}): Promise<PromptAssembly>
```
Source: [`packages/core/system-prompt/src/index.ts:372`](../../packages/core/system-prompt/src/index.ts)
Source: [`packages/core/system-prompt/src/index.ts:340`](../../packages/core/system-prompt/src/index.ts)
## `ctx.tools` — `ToolRegistry`
Tool registry (`ctx.tools`): tool plugins register definitions; the agent loop executes calls through the `tools/pre-execute` → guards → `tools/execute` → `tools/post-execute` → `tools/result` pipeline. The registry contributes its schemas into the system-prompt assembly — WHICH schemas is governed by its `mode` config (see Config.mode); under a non-native mode it also owns the reserved `run_code` presentation transport and the `tools:sdk` prompt section.
Two registration layers (`@deepseek-ai/dsh-scope`): a registration through a plain plugin context is GLOBAL (visible to every agent); one through a scoped context (`agent.ctx`) is filed in that scope's layer — visible to that agent alone, disposed with the scope, and SHADOWING a global tool of the same name for that agent (most-specific-wins; within one layer a duplicate name still throws). restrict masks the global layer per scope. One private visibility resolver feeds prompt assembly, get, and execute — and, under a non-native mode, the SDK section and `run_code`'s bindings — so what the model is shown, what a presenter renders, what a program can call, and what dispatches can never disagree.
Two registration layers (`@deepseek-ai/dsh-scope`): a registration through a plain plugin context is GLOBAL (visible to every agent); one through a scoped context (`agent.ctx`) is filed in that scope's layer — visible to that agent alone, disposed with the scope, and SHADOWING a global tool of the same name for that agent (most-specific-wins; within one layer a duplicate name still throws). restrict masks the global layer per scope. One private visibility resolver feeds the registry's prompt contribution, get, and execute — and, under a non-native mode, the SDK section and `run_code`'s bindings — so those registry-owned presentation and dispatch paths agree. An expert `system-prompt/assemble` listener may deliberately replace the final wire composition and owns any resulting divergence.
```ts cordis-catalog
register(definition: ToolDefinition): () => void
@@ -276,7 +276,7 @@ async execute(exec: ToolExecutionInput): Promise<ToolExecutionResult>
Types: [ToolDefinition](../core-data-structures/tools.md) · [ToolExecutionInput](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md)
Source: [`packages/core/tools/src/index.ts:500`](../../packages/core/tools/src/index.ts)
Source: [`packages/core/tools/src/index.ts:493`](../../packages/core/tools/src/index.ts)
## `ctx.userInteraction` — `UserInteractionService`

View File

@@ -19,7 +19,7 @@ Everything else is documented on a **sub-page**, not here. The rule that draws t
| [scope.md](scope.md) | scoped registration identity, dispatch carriers, and the owned `Scope` context |
| [session.md](session.md) | the full `SessionEventMap` variant catalog, `TurnTrigger`/`TurnEndReason`, `deriveMessages()`, the turn-enclosure invariant |
| [persistence.md](persistence.md) | the durability seam: `SessionPersistence`, JSONL + SQLite backends, `session/flush`, crash recovery, `SessionHeader` |
| [system-prompt.md](system-prompt.md) | per-assembly context, tool-provider results, and canonical contribution protection |
| [system-prompt.md](system-prompt.md) | per-assembly context, tool-provider results, prompt sections, and cooperative assembly |
| [tools.md](tools.md) | `ToolDefinition` full fields, the schema DSL, `ToolExecution`/`ToolResult`, tool-presentation UI types, and the guarded execution pipeline |
| [user-interaction.md](user-interaction.md) | the UI-backed human question/answer seam: `AskUserQuestionRequest`, answer/options vocabulary, provider API, error taxonomy |
| [approval.md](approval.md) | the one-shot user-approval seam: `ApprovalRequest`, `ApprovalOutcome`, per-session policy, audit and answerer contracts |
@@ -201,7 +201,7 @@ The model-facing `ToolSchema` is the wire shape; the registered `ToolDefinition`
### The request envelope: `LlmCallConfig` and the logged header
Requests are built by the loop, not shaped per call: the non-history half of a request — the `EpochHeader`: this call configuration plus the rendered system prompt, the tool schemas in the assembly's canonical order (dsh-system-prompt's `toolOrder` config, lexicographic when unset), and the session prefix — is logged session state (`request/header` snapshot and delta events, [session.md](session.md#the-request-header-events-requestheader-and-requestheader-delta)), so every conversation request is a pure function of the session log ([reconstructability RFC](../rfc/implemented/architecture/2026-07-05-reconstructable-requests.md)). The `agent/request` waterfall receives a frozen `LlmCallConfig` seed and a listener returns a replacement to switch model or sampling; the `agent/session-prefix` waterfall — fired once per loop instance — composes the request-only messages fronting the derived history (recorded as the header's `messagePrefix`) — the loop logs whatever the request actually uses. Loop-built requests arrive at `llm/stream` deep-frozen; mutation throws.
Requests are built by the loop, not shaped per call: the non-history half of a request — the `EpochHeader`: this call configuration plus the rendered system prompt, the tool schemas in the authoritative returned assembly order (initially canonicalized by dsh-system-prompt's `toolOrder` config, or lexicographically when unset), and the session prefix — is logged session state (`request/header` snapshot and delta events, [session.md](session.md#the-request-header-events-requestheader-and-requestheader-delta)), so every conversation request is a pure function of the session log ([reconstructability RFC](../rfc/implemented/architecture/2026-07-05-reconstructable-requests.md)). The `agent/request` waterfall receives a frozen `LlmCallConfig` seed and a listener returns a replacement to switch model or sampling; the `agent/session-prefix` waterfall — fired once per loop instance — composes the request-only messages fronting the derived history (recorded as the header's `messagePrefix`) — the loop logs whatever the request actually uses. Loop-built requests arrive at `llm/stream` deep-frozen; mutation throws.
On the wire, a loop-built request reads in this order: the `system` slot (the rendered prompt assembly) → `messagePrefix` (the frozen session prefix) → the derived history — the boundary snapshot, whose tail is the newest `user/message` on a turn's first step and the previous step's tool results on later steps. The prefix never enters the derived history; its durable record is the header events, and the dev invariant recomputes exactly this equation against every loop-built request.
@@ -353,7 +353,7 @@ interface Agent {
}
```
`AgentStatus` is `'idle' | 'running' | 'disposed'`. `AgentId` is a branded string. `AgentOptions` (`model?`) is merge-extensible — plugins add creation options by declaration merging; the persona is NOT an agent option but the `dsh-system-prompt` plugin's `persona` config, shared context-wide. The `agent/*` event taxonomy (lifecycle emits incl. `agent/session-start`, serial `agent/pre-step`/`agent/turn-stop` checkpoints, and the `agent/prompt-submit`/`agent/request`/`agent/session-prefix`/`agent/step-result`/`agent/turn-continuation` waterfalls) is in [architecture.md § Event taxonomy](../architecture.md#event-taxonomy); turn/step boundaries are durable `session/event` records, not `agent/*` emits.
`AgentStatus` is `'idle' | 'running' | 'disposed'`. `AgentId` is a branded string. `AgentOptions` (`model?`) is merge-extensible — plugins add creation options by declaration merging. Persona is not an agent option: the `dsh-system-prompt` config supplies the global default, and an agent-scoped `deployment:persona` section may shadow it. The `agent/*` event taxonomy (lifecycle emits incl. `agent/session-start`, serial `agent/pre-step`/`agent/turn-stop` checkpoints, and the `agent/prompt-submit`/`agent/request`/`agent/session-prefix`/`agent/step-result`/`agent/turn-continuation` waterfalls) is in [architecture.md § Event taxonomy](../architecture.md#event-taxonomy); turn/step boundaries are durable `session/event` records, not `agent/*` emits.
## Interception decisions

View File

@@ -16,25 +16,23 @@ interface AssembleContext {
## Tool-provider result
`ToolProviderResult.schemas` is the model-visible set for the current assembly. `knownNames` is the provider's pre-restriction name universe used to distinguish a configured-name typo from a known tool that is deliberately hidden in this scope. `ownerFinalNames` identifies tool contributions whose canonical presence or absence survives the assembly waterfall.
`ToolProviderResult.schemas` is the model-visible set for the current assembly. `knownNames` is the provider's pre-restriction name universe used to distinguish a configured-name typo from a known tool that is deliberately hidden in this scope.
```ts type-equiv
interface ToolProviderResult {
readonly schemas: readonly ToolSchema[]
readonly knownNames?: readonly string[]
readonly ownerFinalNames?: readonly string[]
}
```
## Prompt sections and owner finality
## Prompt sections
`PromptSection` is a readonly same-process registration contract. `ownerFinal` is reserved for protocol-owned instructions whose canonical presence and definition must survive the complete assembly waterfall; ordinary sections remain transformable. Tool definitions declare the equivalent fact on their own contribution, and the tool provider reports the resolved names through `ownerFinalNames` above.
`PromptSection` is a readonly same-process registration contract. Its text may be static or resolved from the current assembly context.
```ts type-equiv
interface PromptSection {
readonly name: string
readonly order: number
readonly text: string | ((context: AssembleContext) => string)
readonly ownerFinal?: boolean
}
```

View File

@@ -19,12 +19,6 @@ interface ToolDefinition extends ToolSchema {
* cooperative implementation that can reach quiescence when the signal aborts.
*/
timeoutMs?: number
/**
* Whether this tool name's canonical wire presence or absence survives the
* complete system-prompt assembly waterfall. Reserved for protocol tools
* whose owner must retain the final definition.
*/
readonly ownerFinal?: boolean
/**
* Optional: how to present the PENDING state of one call in a UI, derived from
* the call's `args` (parsed arguments, `unknown` — the tool validates/narrows

View File

@@ -35,8 +35,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:66`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:72`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:82`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude) |
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:45`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | - |
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:55`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:49`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | - |
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:59`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:173`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`emit`) | - |
| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:128`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`timeout-policy`](../packages/timeout/timeout-policy) |
| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:148`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) |

View File

@@ -23,7 +23,7 @@ Key choices recorded here because they are durable, contested, and surprising:
- **Append-only; a crashed turn is closed, never truncated.** Committed events — those at or below a flushed `turn/end` — are never rewritten. The loop only flushes at `turn/end`, so a crash can leave a durable log whose final turn never closed: real, fully-written events sit after the last `turn/end`. **A single turn can be huge in a long-horizon task** (many steps, large tool output spanning a long autonomous run), so discarding the interrupted turn would silently destroy a large amount of real work — truncating a turn is wrong. Instead, on reload `load` PRESERVES those events and CLOSES the orphaned turn by durably appending the minimal synthetic boundary events: an error `tool/result` for every `tool-call` the crash left unanswered, then a `step/end` if a step was still open, then a `turn/end` carrying the merge-extensible `{ kind: 'interrupted' }` reason (a marker that records the turn was cut short by a crash, not completed by the model — no loop ever emits it). The synthetic tool results matter for resume correctness: the loop logs the `assistant/message` (carrying the `tool-call` blocks) BEFORE running the tools, so a crash mid-tool leaves calls without results; `deriveMessages()` would then replay a dangling assistant tool-call, which every provider rejects as an invalid transcript on the next request. Answering each orphaned call with an error result keeps the rehydrated history valid. `load` returns the balanced log, so a resumed session is immediately usable. The ONLY thing discarded is a never-fully-written **torn tail fragment** — a final record whose bytes (JSONL) or row were never completely flushed; that fragment is not a valid event and is dropped before the synthetic closers are written. A parse error or `seq` gap in the COMMITTED region (at or before the last real `turn/end`) is genuine corruption and makes the session unloadable.
- **File backend canonical, DB backend a proven drop-in.** `SessionEvent` maps 1:1 onto a row `(session_id, seq, type, time, data)``append` is INSERT (in a transaction asserting the contiguous-seq contract), `load` is SELECT … ORDER BY seq. `dsh-session-persistence-sqlite` is exactly this: a `SessionPersistence` subclass with no interface change (opencode runs this exact shape on SQLite/WAL), and it passes the same `runPersistenceContract` suite as the JSONL backend — so the contract holds both backends to identical semantics (lazy materialization, interrupted-turn close on load, contiguous-seq), expressed once over file bytes and once over rows.
- **Metadata is out-of-log.** Format version, cwd, and lineage are storage concerns, not replayable conversation state, so they live in a `SessionHeader` owned by `dsh-session` and attached to a `Session` via a new readonly `session.header` — never in `SessionEventMap`, never reaching `deriveMessages()`. The alternative (a merge-extensible `session/meta` event as log line 0) was rejected: an in-log event would ride along with a seeded/forked session for free, but metadata is not replayable state, so the explicit out-of-log header seam is the cleaner cost. (The header was originally split into an immutable `SessionHeader` plus a mutable `SessionSummary` whose union was `SessionMeta`; the mutable summary was later removed as dead state — see [Drop the mutable session summary](../simplification/2026-06-19-drop-mutable-session-summary.md).)
- **Resume is an async factory, not a change to synchronous create.** `ctx.agents.resume({ resumeSessionId })` awaits `ctx.sessionPersistence.load`, recreates the live session with the loaded events (so `lastTurnNumber`/`deriveMessages` continue), and starts a fresh agent on the resumed id (NOT `${agentId}-session`). The agent-loop does NOT hard-inject `sessionPersistence` (that would pend non-persistent demos forever); `resume` rejects with a clear error when it is absent.
- **`ctx.agents.create()` and `ctx.agents.resume()` are async factories; resume additionally crosses the persistence boundary.** `ctx.agents.resume({ resumeSessionId })` awaits `ctx.sessionPersistence.load`, recreates the live session with the loaded events (so `lastTurnNumber`/`deriveMessages` continue), and starts a fresh agent on the resumed id (NOT `${agentId}-session`). The agent-loop does NOT hard-inject `sessionPersistence` (that would pend non-persistent demos forever); `resume` rejects with a clear error when it is absent.
## Alternatives considered

View File

@@ -16,11 +16,11 @@ The assembled system prompt had four defects, all of one family: facts the harne
## Decision
**One principle: every fact in the prompt has exactly one owner.** The model name and workspace are config/session facts → the harness exposes them as variables and the persona references them. Per-tool semantics and when-to-use → the tool's `description`. Cross-call habits a description cannot carry → the tool package's prompt section. Identity and behavior → the deployment's persona, and nothing else.
**One principle: every fact in the prompt has exactly one owner.** The model name and workspace are config/session facts → the harness exposes them as variables and the persona references them. Per-tool semantics and when-to-use → the tool's `description`. Cross-call habits a description cannot carry → the tool package's prompt section. Harness provenance → the static `harness:identity` section. Deployment role and behavior → the deployment's persona.
### Assemble context
`SystemPrompt.assemble(context)` takes an `AssembleContext` — declared EMPTY and merge-extensible in `dsh-system-prompt` (the package stays agnostic of who assembles); `dsh-agent` declaration-merges `agent?: Agent` onto it (a new type-level edge `agent → system-prompt`, no cycle — `tools` already depends on both). The loop passes `{ agent }` each step; section text providers become `string | ((context) => string)` (zero-arg providers stay valid), and the `system-prompt/assemble` waterfall gains the context parameter so a listener can filter or extend per agent.
`SystemPrompt.assemble(context)` takes a merge-extensible `AssembleContext`. `dsh-system-prompt` declares the optional `scope` selector used for scoped routing, while `dsh-agent` declaration-merges the optional typed `agent` field onto it (a type-level edge `agent → system-prompt`, with no runtime dependency cycle). The loop calls `assembleContextFor(agent)` each step so both fields identify the same agent; section text providers may read that context, and the `system-prompt/assemble` waterfall receives it so a listener can filter or extend per agent.
### Prompt variables
@@ -30,7 +30,7 @@ Plugins contribute named values via `ctx.systemPrompt.variable(name, provider)`;
### Persona as the order-0 section
`dsh-system-prompt` itself registers the two harness-owned sections (they must survive a swapped loop plugin, so they do NOT live on `dsh-agent-loop`): the static `harness:identity` at order `-100` — every prompt opens by stating the agent is powered by the DeepSeek Harness SDK — and `deployment:persona` at order 0, whose text is the plugin's own `persona` config. The persona is per-DEPLOYMENT, not per-agent: every agent in the context (subagents included) renders the same one, `AgentOptions.systemPrompt` is deleted along with the per-agent forwarding plumbing (the app configs' `systemPrompt` keys become a `persona` key routed to this plugin through `dsh-agent-core`), and the ACP bridge and `dsh-tool-subagent` stop carrying persona configuration entirely. The loop's special-case join is deleted: `fullSystemPrompt ≡ renderPrompt(assembly)`, one ordered pipeline for everything the model sees, and `agent/pre-step` (compaction's token-pressure input) measures exactly the real prompt. Order bands are now convention: harness identity `-100`, persona `0`, tool guidance `100199`; other negative orders also render before the persona.
`dsh-system-prompt` itself registers the two harness-owned sections (they must survive a swapped loop plugin, so they do NOT live on `dsh-agent-loop`): the static `harness:identity` at order `-100` — every prompt opens by stating the agent is powered by the DeepSeek Harness SDK — and the global default `deployment:persona` at order 0, whose text is the plugin's own `persona` config. `AgentOptions.systemPrompt` and the loop's special-case join are gone: `fullSystemPrompt ≡ renderPrompt(assembly)`, one ordered pipeline for everything the model sees, and `agent/pre-step` (compaction's token-pressure input) measures exactly the real prompt. An agent-scoped section with the same `deployment:persona` name shadows the default for that agent; programmatic setup may register one directly, and the subagent persona feature installs one before publishing an in-process child when the selected provider supports it. Order bands are convention: harness identity `-100`, persona `0`, tool guidance `100199`; other negative orders also render before the persona.
### Tool guidance ownership
@@ -56,7 +56,7 @@ Per-tool semantics and when-to-use live in tool DESCRIPTIONS, which already ship
## Shipped invariants
- `renderPrompt(assemble({ agent }))` for the coding-agent example renders the persona FIRST (with the agent's model name interpolated), then the fs/bash/web guidance sections; the loop has no other prompt-composition path.
- `renderPrompt(await assemble(assembleContextFor(agent)))` for the coding-agent example renders the harness identity, then the persona (with the agent's model name interpolated), then the fs/bash/web guidance sections; the loop has no other prompt-composition path.
- The `subagent_fork` schema description says the child inherits the conversation; the `subagent` one says it does not. The tool follows its provider: absent before the backend activates, present after, gone when the backend unloads, re-worded from the fresh provider on reload.
- Unknown/valueless/malformed/unbalanced `{{…}}` references throw with the section name in the message; duplicate section, variable, and tool-name registrations all throw.
- Snapshot goldens are prompt-independent by construction: llm-replay keys replay on (turn, step) chunk streams and never re-verifies the outgoing request.

View File

@@ -12,7 +12,7 @@ The implementation needs enough state to preserve real ownership and settlement
## Decision
The runtime uses one mechanism per independent fact. Scope routing has an opaque carrier; each live registry object has one entry record; each create or resume operation has one transaction; typed same-process calls borrow readonly values; real data boundaries materialize once; and worker/process code retains separate terminal and quiescence state only where different owners can genuinely race.
The runtime uses one mechanism per independent fact. Scope routing has an opaque carrier; each live registry object has one entry record; each create or resume operation has one transaction; typed same-process calls borrow readonly values; real data boundaries materialize once; the cooperative prompt-assembly result is authoritative; and worker/process code retains separate terminal and quiescence state only where different owners can genuinely race.
The design can be skimmed as seven choices:
@@ -23,7 +23,7 @@ The design can be skimmed as seven choices:
| Coordinate create/resume | One `AgentCreationTransaction` |
| Protect durable, queued, model, or wire data | Materialize once at that boundary |
| Pass typed values inside one process | Readonly borrowed contract |
| Preserve an owner's final prompt/tool policy | Contribution-owned finality and one final observer point |
| Compose the model-visible prompt and tool surface | One shared tool view plus the authoritative assembly-waterfall result |
| Coordinate subagent, worker, and process shutdown | One cancellation signal plus the independent terminal/quiescence facts of that boundary |
The rest of this RFC expands those choices in dependency order. It first explains the Cordis mechanics, then scope routing, creation and session commit, tools and prompts, subagents and workflows, and finally the checks that make the reasoning executable.
@@ -198,13 +198,13 @@ Tests that fabricate hostile getters, replace typed callbacks after handoff, or
Callback containment is separate from data ownership. Listeners are arbitrary extension code and can throw even when their arguments are trusted; publication and post-commit paths still contain failures according to their event contract.
## Tools and prompts: one view, one execution identity, explicit finality
## Tools and prompts: one view, authoritative assembly, committed outcomes
Tool presentation and execution share one private resolver, while prompt/tool owners declare the few contributions that cooperative middleware may not alter finally. No second registry mirrors ownership.
Tool presentation and execution share one private resolver. Prompt assembly remains trusted cooperative composition: registries supply the ordered input, and the assembly waterfall's returned value is exactly what the loop logs and sends. Execution uses separate one-way boundaries only where policy or outcome settlement must be monotonic.
### One resolver defines the tool view
The private resolver applies the current presentation mode, live global restrictions, exact local overlay, and local shadowing. Schemas, lookup, execution, Code Mode SDK generation, restriction validation, and owner-final name derivation all use that resolver or its pre-restriction global-name view.
The private resolver applies the current presentation mode, live global restrictions, exact local overlay, and local shadowing. Schemas, lookup, execution, Code Mode SDK generation, and restriction validation all use that resolver or its pre-restriction global-name view.
The [subagent composition-controls RFC](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md#tool-filtering-is-one-live-global-view-rule) owns the user-visible allow/deny semantics. The implementation requirement is agreement: a filtered-away global cannot remain executable through a different lookup path, and a locally shadowed definition is the same definition presented and executed.
@@ -220,21 +220,17 @@ Arguments are materialized once where model/tool JSON enters the pipeline. Pre-,
After the last post-execute listener, the registry materializes and freezes the accepted final result once. Every synchronous `tools/result` observer receives that exact committed object, and observer failures are contained individually. An outer pipeline failure is normalized into a committed error result, so observers can discard staged work against the same authoritative boundary.
### Contribution-owned finality protects only named invariants
### The assembly waterfall owns the final model-visible composition
Most prompt assembly remains a cooperative waterfall: listeners may reorder, replace, or remove ordinary sections and schemas. A contribution sets `ownerFinal: true` only when its owner must retain final control over that named entry.
SystemPrompt first resolves the global-plus-agent sections, variables, and tool providers into a deterministic registry contribution. The scope-filtered `system-prompt/assemble` waterfall may then reorder, replace, add, or remove any section, variable, or schema. Its returned assembly is authoritative; there is no later restoration pass and no finality metadata on ordinary prompt sections, tool definitions, or provider results.
Prompt sections carry owner-finality directly. Tool definitions carry it through the tool provider's `ownerFinalNames`, including canonical absence when a presentation mode intentionally omits a tool. `tools:sdk`, `run_code`, and structured-output instruction/schema contributions use this flag.
This is a trusted same-process extension seam, not an authority boundary. A listener that changes Code Mode's `run_code` schema or `tools:sdk` instructions, or a structured child's capture schema or instruction, owns preserving a coherent protocol in the assembly it returns. ToolRegistry still reserves `run_code` against ordinary tool registration and restriction because those are registry invariants, but assembly middleware remains free to transform the final model-visible surface.
An owner-final name is reserved across the global and scoped layers: a scoped shadow cannot be added beneath a global owner-final contribution, and a global contribution cannot become owner-final while any scoped shadow already exists. This makes the registered owner definition unambiguous before assembly begins.
Assembly takes one private canonical snapshot before the waterfall. After listeners finish, it restores only owner-final names to their canonical presence, absence, definition, and relative anchor among surviving entries. Unrelated listener additions and reordering remain untouched.
Attaching finality to the owning contribution has two benefits. Registration and cleanup cannot drift from a separate protection registry, and the reader can see why a particular prompt/tool entry is special at its definition.
Scope solves the real isolation problem directly. Structured-output contributions register in the child's exact scope, while Code Mode derives its transport and SDK from the same resolved tool view. A second named-protection system would need another ownership and collision rule across arbitrary schema providers—including providers that intentionally contribute duplicate names—without creating a new trust boundary.
### Structured output commits only authoritative outcomes
Structured output uses the final prompt/tool boundaries as a two-phase commit. The child-scoped `structured_output` tool and its instruction are owner-final; the tool body validates a candidate and stages it by the current `ToolExecution`, but successful capture is decided only by immutable `tools/result` observations.
Structured output combines child-scoped composition with a two-phase execution commit. The child registers its `structured_output` tool and instruction before publication; a trusted assembly listener may transform those ordinary contributions and is responsible for preserving the protocol if the child is expected to complete. The tool body validates a candidate and stages it by the current `ToolExecution`, but successful capture is decided only by immutable `tools/result` observations.
For a native call, the observer deletes the stage and commits its value only when that exact execution's final result succeeds. A post-execute block or outer pipeline failure therefore cannot leave a captured value behind.
@@ -242,20 +238,19 @@ For a Code Mode SDK call, the inner successful result records `{ parentToken, va
Once a value is pending or committed, a scoped monotonic guard denies later tool calls. After commit, the ordinary serial `agent/turn-stop` listener returns a stop decision after continuation and steering have already folded. A schema-validation failure remains an ordinary `INVALID_ARGS` tool error and leaves the child able to retry within the same turn.
Pure Code Mode omits `structured_output` from native wire schemas and exposes it through the generated SDK. Contribution-owned finality preserves that canonical absence, preventing an assembly listener from fabricating a second native route while keeping the instruction and SDK declaration intact.
Pure Code Mode's registry contribution omits `structured_output` from native wire schemas and exposes it through the generated SDK. The assembly waterfall may deliberately change that presentation; execution still validates against the child-scoped definition, and the listener owns the consistency of any alternate model-visible route it creates.
### Four final boundaries have four narrow powers
### Three execution boundaries are deliberately one-way
Owner-final behavior is not a general priority system. Four domain owners need four different one-way powers after cooperative extension points:
Prompt assembly is intentionally cooperative, but three execution facts need one-way settlement after their extensible stages:
| Boundary | Final power | Why ordinary listener order is insufficient |
|---|---|---|
| Prompt assembly | Restore named canonical contributions | A later listener can remove or replace an invariant schema or instruction |
| Tool pre-policy | Deny monotonically | A later listener must not re-allow an already denied call |
| Tool result | Observe the immutable committed outcome | Structured output must commit only the result that actually escaped the pipeline |
| Turn continuation | Stop after ordinary continuation folding | A committed terminal output must end the turn |
`ToolGuard` remains the monotonic policy registry. Final tool observation is the contained `tools/result` point described above. Terminal structured output listens on the ordinary serial `agent/turn-stop` fold after normal continuation and steering decisions; no public `strictSerial()` dispatcher is needed for the typed listener contract.
`ToolGuard` is the monotonic policy registry. Committed tool observation is the contained `tools/result` point described above. Terminal structured output listens on the ordinary serial `agent/turn-stop` fold after normal continuation and steering decisions; no public `strictSerial()` dispatcher is needed for the typed listener contract.
### Skill and approval services trust typed callers
@@ -335,7 +330,7 @@ The plugin does not police trusted setup by scanning registries or reject prompt
The event catalog, service catalog, producer/consumer matrix, configuration catalog, module graph, tool catalog, and type-equivalence blocks are generated or freshness-gated from source. `verify-scoped-dispatch` keeps the declared scoped-event set aligned with runtime invariant coverage.
Behavioral tests pin scoped routing and disposal, final-entry collision cleanup, publication rollback, ordered quiescence, durable pre/post-commit behavior, live tool filtering across presentation and execution, owner-final Code Mode and structured output, async subagent startup and signal cancellation, worker terminal arbitration, ACP settlement, and process teardown.
Behavioral tests pin scoped routing and disposal, final-entry collision cleanup, publication rollback, ordered quiescence, durable pre/post-commit behavior, live tool filtering across presentation and execution, cooperative prompt assembly, structured-output commit in native and Code Mode, async subagent startup and signal cancellation, worker terminal arbitration, ACP settlement, and process teardown.
## Alternatives considered
@@ -361,9 +356,9 @@ Parallel sentinels can all mirror whether one operation is live. One transaction
This splits provider acceptance from readiness and forces every consumer to register a partial run, attach result observation, await readiness, and clean up readiness failure. An async start promise makes provider-to-caller ownership transfer the readiness boundary itself.
### Keep a separate prompt-protection registry
### Restore selected prompt or tool contributions after assembly
A protection registration mirrors the names and lifetime already owned by prompt sections and tool definitions. `ownerFinal` keeps the exceptional policy on the contribution and lets assembly derive the canonical set directly.
A post-waterfall restoration pass would create a second composition rule after the documented cooperative seam. Correctly assigning canonical presence or absence would also require provider ownership and collision rules for arbitrary tool-schema providers, whose ordinary output may contain duplicate names. Scoped registration already supplies the required per-agent isolation, and trusted assembly listeners own the protocol consistency of what they return, so named restoration adds machinery without establishing an independent boundary.
### Remove worker/process lifecycle guards with same-process hardening
@@ -379,8 +374,8 @@ The implementation is smaller and its proof follows the same shape as its owners
- Create and resume expose no partially configured handle; final-entry losers and publication failures clean every prepared resource.
- Disposal retains scoped listeners and persistence through driver drain and final session work, then revokes the scope.
- Durable, queued, model, worker, process, and wire values are owned at their real boundary; typed same-process values follow readonly contracts.
- Tool presentation and execution resolve the same live view, and committed results have one immutable observation point.
- Owner-final prompt/tool contributions survive cooperative assembly without freezing unrelated middleware behavior.
- ToolRegistry's presentation, lookup, and execution resolve the same live view before expert assembly transforms, and committed results have one immutable observation point.
- Registry contributions are deterministic inputs, while the trusted assembly waterfall owns the final model-visible composition.
- Subagent start returns only a ready run, required signals cancel pending or live work, and disposal reaches the backend's quiescence contract.
- Worker/process result precedence and cleanup remain correct under death, late messages, and bounded teardown.
@@ -388,6 +383,8 @@ The implementation is smaller and its proof follows the same shape as its owners
Scope-aware services still maintain global and identity-keyed maps, and operations must carry their real agent explicitly. Async create/resume and subagent start require callers to await ownership transfer and dispose returned handles.
A trusted `system-prompt/assemble` listener can remove or replace Code Mode and structured-output protocol pieces. This is deliberate: the listener owns final composition and must preserve any protocol the deployment expects to remain usable.
The design trusts typed plugins in the same process. It does not defend against arbitrary casts, stateful getters, mutation that violates readonly contracts, or a plugin deliberately using ambient service access outside the supported composition API.
The [security and authority non-goal](2026-07-08-agent-scope-contexts.md#security-and-authority-are-non-goals) remains fundamental. These mechanisms prove registration composition, publication, and lifetime ownership; they do not prove confinement or parent-to-child non-escalation.

View File

@@ -16,7 +16,7 @@ Tool presentation belongs to the registry that owns tool visibility: implementin
Three decisions, each elaborated in its own section below:
1. **Code Mode is a first-class presentation mode of `ToolRegistry`** (`dsh-tools`), selected by a validated `mode` config: `'native'` (the default, contributing the visible capability schemas), `'code'` (the registry contributes only its reserved `run_code` transport plus a generated SDK `.d.ts` in the system prompt), or `'both'` (native schemas and the transport + SDK). The registry shapes its wire contribution at the source and protects the transport pair through final assembly, so the logged request header records the same presentation the model receives.
1. **Code Mode is a first-class presentation mode of `ToolRegistry`** (`dsh-tools`), selected by a validated `mode` config: `'native'` (the default, contributing the visible capability schemas), `'code'` (the registry contributes only its reserved `run_code` transport plus a generated SDK `.d.ts` in the system prompt), or `'both'` (native schemas and the transport + SDK). The registry shapes its canonical contribution at the source; the cooperative prompt-assembly result remains authoritative, and the logged request header records exactly that returned presentation.
2. **Code execution is a capability seam**`packages/code-runtime/` contains the interface package `@deepseek-ai/dsh-code-runtime`, which owns `ctx.codeRuntime` ([capability seams](../../implemented/architecture/2026-06-13-capability-seams.md); consumer = `dsh-tools`, with core-consumes-a-seam precedent in `agent-loop``dsh-llm`). The runtime knows nothing about tools: it is handed a program and named async bindings, runs the program, and reports `{ value, logs, error? }`. Language and substrate are backend properties, so a future Python or container backend is another implementation package, not a redesign.
3. **The shipped implementation is `@deepseek-ai/dsh-code-runtime-worker`**: one fresh Node worker thread per run, executing the model's TypeScript after type-strip, with bindings bridged over the message port, an empty environment, configurable heap/output/time caps, and hard termination. Its trust posture is bash-equivalent by design — no unsafe-acknowledgement flags — because the harness already ships `dsh-bash-local`, which executes arbitrary model-written shell commands with strictly *more* ambient authority.
@@ -24,11 +24,13 @@ Three decisions, each elaborated in its own section below:
`ToolRegistry` gains a schemastery-validated config (`static Config`), its first: `mode: 'native' | 'code' | 'both'`, default `'native'`. A deployment flips it from `cordis.yml` (`tools: { mode: code }`) — no code edit, per the no-hardcoded-tunables convention.
**Wire tool list = the registry's contribution.** The registry feeds assembly through a mode-aware provider: `'native'` contributes every capability visible to that assembly scope, `'code'` contributes only `run_code`, and `'both'` contributes both. Because [`PromptAssembly.tools` is the single source the loop's request header snapshots](../../../../packages/core/system-prompt/src/index.ts), the presentation is logged and reconstructable. The reserved transport is not a capability: it lives outside global/scoped registration and restriction layers, cannot be registered or shadowed, and cannot be named by `ctx.tools.restrict()`. Its `ToolDefinition` declares `ownerFinal: true`, so the provider reports the name as final and assembly restores its canonical schema or canonical absence after the waterfall. The mode governs only the registry's contribution; a deployment that deliberately installs another direct `systemPrompt.tools()` provider still owns that provider's schemas.
**Wire tool list = the registry's contribution before cooperative assembly.** The registry feeds assembly through a mode-aware provider: `'native'` contributes every capability visible to that assembly scope, `'code'` contributes only `run_code`, and `'both'` contributes both. Because [`PromptAssembly.tools` is the single source the loop's request header snapshots](../../../../packages/core/system-prompt/src/index.ts), the final presentation is logged and reconstructable. The reserved transport is not a capability: it lives outside global/scoped registration and restriction layers, cannot be registered or shadowed there, and cannot be named by `ctx.tools.restrict()`. The mode governs this provider's input to assembly; other direct `systemPrompt.tools()` providers own their schemas, and the trusted assembly waterfall owns the returned wire list.
**Interaction with `toolOrder`, stated up front:** a configured `systemPrompt.toolOrder` naming native capabilities rejects every assembly under `mode: 'code'`, because those names are outside that mode's wire-validation universe. This is correct behavior, not a bug: a deployment using Code Mode updates its order config or drops it.
**The SDK prompt section.** Under `'code'` and `'both'` the registry registers one lazy prompt section (`tools:sdk`, in the 100199 tool-guidance order band) whose thunk regenerates, for each assembly scope, a TypeScript declaration of every visible end-capability tool plus fixed usage instructions. It uses the same visibility resolver as lookup and execution, so scoped grants and shadows appear while restricted globals disappear; the reserved `run_code` transport itself is excluded. The thunk emits tools in lexicographic name order, so an unchanged visible set produces byte-identical text. The section declares `ownerFinal: true`, which restores its canonical contribution after every assembly listener and reserves the global name against scoped shadows.
**The SDK prompt section.** Under `'code'` and `'both'` the registry registers one lazy prompt section (`tools:sdk`, in the 100199 tool-guidance order band) whose thunk regenerates, for each assembly scope, a TypeScript declaration of every visible end-capability tool plus fixed usage instructions. It uses the same visibility resolver as lookup and execution, so scoped grants and shadows appear while restricted globals disappear; the reserved `run_code` transport itself is excluded. The thunk emits tools in lexicographic name order, so an unchanged visible set produces byte-identical text.
**Assembly ownership.** `run_code` and `tools:sdk` enter the trusted `system-prompt/assemble` waterfall as normal assembly inputs. A scoped `tools:sdk` section may shadow the global default before dispatch, and a listener may remove or replace either contribution. The waterfall's returned assembly is final, so whoever changes these inputs owns preserving a viable Code Mode protocol when the deployment expects Code Mode to remain usable; no restoration pass overrides deliberate composition.
**Codegen.** A pure `jsonSchemaToTs(schema)` module inside `dsh-tools` (sibling of `json-schema.ts``schemas()` and the SDK are two projections of the same store) maps the JSON-Schema subset the `defineTool` DSL emits (object/string/number/boolean/array, `properties`, `required`, string `enum` → literal union, nested objects, array `items`, `description` → JSDoc) to a TS type literal. It is **total**: any construct outside that subset (`$ref`, `oneOf`/`anyOf`, `integer`, future MCP shapes, …) degrades to `unknown` without throwing. Because `ToolSchema.name` is an arbitrary string, the SDK is declared as one object constant — `declare const tools: { "some-mcp-tool"(args: …): Promise<string>; bash(args: …): Promise<string>; … }` — quoted keys make every name reachable with no sanitization or alias-collision logic. Typing is advisory (the runtime executes type-stripped JS); the instructions say so.
@@ -89,16 +91,16 @@ The design consists of the `dsh-code-runtime` interface package, the `dsh-code-r
Shipped surface:
- **The seam**: `packages/code-runtime/``@deepseek-ai/dsh-code-runtime` (abstract `CodeRuntime`, the vocabulary above, `ctx.codeRuntime`) and `@deepseek-ai/dsh-code-runtime-worker` (the worker-thread backend, every cap a validated config field). Rows in the service map, capability-seams graph, config catalog, and cordis catalog.
- **The registry surface**: `ToolRegistry`'s `mode` config, mode-aware wire contribution, protected `tools:sdk` section and reserved `run_code` transport, `jsonSchemaToTs`/`renderToolsSdk` (exported), the dispatch bridge and `CodeRunFailedError`, and the `tool/code-dispatch` log event (declaration-merged into `SessionEventMap`, regenerated into the persistence catalog; `run_code` in the tool catalog).
- **The registry surface**: `ToolRegistry`'s `mode` config, mode-aware wire contribution, lazy `tools:sdk` section and reserved `run_code` transport, `jsonSchemaToTs`/`renderToolsSdk` (exported), the dispatch bridge and `CodeRunFailedError`, and the `tool/code-dispatch` log event (declaration-merged into `SessionEventMap`, regenerated into the persistence catalog; `run_code` in the tool catalog).
- **The composed surface**: the `tools` config forwards through `agent-core` and both app packages (`stdio-agent`, `acp-agent`); `demo:code-mode` boots each UI example's `code-mode.cordis.yml` overlay (the worker runtime + `mode: 'code'` over the base tree); every program sub-dispatch resolves the same scoped capability view and re-enters the complete tool pipeline with an immutable link to its enclosing transport execution.
- **Interactions inherited by deployments**: a `toolOrder` naming native tools rejects every assembly under `'code'` (update or drop the order config when switching modes); restrictions can hide end capabilities but cannot remove the presentation transport; sub-call `additionalContext` is dropped by the bridge (a plural context channel is deferred until a real hook needs it through Code Mode); sub-dispatch stays serialized until tools can declare concurrency safety — the same metadata the native parallel-dispatch TODO waits on.
- **Interactions inherited by deployments**: a `toolOrder` naming native tools rejects every assembly under `'code'` (update or drop the order config when switching modes); restrictions can hide end capabilities but cannot remove the registry-owned presentation transport, while assembly listeners may rewrite the final model-visible surface and own its protocol integrity; sub-call `additionalContext` is dropped by the bridge (a plural context channel is deferred until a real hook needs it through Code Mode); sub-dispatch stays serialized until tools can declare concurrency safety — the same metadata the native parallel-dispatch TODO waits on.
## Testing
What the suites pin, per tier:
- **Unit — worker runtime** (real workers, no mocks): output/value capture and log-source attribution; error kinds (exception incl. non-erasable syntax, abort, worker-exit under OOM); the two budgets from both sides (a hot loop behind an un-awaited pending dispatch dies at `computeMs` busy time; a program idling on a slow binding outlives `computeMs` and dies only at `maxWallMs`); binding-bridge hostility (junk/forged port traffic incl. non-object messages and forged `log`/`done` cap bypass attempts, unknown names, duplicate ids, post-settlement replies, `__proto__`/`constructor`/`toString` binding names); structured-clone fallback and cap truncation; `env` emptiness verified from inside a program; dispose-awaits-exit. A real-load-path e2e runs the BUILT package under plain `node` so the worker entry resolves both unbuilt (tsx) and built — the published-artifact guard from [docs/testing.md](../../../testing.md).
- **Unit — registry integration**: the codegen table (DSL subset, quoted names, `unknown` degradation, byte-identical determinism); provider contribution per mode (`'native'` capabilities, `'code'` exactly `[run_code]`, `'both'` capabilities + `run_code`); reserved-name, restriction, shadow, assembly-protection, and `toolOrder × mode` invariants; missing-runtime / wrong-language loud failures; full-pipeline and opaque parent-token behavior for sub-dispatches; serialization non-overlap (a probe tool records enter/exit under `Promise.all`); abort aborting the in-flight sub-dispatch and abandoning queued ones; binding rejection on `isError` and on JSON-unrepresentable arguments; `CodeRunFailedError` → structured `isError` carrying kind + logs; `tool/code-dispatch` payloads (JSON-normalized arguments identical to what dispatched); `deriveMessages()` ignoring the event; sub-call `additionalContext` suppression; HMR safety.
- **Unit — registry integration**: the codegen table (DSL subset, quoted names, `unknown` degradation, byte-identical determinism); provider contribution per mode (`'native'` capabilities, `'code'` exactly `[run_code]`, `'both'` capabilities + `run_code`); reserved-name, restriction, scoped shadowing, authoritative assembly transformation, and `toolOrder × mode` invariants; missing-runtime / wrong-language loud failures; full-pipeline and opaque parent-token behavior for sub-dispatches; serialization non-overlap (a probe tool records enter/exit under `Promise.all`); abort aborting the in-flight sub-dispatch and abandoning queued ones; binding rejection on `isError` and on JSON-unrepresentable arguments; `CodeRunFailedError` → structured `isError` carrying kind + logs; `tool/code-dispatch` payloads (JSON-normalized arguments identical to what dispatched); `deriveMessages()` ignoring the event; sub-call `additionalContext` suppression; HMR safety.
- **e2e (with-key, self-skips)**: a real model under `mode: 'code'` composes two bash calls in one program (`examples/coding-agent/tests/code-mode.e2e.ts`) — every logged `request/header` carries exactly `[run_code]`, the dispatch events land under the parent call, the file the program wrote exists, and the final answer is the curated output.
- **Snapshot (keyless replay)**: goldens for a `run_code` turn under `'code'` and `'both'` (`code-mode-turn`, `both-mode-turn`), each its own header-pinning class — the SDK section text, the collapsed header tool list, the dispatch events, and the result card are committed and replayed.

View File

@@ -41,7 +41,7 @@ A provider exposes `start(request) → Promise<SubagentRun>`. Promise fulfillmen
### Two kinds of optional capability, discovered two ways
- **Start-time features** (`outputSchema`, `depthLimit`, `toolFilter`) ride on a static `provider.capabilities` descriptor. The service checks every requested one BEFORE delegating and **rejects loud** (`SubagentError('UNSUPPORTED_CAPABILITY')`) if the provider lacks it — never accepted-then-ignored. They must be checked before a run exists, which is why they cannot be runtime methods.
- **Start-time features** (`outputSchema`, `depthLimit`, `toolFilter`, `persona`) ride on a static `provider.capabilities` descriptor. The service checks every requested one BEFORE delegating and **rejects loud** (`SubagentError('UNSUPPORTED_CAPABILITY')`) if the provider lacks it — never accepted-then-ignored. They must be checked before a run exists, which is why they cannot be runtime methods.
- **Runtime features** (steering via `sendMessage`, follow-up via `resume`) are **optional methods** on `SubagentRun`. The method's presence IS the capability, and TypeScript narrowing is the discovery mechanism: a consumer cannot call an absent method without narrowing first, so there is no silent-degradation path and no separate flags object to keep in sync.
### Fork vs. fresh are separate backends, not a flag

View File

@@ -40,7 +40,7 @@ A `workflow` tool mirroring `dsh-tool-subagent`'s synchronous shape: start, awai
`SubagentStartRequest.outputSchema` is implemented by `dsh-subagent-inprocess` for both in-process backends. Each structured child receives its own scoped capture tool, instruction, and enforcement registrations on `child.ctx`; concurrent children can use different schemas without sharing mutable policy, and disposing the child removes the entire attachment.
An output schema makes a schema-valid committed capture mandatory for successful child completion. The scoped runtime preserves the canonical capture tool and instruction, commits only a successful final outcome—including the enclosing `run_code` outcome for an SDK call—denies later side effects after capture becomes pending, and stops the child without another model step after commit. A validation failure remains a retryable tool error; clean completion without a committed capture settles as an error.
An output schema makes a schema-valid committed capture mandatory for successful child completion. The scoped runtime presents the capture tool and instruction, commits only a successful final outcome—including the enclosing `run_code` outcome for an SDK call—denies later side effects after capture becomes pending, and stops the child without another model step after commit. A validation failure remains a retryable tool error; clean completion without a committed capture settles as an error.
`StructuredOutputSchema` is the raw enforceable JSON-Schema subset in `dsh-tools` (single-string `type`, `properties`/`required`/`additionalProperties`, `items`, scalar `enum`/`const`), and unsupported keywords fail loudly because that wire data becomes the capture tool's parameters verbatim. The [agent-scope runtime-design RFC](../architecture/2026-07-12-agent-scope-runtime-design.md#structured-output-commits-only-authoritative-outcomes) owns the assembly, commit, guard, and terminal-stop correctness algorithms.

View File

@@ -17,7 +17,7 @@ The system-prompt assembly owns the canonical model-facing tool order, exactly w
- The list must contain the rest entry exactly once and no duplicate names.
- When `toolOrder` is unset, the canonical order is plain lexicographic name order (code-unit comparison, locale-independent), so determinism requires no configuration.
The policy is applied where the list is born: `assemble()`, before the `system-prompt/assemble` waterfall. The assembly canonicalizes the tools it collects from providers the same way it sorts sections by their `order` field — on the initial assembly, killing the registration-order entropy at its source. Everything downstream inherits the order untouched: the waterfall, the loop's `EpochHeader`, the `request/header` event, the deep-frozen request, and the dev invariant's cross-check all see one deterministic list, with no new loop change.
The policy is applied where the list is born: `assemble()`, before the `system-prompt/assemble` waterfall. The assembly canonicalizes the tools it collects from providers the same way it sorts sections by their `order` field — on the initial assembly, killing the registration-order entropy at its source. The waterfall therefore starts from one deterministic list; when a listener leaves that order intact, the loop's `EpochHeader`, the `request/header` event, the deep-frozen request, and the dev invariant's cross-check inherit it with no new loop change.
Scope is deliberately narrow: this fixes the REGISTRATION-ORDER race, not plugin behavior. A `system-prompt/assemble` listener may still add, remove, or rearrange tools — same as it may edit sections after their sort — and owns the determinism of what it emits; the waterfall contract already demands deterministic listeners (the reconstructability invariant would catch a listener that diverges between build and replay).
@@ -36,8 +36,8 @@ Config plumbing follows the `persona` precedent, and `toolOrder` sits beside it:
## Consequences
- Every assembly — and therefore every `request/header` event and model request — has a deterministic tool order on every host; the CI-vs-local golden flip is structurally gone. The default order is lexicographic, no longer registration order.
- `PromptAssembly.tools` itself is canonical, so every assembly consumer (the loop, waterfall listeners, any future prompt inspector) sees the model-facing order; provider registration order is observable nowhere downstream of the registry.
- Every registry-built assembly starts with a deterministic tool order on every host; absent an expert listener that deliberately changes it, every `request/header` event and model request inherits that order. The CI-vs-local registration-order flip is structurally gone, and the default is lexicographic.
- The initial `PromptAssembly.tools` is canonical, so waterfall listeners start from the model-facing order; provider registration order is observable nowhere before that cooperative seam.
- The snapshot suite's single pinned request-header fixture (`text-turn`) carries the new canonical tool order; every other ACP snapshot keeps the header bulk scrubbed as `{{system}}`/`{{tools}}`, per the pinned-header design.
- A pure tool reordering between steps is representable only as a `request/header` `'fallback'` snapshot (the name-keyed `ToolsDelta` cannot express it); with a stable canonical order such reorders no longer occur in practice, so the fallback path stays a safety valve.
- The `toolOrder` key rides the app → `agent-core``SystemPrompt` forwarding chain, so deployments set it next to `persona` in the app config; `dsh-llm` and the agent loop are untouched.

View File

@@ -16,7 +16,7 @@ This table connects model-visible tool names to the plugin package and service s
| Tool package | Model-visible names | Requires | Writes / affects | Shipped aliases | Deployment note |
| --- | --- | --- | --- | --- | --- |
| `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`, `ctx.userInteraction` | `tool/call`, `tool/result after a UI/provider answers the question` | - | ask_user_question pauses the tool call until the active UI provider returns a human answer. |
| `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`, `ctx.codeRuntime (execution time)`, `ctx.systemPrompt` | `tool/call`, `one tool/code-dispatch per bridged sub-call`, `tool/result` | - | Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode RFC). Under `code` it is the registry's only canonical wire contribution; the other visible capabilities are declared in a protected TypeScript SDK section, and a program calls them through serialized bindings that re-enter the complete guarded tool pipeline and link each nested execution to this outer result. |
| `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`, `ctx.codeRuntime (execution time)`, `ctx.systemPrompt` | `tool/call`, `one tool/code-dispatch per bridged sub-call`, `tool/result` | - | Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode RFC). Under `code` it is the registry's only wire contribution; the other visible capabilities are declared in a generated TypeScript SDK section, and a program calls them through serialized bindings that re-enter the complete guarded tool pipeline and link each nested execution to this outer result. |
| `@deepseek-ai/dsh-tool-bash` | `bash`, `bash_kill`, `bash_output` | `ctx.tools`, `ctx.bash` | `tool/call`, `tool/result`, `context/message via agent.inject() for background completion notices` | - | The bash/bash_output/bash_kill tools are model-facing consumers of the bash executor seam. |
| `@deepseek-ai/dsh-tool-cordis` | `cordis_inspect`, `cordis_mount`, `cordis_unmount` | `ctx.tools` | `tool/call`, `tool/result`, `live plugin-tree mutations (mount/unmount)` | - | Ships in examples/cordis-agent only (a deliberate opt-in — mounted code gets the real ctx, see docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). Plugins the model mounts may register ADDITIONAL model-visible tools at runtime; the request-header ToolsDelta logs those tool-set changes. |
| `@deepseek-ai/dsh-tool-fs` | `edit`, `read`, `write` | `ctx.tools`, `ctx.fs`, `ctx.systemPrompt` | `tool/call`, `fs/write-intent or fs/edit-intent for mutations`, `fs/observed after successful file operations`, `tool/result` | - | The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. The tool schemas above are identical with or without the policy plugin. |
@@ -119,7 +119,7 @@ Execute a TypeScript program against the available tools. Write the BODY of an a
Source: [`packages/core/tools/src/code-mode.ts`](../packages/core/tools/src/code-mode.ts)
Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode RFC). Under `code` it is the registry's only canonical wire contribution; the other visible capabilities are declared in a protected TypeScript SDK section, and a program calls them through serialized bindings that re-enter the complete guarded tool pipeline and link each nested execution to this outer result.
Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode RFC). Under `code` it is the registry's only wire contribution; the other visible capabilities are declared in a generated TypeScript SDK section, and a program calls them through serialized bindings that re-enter the complete guarded tool pipeline and link each nested execution to this outer result.
## `@deepseek-ai/dsh-tool-bash`