diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 90b5b7354f..a837908d90 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -40,7 +40,7 @@ Every package README ends with this table immediately before `## Known Limitatio | Context surface | What the model sees | Token effect | |---|---|---| -Rows state what reaches which model and classify token cost or lifetime; zero-direct rows name the indirect path. `verify-package-readme-model-experience` gates shape and order, while review owns accuracy ([rationale](rfc/implemented/process/2026-07-12-package-model-experience-contract.md)). +Rows state what reaches which model and classify token cost or lifetime. Treat prompt text and tool schemas separately when their visibility conditions differ; zero-direct rows name the indirect path. `verify-package-readme-model-experience` gates shape and order, while review owns accuracy ([rationale](rfc/implemented/process/2026-07-12-package-model-experience-contract.md)). ## Wordcount Budgets diff --git a/docs/cookbook/adding-a-package.md b/docs/cookbook/adding-a-package.md index 0ccee2b217..d8def5ebfe 100644 --- a/docs/cookbook/adding-a-package.md +++ b/docs/cookbook/adding-a-package.md @@ -56,7 +56,7 @@ Keep package-specific service API, config, events, extension points, and design - **Consumer-visible gap** — exact boundary or deliberately deferred work. ``` -Fill [Model Experience](../AGENTS.md#package-model-experience) from the implementation: name every direct request contribution and token-growth condition, or state zero direct tokens and its indirect path. Every package participates, including type-only libraries and backend seams. A package with genuinely no limitations joins the justified allowlist in [`verify-readme-limitations.ts`](../../scripts/verify-readme-limitations.ts) and ends after Model Experience. +Fill [Model Experience](../AGENTS.md#package-model-experience) from the implementation: name every direct request contribution and token-growth condition, or state zero direct tokens and its indirect path. Do not infer prompt visibility from tool-schema visibility; independently registered guidance can remain after a scoped tool restriction. Every package participates, including type-only libraries and backend seams. A package with genuinely no limitations joins the justified allowlist in [`verify-readme-limitations.ts`](../../scripts/verify-readme-limitations.ts) and ends after Model Experience. ## 5. Verify diff --git a/docs/core-data-structures/bash.md b/docs/core-data-structures/bash.md index 51f7cb0696..94c5cb9a7f 100644 --- a/docs/core-data-structures/bash.md +++ b/docs/core-data-structures/bash.md @@ -154,7 +154,7 @@ interface CollectedOutput { ## File sandbox: `BashSandboxInfo` -A sandbox-consuming executor (`dsh-bash-sandbox`) exposes its configured fallback through `BashExecutor.sandboxMode`. The tool layer folds each agent session's durable `bash/sandbox-mode` override, stamps the effective mode onto the request, states it in the per-agent prompt, and may replace it for one user-approved strictly wider call. The mode/enforcement vocabulary is owned and cataloged by the [`@deepseek-ai/dsh-sandbox` seam](sandbox.md), whose provider wraps the executor's argv; modes govern FILE effects only, not network or process visibility. +A sandbox-consuming executor (`dsh-bash-sandbox`) exposes its configured fallback through `BashExecutor.sandboxMode`. The tool layer folds each agent session's durable `bash/sandbox-mode` override, stamps the effective mode onto the request, and may replace it for one user-approved strictly wider call. It deliberately neither states the standing mode nor narrates switches; a denial result names the mode that command actually ran under. The mode/enforcement vocabulary is owned and cataloged by the [`@deepseek-ai/dsh-sandbox` seam](sandbox.md), whose provider wraps the executor's argv; modes govern FILE effects only, not network or process visibility. A sandboxed run always reports the facts it executed under on `BashRunResult.sandbox`: `denied` is the executor's conservative classification of a failure as sandbox-caused (a failed exit whose stderr carries a filesystem-permission signature — never a clean exit or a signal kill), read from the collected stderr tail; `enforcement` reports how completely the selected backend governs the mode's file effects (`SandboxEnforcement = 'full' | 'partial'` — `partial` when an older Landlock ABI governs only a subset of the requested accesses; absent under `danger-full-access`, where nothing is confined); `runnerFailed` marks the opposite of a denial — the sandbox RUNNER itself failed and the command never ran (stamped only on settled background tasks; a foreground run surfaces the same condition as the thrown `SANDBOX_UNAVAILABLE` error): @@ -193,7 +193,7 @@ interface BashSandboxInfo { } ``` -One more piece completes the vocabulary: the `SANDBOX_UNAVAILABLE` error code (owned by the [sandbox seam](sandbox.md)) is what the `ctx.sandbox` provider throws — and the executor propagates — when a confined mode has no usable backend. A selected runner refusing its profile reaches the same fail-closed foreground error; a settled background task records `runnerFailed`. The model sees the current effective mode in the prompt, receives denial/runner facts in results, and can request a one-shot strictly wider retry through `sandbox_permissions` plus `justification`; `ctx.approval` must grant that exact call before anything executes. The complete policy and switching design is the [sandbox RFC](../rfc/implemented/feature/2026-07-06-sandbox.md). +One more piece completes the vocabulary: the `SANDBOX_UNAVAILABLE` error code (owned by the [sandbox seam](sandbox.md)) is what the `ctx.sandbox` provider throws — and the executor propagates — when a confined mode has no usable backend. A selected runner refusing its profile reaches the same fail-closed foreground error; a settled background task records `runnerFailed`. The model receives denial/runner facts in results, learns the effective mode only when a denial marker names it, and can request a one-shot strictly wider retry through `sandbox_permissions` plus `justification`; `ctx.approval` must grant that exact call before anything executes. The complete policy and switching design is the [sandbox RFC](../rfc/implemented/feature/2026-07-06-sandbox.md). ## Background tasks: `BashTask` diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 245d25fa5f..47640ba8ee 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -75,7 +75,7 @@ Source: [`packages/core/session/src/types.ts:329`](../packages/core/session/src/ #### `bash/sandbox-mode` — log-only -The session's sandbox mode was switched — log-only (like `approval/*`; NOT a surface event, carries no `surfaceOp`): durable and replayable, never in the model transcript. The LAST such event is the session's override (effectiveSandboxMode); who asked for it is derivable from position (an event after the log's last `request/header*` was a runtime switch by the user; see the tool layer's narrator). +The session's sandbox mode was switched — log-only (like `approval/*`; NOT a surface event, carries no `surfaceOp`): durable and replayable, never in the model transcript. The LAST such event is the session's override (effectiveSandboxMode); execution and ACP config-option reporting fold it without adding prompt text or a context notice. ```ts persistence-catalog 'bash/sandbox-mode': { mode: SandboxMode } diff --git a/docs/rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md b/docs/rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md index 5b6eeb4ccb..3c2e17aebd 100644 --- a/docs/rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md +++ b/docs/rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md @@ -32,7 +32,7 @@ This uses the normal system-prompt registration mechanism rather than a second p ### Tool filtering is one live global-view rule -The tool filter controls visibility and executable lookup together. An in-process provider installs `ToolRegistry.restrict()` in the child's scope before publication, and the registry's single resolver applies the same result to prompt schemas, lookup, execution, and Code Mode SDK generation. +The tool filter controls capability visibility and executable lookup together. An in-process provider installs `ToolRegistry.restrict()` in the child's scope before publication, and the registry's single resolver applies the same result to wire tool schemas, lookup, execution, and Code Mode SDK generation. Independently registered system-prompt sections are outside `ToolRegistry`, so filtering a tool does not remove that plugin's standalone guidance. Resolution follows these rules: diff --git a/docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md b/docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md index d4814ce283..e2fbe9cdc6 100644 --- a/docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md +++ b/docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md @@ -4,7 +4,7 @@ Status: implemented ## Problem -The [documentation standard](../../../AGENTS.md) assigns limitations to the package-README tier ("the per-package contract: config, semantics, limitations, extension points"), but nothing enforced that the section exists or shares a shape. Ten READMEs carried the content under five ad-hoc headings — "What is NOT here (TODO)", "What is NOT here", "Deferred (faithful-but-degraded)", "Limitations (MVP, documented deliberately)", "Known limitations (tracked TODOs)" — and the other forty-odd carried nothing, so a reader could not distinguish "this package has no known limitations" from "nobody wrote them down", and no grep could enumerate the repo's known gaps. +The [documentation standard](../../../AGENTS.md) assigns limitations to the package-README tier ("the per-package contract: config, semantics, limitations, extension points"). Without a required shared shape, variant headings and omissions make "this package has no known limitations" indistinguishable from "nobody wrote them down", and no single grep can enumerate the repo's known gaps. ## Decision @@ -16,13 +16,13 @@ The gate checks presence, shape, and the whitelist; the bullets' truthfulness an ## Alternatives considered -- **Free-form headings, gate only that "something limitations-like" exists** — keeps the five variants, stays un-greppable, and needs the same near-miss heuristics anyway without buying uniformity. +- **Free-form headings, gate only that "something limitations-like" exists** — preserves variant headings, stays un-greppable, and needs the same near-miss heuristics anyway without buying uniformity. - **Require the section in ALL READMEs, allowing an empty body or "None."** — boilerplate "None" rots silently as a package gains real limitations; the whitelist inversion turns "nothing declared" into an explicit, lintable claim that review can challenge. - **A word-count ceiling on the section** — limitation lists are legitimately variable in length; package READMEs are deliberately unbudgeted (per the [budget policy](../../../AGENTS.md)) and review governs their prose. ## Consequences - A new package cannot ship without either declaring its gaps or explicitly claiming it has none; a missing, drifted, or empty section fails `doc-sync` locally (pre-push) and in CI (`readme-limitations` in the run-gates doc-sync leaf set). -- The pre-existing variant sections are normalized to the canonical heading, and every package README now answers the limitations question one way or the other. +- Every package README answers the limitations question through the canonical heading or an explicit no-limitations allowlist entry. - One more fast tsx script in the `doc-sync` chain; no new dependency (plain `node:fs` glob + line scan). - The canonical heading is enforced verbatim, so renaming it later is a mechanical one-script-plus-all-READMEs change guarded by the same gate. diff --git a/docs/rfc/implemented/process/2026-07-12-package-model-experience-contract.md b/docs/rfc/implemented/process/2026-07-12-package-model-experience-contract.md index a083e69cb8..8fb3e45a83 100644 --- a/docs/rfc/implemented/process/2026-07-12-package-model-experience-contract.md +++ b/docs/rfc/implemented/process/2026-07-12-package-model-experience-contract.md @@ -8,11 +8,11 @@ A package README can explain APIs and runtime mechanics without answering the qu ## Decision -Every workspace package README ends with the canonical [Model Experience table](../../../AGENTS.md#package-model-experience), immediately before `## Known Limitations and Deferred Work`; a package on the no-limitations allowlist ends with Model Experience itself. Each row identifies a concrete request surface, says what the relevant model literally receives and when, and classifies the token effect. The default subject is the conversation model; a package that invokes an auxiliary model, such as a summarizer or search provider, names that request separately. Agent-scoped visibility is stated where it changes which agent receives a contribution. +Every workspace package README ends with the canonical [Model Experience table](../../../AGENTS.md#package-model-experience), immediately before `## Known Limitations and Deferred Work`; a package on the no-limitations allowlist ends with Model Experience itself. Each row identifies a concrete request surface, says what the relevant model literally receives and when, and classifies the token effect. The default subject is the conversation model; a package that invokes an auxiliary model, such as a summarizer or search provider, names that request separately. Agent-scoped visibility is stated where it changes which agent receives a contribution. Prompt text and tool schemas are described separately whenever configuration or scoping can hide one without the other. Every package participates. A service seam, storage backend, test helper, or type-only library that contributes no prompt text, tool schema, message, or auxiliary request records zero direct tokens and names the consumer or control path through which it can still change model-visible material. This explicit negative contract prevents readers from having to infer whether the section was forgotten. -`verify-package-readme-model-experience` discovers packages from `packages/*/*/package.json`, requires one sibling README, the canonical final-section order, one exact `## Model Experience` heading and three-column table header, and at least one complete row. It runs in `doc-sync` and the parallel gate runner. The check owns shape and completeness; implementation review owns the truth of the prose. +`verify-package-readme-model-experience` discovers packages from `packages/*/*/package.json`, requires one sibling README, the canonical final-section order, one exact `## Model Experience` heading and three-column table header outside fenced code, and at least one complete row. It runs in `doc-sync` and the parallel gate runner. The check owns structural presence, shape, and order; implementation review owns coverage and the truth of the prose. ## Alternatives considered diff --git a/packages/bash/bash-sandbox/README.md b/packages/bash/bash-sandbox/README.md index f971ff322a..99cb3b2423 100644 --- a/packages/bash/bash-sandbox/README.md +++ b/packages/bash/bash-sandbox/README.md @@ -1,6 +1,6 @@ # @deepseek-ai/dsh-bash-sandbox -Sandbox-consuming implementation of the [`@deepseek-ai/dsh-bash`](../bash/) executor seam. Load it **instead of** `@deepseek-ai/dsh-bash-local`, together with a [`ctx.sandbox`](../../sandbox/sandbox/) provider (e.g. [`@deepseek-ai/dsh-sandbox-local`](../../sandbox/sandbox-local/)) — the model-facing tool layer (`dsh-tool-bash`) is untouched; that swap is exactly what the seams exist for. +Sandbox-consuming implementation of the [`@deepseek-ai/dsh-bash`](../bash/) executor seam. Load it **instead of** `@deepseek-ai/dsh-bash-local`, together with a [`ctx.sandbox`](../../sandbox/sandbox/) provider (e.g. [`@deepseek-ai/dsh-sandbox-local`](../../sandbox/sandbox-local/)) — no alternate tool plugin is needed; `dsh-tool-bash` detects the executor's `sandboxMode` capability and adds the escalation fields. Every command is confined by handing the provider the exact `['bash', '-c', command]` argv this executor is about to spawn and spawning the returned (wrapped) argv instead. WHICH platform runner confines it — and whether one is usable at all (fail closed with a structured `SANDBOX_UNAVAILABLE` error, never a silent unconfined run) — is the provider's concern; this package owns the bash side only. @@ -36,7 +36,7 @@ The keyless consumer-integration proofs are `tests/bwrap.e2e.ts`, `tests/landloc | Context surface | What the model sees | Token effect | |---|---|---| -| System prompt, indirectly | By advertising a confining `sandboxMode`, this backend makes `dsh-tool-bash` state the calling session's effective mode and expose escalation fields. The backend itself adds no prose. | Small fixed per-request cost through the consumer, plus a retained notice when the session mode changes. | +| Bash tool schema, indirectly | By advertising a confining `sandboxMode`, this backend makes `dsh-tool-bash` expose `sandbox_permissions` and `justification`. The backend adds no prompt prose, and the session's effective mode remains unstated. | Small fixed schema increment on requests where `bash` is visible; mode switches add no context tokens. | | Bash tool result, indirectly | The model sees ordinary bounded command output plus denial markers, the mode used, and sandbox-unavailable failures shaped by `dsh-tool-bash`; runner details stay internal. | Zero additional tokens on an unremarkable allowed run beyond ordinary output. Denial or failure adds a small conditional marker or error retained until compaction. | ## Known Limitations and Deferred Work diff --git a/packages/bash/bash-sandbox/src/index.ts b/packages/bash/bash-sandbox/src/index.ts index 090d06b2fe..65d5d8b989 100644 --- a/packages/bash/bash-sandbox/src/index.ts +++ b/packages/bash/bash-sandbox/src/index.ts @@ -143,9 +143,9 @@ function matchesSignature(exitCode: number | null, stderr: string, signatures: r * fallback exposed by {@link sandboxMode}; `dsh-tool-bash` folds a session's * durable `bash/sandbox-mode` override and stamps the effective mode onto each * request, while an approved escalation may stamp a strictly wider mode for - * one call. The tool's per-agent prompt section states that same effective - * mode, and each run's `result.sandbox` reports what actually executed plus - * enforcement completeness. + * one call. The prompt deliberately does not state the mode; each run's + * `result.sandbox` reports what actually executed plus enforcement + * completeness, and the tool layer renders denial or runner-failure facts. */ export class SandboxBashExecutor extends LocalBashExecutor { static inject = ['sandbox'] diff --git a/packages/bash/bash/README.md b/packages/bash/bash/README.md index 36f6686522..97c1b91dff 100644 --- a/packages/bash/bash/README.md +++ b/packages/bash/bash/README.md @@ -11,7 +11,7 @@ This package is the interface quarter of the bash capability, split so each conc | `@deepseek-ai/dsh-bash-sandbox` | an implementation: `dsh-bash-local`'s mechanics with every spawn confined via [`ctx.sandbox`](../../sandbox/sandbox/), denials reported as result facts | | `@deepseek-ai/dsh-tool-bash` | the model-facing tool schemas over `ctx.bash` | -The split mirrors the LLM seam (`LlmService`/`LlmAdapter`) and the agent-tool survey: pi hides execution behind a `BashOperations` interface (local shell / SSH / VM backends), Codex behind an exec-server protocol. `dsh-bash-sandbox` is exactly that swap in action — a sandboxing executor behind the same interface, tool schemas untouched; a containerized or remote executor slots in the same way. +The split mirrors the LLM seam (`LlmService`/`LlmAdapter`) and the agent-tool survey: pi hides execution behind a `BashOperations` interface (local shell / SSH / VM backends), Codex behind an exec-server protocol. `dsh-bash-sandbox` is exactly that swap in action — a sandboxing executor behind the same interface; the consumer detects its `sandboxMode` capability and adds escalation fields without importing the implementation. A containerized or remote executor slots in the same way. ## Service API (`ctx.bash`) diff --git a/packages/bash/bash/src/session-mode.ts b/packages/bash/bash/src/session-mode.ts index 03ad6e3d7c..ff0b9ceba1 100644 --- a/packages/bash/bash/src/session-mode.ts +++ b/packages/bash/bash/src/session-mode.ts @@ -5,12 +5,13 @@ * `effective = fold(events) ?? the executor's configured default`, so an * override survives restart by replay, two sessions can never see each * other's state, and there is no external config store. The event is - * log-only (the `approval/*` precedent): the model learns the mode from the - * prompt section and the boundary notices in `@deepseek-ai/dsh-tool-bash`, - * never from the event itself. EXECUTION honors the fold in the tool layer — - * it stamps the effective mode onto each call's `BashExecRequest.sandboxMode` - * (weakest-precedence: an escalation grant for the call outranks it) — the - * executor itself stays a config-fixed default plus per-call overrides. + * log-only (the `approval/*` precedent): the model receives neither this event + * nor a standing mode statement. `@deepseek-ai/dsh-tool-bash` names the mode + * only when it renders a sandbox denial. EXECUTION honors the fold in the tool + * layer — it stamps the effective mode onto each call's + * `BashExecRequest.sandboxMode` (weakest-precedence: an escalation grant for + * the call outranks it) — the executor itself stays a config-fixed default + * plus per-call overrides. * * @module dsh-bash/session-mode */ @@ -24,9 +25,8 @@ declare module '@deepseek-ai/dsh-session' { * The session's sandbox mode was switched — log-only (like `approval/*`; * NOT a surface event, carries no `surfaceOp`): durable and replayable, * never in the model transcript. The LAST such event is the session's - * override ({@link effectiveSandboxMode}); who asked for it is derivable - * from position (an event after the log's last `request/header*` was a - * runtime switch by the user; see the tool layer's narrator). + * override ({@link effectiveSandboxMode}); execution and ACP config-option + * reporting fold it without adding prompt text or a context notice. */ 'bash/sandbox-mode': { mode: SandboxMode } } @@ -54,8 +54,8 @@ export function effectiveSandboxMode(events: readonly SessionEvent[]): SandboxMo /** * THE write path for a session's sandbox-mode override: appends exactly one * `bash/sandbox-mode` event — the switch IS its event; nothing mutates mode - * state out of band. Takes effect on the session's next bash call and next - * prompt assembly (the consumers fold on every read). + * state out of band. Subsequent execution and ACP config-option reporting fold + * it on read; no prompt assembly consumes it. * @param session - the session the override belongs to. * @param mode - the mode every subsequent bash call in this session runs * under (until the next switch). diff --git a/packages/bash/tool-bash/README.md b/packages/bash/tool-bash/README.md index da1ab41c14..8bab38caa5 100644 --- a/packages/bash/tool-bash/README.md +++ b/packages/bash/tool-bash/README.md @@ -1,10 +1,10 @@ # @deepseek-ai/dsh-tool-bash -The model-facing bash tools — `bash`, `bash_output`, `bash_kill` — registered over the `ctx.bash` executor seam (`@deepseek-ai/dsh-bash`). Pure schema + text shaping; every process concern lives behind the seam, so sandboxed or remote executor implementations swap in without changing what the model sees. +The model-facing bash tools — `bash`, `bash_output`, `bash_kill` — registered over the `ctx.bash` executor seam (`@deepseek-ai/dsh-bash`). This package owns schema and text shaping while process concerns stay behind the seam. Executor facts can change rendered results, and a sandboxing executor activates the escalation fields, without moving those presentation rules into the backend. Requires a loaded executor implementation (e.g. `@deepseek-ai/dsh-bash-local`); the plugin stays pending until `ctx.bash` exists (`inject: ['tools', 'bash', 'systemPrompt']`). -The plugin also contributes the `tool:bash` prompt section (order 105) — the cross-call habit the per-tool descriptions cannot carry: check the `[exit code: N]` marker on every result and investigate failures before moving on. Under a sandboxing executor it additionally contributes the per-agent `env:bash-sandbox` section (order 110) stating each session's EFFECTIVE mode, and the pre-step narrator — see [Per-session mode](#per-session-mode-switching-and-visibility). +The plugin also contributes the `tool:bash` prompt section (order 105) — the cross-call habit the per-tool descriptions cannot carry: check the `[exit code: N]` marker on every result and investigate failures before moving on. A sandboxing executor changes the `bash` schema and result markers but adds no mode statement or switch notice; see [Per-session mode](#per-session-mode-switching). ## Tools @@ -62,7 +62,7 @@ Under a sandboxing executor this plugin makes the session's standing mode overri | Context surface | What the model sees | Token effect | |---|---|---| -| System prompt | Every request for an agent that can see these tools carries the short `tool:bash` exit-code instruction. With a sandboxing executor it also carries that session's effective sandbox mode, plus a logged context notice after a mode change. | Small fixed input cost per request; a mode-change notice is conditional and then remains in conversation history. | +| System prompt | Every request in this plugin's registration scope carries the short `tool:bash` exit-code instruction. A sandboxing executor adds no mode statement or switch notice. Scoped tool restrictions can hide the schemas without removing this independently registered section. | Small fixed input cost per request while the plugin is active, unchanged by sandbox mode or mode switches. | | Tool schemas | The model sees `bash`, `bash_output`, and `bash_kill`. `sandbox_permissions` and `justification` appear on `bash` only when the mounted executor advertises sandboxing. Agent-scoped tool restrictions can remove the definitions for that agent. | Fixed schema cost on every request where the tools are visible; sandbox support adds the escalation fields. | | Tool-call history and results | Calls retain their arguments. Results contain bounded stdout and stderr, status markers, task ids, incremental background output, kill outcomes, and sandbox denial or failure markers. | Data-dependent tokens are added after each call and resent on later steps until compaction. Executor output caps and incremental reads bound each result; spill paths let the model fetch omitted output deliberately. | diff --git a/packages/bash/tool-bash/src/index.ts b/packages/bash/tool-bash/src/index.ts index 31e6512ae0..c22e789c79 100644 --- a/packages/bash/tool-bash/src/index.ts +++ b/packages/bash/tool-bash/src/index.ts @@ -483,12 +483,12 @@ export function apply(ctx: Context): void { /** * The session's standing mode override for an ordinary (non-escalating) * call: the `bash/sandbox-mode` fold of the calling agent's log, stamped - * onto the request so EXECUTION follows the same effective mode the prompt - * section states. Weakest precedence — an escalation grant (freshly - * approved for exactly this call) outranks it, and without either the - * executor's `resolve()` applies its configured default. Undefined for a - * non-sandboxing executor (nothing honors it) and for agent-less callers - * (no session to fold). + * onto the request so execution follows the fold without stating it in the + * prompt. Weakest precedence — an escalation grant (freshly approved for + * exactly this call) outranks it, and without either the executor's + * `resolve()` applies its configured default. Undefined for a non-sandboxing + * executor (nothing honors it) and for agent-less callers (no session to + * fold). */ const sessionOverride = (exec: ToolExecution): SandboxMode | undefined => defaultMode === undefined || exec.agent === undefined ? undefined : effectiveSandboxMode(exec.agent.session.events) diff --git a/packages/core/scope/README.md b/packages/core/scope/README.md index 64e2df91ea..f16d527bb4 100644 --- a/packages/core/scope/README.md +++ b/packages/core/scope/README.md @@ -23,7 +23,7 @@ Handing out a scoped context hands out the minting plugin's service-resolution s | Context surface | What the model sees | Token effect | |---|---|---| -| Per-agent visibility control | This package emits no text or schema. It routes scoped prompt sections, variables, tools, restrictions, and listeners to one agent: scoped registrations can shadow same-named globals, while restrictions filter global tools before scope-local tools are merged. All disappear with that agent. This is request composition, not authority confinement. | Zero direct tokens. It can add, replace, or remove whole contributions for one agent without changing another agent's request. | +| Per-agent visibility control | This package emits no text or schema. It routes scoped prompt sections, variables, tools, restrictions, and listeners to one agent: scoped registrations can shadow same-named globals, while restrictions filter global tool schemas and Code Mode bindings before scope-local tools are merged. Restrictions do not filter independently registered prompt sections. All scoped state disappears with that agent. This is request composition, not authority confinement. | Zero direct tokens. Scoped registrations can add or replace whole contributions for one agent; restrictions remove schema entries or SDK bindings and reduce that agent's repeated envelope cost without changing another agent's request. | ## Known Limitations and Deferred Work diff --git a/packages/core/system-prompt/README.md b/packages/core/system-prompt/README.md index 9e1d8d5032..f07cced7ec 100644 --- a/packages/core/system-prompt/README.md +++ b/packages/core/system-prompt/README.md @@ -45,7 +45,7 @@ Design rationale: [the prompt-variables RFC](../../../docs/rfc/implemented/archi | Context surface | What the model sees | Token effect | |---|---|---| | System prompt | Every assembly starts with `You are an AI agent powered by the DeepSeek Harness SDK.`, then the configured persona and ordered plugin sections after strict variable interpolation. Empty sections disappear; scoped sections and variables can shadow globals for one agent. The final `system-prompt/assemble` waterfall result is authoritative, so an expert listener's changes determine the delivered prompt and tool schemas. | Identity is a fixed per-request cost. Persona and plugin text are repeated per request and scale with their rendered content. | -| Tool schemas | The model receives the collected, per-agent-visible tool names, descriptions, and JSON schemas in configured or lexicographic order after restrictions and assembly interception. | Schema tokens repeat on every request. Restricting a tool removes its entire schema cost for that agent; reordering changes cache shape but not semantic content. | +| Tool schemas | The model receives the collected, per-agent-visible tool names, descriptions, and JSON schemas in configured or lexicographic order after restrictions and assembly interception. Sections and schema providers are separate assembly inputs, so a tool restriction does not remove independently registered guidance. | Schema tokens repeat on every request. Restricting a tool removes its entire schema cost for that agent but not a separate prompt section; reordering changes cache shape but not semantic content. | ## Known Limitations and Deferred Work diff --git a/packages/core/tools/README.md b/packages/core/tools/README.md index 252ed65580..ff09be73ee 100644 --- a/packages/core/tools/README.md +++ b/packages/core/tools/README.md @@ -142,7 +142,7 @@ The wire collapse is the registry's own contribution (`systemPrompt.tools()` is | Context surface | What the model sees | Token effect | |---|---|---| -| Tool schemas and Code Mode SDK | In normal mode the model sees each visible definition's name, description, and JSON schema. Code Mode instead contributes one reserved `run_code` wire schema and a generated TypeScript `tools` SDK section; `both` exposes both forms. Agent-scoped restrictions and shadows change that agent's end-tool set without filtering the transport. The expert `system-prompt/assemble` waterfall can replace the final assembly and then owns preserving Code Mode protocol coherence. | Fixed per-request cost proportional to the visible definitions. Code Mode trades end-tool schemas for generated SDK text plus one transport schema rather than promising a universal reduction. | +| Tool schemas and Code Mode SDK | In normal mode the model sees each visible definition's name, description, and JSON schema. Code Mode instead contributes one reserved `run_code` wire schema and a generated TypeScript `tools` SDK section; `both` exposes both forms. Agent-scoped restrictions and shadows change that agent's end-tool set and registry-owned SDK without filtering the transport or another plugin's independently registered prompt sections. The expert `system-prompt/assemble` waterfall can replace the final assembly and then owns preserving Code Mode protocol coherence. | Fixed per-request cost proportional to the visible definitions. Code Mode trades end-tool schemas for generated SDK text plus one transport schema rather than promising a universal reduction. | | Tool-call history and results | The loop retains model-emitted arguments and the registry's final normalized content or structured error. Post-execute listeners may append source-attributed context after the result. Code Mode exposes only the outer program's printed or returned value; inner dispatch events stay log-only. | Arguments, results, and additional context are data-dependent and resent until compaction. Restrictions that hide tools also remove their schemas before the model can call them. | ## Known Limitations and Deferred Work diff --git a/packages/fs/tool-fs/README.md b/packages/fs/tool-fs/README.md index c955956f55..0f0b13ac8f 100644 --- a/packages/fs/tool-fs/README.md +++ b/packages/fs/tool-fs/README.md @@ -52,7 +52,7 @@ The read rendering (line windowing + output formatting) lives in `src/read-rende | Context surface | What the model sees | Token effect | |---|---|---| -| System prompt | Visible agents receive three short sections explaining line-windowed reads, whole-file writes, literal edits, and the default read-before-mutate habit. | Fixed guidance cost per request while the tools are visible. | +| System prompt | Every request in this plugin's registration scope receives three short sections explaining line-windowed reads, whole-file writes, literal edits, and the default read-before-mutate habit. Scoped tool restrictions can hide schemas without removing these independently registered sections. | Fixed guidance cost per request while the plugin is active, even when a restriction hides one or more tools. | | Tool schemas | The model sees `read`, `write`, and `edit` with their snake_case arguments. Scoped tool restrictions can remove any definition for one agent. | Fixed schema cost on every request in that tool view. | | Tool-call history and results | Read returns numbered UTF-8 lines and a pagination or cap footer; write and edit return concise success text or structured errors. The model-emitted write or edit content also remains in the assistant tool-call arguments. | Read output is capped by `readLimit`, `readMaxLineLength`, and `readMaxBytes`. Call arguments and results are resent until compaction, so large write payloads can dominate history even though the success result is small. | diff --git a/packages/subagent/subagent-fork/README.md b/packages/subagent/subagent-fork/README.md index 930c50836b..bafd7792a2 100644 --- a/packages/subagent/subagent-fork/README.md +++ b/packages/subagent/subagent-fork/README.md @@ -27,7 +27,7 @@ See [`dsh-subagent-spawn`](../subagent-spawn/README.md) for the run lifecycle, m | Context surface | What the model sees | Token effect | |---|---|---| -| Child-agent history | The child receives the parent's balanced completed-turn surface prefix, then the new task. Configured persona and tool restrictions compose only in the child's fresh scope; the parent's tool view and authority are not inherited. An optional structured-output request adds its child-only contract. The parent's current in-flight turn is excluded. | Forking duplicates the retained completed history into a separate child's requests; the child then accumulates its own tokens independently. A first-turn fork has no inherited history. | +| Child-agent history and envelope | The child receives the parent's balanced completed-turn surface prefix, then the new task. A configured persona shadows prompt text in the child's fresh scope; a tool restriction filters its global wire schemas, executable lookup, and Code Mode SDK bindings but not standalone guidance. The parent's tool view and authority are not inherited. An optional structured-output request adds its child-only contract. The parent's current in-flight turn is excluded. | Forking duplicates retained completed history into separate child requests; the child then accumulates its own tokens independently. Persona changes repeated prompt cost, filtering changes schema or generated SDK cost, and a first-turn fork has no inherited history. | | Parent tool result, indirectly | The parent receives only the child's own final output through `dsh-tool-subagent`, not the inherited prefix or intermediate work. | Parent input grows by one data-dependent final result retained until compaction. | ## Known Limitations and Deferred Work diff --git a/packages/subagent/subagent-inprocess/README.md b/packages/subagent/subagent-inprocess/README.md index c877ee3284..0d7c2886b5 100644 --- a/packages/subagent/subagent-inprocess/README.md +++ b/packages/subagent/subagent-inprocess/README.md @@ -44,7 +44,7 @@ A clean turn that never commits the required structured value reports `error`; t | Context surface | What the model sees | Token effect | |---|---|---| -| Child-agent request | The shared driver sends the task as the child's user message and, when requested, composes persona and global-tool restrictions in the unpublished child's fresh scope; parent restrictions are not inherited. Structured runs add a scoped instruction plus `structured_output` in the visible schema or Code Mode SDK, then stop after a committed capture. Spawn supplies no history; fork supplies its balanced seed. | Child input is isolated from the parent and grows through the child's own steps. Optional persona, filtering, and structured-output changes affect only that child; structured output adds fixed instruction and capability tokens for the run. | +| Child-agent request | The shared driver sends the task as the child's user message and, when requested, shadows the persona and restricts global tool schemas, lookup, execution, and Code Mode SDK bindings in the unpublished child's fresh scope; parent restrictions are not inherited, and standalone tool-guidance sections remain. Structured runs add a scoped instruction plus `structured_output` in the visible schema or Code Mode SDK, then stop after a committed capture. Spawn supplies no history; fork supplies its balanced seed. | Child input is isolated from the parent and grows through the child's own steps. A persona changes repeated prompt text; filtering changes schema or generated SDK cost but not independently registered guidance. Structured output adds fixed instruction and capability tokens for that child. | | Parent result, indirectly | The driver extracts only the child's own last assistant output or captured structured value; seeded parent messages and intermediate child work do not become the result. | The parent receives one data-dependent result through the consumer; all other child tokens stay in the child session. | ## Known Limitations and Deferred Work diff --git a/packages/subagent/subagent-spawn/README.md b/packages/subagent/subagent-spawn/README.md index 8ad605d68b..78ecb4c035 100644 --- a/packages/subagent/subagent-spawn/README.md +++ b/packages/subagent/subagent-spawn/README.md @@ -22,7 +22,7 @@ Spawn advertises `{ outputSchema: true, depthLimit: true, toolFilter: true, pers | Context surface | What the model sees | Token effect | |---|---|---| -| Child-agent request | The fresh child receives the standalone task, inherits the parent model and workspace by default, and sees the globally composed prompt and tools after any configured child-scoped persona shadow and global-tool restriction. It receives zero parent conversation messages; the filter is visibility/composition, not an authority grant inherited from the parent. | The child pays for a new independent context and history; no parent-history tokens are duplicated. Persona or filtering changes only this child's repeated prompt/schema cost. | +| Child-agent request | The fresh child receives the standalone task, inherits the parent model and workspace by default, and sees the global prompt with any configured child-scoped persona shadow. A tool filter removes global wire schemas, executable lookup, and Code Mode SDK bindings for that child but leaves independently registered guidance. It receives zero parent conversation messages; the filter is visibility/composition, not an authority grant inherited from the parent. | The child pays for a new independent context and history; no parent-history tokens are duplicated. Persona changes this child's repeated prompt cost, while filtering changes its schema or generated SDK cost. | | Parent tool result, indirectly | Through `dsh-tool-subagent`, the parent receives only the child's final output or stop-reason error. | Parent input grows by one data-dependent result retained until compaction. | ## Known Limitations and Deferred Work diff --git a/packages/subagent/tool-subagent/README.md b/packages/subagent/tool-subagent/README.md index 8c0f5460d4..416c24af5a 100644 --- a/packages/subagent/tool-subagent/README.md +++ b/packages/subagent/tool-subagent/README.md @@ -31,7 +31,7 @@ A non-`completed` stop reason becomes an `isError` tool result; partial child ou | Context surface | What the model sees | Token effect | |---|---|---| -| Tool schema | While the configured provider exists, the parent model sees one `{ description, prompt }` tool under `toolName`. Its description explicitly says whether the child inherits completed turns or needs a standalone prompt; persona, model, filter, depth, and provider choice remain deployment config. The filter changes the child's visible global tools, not an inherited authority ceiling. | Fixed schema cost per parent request while mounted. Removing the provider removes the whole schema; exposing multiple providers adds one independently named schema per load. | +| Tool schema | While the configured provider exists, the parent model sees one `{ description, prompt }` tool under `toolName`. Its description explicitly says whether the child inherits completed turns or needs a standalone prompt; persona, model, filter, depth, and provider choice remain deployment config. For a capable in-process provider, the filter changes the child's global wire schemas, lookup, execution, and Code Mode SDK bindings, not standalone guidance or an inherited authority ceiling. | Fixed schema cost per parent request while mounted. Removing the provider removes the whole schema; exposing multiple providers adds one independently named schema per load. | | Tool-call history and result | The task description and full prompt remain in the parent assistant tool call. The result contains only the child's final text or a stop-reason error, never intermediate child steps. | Prompt and final output are data-dependent retained tokens. All child working context is paid in the child and omitted from the parent. | ## Known Limitations and Deferred Work diff --git a/packages/web/tool-web/README.md b/packages/web/tool-web/README.md index 9b821aaf82..a8b49180f8 100644 --- a/packages/web/tool-web/README.md +++ b/packages/web/tool-web/README.md @@ -38,8 +38,8 @@ The tool never calls a provider's `status()` and never enumerates providers — | Context surface | What the model sees | Token effect | |---|---|---| -| System prompt | Each enabled tool adds one short section: search guidance says to discover current sources and follow with fetch; fetch guidance says to retrieve a specific HTTP(S) URL and cite it. | Fixed guidance cost per request for each enabled tool. | -| Tool schemas | According to config, the model sees `web_search(query)`, `web_fetch(url)`, or both. Result-count and timeout budgets are deployment settings, not model arguments. | Fixed schema cost per request; disabling a tool removes its schema and guidance. | +| System prompt | Each config-enabled tool adds one short section: search guidance says to discover current sources and follow with fetch; fetch guidance says to retrieve a specific HTTP(S) URL and cite it. A scoped tool restriction does not remove these independently registered sections. | Fixed guidance cost per request for each config-enabled tool, even when a restriction hides its schema. | +| Tool schemas | According to config and scoped restrictions, the model sees `web_search(query)`, `web_fetch(url)`, or both. Result-count and timeout budgets are deployment settings, not model arguments. | Fixed schema cost per request; config disablement removes both schema and guidance, while a scoped restriction removes only the schema. | | Tool-call history and results | Search returns an optional answer and bounded source entries; fetch returns status plus decoded text or markdown-shaped HTML, or a structured error. Queries and URLs remain in call history. | Data-dependent results are resent until compaction. Search sources are capped by `searchMaxResults`; fetch providers cap body size, and timeout policy can replace a late result with a short error. | ## Known Limitations and Deferred Work diff --git a/packages/workflow/tool-workflow/README.md b/packages/workflow/tool-workflow/README.md index 83b46a6852..9fd79b5d52 100644 --- a/packages/workflow/tool-workflow/README.md +++ b/packages/workflow/tool-workflow/README.md @@ -1,6 +1,6 @@ # @deepseek-ai/dsh-tool-workflow -The model-facing **`workflow` tool**: run a JavaScript orchestration script that fans out subagents, and return the script's final value. Pure schema + lifecycle shaping over [`ctx.workflows`](../workflow/README.md) — script parsing, execution, caps, and cancellation live behind the seam, so a hardened engine swaps in without touching what the model sees. +The model-facing **`workflow` tool**: run a JavaScript orchestration script that fans out subagents, and return the script's final value. This package owns schema and lifecycle shaping over [`ctx.workflows`](../workflow/README.md); script parsing, execution, caps, and cancellation live behind the seam, while the consumer retains ownership of the parent-facing schema and result envelope. ## What the model sees @@ -25,7 +25,8 @@ Decided up front (per the [render-intent RFC](../../../docs/rfc/implemented/arch | Context surface | What the model sees | Token effect | |---|---|---| -| System prompt and tool schema | The parent model receives a short use-only-for-large-orchestration section plus the `workflow` schema. The schema description carries the complete JavaScript hook and metadata contract; the model submits script, metadata, and optional args. | Substantial but fixed per-request guidance and schema cost while visible. | +| System prompt | Every parent request in this plugin's registration scope receives a short use-only-for-large-orchestration section. A scoped tool restriction can hide the schema without removing this independently registered guidance. | Small fixed guidance cost per request while the plugin is active. | +| Tool schema | When visible, the `workflow` schema description carries the complete JavaScript hook and metadata contract; the model submits script, metadata, and optional args. | Substantial fixed schema cost on each request where the tool is visible. | | Tool-call history and result | The full model-written script, metadata, and args remain in the assistant tool call. The result contains the workflow name, child count, and final JSON value or a shaped error; intermediate child messages are omitted. | Call tokens can be large and remain until compaction. Result rendering is capped by `maxResultChars`; child-model tokens are separate from the parent's retained context. | ## Known Limitations and Deferred Work diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts index 3144c0e560..33f383b4f1 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -10,16 +10,41 @@ import { relative, resolve } from 'node:path' const root = resolve(import.meta.dirname, '..') const HEADING = '## Model Experience' -const HEADING_PATTERN = /^## Model Experience$/gm const LIMITATIONS_HEADING = '## Known Limitations and Deferred Work' const TABLE_HEADER = '| Context surface | What the model sees | Token effect |' const TABLE_DIVIDER = '|---|---|---|' +const H2_HEADING = /^## .+$/ interface Failure { path: string message: string } +interface Line { + index: number + raw: string +} + +/** Split Markdown into prose lines, excluding fenced code that may quote the contract. */ +function proseLines(text: string): Line[] { + let fence: { marker: '`' | '~'; length: number } | undefined + const kept: Line[] = [] + text.split('\n').forEach((raw, i) => { + const token = /^ {0,3}(`{3,}|~{3,})/.exec(raw)?.[1] + if (token !== undefined) { + const marker = token[0] as '`' | '~' + if (fence === undefined) { + fence = { marker, length: token.length } + } else if (marker === fence.marker && token.length >= fence.length) { + fence = undefined + } + return + } + if (fence === undefined) kept.push({ index: i + 1, raw }) + }) + return kept +} + const failures: Failure[] = [] const packageJsons = globSync('packages/*/*/package.json', { cwd: root }).sort() @@ -31,25 +56,20 @@ for (const packageJson of packageJsons) { continue } - const source = readFileSync(abs, 'utf8') - const matches = [...source.matchAll(HEADING_PATTERN)] - if (matches.length !== 1) { + const lines = proseLines(readFileSync(abs, 'utf8')) + const h2Headings = lines.filter(line => H2_HEADING.test(line.raw)) + const modelHeadings = h2Headings.filter(line => line.raw === HEADING) + if (modelHeadings.length !== 1) { failures.push({ path: readme, - message: matches.length === 0 ? `missing ${HEADING}` : `contains ${matches.length} copies of ${HEADING}`, + message: modelHeadings.length === 0 ? `missing ${HEADING}` : `contains ${modelHeadings.length} copies of ${HEADING}`, }) continue } - const match = matches[0] - if (match?.index === undefined) { - failures.push({ path: readme, message: `could not locate ${HEADING}` }) - continue - } - const headingIndex = match.index - const h2Headings = [...source.matchAll(/^## .+$/gm)] - const modelH2Index = h2Headings.findIndex(heading => heading.index === headingIndex) - const limitationsH2Index = h2Headings.findIndex(heading => heading[0] === LIMITATIONS_HEADING) + const modelHeading = modelHeadings[0] as Line + const modelH2Index = h2Headings.indexOf(modelHeading) + const limitationsH2Index = h2Headings.findIndex(heading => heading.raw === LIMITATIONS_HEADING) if (limitationsH2Index >= 0) { if (modelH2Index !== h2Headings.length - 2 || limitationsH2Index !== h2Headings.length - 1) { failures.push({ @@ -63,25 +83,30 @@ for (const packageJson of packageJsons) { continue } - const bodyStart = headingIndex + HEADING.length - const nextHeadingOffset = source.slice(bodyStart).search(/^## /m) - const section = source.slice(bodyStart, nextHeadingOffset < 0 ? undefined : bodyStart + nextHeadingOffset) - const lines = section.split('\n') - const headerIndex = lines.indexOf(TABLE_HEADER) - if (headerIndex < 0 || lines[headerIndex + 1] !== TABLE_DIVIDER) { + const body = lines.slice(lines.indexOf(modelHeading) + 1) + const nextH2 = body.findIndex(line => H2_HEADING.test(line.raw)) + const section = nextH2 < 0 ? body : body.slice(0, nextH2) + const headers = section.filter(line => line.raw === TABLE_HEADER) + const header = headers[0] + const headerIndex = header === undefined ? -1 : section.indexOf(header) + if (headers.length !== 1 || headerIndex < 0 || section[headerIndex + 1]?.raw !== TABLE_DIVIDER) { failures.push({ path: readme, message: `must contain the exact table header ${TABLE_HEADER}` }) continue } - const rows = lines.slice(headerIndex + 2).filter(line => line.startsWith('|')) + const rows: Line[] = [] + for (const line of section.slice(headerIndex + 2)) { + if (!line.raw.startsWith('|')) break + rows.push(line) + } if (rows.length === 0) { failures.push({ path: readme, message: 'Model Experience table must contain at least one data row' }) continue } for (const row of rows) { - const cells = row.split('|').slice(1, -1).map(cell => cell.trim()) + const cells = row.raw.split('|').slice(1, -1).map(cell => cell.trim()) if (cells.length !== 3 || cells.some(cell => cell.length === 0)) { - failures.push({ path: readme, message: `invalid three-column Model Experience row: ${row}` }) + failures.push({ path: readme, message: `line ${row.index}: invalid three-column Model Experience row: ${row.raw}` }) } } }