From 027970043c30048048433fea95e7e79a55bd7a4b Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 12 Jul 2026 02:12:36 +0800 Subject: [PATCH] docs: document package model experience --- docs/AGENTS.md | 11 ++- docs/cookbook/adding-a-package.md | 5 +- docs/rfc/INDEX.md | 1 + ...07-12-package-model-experience-contract.md | 27 +++++++ package.json | 3 +- packages/AGENTS.md | 1 + packages/README.md | 2 +- packages/bash/bash-local/README.md | 6 ++ packages/bash/bash-sandbox/README.md | 7 ++ packages/bash/bash/README.md | 6 ++ packages/bash/tool-bash/README.md | 8 ++ .../code-runtime-worker/README.md | 6 ++ packages/code-runtime/code-runtime/README.md | 6 ++ packages/compact/compact-basic/README.md | 7 ++ packages/compact/compact/README.md | 6 ++ packages/cordis/tool-cordis/README.md | 8 ++ packages/core/agent-core/README.md | 7 ++ packages/core/agent-loop/README.md | 7 ++ packages/core/agent/README.md | 7 ++ packages/core/scope/README.md | 6 ++ packages/core/session/README.md | 7 ++ packages/core/system-prompt/README.md | 7 ++ packages/core/tools/README.md | 7 ++ packages/fs/fs-local/README.md | 6 ++ packages/fs/fs-policy/README.md | 6 ++ packages/fs/fs/README.md | 6 ++ packages/fs/tool-fs/README.md | 8 ++ packages/guard/repeat-tool-guard/README.md | 6 ++ packages/hooks/hook-protocol/README.md | 6 ++ packages/hooks/hooks-claude/README.md | 7 ++ packages/hooks/hooks-codex/README.md | 7 ++ packages/llm/llm-deepseek/README.md | 7 ++ packages/llm/llm-pi-ai/README.md | 7 ++ packages/llm/llm/README.md | 7 ++ packages/sandbox/sandbox-local/README.md | 6 ++ packages/sandbox/sandbox/README.md | 6 ++ .../session-persistence-jsonl/README.md | 6 ++ .../session-persistence-sqlite/README.md | 6 ++ .../session-persistence/README.md | 6 ++ packages/skill/skill-local/README.md | 6 ++ packages/skill/skill/README.md | 6 ++ packages/skill/tool-skill/README.md | 7 ++ packages/subagent/subagent-acp/README.md | 7 ++ packages/subagent/subagent-fork/README.md | 7 ++ .../subagent/subagent-inprocess/README.md | 7 ++ packages/subagent/subagent-spawn/README.md | 7 ++ .../subagent/subagent-subprocess/README.md | 6 ++ packages/subagent/subagent/README.md | 6 ++ packages/subagent/tool-subagent/README.md | 7 ++ packages/support/acp-snapshot/README.md | 6 ++ packages/support/invariants/README.md | 6 ++ packages/support/llm-replay/README.md | 6 ++ packages/support/subagent-mock/README.md | 6 ++ packages/timeout/timeout-policy/README.md | 6 ++ packages/todo/tool-todo/README.md | 7 ++ packages/ui/acp-agent/README.md | 6 ++ packages/ui/acp/README.md | 8 ++ packages/ui/app-boot/README.md | 6 ++ packages/ui/stdio-agent/README.md | 6 ++ packages/ui/tool-ask-user/README.md | 7 ++ packages/ui/user-approval/README.md | 7 ++ packages/ui/user-interaction/README.md | 6 ++ packages/util/brand/README.md | 6 ++ packages/util/timeout/README.md | 6 ++ packages/web/tool-web/README.md | 8 ++ packages/web/web-fetch-local/README.md | 6 ++ packages/web/web-search-deepseek/README.md | 7 ++ packages/web/web-search-exa/README.md | 6 ++ packages/web/web-search-perplexity/README.md | 7 ++ packages/web/web/README.md | 6 ++ packages/workflow/tool-workflow/README.md | 9 ++- .../workflow/workflow-workerthread/README.md | 7 ++ packages/workflow/workflow/README.md | 6 ++ scripts/run-gates.ts | 1 + .../verify-package-readme-model-experience.ts | 81 +++++++++++++++++++ 75 files changed, 561 insertions(+), 5 deletions(-) create mode 100644 docs/rfc/implemented/process/2026-07-12-package-model-experience-contract.md create mode 100644 scripts/verify-package-readme-model-experience.ts diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 6824c71cc6..239af47e48 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -15,7 +15,7 @@ Every fact has exactly one home — the tier whose job it is — and every other | [rfc/](rfc/README.md) | Decision records: the why and the what-was-given-up; `implemented/` RFCs describe shipped reality in present tense | Migration plans, test checklists, and spec-speak ("should…") once the decision has shipped | | [postmortem/](postmortem/README.md) | Incident stories — the only tier where war-story narrative belongs | — | | [cookbook/](cookbook/adding-a-package.md) | Step-by-step how-tos with numbered verify steps | Design rationale (→ the RFC each guide links) | -| Package README | The per-package contract: config, semantics, limitations, extension points | JSDoc restatement, generated-catalog restatement (event/tool tables), other packages' concerns | +| Package README | The per-package contract: config, semantics, limitations, extension points, and [Model Experience](#package-model-experience) | JSDoc restatement, generated-catalog restatement (event/tool tables), other packages' concerns | | [development.md](development.md) | First-stop contributor onboarding: local setup, daily workflow, and CI shape at summary level; a bilingual pair under the [i18n contract](i18n/README.md) | Runtime/version rationale (→ RFCs), gate-by-gate enumerations that drift from `package.json` scripts | | Generated catalogs: [cordis events](cordis-catalog/events.md), [cordis services](cordis-catalog/services.md), [tool-catalog](tool-catalog.md), [config-catalog](config-catalog.md), [persistence-catalog](persistence-catalog.md), [module-graph.md](module-graph.md) | Exhaustive enumerations regenerated from source, freshness-gated | Hand edits of any kind | | Skills (`.agents/skills/`) | Workflows: how to carry out a recurring task against the contracts | The contracts themselves (→ docs) | @@ -33,6 +33,15 @@ Placement test: a story about a bug → postmortem. Why we chose X → RFC. How - **Bilingual pairs update together**: editing either side obligates the counterpart and a re-record in the same change ([i18n contract](i18n/README.md)). - Your audience is professional programmers. Prefer concise and straight-forward English over metaphor. Do not overuse words like "gate", "vocabulary", "surface", "seams". +## Package Model Experience + +Every package README carries `## Model Experience` and this table: + +| Context surface | What the model sees | Token effect | +|---|---|---| + +Rows name the request surface and condition, including agent scope, then classify tokens as fixed per request, conditional per call, retained, replaced, capped, or zero-direct. Distinguish the conversation model from auxiliary calls. A zero-direct package names its indirect path. `verify-package-readme-model-experience` enforces shape; review owns accuracy. + ## Wordcount Budgets Every PR has a lesson it wants to append, and without pressure nothing leaves. [scripts/doc-budgets.manifest.json](../scripts/doc-budgets.manifest.json) stores the allowed word-count ceiling for each budgeted standing doc; `pnpm run verify-doc-budgets` fails when a doc exceeds its ceiling or a budgeted file is missing. diff --git a/docs/cookbook/adding-a-package.md b/docs/cookbook/adding-a-package.md index 1ab6931696..5a17e572d2 100644 --- a/docs/cookbook/adding-a-package.md +++ b/docs/cookbook/adding-a-package.md @@ -13,9 +13,11 @@ packages/// # you use Config, + ../..// for each dsh dep) src/index.ts # service default export or plugin (name/inject/apply/Config) tests/.spec.ts - README.md # service API, events, extension points, design notes + README.md # contract plus the required Model Experience table ``` +Fill the canonical [Model Experience table](../AGENTS.md#package-model-experience) from the implementation: name every direct request contribution and token-growth condition, or state zero direct tokens and the exact indirect path. Every package participates, including type-only libraries and backend seams. + Choose an existing group when one matches the package's role (`core`, `llm`, `bash`, `compact`, `subagent`, `todo`, `session-persistence`, `ui`, `util`, or `support`). A new group is allowed, but it is a pure container: no `package.json`, no source files, and packages still sit exactly one level below it. package.json invariants (enforced by `pnpm run constraints` / `scripts/check-workspace-constraints.ts`): `private: true`, `version: 0.0.1`, `type: module`, `main: "lib/index.js"`, `types: "lib/types/index.d.ts"`, `exports["."].types: "./lib/types/index.d.ts"`, `exports["."].default: "./lib/index.js"`, `cordis` in BOTH peerDependencies and devDependencies (same range). Mirror every dsh peer dependency in devDependencies. `schemastery` goes in `dependencies` (it is a runtime validator), matching agent-loop. The `files` list is precise: `lib/index.js`, `lib/types/**/*.d.ts`, `lib/types/**/*.d.ts.map`, and `src`; do not publish `lib/types` JS or JS-map intermediates or stale root declaration files. CLI app packages with a package `bin` include `lib/bin.js` immediately after `lib/index.js` in `files`. @@ -41,6 +43,7 @@ For a swappable capability, split interface / implementation / consumer into sep ```sh pnpm install # registers the workspace +pnpm run verify-package-readme-model-experience pnpm run constraints && pnpm run typecheck && pnpm run lint pnpm run test:coverage # 100% per-file over src (types.ts exempt) pnpm run build && pnpm run hygiene diff --git a/docs/rfc/INDEX.md b/docs/rfc/INDEX.md index 654b68ffb5..6f684de03e 100644 --- a/docs/rfc/INDEX.md +++ b/docs/rfc/INDEX.md @@ -161,6 +161,7 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand; | [Raise the Node LTS engine floor to 22.19](implemented/process/2026-07-06-node-engine-floor.md) | 2026-07-06 | | [Parallel GitHub CI gates](implemented/process/2026-07-06-parallel-github-ci-gates.md) | 2026-07-06 | | [Parallel pre-push gates](implemented/process/2026-07-06-parallel-pre-push-gates.md) | 2026-07-06 | +| [Package Model Experience contract](implemented/process/2026-07-12-package-model-experience-contract.md) | 2026-07-12 | ### Testing 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 new file mode 100644 index 0000000000..755bb5a80b --- /dev/null +++ b/docs/rfc/implemented/process/2026-07-12-package-model-experience-contract.md @@ -0,0 +1,27 @@ +# RFC: Package Model Experience contract + +Status: implemented + +## Problem + +A package README can explain APIs and runtime mechanics without answering the question that dominates an agent harness's behavior and cost: what from this package reaches a model request, under which conditions, and how long those tokens remain. The omission is especially hard to audit in a plugin architecture. A consumer may turn a backend result into a tool message, a policy plugin may replace success with an error, compaction may remove old history, and an agent-scoped registration may change one agent's prompt or schemas while leaving every other agent unchanged. Reading only the nominally model-facing packages therefore misses real context effects, while reading source across every dependency is too expensive for routine review. + +## Decision + +Every workspace package README carries the canonical [Model Experience table](../../../AGENTS.md#package-model-experience). 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 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, one exact `## Model Experience` heading, the canonical 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. + +## Alternatives considered + +- **Document only packages that register prompts or tools** — rejected because backends, policy plugins, adapters, persistence, scoping, and compaction change the content or lifetime of tokens without owning a model-facing schema. +- **Generate one central context-cost catalog from source** — rejected because an AST can find registrations but cannot infer semantic conditions such as history retention, output truncation, parent-versus-child visibility, or an auxiliary model boundary. The package README is the implementation-local contract; a central copy would add another drift surface. +- **Require numeric token counts** — rejected because exact counts depend on the selected model tokenizer, adapter serialization, configuration, and runtime data. The stable contract is the growth shape: fixed per request, conditional per call, retained, replaced, capped, or zero-direct. +- **Allow zero-impact packages to omit the section** — rejected because absence is ambiguous between an audited zero and forgotten documentation. One explicit row is cheap and mechanically distinguishable. +- **Convention without a gate** — rejected because a repo-wide contract must also cover every future package; review memory cannot reliably detect an omitted README section. + +## Consequences + +A reviewer can start at any package and see its contribution to the conversation model, child models, and auxiliary calls without reconstructing the full plugin graph. Token-budget work can distinguish repeated request overhead from data-dependent history, and agent-scoped changes have an explicit documentation checkpoint. Package authors pay for one small table and must update it whenever model-visible behavior changes. The table deliberately does not promise provider-exact token counts; measurements remain model- and workload-specific, while the documented growth and visibility contract stays stable. diff --git a/package.json b/package.json index ed0b603feb..f4428f0df0 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "verify-md-links": "tsx scripts/verify-md-links.ts", "verify-doc-refs": "tsx scripts/verify-doc-refs.ts", "verify-package-paths": "tsx scripts/verify-package-paths.ts", + "verify-package-readme-model-experience": "tsx scripts/verify-package-readme-model-experience.ts", "verify-mermaid": "tsx scripts/verify-mermaid.ts", "verify-rfc-classification": "tsx scripts/verify-rfc-classification.ts", "verify-rfc-format": "tsx scripts/verify-rfc-format.ts", @@ -63,7 +64,7 @@ "verify-scoped-dispatch": "tsx scripts/verify-scoped-dispatch.ts", "verify-module-graph": "tsx scripts/gen-module-graph.ts --check", "constraints": "tsx scripts/check-workspace-constraints.ts", - "doc-sync": "pnpm run doc-typecheck && pnpm run verify-cordis-catalog && pnpm run verify-cordis-api && pnpm run verify-export-jsdoc && pnpm run verify-tool-catalog && pnpm run verify-config-catalog && pnpm run verify-persistence-catalog && pnpm run verify-doc-graphs && pnpm run verify-scoped-dispatch && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-mermaid && pnpm run verify-rfc-classification && pnpm run verify-rfc-format && pnpm run verify-type-equiv && pnpm run verify-translation-pairing && pnpm run verify-doc-budgets", + "doc-sync": "pnpm run doc-typecheck && pnpm run verify-cordis-catalog && pnpm run verify-cordis-api && pnpm run verify-export-jsdoc && pnpm run verify-tool-catalog && pnpm run verify-config-catalog && pnpm run verify-persistence-catalog && pnpm run verify-doc-graphs && pnpm run verify-scoped-dispatch && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-package-readme-model-experience && pnpm run verify-mermaid && pnpm run verify-rfc-classification && pnpm run verify-rfc-format && pnpm run verify-type-equiv && pnpm run verify-translation-pairing && pnpm run verify-doc-budgets", "hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-node-next-types", "demo:echo": "node --expose-internals --import tsx packages/ui/stdio-agent/src/bin.ts examples/echo-agent/cordis.yml", "demo:repl": "node --expose-internals --import tsx packages/ui/stdio-agent/src/bin.ts examples/coding-agent/cordis.yml", diff --git a/packages/AGENTS.md b/packages/AGENTS.md index ac26b926f7..2ae11e83ae 100644 --- a/packages/AGENTS.md +++ b/packages/AGENTS.md @@ -12,5 +12,6 @@ Naming notes: - `src/types.ts` contains only types — no runtime code. - Tests live at package level under `tests/`, not `src/__tests__/`. - A package's README and JSDoc are part of the change: altered behavior (config keys, defaults, error codes, wire fields) updates them in the same commit. `doc-sync` gates what it can; prose accuracy stays on the author ([the documentation standard](../docs/AGENTS.md)). +- Every package README carries the [Model Experience table](../docs/AGENTS.md#package-model-experience); update it when code changes a prompt section, session prefix, tool schema/result, history rewrite, auxiliary model call, or indirect visibility policy. Read the per-package README.md for package-specific details: service API, events, extension points, TODOs. diff --git a/packages/README.md b/packages/README.md index 35c4652a1f..651ba370e6 100644 --- a/packages/README.md +++ b/packages/README.md @@ -37,4 +37,4 @@ The inter-package dependency graph is generated: [docs/module-graph.md](../docs/ The rule it must obey: **extension plugins depend on interfaces, never on the concrete loop.** `dsh-agent-loop` is swappable — UI/hook/tool plugins keep working against the `dsh-agent` vocabulary if the loop is replaced. The sanctioned exception is a **composition/bundle** package like `dsh-agent-core`, whose whole job is to assemble the concrete spine: it depends on `dsh-agent-loop` (and the other concrete spine plugins) on purpose. The rule constrains plugins that EXTEND the system, not the bundle that COMPOSES it. A swappable capability splits into interface / implementation / consumer packages (the bash trio is the template — see [capability seams](../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md)). -Each package has its own `README.md` with purpose, service API, events, extension points, and deliberate non-goals (TODOs). +Each package has its own `README.md` with purpose, service API, events, extension points, deliberate non-goals, and the standard [Model Experience table](../docs/AGENTS.md#package-model-experience). diff --git a/packages/bash/bash-local/README.md b/packages/bash/bash-local/README.md index 1ac8a7d06b..8643fd2652 100644 --- a/packages/bash/bash-local/README.md +++ b/packages/bash/bash-local/README.md @@ -2,6 +2,12 @@ Local-subprocess implementation of the `@deepseek-ai/dsh-bash` executor seam: `LocalBashExecutor` spawns `bash -c ` per call in its own process group, collects bounded output with full-stream spill files, and escalates kills SIGTERM→SIGKILL across the whole group. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Bash tool results, indirectly | Through `dsh-tool-bash`, the conversation model sees the retained stdout and stderr tail, exit and timeout markers, background-task state, and a spill-file path when full output is available. This backend adds no prompt or schema itself. | Zero tokens until a bash tool runs. Foreground output is bounded per stream by `maxOutputBytes`; background reads return only new output, so polling does not repeat already-delivered text. Results remain in history until compaction. | + ## Config ```yaml diff --git a/packages/bash/bash-sandbox/README.md b/packages/bash/bash-sandbox/README.md index 2a5ae63b8e..fc83753774 100644 --- a/packages/bash/bash-sandbox/README.md +++ b/packages/bash/bash-sandbox/README.md @@ -31,3 +31,10 @@ Deny-only at the seam: a denial is a reported fact, and this executor never nego ``` The keyless consumer-integration proofs are `tests/bwrap.e2e.ts`, `tests/landlock.e2e.ts`, and `tests/seatbelt.e2e.ts` (the real provider + real runner driven through `ctx.bash`, world-verified, each self-skipping where its runner is absent); see [`examples/sandbox-acp-agent`](../../../examples/sandbox-acp-agent/) for the runnable demo. + +## Model Experience + +| 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 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. | diff --git a/packages/bash/bash/README.md b/packages/bash/bash/README.md index ac7e00c3a3..34accb1c69 100644 --- a/packages/bash/bash/README.md +++ b/packages/bash/bash/README.md @@ -13,6 +13,12 @@ This package is the interface quarter of the bash capability, split so each conc 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. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| None directly | This interface registers no prompt, tool schema, or message. `dsh-tool-bash` turns an implementation's stdout, stderr, task state, and sandbox facts into model-visible tool results and guidance. | Zero direct tokens. Result size and sandbox state affect input tokens only when a consumer renders them. | + ## Service API (`ctx.bash`) | Member | Semantics | diff --git a/packages/bash/tool-bash/README.md b/packages/bash/tool-bash/README.md index 800de0132c..67007bc5cc 100644 --- a/packages/bash/tool-bash/README.md +++ b/packages/bash/tool-bash/README.md @@ -6,6 +6,14 @@ Requires a loaded executor implementation (e.g. `@deepseek-ai/dsh-bash-local`); 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). +## Model Experience + +| 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. | +| 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. | + ## Tools ### `bash` diff --git a/packages/code-runtime/code-runtime-worker/README.md b/packages/code-runtime/code-runtime-worker/README.md index f691904e88..a2ebd2f496 100644 --- a/packages/code-runtime/code-runtime-worker/README.md +++ b/packages/code-runtime/code-runtime-worker/README.md @@ -2,6 +2,12 @@ Worker-thread implementation of the [`@deepseek-ai/dsh-code-runtime`](../code-runtime/README.md) seam: `WorkerCodeRuntime` runs each program in ONE fresh Node `worker_threads.Worker` — TypeScript in, type-stripped host-side, bindings bridged over the message port, `{ value, logs, error? }` out. **Containment, not a security boundary**: trust posture is bash-equivalent by design (the [Code Mode RFC](../../../docs/rfc/implemented/feature/2026-06-15-code-mode.md) § Trust posture), with containment bash does not have — separate isolate, empty environment, heap cap, hard termination. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| `run_code` result, indirectly | The conversation model sees only what the model-written program prints or returns, or a shaped failure; binding-call traffic and worker internals stay outside its context. This backend contributes no schema or prompt itself. | Zero tokens until Code Mode executes a program. `maxLogBytes` and `maxValueBytes` cap the model-visible result, which then remains in tool history until compaction. | + ## Config ```yaml diff --git a/packages/code-runtime/code-runtime/README.md b/packages/code-runtime/code-runtime/README.md index 20c9274b9c..4c66881683 100644 --- a/packages/code-runtime/code-runtime/README.md +++ b/packages/code-runtime/code-runtime/README.md @@ -4,6 +4,12 @@ The **code-execution seam**: an abstract `CodeRuntime` service (`ctx.codeRuntime This package is the interface third of the capability (the bash trio is the template — see [capability seams](../../../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md)): implementations subclass `CodeRuntime` and register the service; the consumer is the tool registry's Code Mode, which generates the model-facing SDK and bridges tool dispatch — both specified in the [Code Mode RFC](../../../docs/rfc/implemented/feature/2026-06-15-code-mode.md), whose first implementation is a Node worker-thread backend. The runtime knows nothing about tools or sessions: it is handed named async functions and a program string, and everything tool-shaped stays with the consumer. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| None directly | The seam receives a program and host bindings but registers no prompt, schema, or message. Code Mode in `dsh-tools` exposes the SDK and `run_code`, then converts `CodeRunResult` into the outer tool result. | Zero direct tokens. Program logs, values, and failures affect the conversation only through the Code Mode consumer. | + ## Service API (`ctx.codeRuntime`) | Member | Semantics | diff --git a/packages/compact/compact-basic/README.md b/packages/compact/compact-basic/README.md index a06e74b818..3b639802b6 100644 --- a/packages/compact/compact-basic/README.md +++ b/packages/compact/compact-basic/README.md @@ -4,6 +4,13 @@ The **basic compaction backend**: a `BasicCompactService` implementing the `@dee This is the implementation tier of the compaction capability — see the [interface package](../compact/README.md) for the seam and the [capability-seam RFC](../../../docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md) for the design. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Conversation history | Before a step whose estimated system prompt, session prefix, and history exceed the threshold, the conversation model receives one framed summary checkpoint in place of the older balanced surface range, followed by the retained recent units. | The replacement reduces future input history rather than appending a second copy. The summary remains until a later compaction replaces it; one oversized indivisible unit can still exceed the budget. | +| Auxiliary summarizer request | The summarization model sees a fixed checkpoint-writing system instruction and a flattened transcript of the selected range. The conversation model never sees this private request or its reasoning; only returned text is stored. | This is a separate model call with data-dependent input and `maxTokens`-capped output. Convergence retries can pay this cost more than once. | + ## What it owns The abstract contract states only WHAT compaction does; this backend owns every HOW decision: diff --git a/packages/compact/compact/README.md b/packages/compact/compact/README.md index e98f00899f..5842187e02 100644 --- a/packages/compact/compact/README.md +++ b/packages/compact/compact/README.md @@ -12,6 +12,12 @@ This package is the interface tier of the compaction capability, split so each c Unlike the bash seam, this interface depends on `@deepseek-ai/dsh-session` and `@deepseek-ai/dsh-llm` — the contract's verbs are defined over a `Session` and its output is the `ContentBlock` vocabulary, so they cannot be expressed without naming those packages. That deviation from the "interface depends only on cordis" guidance is intentional and recorded in the [compaction capability-seam RFC](../../../docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md). +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Conversation history, when a backend is invoked | A successful implementation replaces an older surface range with one user-role summary checkpoint; the raw events stay logged but stop appearing in derived model messages. The seam itself performs no rewrite. | Zero direct tokens from this interface. A backend trades many retained history tokens for one summary and leaves the recent tail unchanged. | + ## Service API (`ctx.compact`) Both methods are **abstract** — the backend owns the entire strategy (token estimation, retention policy, event sequencing, summarization). diff --git a/packages/cordis/tool-cordis/README.md b/packages/cordis/tool-cordis/README.md index c4f3cf11b9..45fefda540 100644 --- a/packages/cordis/tool-cordis/README.md +++ b/packages/cordis/tool-cordis/README.md @@ -2,6 +2,14 @@ The self-referential cordis toolset: three model-facing tools over the live runtime the agent runs inside. Design home — sandbox semantics, mount lifecycle, cross-mount composition, the generated API catalog, standing decisions: [the toolset RFC](../../../docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Tool schemas | The conversation model sees `cordis_inspect`, `cordis_mount`, and `cordis_unmount` whenever this plugin is visible. | Fixed schema cost on every request in that tool view. | +| Tool-call history and results | Inspect returns selected live services, plugins, tools, or generated API and event references; mount and unmount return lifecycle facts or structured errors. The submitted mount program remains in the assistant tool-call history. | Inspect output and mount code are data-dependent and resent until compaction; lifecycle acknowledgements are small. | +| Later requests after a mount | A mounted plugin may register tools, prompt contributions, or listeners that change later requests for the scopes it targets; unmount removes those contributions after quiescence. | Indirect token impact equals the mounted plugin's contributions and lasts only for the mount lifetime. | + ## What it does - `cordis_inspect` — read-only report over the runtime: services, the loaded-plugin list, registered tools, the dynamic-mount table, and the catalog-backed `api` / `events` references. diff --git a/packages/core/agent-core/README.md b/packages/core/agent-core/README.md index 33cd0faddd..1cc957e82b 100644 --- a/packages/core/agent-core/README.md +++ b/packages/core/agent-core/README.md @@ -4,6 +4,13 @@ The **default executor-less, UI-less agent spine** as ONE Cordis bundle plugin. This is the package to read to see **the whole plugin tree at once** — the teaching role the inlined `echo-agent` `cordis.yml` used to play before the spine moved behind this bundle. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Composed system prompt and session prefix | Through its children, the bundle supplies the harness identity, configured persona, and the local skill catalog when skills exist. | The bundle adds no wrapper prose; input cost is exactly the sum of the child contributions, repeated on each request according to their lifecycles. | +| Composed tool surface | The `skill` schema is present, and the three bash schemas appear when a bash executor activates `dsh-tool-bash`; `tools` config can select normal, Code Mode, or both. | Fixed per-request schema or SDK cost for the visible composition. Tool results add data-dependent retained history. | + ## The tree it loads `apply(ctx, config)` mounts each of these as a child of the bundle fiber: diff --git a/packages/core/agent-loop/README.md b/packages/core/agent-loop/README.md index 217ba0a643..df44a6dd05 100644 --- a/packages/core/agent-loop/README.md +++ b/packages/core/agent-loop/README.md @@ -4,6 +4,13 @@ THE concrete agent plugin: `ReactLoopAgent` and the loop driver. Implements the This is the only package in the harness that contains concrete loop logic. Everything else is an abstract service or a plugin against extension seams — new behavior goes into plugins, not here. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Complete conversation request | For each step, the loop sends the rendered per-agent system prompt, visible tool schemas, the frozen session prefix, and the session's derived messages. It supplies `model` and `cwd` variable values but no additional fixed prose. | System text, schemas, and prefix are paid again on every step. Per-agent scoping can substitute or remove individual contributions. | +| Retained message history | Accepted user messages, assistant messages, tool calls and results, injected context, and steering are logged and sent on later steps. Raw stream chunks, lifecycle boundaries, and other log-only events are excluded. | Input grows with every surface message until a compaction replacement shadows older nodes; a multi-step tool turn resends the accumulated prefix and history each step. | + ## Service: `AgentLoop` (ctx key: `agentLoop`) ### Public API diff --git a/packages/core/agent/README.md b/packages/core/agent/README.md index 71e2be279d..e66f4dae6d 100644 --- a/packages/core/agent/README.md +++ b/packages/core/agent/README.md @@ -2,6 +2,13 @@ Agent interface, registry, and `agent/*` event vocabulary. Every plugin (UI, hooks, orchestrators) programs against the `Agent` handle defined here — it has zero loop dependency, so the loop is swappable. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| User, steering, and injected messages | `send`, `steer`, and `inject` feed the owning session. `agent/prompt-submit`, `agent/session-prefix`, and other declared events let plugins block a prompt or add request material; this interface contributes no fixed prose itself. | Accepted content becomes retained history or a repeated session prefix; blocked content contributes no request tokens. Size is caller- and plugin-dependent. | +| Agent-scoped request composition | Registrations through `agent.ctx` can shadow prompt sections or tools and can install agent-only interceptors during unpublished setup. | The package adds zero tokens itself; scoped contributions affect only that agent and disappear on disposal. | + ## Service: `AgentRegistry` (ctx key: `agents`) Tracks live agents so UI, hook, and orchestrator plugins can find them without importing the concrete loop package. diff --git a/packages/core/scope/README.md b/packages/core/scope/README.md index e57f475816..887689d375 100644 --- a/packages/core/scope/README.md +++ b/packages/core/scope/README.md @@ -2,6 +2,12 @@ Scoped-context registration primitive. `createScope(ctx, key)` mints a Cordis context that TAGS everything registered through it with an opaque `ScopeKey` and OWNS those registrations' lifetime (one backing fiber drives both facts); `scopeOf(ctx)` reads the tag; `scopeTarget(base, key)` builds the dispatch carrier that makes an event scope-filtered — listeners registered through a scoped context fire only for their key's subject, while plain plugin listeners keep firing for every subject. The agent loop is the one scope minter today (one scope per live agent, key = the `Agent` object — the `Agent.ctx` contract in `dsh-agent`), but the mechanism is key-agnostic so packages below the agent layer (`dsh-session`, `dsh-system-prompt`) depend on it without a dependency cycle. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Per-agent visibility control | This package emits no text or schema. It decides whether agent-scoped prompt sections, variables, tools, restrictions, and listeners apply to one agent, can shadow same-named global contributions, and removes them with that agent. | Zero direct tokens. It can add, replace, or remove whole contributions for one agent without changing another agent's request. | + ## Public API - `createScope(ctx: Context, key: ScopeKey): Scope` Mint a scope under `ctx`'s fiber. Usable synchronously (effect collection is uid-gated; service resolution falls through to the minting plugin's dependency surface). Throws on a primitive key, or when `ctx`'s fiber is disposing (`INACTIVE_EFFECT`). diff --git a/packages/core/session/README.md b/packages/core/session/README.md index 094c3073df..7307689e7b 100644 --- a/packages/core/session/README.md +++ b/packages/core/session/README.md @@ -2,6 +2,13 @@ Event-sourced session log and in-memory store. A `Session` is the append-only source of truth for an agent's whole interaction history — the LLM message history is *derived* from it. A **surface** layer (a linked list of message-producing events) is maintained on top of the raw log for efficient derivation and compaction. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Derived message history | The model receives projections of `user/message`, `assistant/message`, `tool/result`, `context/message`, and `steering/message` surface nodes. Tool calls live inside assistant messages. Chunks, boundaries, usage, hook records, todo records, and other log-only events add no message. | Appended surface nodes are resent on later steps. A `replace` surface operation removes the shadowed nodes from future inputs without deleting their raw log records. | +| Logged request header | The session reconstructs the system prompt, tool schemas, call config, and session prefix that the loop actually sent. Header events do not add a second copy to message history; the prefix is prepended outside `deriveMessages()`. | Zero duplicate tokens from logging. The reconstructed prefix, system text, and schemas still incur their normal per-request cost. | + ## Service: `SessionStore` (ctx key: `sessions`) Creates and holds event-sourced `Session` instances. Persistence is intentionally not implemented here — plugins subscribe to `session/event` and flush on `session/flush`. diff --git a/packages/core/system-prompt/README.md b/packages/core/system-prompt/README.md index d712cd4b5b..648df82b74 100644 --- a/packages/core/system-prompt/README.md +++ b/packages/core/system-prompt/README.md @@ -2,6 +2,13 @@ System prompt assembly registry. Plugins contribute ordered text sections, tool-schema providers, named prompt variables, and authoritative named protections; the agent loop calls `assemble(context)` once per step, and `renderPrompt(assembly)` is the full system prompt the model sees. The plugin registers the harness-owned openers itself — the static `harness:identity` section and the deployment's `deployment:persona` section — so they exist for every agent regardless of which loop plugin drives it. +## Model Experience + +| 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. | 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. | + ## Config | Key | Default | Meaning | diff --git a/packages/core/tools/README.md b/packages/core/tools/README.md index 2a678408e0..bf8e92b3e6 100644 --- a/packages/core/tools/README.md +++ b/packages/core/tools/README.md @@ -2,6 +2,13 @@ Tool registry and execution pipeline. Tool plugins register their schemas and executors; the agent loop executes each call through `tools/pre-execute` (the extensible allow/deny gate) → monotonic registered guards → `tools/execute` (an around-dispatch wrapper for timeout/retry/metrics plugins) → `tools/post-execute` (inspect/replace the result, attach context) → the observe-only `tools/result` notification. The registry also owns HOW its tools are presented to the model — its `mode` config selects native function calling, [Code Mode](#code-mode), or both. +## Model Experience + +| 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 protects one `run_code` wire schema and adds a generated TypeScript `tools` SDK section; `both` exposes both forms. Agent-scoped restrictions and shadows change that agent's set. | 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. | + ## Service: `ToolRegistry` (ctx key: `tools`) ### Config diff --git a/packages/fs/fs-local/README.md b/packages/fs/fs-local/README.md index 26524fad24..4cd2d6cb7e 100644 --- a/packages/fs/fs-local/README.md +++ b/packages/fs/fs-local/README.md @@ -10,6 +10,12 @@ await ctx.plugin(LocalFileSystem, { cwd: process.cwd() }) // freshness policy gate and @deepseek-ai/dsh-tool-fs to expose read/write/edit. ``` +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Filesystem tool results, indirectly | Through `dsh-tool-fs`, the model sees line-windowed UTF-8 file content, mutation acknowledgements, or structured filesystem errors. Real paths, versions, atomic-write mechanics, and directory metadata remain internal unless a consumer renders them. | Zero direct tokens. Read tokens are bounded by the tool's line, line-length, and byte caps; mutation results are small and remain in history until compaction. | + ## Behavior - **`resolve(path, opts?)`** — a relative `path` resolves against `opts.cwd` when the caller supplies one (the model-facing tools pass the calling agent's session cwd — see [the per-session cwd RFC](../../../docs/rfc/implemented/architecture/2026-07-02-fs-per-session-cwd.md)), else `config.cwd` (default `process.cwd()`); an absolute `path` ignores both. The `targetKey` is the file's `realpath`, so two input paths reaching the same file through symlinks share one identity, and writes/edits land on the link target (preserving the link). A not-yet-existing path uses the realpathed parent directory plus basename when the parent exists; only an unresolvable parent falls back to the absolute path. `displayPath` is the absolute (un-resolved) path. diff --git a/packages/fs/fs-policy/README.md b/packages/fs/fs-policy/README.md index ad912bfc95..01cc8a05b0 100644 --- a/packages/fs/fs-policy/README.md +++ b/packages/fs/fs-policy/README.md @@ -16,6 +16,12 @@ declare const ctx: Context await ctx.plugin(FsPolicy) ``` +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Filesystem tool outcome | This plugin adds no prompt or schema. It can turn an unobserved or stale write or edit into a structured `FS_NOT_OBSERVED` or `FS_STALE_VERSION` error result instead of a success; observation state itself is never shown. | Zero tokens on allowed operations beyond the ordinary tool result. A denial adds a small retained error result and avoids any success payload. | + ## The four-layer split | Layer | Package | Role | diff --git a/packages/fs/fs/README.md b/packages/fs/fs/README.md index ac2866802b..e492af2e3e 100644 --- a/packages/fs/fs/README.md +++ b/packages/fs/fs/README.md @@ -13,6 +13,12 @@ This package is the provider-seam layer of the four-layer filesystem stack, spli A future sandboxed, virtual, or remote backend implements this interface and the policy/tool layers don't change. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| None directly | The provider seam registers no prompt or tool. `dsh-tool-fs` converts provider text and structured `FsError` values into model-visible read, write, and edit results; policy listeners can change which outcome it receives. | Zero direct tokens. File content and errors enter context only through a consumer, whose window and byte caps determine result size. | + ## Service API (`ctx.fs`) A backend subclasses `FileSystem` and implements seven primitives. diff --git a/packages/fs/tool-fs/README.md b/packages/fs/tool-fs/README.md index fedd1ff7a2..732d9d5ddc 100644 --- a/packages/fs/tool-fs/README.md +++ b/packages/fs/tool-fs/README.md @@ -11,6 +11,14 @@ await ctx.plugin(ToolFs) // this package — re `@deepseek-ai/dsh-fs-policy` is **optional**: omit it and the tools run against the bare provider (unconditional write/overwrite/edit, no observed-state). A deployment that loads these tools is expected to also load it, so the behavior is read-before-write/edit. +## Model Experience + +| 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. | +| 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. | + ## Config All keys are optional; the defaults are the shipped read caps. diff --git a/packages/guard/repeat-tool-guard/README.md b/packages/guard/repeat-tool-guard/README.md index dc385bc033..11975af2bb 100644 --- a/packages/guard/repeat-tool-guard/README.md +++ b/packages/guard/repeat-tool-guard/README.md @@ -2,6 +2,12 @@ An advisory loop-breaker, not a model-facing tool: it never appears in the tool list, never vetoes or rewrites a call, and adds exactly one behavior — it watches each agent's stream of tool calls, counts runs of consecutive calls to the same tool with identical canonicalized arguments, and at configured run lengths injects an escalating advisory reminder telling the model to stop repeating itself, re-read the last result, and either change approach or conclude. The decision (retry differently, gather more evidence, or finish) stays entirely with the model: a legitimately repeated call is delayed by nothing and blocked by nothing. Decision record: [the repeat-tool-guard RFC](../../../docs/rfc/implemented/feature/2026-07-08-repeat-tool-guard.md). +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Conditional context message | At configured consecutive-repeat thresholds, that agent receives a source-attributed synthetic user reminder after the tool results, asking it to inspect the prior result and change approach or finish. No tool schema or normal-call text is added. | Zero tokens before a threshold. Each reminder is retained history; the detailed form caps the quoted canonical arguments at `argumentsPreviewChars`, while agents keep independent counters. | + ## Config ```yaml diff --git a/packages/hooks/hook-protocol/README.md b/packages/hooks/hook-protocol/README.md index 8296821503..2ac7067d14 100644 --- a/packages/hooks/hook-protocol/README.md +++ b/packages/hooks/hook-protocol/README.md @@ -4,6 +4,12 @@ The **shared core** of the Claude Code / Codex hook wire protocol. NOT a cordis Why a shared lib at all: Codex deliberately reimplements a *subset* of the Claude Code hook protocol — the same `hooks.json` matcher-group shape, the same exit-code/stdout output contract, the same command-hook execution model. The genuinely-shared parts live here; each bridge owns only what differs. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| None directly | This library registers nothing. Its `hook/invoked` and `hook/result` events are log-only and do not enter derived messages; bridge packages decide whether parsed `additionalContext`, blocks, or continuation feedback reach the model. | Zero direct tokens. Persisted hook audit records add no context tokens. | + ## What's shared (here) vs. per-dialect (the bridges) | Concern | Here (`dsh-hook-protocol`) | The bridge (`dsh-hooks-claude` / `-codex`) | diff --git a/packages/hooks/hooks-claude/README.md b/packages/hooks/hooks-claude/README.md index f97cdb5cfc..eace3116cd 100644 --- a/packages/hooks/hooks-claude/README.md +++ b/packages/hooks/hooks-claude/README.md @@ -4,6 +4,13 @@ A cordis plugin that runs a user's existing **Claude Code** hook config (a `hook A native cordis plugin could do everything this bridge does — more powerfully, with typed returns and no serialization boundary. **The bridge exists only to run UNMODIFIED external CC hooks faithfully**; anything bespoke should be a native plugin on the same seams (see [the interception-seams RFC](../../../docs/rfc/implemented/feature/2026-06-30-interception-seams.md)). +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Hook-provided context | `SessionStart`, accepted prompt, post-tool, and live in-process subagent-start hooks can add source-attributed context messages; a blocking `Stop` hook adds its reason as next-step steering. Remote-child injection has no local target. | No cost when hooks return no context. Hook text is data-dependent, logged, and resent in later conversation requests until compaction. | +| Blocked prompt or tool outcome | A hook can prevent a user prompt from reaching the model, deny or ask before a tool, block a post-tool result with feedback, or force another model step. `systemMessage` and `updatedInput` are logged or warned but are not model-visible in this implementation. | Blocking a prompt removes that prompt's request tokens; denial or feedback adds a retained error or context result; forced continuation pays another full request. | + ## Config ```ts diff --git a/packages/hooks/hooks-codex/README.md b/packages/hooks/hooks-codex/README.md index b3fbc39928..6ac33a46fb 100644 --- a/packages/hooks/hooks-codex/README.md +++ b/packages/hooks/hooks-codex/README.md @@ -12,6 +12,13 @@ Codex's hook protocol is a deliberate **subset** of Claude Code's (same `hooks.j A native cordis plugin could do everything this bridge does, more powerfully; the bridge exists only to run UNMODIFIED external Codex hooks faithfully (see [the interception-seams RFC](../../../docs/rfc/implemented/feature/2026-06-30-interception-seams.md)). +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Hook-provided context | `SessionStart`, accepted prompt, and post-tool hooks can add source-attributed context messages; a blocking `Stop` hook adds its reason as next-step steering. | No cost when hooks return no context. Hook text is data-dependent, logged, and resent until compaction. | +| Blocked prompt or tool outcome | A hook can prevent a user prompt from reaching the model, deny a tool, block a post-tool result with feedback, or force another model step. Codex `systemMessage` is not surfaced. | Blocking a prompt removes its request tokens; denial or feedback adds retained result text; forced continuation pays another full request. | + ## Config ```ts diff --git a/packages/llm/llm-deepseek/README.md b/packages/llm/llm-deepseek/README.md index 15fad9f881..00eecc7e89 100644 --- a/packages/llm/llm-deepseek/README.md +++ b/packages/llm/llm-deepseek/README.md @@ -4,6 +4,13 @@ DeepSeek chat-completions adapter for the harness LLM seam: hand-rolled `fetch` A second, independent implementation of the same seam exists in `@deepseek-ai/dsh-llm-pi-ai` (library-backed). Same Config shape — pick one per context (registering both for the same model names throws by design). +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| DeepSeek request | The selected DeepSeek model receives the harness system prompt, message history, tool schemas, stop sequences, and call config without adapter-authored prompt prose. On a prior assistant turn with tool calls, its reasoning content is passed back as required; reasoning from tool-call-free turns is omitted. | Provider tokenization governs exact input. Conditional reasoning passback increases tool-round-trip context, while dropping other reasoning avoids paying those tokens again; cache-read usage is reported when available. | +| DeepSeek response | Reasoning, text, and raw-string tool arguments are translated into harness chunks for the loop to log and assemble. | Generated tokens follow provider thinking and effort settings plus the request's `maxTokens`; only loop-retained blocks affect later input. | + ## Config ```yaml diff --git a/packages/llm/llm-pi-ai/README.md b/packages/llm/llm-pi-ai/README.md index f74ccd2246..ee4a4b32b4 100644 --- a/packages/llm/llm-pi-ai/README.md +++ b/packages/llm/llm-pi-ai/README.md @@ -2,6 +2,13 @@ DeepSeek adapter for the harness LLM seam backed by [`@earendil-works/pi-ai`](https://www.npmjs.com/package/@earendil-works/pi-ai) (the LLM library behind the pi agent). +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| DeepSeek request through pi-ai | The selected model receives the same logical system prompt, history, tools, stop sequences, and raw replayed tool arguments as the hand-written adapter. This package adds no prompt prose and removes pi-ai's own per-tool `strict` default to preserve that contract. | Provider tokenization governs exact input. Reasoning level changes generated and passback content; pi-ai reports reasoning inside output usage rather than as a separate count. | +| DeepSeek response | pi-ai events become harness reasoning, text, tool-call, usage, and finish chunks; parsed tool arguments are restored to raw JSON strings at the harness boundary. | Generated content affects later inputs only after the loop records it; adapter conversion adds no model-visible text. | + ## Why a second adapter exists `@deepseek-ai/dsh-llm-deepseek` already talks to the same endpoint. This package is its **design-verification twin**: same models, same wire protocol, completely different internals — a unified LLM library with its own event vocabulary versus hand-rolled fetch/SSE. Anything the harness `StreamChunk` protocol cannot express for BOTH implementations is a core-vocabulary bug. The differences it exercised on purpose: diff --git a/packages/llm/llm/README.md b/packages/llm/llm/README.md index 42694ba5ea..cb6fd18b9f 100644 --- a/packages/llm/llm/README.md +++ b/packages/llm/llm/README.md @@ -2,6 +2,13 @@ Provider-neutral LLM vocabulary and abstract service. This package defines the canonical language spoken by the agent loop, session logs, and every plugin. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Provider request transport | This service adds no system text, schema, or message. It routes the already-assembled frozen `GenerateOptions` to one adapter, while `llm/stream` listeners may cache, retry, or replace the stream without mutating that request. | Zero direct context tokens. The selected adapter and provider tokenizer determine billing, cache accounting, and serialization overhead for the existing content. | +| Streamed model output | Text, reasoning, and tool-call chunks are exposed to the loop, which decides what becomes retained assistant history. | Output usage is provider-reported; later input cost arises only after the loop records assembled content. | + ## Service: `LlmService` (ctx key: `llm`) An adapter registry plus a single streaming call surface, interceptable via a waterfall event. diff --git a/packages/sandbox/sandbox-local/README.md b/packages/sandbox/sandbox-local/README.md index abf7511438..0e47fe5fe6 100644 --- a/packages/sandbox/sandbox-local/README.md +++ b/packages/sandbox/sandbox-local/README.md @@ -16,3 +16,9 @@ Every rung has its keyless world-proof (`tests/bwrap.e2e.ts`, `tests/landlock.e2 ``` Consumers: [`@deepseek-ai/dsh-bash-sandbox`](../../bash/bash-sandbox/); see [`examples/sandbox-acp-agent`](../../../examples/sandbox-acp-agent/) for the runnable composition. + +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Sandbox result facts, indirectly | This provider adds no prompt or tool. It supplies the selected enforcement and denial dialect to `dsh-bash-sandbox`, which can become a denial marker or sandbox-unavailable error in the bash result. Runner selection and profiles are not shown. | Zero direct tokens; only a conditional small fact or error reaches context through the bash consumer. | diff --git a/packages/sandbox/sandbox/README.md b/packages/sandbox/sandbox/README.md index ef381625b2..d09b861046 100644 --- a/packages/sandbox/sandbox/README.md +++ b/packages/sandbox/sandbox/README.md @@ -9,3 +9,9 @@ Policy rides the call, not the provider: two consumers may confine under differe **Same-world confinement only.** A backend shares the host's filesystem and kernel (`bwrap`, Landlock, Seatbelt); `workspaceRoot` names a real host path. Containers, microVMs, and remote executors are NOT backends of this seam — they replace whole capability implementations (`ctx.bash`, `ctx.fs`) as environment-coherent groups. The boundary and its rationale: [the sandbox RFC](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md). Implementations: [`@deepseek-ai/dsh-sandbox-local`](../sandbox-local/) (Linux: `bwrap`, else the per-platform Landlock launcher; macOS: `sandbox-exec`/Seatbelt). Consumers: [`@deepseek-ai/dsh-bash-sandbox`](../../bash/bash-sandbox/) (wraps `['bash', '-c', command]`). + +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| None directly | This seam registers no prompt, schema, or message. A consumer may expose configured mode, enforcement, denial, or `SANDBOX_UNAVAILABLE` facts in its own guidance or result. | Zero direct tokens. Confinement changes model-visible text only through the consuming capability. | diff --git a/packages/session-persistence/session-persistence-jsonl/README.md b/packages/session-persistence/session-persistence-jsonl/README.md index 9a76381614..714a036a9d 100644 --- a/packages/session-persistence/session-persistence-jsonl/README.md +++ b/packages/session-persistence/session-persistence-jsonl/README.md @@ -2,6 +2,12 @@ The JSONL durable session-persistence backend — a concrete `SessionPersistence` (the `dsh-session-persistence` seam). One append-only `.jsonl` event log per session. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Resumed conversation history | JSONL storage contributes no live prompt or schema. Loading restores stored surface history and preserves prior request headers for reconstruction; the new loop composes its current envelope. An interrupted tail is balanced with error tool results. Raw `assistant/chunk` records do not duplicate messages. | Zero live-request tokens. A resumed agent pays for retained history and its current envelope, plus small repair results only after an interrupted tool turn. | + ## On-disk layout ``` diff --git a/packages/session-persistence/session-persistence-sqlite/README.md b/packages/session-persistence/session-persistence-sqlite/README.md index c14d3a70ea..cc6add3b56 100644 --- a/packages/session-persistence/session-persistence-sqlite/README.md +++ b/packages/session-persistence/session-persistence-sqlite/README.md @@ -4,6 +4,12 @@ A SQLite durable session-persistence backend — a second `SessionPersistence` i > **TODO:** this backend talks to `node:sqlite` directly. If a cordis database service (`cordis/db` / a `@cordisjs` SQL driver plugin) is adopted, route through that instead of holding a raw `DatabaseSync` here — the contract surface (`SessionPersistence`) would not change, only the storage driver. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Resumed conversation history | SQLite storage contributes no live prompt or schema. Loading restores the same surface history as JSONL and preserves prior headers for reconstruction; the new loop composes its current envelope. Interrupted rows are balanced with error tool results. Row metadata and raw chunks are not messages. | Zero live-request tokens. Resume restores retained history and pays the current envelope, with small repair-result tokens only for an interrupted tool turn. | + ## Storage model Each `SessionEvent` maps 1:1 onto a row in an `events` table `(session_id, seq, type, time, data, source_event_seqs, surface_op)` — `data` is the event payload as JSON text, so the row shape is the event verbatim (including `assistant/chunk`, keeping `seq` contiguous). The two `TEXT` columns `source_event_seqs` and `surface_op` are nullable; they store the event's optional surface-metadata fields (see [session surface](../../../docs/rfc/implemented/architecture/2026-06-18-session-surface.md)). Out-of-log metadata (`SessionHeader`) lives in a `sessions` row. A `sessions` row is written only by the first `append` — its existence is the lazy-materialization signal (`list` reports exactly the sessions that have a row), so no separate column is needed. diff --git a/packages/session-persistence/session-persistence/README.md b/packages/session-persistence/session-persistence/README.md index 8bd3fed568..87ab386ba6 100644 --- a/packages/session-persistence/session-persistence/README.md +++ b/packages/session-persistence/session-persistence/README.md @@ -4,6 +4,12 @@ The abstract durable session-persistence seam (`ctx.sessionPersistence`). Define The persisted unit IS the existing `SessionEvent` (event-sourced model — the log is the single source of truth), so there is no parallel "persisted message" type. Metadata that is NOT replayable conversation state (format version, cwd, lineage, seed boundary) travels separately as `SessionHeader`, owned by `dsh-session` and re-exported here. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Resumed conversation history | This seam adds no prompt or schema. Resume restores stored surface events as message history; stored request headers reconstruct earlier calls, while the new loop composes the current system prompt, tools, and session prefix for its next request. Crash repair inserts an error result for each unanswered tool call. | Zero tokens during ordinary persistence. Resume restores retained history cost and pays the current request envelope normally; crash repair can add small error-result tokens that keep the provider transcript valid. | + ## Service API (`ctx.sessionPersistence`) | Method | Contract | diff --git a/packages/skill/skill-local/README.md b/packages/skill/skill-local/README.md index c885416ff5..b48b444d2d 100644 --- a/packages/skill/skill-local/README.md +++ b/packages/skill/skill-local/README.md @@ -4,6 +4,12 @@ Local filesystem provider for the `ctx.skills` registry. This package implements one skill source. It scans local project, custom, and user skill roots, parses `SKILL.md` or flat Markdown skill files, and registers the provider on `ctx.skills`. The registry remains in `@deepseek-ai/dsh-skill`; the session-prefix catalog and model-facing loader tool remain in `@deepseek-ai/dsh-tool-skill`. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Skill catalog and loaded body, indirectly | When `dsh-tool-skill` is visible, discovered model-invocable skill names and descriptions enter its session-prefix catalog; a `skill` call returns the selected instructions and resource-base guidance. Paths, provider ranks, and disabled skills stay out of the catalog. | Zero direct tokens from this provider. Catalog cost scales with discovered entries under the consumer's per-description cap; a full body is added only after selection and remains in tool history. | + ## Plugin Requires `ctx.skills` (`inject: ['skills']`). diff --git a/packages/skill/skill/README.md b/packages/skill/skill/README.md index 33d16e1b69..f51f525ee7 100644 --- a/packages/skill/skill/README.md +++ b/packages/skill/skill/README.md @@ -4,6 +4,12 @@ Pure agent skill provider registry. This package owns the `ctx.skills` interface. It does not know whether skills come from local files, embedded plugin data, HTTP, or another backend; providers register those sources with `ctx.skills.registerProvider(...)`. The shipped local implementation is [`@deepseek-ai/dsh-skill-local`](../skill-local). +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| None directly | The registry renders nothing and registers no tool. `dsh-tool-skill` turns `list()` summaries into a session prefix and a selected `get()` body into a tool result; provider failures can remove entries from that request's catalog. | Zero direct tokens. Catalog size, descriptions, and loaded body length affect context only through the consumer. | + ## Service: `SkillService` (ctx key: `skills`) ### Public API diff --git a/packages/skill/tool-skill/README.md b/packages/skill/tool-skill/README.md index 7e72f89000..333a0d00bc 100644 --- a/packages/skill/tool-skill/README.md +++ b/packages/skill/tool-skill/README.md @@ -4,6 +4,13 @@ The model-facing skill catalog and `skill` tool. Requires `ctx.tools` and `ctx.skills` (`inject: ['tools', 'skills']`). +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Session prefix | If model-invocable skills exist and this exact `skill` tool is visible, the agent receives one user-role `` listing sorted names and capped descriptions. The composed catalog is frozen for the loop instance and prepended to every request, outside ordinary history. | Repeated input cost scales with skill count and `catalogDescriptionMaxLength`; no catalog tokens are sent when the list is empty or the tool is hidden or shadowed. | +| Tool schema and result | The model sees the fixed `skill(name)` schema. A successful call returns the selected full instructions plus resource-resolution guidance; no duplicate `agent.inject()` copy is made. | Fixed schema cost per request. Loaded instructions are data-dependent tool-result tokens, resent on later steps until compaction. | + ## Session-prefix catalog The plugin contributes one user-role `` catalog through `agent/session-prefix`. It resolves skills for the calling session's cwd, forwards the prefix abort signal to discovery, and lists only sorted `name` and `description` entries; skill bodies, paths, sources, providers, and `whenToUse` hints remain outside the catalog. The catalog is omitted when no model-invocable skills are available, and also when that agent's tool view restricts away the shipped `skill` tool or resolves a same-name scoped shadow instead. This exact-definition check keeps prompt guidance, the model-visible schema, and executable dispatch aligned. diff --git a/packages/subagent/subagent-acp/README.md b/packages/subagent/subagent-acp/README.md index cb51986915..0c07fa2953 100644 --- a/packages/subagent/subagent-acp/README.md +++ b/packages/subagent/subagent-acp/README.md @@ -4,6 +4,13 @@ The out-of-process **ACP subagent backend**: runs each child agent in a spawned It is the direction-inverted twin of the server-side bridge in [`@deepseek-ai/dsh-acp`](../../ui/acp): that package is the ACP *agent* (it answers `initialize`/`newSession`/`prompt`); this one is the ACP *client* (it *calls* them and implements the `sessionUpdate`/`requestPermission` callbacks). Point the configured command at the `acp-agent` example to "talk to our own process". +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Child-agent request | The remote child receives the standalone task through ACP plus its own process's configured system prompt, tools, and fresh session. It receives no parent conversation and cannot enforce the parent's scoped persona or tool filter. | The child pays for an independent full context and its own multi-step history. These tokens never enter the parent's context. | +| Parent tool result, indirectly | Through `dsh-tool-subagent`, the parent receives only the child's final streamed assistant text or a stop-reason error, not intermediate messages or tool traffic. | Parent input grows only by the final result, which is data-dependent and retained until compaction. This provider adds no parent schema itself. | + ## What it does `start(request)` spawns the configured command, wraps its stdio in an ACP `ClientSideConnection`, and drives one session: `initialize` → `newSession` → `prompt`. `run.started` resolves after `newSession` publishes the remote session and rejects when initialization fails or cancellation wins first; the service emits no start/end pair for a child that never became live. The child's streamed `agent_message_chunk` text becomes the `SubagentResult.output`; the prompt's terminal `StopReason` maps to the stop reason. `dispose()` kills the subprocess and awaits its exit. diff --git a/packages/subagent/subagent-fork/README.md b/packages/subagent/subagent-fork/README.md index 1434601c8e..174508b324 100644 --- a/packages/subagent/subagent-fork/README.md +++ b/packages/subagent/subagent-fork/README.md @@ -2,6 +2,13 @@ The in-process **fork** subagent backend: a [`SubagentProvider`](../subagent/README.md) that runs each child as a child [`Agent`](../../core/agent) **seeded with a prefix of the parent's session log** — so the child inherits the parent's conversation context instead of starting fresh. Shares the run driver (`startInProcessRun`) with [`dsh-subagent-spawn`](../subagent-spawn/README.md); the only difference is the seed. The shared `run.started` boundary resolves only after the seeded child is published, so `subagent/start` observers see a live registry entry. +## Model Experience + +| 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, along with its own scoped persona, tool filter, and optional structured-output 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. | +| 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. | + ## The seed boundary (the crux) At the moment a subagent tool's `execute` runs, the parent's CURRENT turn is open and unbalanced: the log holds the `assistant/message` carrying this spawn's tool-call and the dangling `tool/call` with no `tool/result` yet. Seeding that raw prefix would give the child an open turn that the session constructor and the dev-mode [invariants](../../support/invariants) replay **reject**. diff --git a/packages/subagent/subagent-inprocess/README.md b/packages/subagent/subagent-inprocess/README.md index caa7b9b161..55dc526915 100644 --- a/packages/subagent/subagent-inprocess/README.md +++ b/packages/subagent/subagent-inprocess/README.md @@ -2,6 +2,13 @@ The shared **in-process subagent run driver**. A library with no provider or import-time registration that the in-process backends — [spawn](../subagent-spawn/README.md) (a fresh child) and [fork](../subagent-fork/README.md) (a child seeded with a prefix of the parent's log) — both build on. Each accepted run installs one provider-owned cleanup effect. The backends are thin shells that differ ONLY in the session seed they pass; everything downstream lives here, so neither backend depends on the other. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Child-agent request | The shared driver sends the task as the child's user message and composes per-child scoped persona and tool restrictions. 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. Structured output adds fixed instruction and capability tokens only to that child for that run. | +| 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. | + ## What it exports ### `startInProcessRun(ctx, request, options): SubagentRun` diff --git a/packages/subagent/subagent-spawn/README.md b/packages/subagent/subagent-spawn/README.md index e411d44ab0..3c960af2aa 100644 --- a/packages/subagent/subagent-spawn/README.md +++ b/packages/subagent/subagent-spawn/README.md @@ -4,6 +4,13 @@ The in-process **spawn** subagent backend: a [`SubagentProvider`](../subagent/RE The run mechanics live in the shared [`@deepseek-ai/dsh-subagent-inprocess`](../subagent-inprocess/README.md) driver (`startInProcessRun`); this backend just passes **no seed** (a fresh child). The [fork](../subagent-fork/README.md) backend is an independent peer over the same driver — neither knows about the other. +## Model Experience + +| 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 its scoped persona and tool filter. It receives zero parent conversation messages. | The child pays for a new independent context and history; no parent-history tokens are duplicated. | +| 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. | + ## What it does `start(request)` delegates to `startInProcessRun(ctx, request, {})` with no seed: a fresh child agent with the parent's `cwd`/`parentSession` lineage and (by default) the parent's model. The driver creates one run-owner fiber under `parent.ctx`; parent teardown, this provider's teardown, and manual disposal all converge there before child publication. Its `run.started` boundary resolves only after the fresh child is published, so `subagent/start` observers see a live registry entry. See the [driver README](../subagent-inprocess/README.md) for the full lifecycle (depth check, one-shot drive, result read, dispose). diff --git a/packages/subagent/subagent-subprocess/README.md b/packages/subagent/subagent-subprocess/README.md index ccc68bf31b..a24fcc813b 100644 --- a/packages/subagent/subagent-subprocess/README.md +++ b/packages/subagent/subagent-subprocess/README.md @@ -4,6 +4,12 @@ Shared machinery for **out-of-process subagent backends** — providers that spa Every tunable is a **parameter**: the dispose ladder takes its grace periods per call, the config-dir helper takes an optional pinned path. Defaults live in each consuming plugin's Config (defaulted, validated fields changeable from `cordis.yml`), never in this library. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| None directly | This process utility registers no provider, prompt, tool, or message. A consuming backend's child application decides the child's model context; environment scrubbing and isolated config directories prevent ambient credentials and user state from silently changing that composition. | Zero direct tokens. It can indirectly stabilize child context, but it adds no text to parent or child requests. | + ## What it exports ### `SENSITIVE_ENV_PATTERN` / `buildChildEnv(extra)` diff --git a/packages/subagent/subagent/README.md b/packages/subagent/subagent/README.md index e214ba58c6..26bb8653ed 100644 --- a/packages/subagent/subagent/README.md +++ b/packages/subagent/subagent/README.md @@ -14,6 +14,12 @@ This package is the interface third of the capability seam, split so each concer Unlike the bash seam (one executor per context, second load throws), **multiple providers coexist** here. Each registers under a unique name and a caller picks one by name — the shape mirrors the LLM adapter registry (`LlmService.registerAdapter`), not the single-service bash executor. This is the requirement that rules out the bash shape: an agent may want an in-process child for a cheap subtask and an out-of-process ACP child for an isolated one, in the same runtime. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| None directly | The provider registry registers no prompt or tool. Provider lifecycle makes a bound `dsh-tool-subagent` schema appear or disappear, and `inheritsParentContext` selects truthful fresh-versus-fork wording. Run events are observe-only. | Zero direct tokens. Child prompts and final results enter model contexts only through a provider and consumer. | + ## Service API (`ctx.subagents`) | Member | Semantics | diff --git a/packages/subagent/tool-subagent/README.md b/packages/subagent/tool-subagent/README.md index 715fb0b9db..cb5f9c2a89 100644 --- a/packages/subagent/tool-subagent/README.md +++ b/packages/subagent/tool-subagent/README.md @@ -2,6 +2,13 @@ The model-facing `subagent` tool: delegate a self-contained task to a child agent and return its final output. Pure schema + lifecycle shaping over the [`ctx.subagents`](../subagent/README.md) provider registry — an in-process, ACP, or future A2A backend swaps in without changing what the model sees. +## Model Experience + +| 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. | 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. | + ## Provider selection is config, not model-facing This plugin binds to **exactly one** provider (`Config.provider`). The model sees only `{ description, prompt }` — there is no provider/type parameter in the schema. To expose more than one transport, load the plugin more than once, each bound to a different provider **and a distinct `toolName`** (the tool registry rejects a duplicate name, so a second load that kept the default `subagent` name would throw). Keeping selection in config (not the schema) is the deliberate split: the *service* holds a multi-provider registry; the *tool* picks one. diff --git a/packages/support/acp-snapshot/README.md b/packages/support/acp-snapshot/README.md index 501b98b793..e2dfa564bb 100644 --- a/packages/support/acp-snapshot/README.md +++ b/packages/support/acp-snapshot/README.md @@ -40,3 +40,9 @@ A scenario booting a differently-composed tree sets its own `configPath` (an ove The example also ships a `cordis.snapshot.yml` replay overlay next to its `cordis.yml` (the bin swaps them under `DSH_SNAPSHOT=replay` — [single-source replay config RFC](../../../docs/rfc/implemented/testing/2026-07-04-single-source-acp-replay-config.md)); replay fixtures are served by [`dsh-llm-replay`](../llm-replay/README.md), which this package points at via the `DSH_SNAPSHOT_*` env vars it sets on the child. `pnpm run test:snapshot:record` calls the live LLM and rewrites the recorded scenarios' model fixtures; `pnpm run test:snapshot:refresh` stays keyless, runs the replay overlay, and rewrites stdout, comparable session-log goldens, and each pin's Markdown prompt snapshot from the committed model scripts. Fixture roles, record/replay/refresh semantics, and scenario-table fields are documented on `Scenario` and in the [snapshot RFC](../../../docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md). Constraints: `suite.ts` imports vitest, so the package is importable only inside a vitest run (the harness and normalizers have no such dependency but ship from the same entry). ACP-specific by design — the harness speaks the SDK's `ClientSideConnection`. Permission round-trips are scriptable: `InputScript.permissionAnswers` is a FIFO queue of option-kind selections (`allow_once`, `reject_once`, …) the client maps to the agent-issued `optionId` at answer time; an absent or exhausted queue answers `cancelled`, and a kind the request never offered rejects the run (the agent is answered `cancelled`, so a tolerant agent cannot absorb the scenario bug). Session config options are scriptable too: the `setConfigOption` step switches a knob over `session/set_config_option`, and `setConfigOptionExpectError` asserts the bridge rejects an unknown id or out-of-vocabulary value (the error frame stays in the transcript). + +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| None in production | The test harness records, normalizes, scrubs, and compares request headers and ACP transcripts but does not alter the agent's assembled context. Replay scenarios obtain assistant chunks from `dsh-llm-replay`; record mode uses the real composition. | Zero production tokens. Replay spends no provider tokens, while record mode pays the composition's ordinary model cost; golden scrubbing changes files only, never the live request. | diff --git a/packages/support/invariants/README.md b/packages/support/invariants/README.md index 94c2682f9d..1bef6216f5 100644 --- a/packages/support/invariants/README.md +++ b/packages/support/invariants/README.md @@ -4,6 +4,12 @@ Dev-mode event-contract invariants and session-log freeze. A pure-listener plugi **Off in production.** Enable it in tests and the demos, where a contract violation should fail loudly. It costs nothing when not registered, and doubles as executable documentation of the event taxonomy — the assertions *are* the contract. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| None | The plugin observes and validates session events, agent states, and frozen model requests; it does not rewrite a prompt, schema, message, or stream. An invariant failure aborts the faulty turn instead of adding guidance. | Zero model tokens when checks pass; a failure prevents or ends a request rather than contributing context. | + ## Plugin A functional plugin — register the module namespace (this is what loading by name in `cordis.yml` does): diff --git a/packages/support/llm-replay/README.md b/packages/support/llm-replay/README.md index 78b411d54c..84a6f71dfb 100644 --- a/packages/support/llm-replay/README.md +++ b/packages/support/llm-replay/README.md @@ -4,6 +4,12 @@ A replay LLM plugin for keyless snapshot tests. It installs a single `llm/stream Its consumer is the ACP snapshot harness in `examples/acp-agent`, which loads this plugin (via `cordis.snapshot.yml`) in place of a real LLM adapter. The package exists so its derive/parse/replay logic falls under the per-file 100% coverage gate on `packages/*/src` (the same logic, while it lived under `examples/`, was outside the gate). +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Keyless test stream | The real loop still assembles its normal system prompt, tools, prefix, and history, but no provider model receives them. Recorded assistant chunks are replayed as the response and then enter later history exactly like live output. | Zero billed or tokenizer-evaluated model tokens. Fixture output creates deterministic retained test context for later replay steps. | + ## How the fixture works The fixture IS the persisted session log (`/session.jsonl`). Its `assistant/chunk` events carry every `StreamChunk`, so grouping them by `(turn, step)` reconstructs each `stream()` call's chunk sequence (one model call per loop step). Recording is therefore "run the real agent once and harvest the `.jsonl`", done by the snapshot harness — this plugin does not record. A fixture may carry its `request/header` content tokenized to `{{system}}`/`{{tools}}` (the harness pins that content in one scenario and scrubs the rest); replay is indifferent — derivation reads only `assistant/chunk` events and the line-0 session header. diff --git a/packages/support/subagent-mock/README.md b/packages/support/subagent-mock/README.md index af169ed4c2..74cfbf6bc8 100644 --- a/packages/support/subagent-mock/README.md +++ b/packages/support/subagent-mock/README.md @@ -4,6 +4,12 @@ A scripted `SubagentProvider` for testing the [subagent seam](../../subagent/sub It lets a test drive `ctx.subagents` and the model-facing `dsh-tool-subagent` through the **real cordis Loader / export path**, exercising provider registration, start-time capability validation, the run lifecycle (`result` / `cancel` / `dispose`), and the structured-output branch — all deterministically and keylessly. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Deterministic test result | No child model runs. When exercised through `dsh-tool-subagent`, the parent sees the mock provider's configured reply or stop-reason error, and structured tests receive the configured object. | Zero child-model tokens. Only the scripted final result is added to the parent test history. | + ## Usage Load it as a plugin (functional shape: `name`/`inject`/`Config`/`apply`, no default). Config (all optional): diff --git a/packages/timeout/timeout-policy/README.md b/packages/timeout/timeout-policy/README.md index e637a658bf..84f7d07f04 100644 --- a/packages/timeout/timeout-policy/README.md +++ b/packages/timeout/timeout-policy/README.md @@ -2,6 +2,12 @@ Tool-call timeout enforcer: a single `tools/execute` around-dispatch listener that arms a per-call cooperative deadline on `exec.signal` for a tool declaring `timeoutMs` on its `ToolDefinition` and returns a structured `TOOL_TIMEOUT` result when that deadline wins. The budget is read from the tool's own declaration (`ToolDefinition.timeoutMs`, set by the owning tool plugin), so this plugin is **zero-config**. It is the reference `tools/execute` wrapper and the enforcement home for model-facing tool-call budgets (the timeout-library RFC's foreseen middleware). +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Conditional tool result | This plugin adds no prompt or schema. If a declared deadline wins, it replaces the provider's outcome with `Error: tool call timed out after ms` plus structured `TOOL_TIMEOUT`; otherwise the original result passes through unchanged. | Zero tokens on non-timeout calls. A timeout adds one small retained error result and can prevent a larger late provider result from entering context. | + ## Plugin (namespace: `timeout-policy`) A function/namespace plugin (`name` / `inject` / `apply`), not a service. It registers no tool and takes no config — it consumes `ctx.tools`'s `tools/execute` waterfall (which the `dsh-tools` registry always provides) and reads each dispatched tool's declared `timeoutMs` from the registry (`ctx.tools.get(exec.name)`). diff --git a/packages/todo/tool-todo/README.md b/packages/todo/tool-todo/README.md index b27cc65227..bdab52d492 100644 --- a/packages/todo/tool-todo/README.md +++ b/packages/todo/tool-todo/README.md @@ -2,6 +2,13 @@ The model-facing `todo_write` tool: the agent's whole task list, replaced wholesale on each call. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Tool schema | The model sees `todo_write` with the complete-list array and the three status values. | Fixed schema cost on every request where the tool is visible. | +| Tool-call history and result | Each assistant tool call retains the entire replacement list in its arguments. The tool result reports only pending, in-progress, and completed counts; the full `todo/write` session event is UI and replay state, not a second model message. | Token growth scales with every full list the model submits, and those call arguments remain until compaction. The result itself is small and fixed-shape. | + ## What it does Registers one tool, `todo_write(todos: [{ content, status }])`, on `ctx.tools`. The model sends the ENTIRE list every call — there are no partial updates or per-item edits. Each call appends a `todo/write` event (the full list snapshot) to the calling agent's session log via `agent.session.append('todo/write', { todos })`; the current list is the most recent such event (last-write-wins on replay). diff --git a/packages/ui/acp-agent/README.md b/packages/ui/acp-agent/README.md index d81c9cc091..f7bba0c5d8 100644 --- a/packages/ui/acp-agent/README.md +++ b/packages/ui/acp-agent/README.md @@ -4,6 +4,12 @@ The **ACP server app**: a Cordis app plugin that composes the default agent spin It is the structured counterpart to [`@deepseek-ai/dsh-stdio-agent`](../stdio-agent/README.md): both consume the same spine, but this one bakes in the OPPOSITE front-door cluster. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Composed ACP agent request | Through `dsh-agent-core`, an ACP-created agent receives the harness identity, configured persona, skill catalog, visible tools, and its own ACP prompt history. This app adds no extra prompt prose and omits `ask_user_question` unless a leaf opts in. | Per-request cost is the sum of the composed child packages. ACP framing, JSON-RPC, persistence, and UI rendering add zero model tokens. | + ## What it bakes in — and what it deliberately omits stdout is the ACP JSON-RPC channel, so the cluster is defined as much by what it LEAVES OUT as what it includes: diff --git a/packages/ui/acp/README.md b/packages/ui/acp/README.md index 2f1f35adc4..1721caf5e5 100644 --- a/packages/ui/acp/README.md +++ b/packages/ui/acp/README.md @@ -4,6 +4,14 @@ The **Agent Client Protocol (ACP)** bridge: exposes DeepSeek Harness SDK agents It is a **client-driver / UI plugin**, the structured analogue of the readline `stdio-chat` plugin — NOT a loop change and NOT a [capability seam](../../../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md). It consumes the existing `agent/*` event taxonomy, the `dsh-agent` create/resume factory, and `dsh-session-persistence`. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| User messages | Each ACP `session/prompt` becomes an agent user message: text passes through and a `resource_link` is rendered as text. Unsupported image, audio, and embedded-resource blocks are rejected rather than silently omitted. | Prompt tokens are data-dependent and remain in that session's history until compaction. Concurrent ACP sessions keep separate contexts. | +| Human answers and permissions | When optional consumers are loaded, ACP form answers become `ask_user_question` tool results and permission decisions control whether a tool yields success or denial. ACP tool cards, terminal output, diffs, and streamed session updates are UI-only. | Answer and denial text enters context only through the owning tool result; presentation metadata adds zero model tokens. | +| Loaded sessions | `session/load` resumes the persisted log, after which the loop sends its reconstructed history and request header. Replaying that log to the editor is not an extra model message. | Restored context has the persistence and session packages' normal retained cost; ACP replay to the client adds none. | + ## Service / plugin `apply(ctx, config)` — wires an `AgentSideConnection` (from `@agentclientprotocol/sdk`) to `process.stdin`/`process.stdout` and implements the ACP `Agent` method surface. diff --git a/packages/ui/app-boot/README.md b/packages/ui/app-boot/README.md index 92fbc333be..7630d73208 100644 --- a/packages/ui/app-boot/README.md +++ b/packages/ui/app-boot/README.md @@ -13,3 +13,9 @@ Shared boot glue for the app bins ([`dsh-stdio-agent`](../stdio-agent/README.md) Two failure classes the guards handle: `loader.await()` swallows init rejections (`Promise.allSettled`) — Node still exits non-zero on the resulting unhandled rejection, and `installFailLoud` replaces the noisy dump with one labelled line and a guaranteed `exit(1)`; a failed plugin IMPORT is only logged by the Loader (the process would otherwise exit 0 on a usable config typo), leaving a fiber-less entry that `assertEntriesLoaded` turns into a `boot()` rejection. Bare plugin specifiers in a config (`@deepseek-ai/dsh-*`) resolve through the cordis Loader's internal module loader, active only under `node --expose-internals`; the bins' subprocess smokes exercise that path, while this package's unit suite drives `boot()` in-process against configs with relative specifiers. + +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| None directly | Boot and environment helpers load the configured plugin tree but register no prompt, schema, or message of their own. `.env`, loader diagnostics, and config-path selection are process concerns, not model context. | Zero direct tokens. The selected configuration indirectly determines which other packages contribute context. | diff --git a/packages/ui/stdio-agent/README.md b/packages/ui/stdio-agent/README.md index 27f0d20be7..5e8ef1bab7 100644 --- a/packages/ui/stdio-agent/README.md +++ b/packages/ui/stdio-agent/README.md @@ -4,6 +4,12 @@ The **terminal stdio chat app**: a Cordis app plugin that composes the default a It is the readline counterpart to [`@deepseek-ai/dsh-acp-agent`](../acp-agent/README.md): both consume the same spine, but each bakes in the OPPOSITE front-door cluster. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Composed terminal agent request | Through `dsh-agent-core`, the `main` agent receives the harness identity, configured persona, skill catalog, and visible tools; this app also composes the `ask_user_question` schema. Each readline submission becomes a user message. | Child prompt and schema costs repeat per request; user input and tool history grow until compaction. The welcome banner, logger output, and rendered transcript are terminal-only and add zero model tokens. | + ## What it bakes in A terminal chat always wants the same cluster, so the package owns it rather than trusting each leaf to re-wire it: diff --git a/packages/ui/tool-ask-user/README.md b/packages/ui/tool-ask-user/README.md index 10d4a082ba..9aefb6aef4 100644 --- a/packages/ui/tool-ask-user/README.md +++ b/packages/ui/tool-ask-user/README.md @@ -2,6 +2,13 @@ Model-facing `ask_user_question` tool over `ctx.userInteraction`. It lets the model ask the human a concise question when it needs confirmation, a choice, or missing information before continuing. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Tool schema | The model sees `ask_user_question` with question ids, prompts, headings, options, and multi-select flags. | Fixed schema cost on every request where the tool is visible. | +| Tool-call history and result | The model's full questions remain in the assistant tool-call arguments. After the human answers, the next step sees JSON containing selected labels and optional custom text. UI interaction while the call is pending is not model context. | Arguments and answer JSON are data-dependent retained tokens; there is no token cost while waiting for the human. | + ## Tool `ask_user_question` accepts: diff --git a/packages/ui/user-approval/README.md b/packages/ui/user-approval/README.md index 821e98638f..b4358ccbfa 100644 --- a/packages/ui/user-approval/README.md +++ b/packages/ui/user-approval/README.md @@ -11,3 +11,10 @@ The seam also owns the per-session POLICY tier ([the sandbox RFC § Per-session One seam serves both ask paths of [the sandbox RFC](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md): the `tools/pre-execute` `ask` decision (routed by [`@deepseek-ai/dsh-tools`](../../core/tools/) when this service is mounted; degrading to deny when it is not), and the sandbox post-denial escalated retry (the bash tool's `sandbox_permissions` gate in [`@deepseek-ai/dsh-tool-bash`](../../bash/tool-bash/) — [the sandbox RFC § Escalation](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md)). The full design: [the approval-seam RFC](../../../docs/rfc/implemented/feature/2026-07-06-approval-seam.md). Answerers today: the ACP bridge ([`@deepseek-ai/dsh-acp`](../../ui/acp/)) forwards to the editor's `session/request_permission` prompt for agents it owns. The audit events are log-only session records — the model only ever sees the tool result the asker derives from the outcome. + +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| System prompt and policy notice | Each agent request carries a source-owned approval-policy marker. Under `never`, it also states that approval-requiring actions are rejected and sandbox escalation must not be requested. A policy change injects at most one attributed notice before the next step. | Small fixed per-request cost, larger under `never`; a change notice is conditional and retained in history. | +| Tool outcome | `approval/asked` and `approval/decided` are log-only. The model sees only the asking consumer's eventual allowed, rejected, cancelled, or unavailable tool outcome; the human permission UI is not context. | Zero duplicate audit tokens. A rejection may replace a normal tool result with a small retained error, while an allowance leaves the consumer's ordinary result. | diff --git a/packages/ui/user-interaction/README.md b/packages/ui/user-interaction/README.md index 6377c2b7ef..e3b4748e80 100644 --- a/packages/ui/user-interaction/README.md +++ b/packages/ui/user-interaction/README.md @@ -2,6 +2,12 @@ Abstract user-interaction seam. It owns `ctx.userInteraction`, the service a model-facing tool or permission plugin uses when it needs to pause work and ask the human for a decision. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| None directly | This UI-neutral seam registers no prompt or tool. A consumer such as `dsh-tool-ask-user` turns a model call into an `ask()` request and converts the provider's human answer into a model-visible tool result. | Zero direct tokens. Question and answer size affect context only through the consumer. | + ## Service: `UserInteractionService` (ctx key: `userInteraction`) ### Public API diff --git a/packages/util/brand/README.md b/packages/util/brand/README.md index 8f7943def7..22c9c47bb3 100644 --- a/packages/util/brand/README.md +++ b/packages/util/brand/README.md @@ -2,6 +2,12 @@ The `Branded` nominal-typing primitive — a tiny, **type-only** package (no runtime code, no harness-package dependency) shared by every package that owns a cross-boundary id. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| None | `Branded` is erased at compile time and registers no runtime plugin, prompt, schema, event, or message. Branded ids serialize exactly as their underlying strings when another package logs or renders them. | Zero direct or indirect token overhead beyond the string another package already chose to expose. | + ## What `Branded` is A brand makes structurally-identical strings non-interchangeable at the type level: an `AgentId` cannot be passed where a `CallId` is expected, even though both are plain `string`s at runtime. diff --git a/packages/util/timeout/README.md b/packages/util/timeout/README.md index db2b06ba53..e5418cd7e2 100644 --- a/packages/util/timeout/README.md +++ b/packages/util/timeout/README.md @@ -6,6 +6,12 @@ It owns **no termination**. The signal it hands out only *notifies*; actually st It is a **library, not a service or plugin**: no `ctx`, registers nothing, holds no state, emits no events. A "timeout service" would have to understand how to stop every capability's work — exactly the knowledge a microkernel keeps out of shared layers. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| None directly | This library only creates and classifies abort signals. It registers no prompt, schema, or message; consumers decide whether a timeout becomes a marker, a structured error, or no model-visible change. | Zero direct tokens. It can indirectly cap or replace a consumer's result when that consumer renders a timeout. | + ## Surface ```ts diff --git a/packages/web/tool-web/README.md b/packages/web/tool-web/README.md index ab1326a21e..6692fa5791 100644 --- a/packages/web/tool-web/README.md +++ b/packages/web/tool-web/README.md @@ -4,6 +4,14 @@ The model-facing web tool suite — `web_search` and `web_fetch` — over the [w Each tool is registered independently; a product that wants only one disables the other via config (`{ search: false }` / `{ fetch: false }`). +## Model Experience + +| 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. | +| 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. | + ## Tools | Tool | Args | Behavior | diff --git a/packages/web/web-fetch-local/README.md b/packages/web/web-fetch-local/README.md index 9c2ef0030f..a300f80c56 100644 --- a/packages/web/web-fetch-local/README.md +++ b/packages/web/web-fetch-local/README.md @@ -4,6 +4,12 @@ An anonymous public HTTP(S) `WebFetchProvider` for the harness [web capability s This is an **implementation** package: it registers a provider into `ctx.web`, it does not own the key and it does not register a model-facing tool. It is a function/namespace plugin (`inject: ['web']`). +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Web fetch result, indirectly | Through `dsh-tool-web`, the conversation model sees the final URL, HTTP status, and decoded text or markdown-shaped HTML, or a structured retrieval error. Redirects, headers, and transport mechanics are not added to context unless reflected in an error. | Zero direct tokens. `maxBodyChars` bounds decoded result length before the tool records it; the retained result is resent until compaction. | + ## Responsibility split The provider owns **safe resource retrieval**: URL validation, HTTP transport, redirect policy, a resource-backstop timeout, abort propagation, byte caps, charset decoding, content-type classification, and binary rejection. `@deepseek-ai/dsh-tool-web` owns **presentation** (HTML→markdown, truncation formatting). A non-2xx HTTP response is a *result* (status code + decoded body), not an error; `WebError` is reserved for failures to safely retrieve or represent the resource. diff --git a/packages/web/web-search-deepseek/README.md b/packages/web/web-search-deepseek/README.md index 41b000d26a..eed8f993dd 100644 --- a/packages/web/web-search-deepseek/README.md +++ b/packages/web/web-search-deepseek/README.md @@ -4,6 +4,13 @@ A [DeepSeek](https://deepseek.com)-backed `WebSearchProvider` for the harness [w This is an **implementation** package: it registers a provider into `ctx.web`, it does not own the key and it does not register a model-facing tool. Like `@deepseek-ai/dsh-llm-deepseek`, it is a function/namespace plugin (`inject: ['web']`). The Anthropic wire shape is a provider-private detail — it does **not** make this provider depend on `ctx.llm`. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Auxiliary DeepSeek search request | A separate DeepSeek model receives the search query and native `web_search` server-tool definition. This request is not part of the conversation model's context. | Separate provider input and output tokens are incurred for each search; `maxTokens` caps generated output and `maxUses` caps native search uses. | +| Conversation tool result, indirectly | Through `dsh-tool-web`, the conversation model sees deduplicated URLs, titles, dates, and citation snippets from structured search blocks; provider prose is not trusted as an answer. | Zero direct conversation tokens from registration. Result tokens scale with returned sources and snippets, then the seam enforces the requested source bound. | + ## How it differs from a dedicated search endpoint Exa and Perplexity expose dedicated search endpoints; DeepSeek does not. Instead this provider issues a **full Messages model call** carrying the `web_search` server tool, so one search costs a complete model turn in latency and tokens — heavier than a pure retrieval endpoint. DeepSeek runs the search server-side and returns **structured** `web_search_tool_result` blocks; the provider parses those blocks and **never scrapes URLs out of model prose**. diff --git a/packages/web/web-search-exa/README.md b/packages/web/web-search-exa/README.md index 0bc58d6559..11b7cd7473 100644 --- a/packages/web/web-search-exa/README.md +++ b/packages/web/web-search-exa/README.md @@ -4,6 +4,12 @@ An [Exa](https://exa.ai)-backed `WebSearchProvider` for the harness [web capabil This is an **implementation** package: it registers a provider into `ctx.web`, it does not own the `ctx.web` key and it does not register a model-facing tool (that is `@deepseek-ai/dsh-tool-web`). Like `@deepseek-ai/dsh-llm-deepseek`, it is a function/namespace plugin (`inject: ['web']`) that registers its backend, not a default-export service. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Web search result, indirectly | Through `dsh-tool-web`, the conversation model sees Exa result URLs, titles, first highlight snippets, and publication dates. No generated answer or provider-private response fields enter the tool result. | Zero direct harness-model tokens. Result size scales with the bounded source list and snippets; the seam enforces `maxResults`, and retained results remain until compaction. | + ## Config | Key | Default | Meaning | diff --git a/packages/web/web-search-perplexity/README.md b/packages/web/web-search-perplexity/README.md index f944413c96..ffc475e539 100644 --- a/packages/web/web-search-perplexity/README.md +++ b/packages/web/web-search-perplexity/README.md @@ -4,6 +4,13 @@ A [Perplexity](https://perplexity.ai)-backed `WebSearchProvider` for the harness This is an **implementation** package: it registers a provider into `ctx.web`, it does not own the key and it does not register a model-facing tool. Like `@deepseek-ai/dsh-llm-deepseek`, it is a function/namespace plugin (`inject: ['web']`). The OpenAI-compatible wire shape is a provider-private detail — it does **not** make this provider depend on `ctx.llm`. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Auxiliary Perplexity request | A separate Perplexity model receives the search query through its chat-completions endpoint. This request is not part of the conversation model's context. | Separate provider tokens are incurred per search; `maxTokens` caps the generated answer. | +| Conversation tool result, indirectly | Through `dsh-tool-web`, the conversation model sees the generated answer plus structured result metadata or URL-only citations. | Zero direct conversation tokens from registration. Answer and source tokens are data-dependent, source count is seam-bounded, and the retained result is resent until compaction. | + ## Config | Key | Default | Meaning | diff --git a/packages/web/web/README.md b/packages/web/web/README.md index fe5f1e650e..97ca9d97bb 100644 --- a/packages/web/web/README.md +++ b/packages/web/web/README.md @@ -14,6 +14,12 @@ This package is the interface third of the web capability. Unlike bash/fs it spa Search and fetch share no request schema and no business logic, but they are deliberately one seam: `ctx.web` is a single web-access middle layer with one provider-selection policy owner, one abort/error vocabulary, and one product-facing "how this harness reaches the web" config surface. The cost is the parallel `Search`/`Fetch` method pairs; that parallelism is intentional, not a missed extraction. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| None directly | The seam registers providers, not tools or prompt text. `dsh-tool-web` renders normalized search answers, sources, fetched bodies, and structured `WebError` values. Provider selection details stay internal except for an execution error. | Zero direct tokens. The seam indirectly bounds search result tokens by truncating sources to `maxResults`; all rendered size comes through a consumer. | + ## Service API (`ctx.web`) | Member | Semantics | diff --git a/packages/workflow/tool-workflow/README.md b/packages/workflow/tool-workflow/README.md index 44160d8fc1..abf2030706 100644 --- a/packages/workflow/tool-workflow/README.md +++ b/packages/workflow/tool-workflow/README.md @@ -2,9 +2,16 @@ 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. +## Model Experience + +| 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. | +| 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. | + ## What the model sees -Two parameters: `script` (required — the full `export const meta = {...}` + body text; the tool DESCRIPTION carries the complete authoring contract: hooks, semantics, the supported schema subset) and `args` (optional JSON object exposed to the script as the `args` global; a bare list is wrapped as a field, a deliberate deviation from Claude Code's any-JSON `args` so the wire schema stays honest). The plugin also contributes a `tool:` system-prompt section carrying the usage policy — use the tool only on an explicit user ask for a workflow / large orchestration; prefer plain subagent calls for one or two delegations — per the convention that tool guidance ships with the tool plugin, never in the deployment persona. +Three parameters: `script` (required JavaScript body with top-level await and return, but no `export const meta` statement; the tool description carries the complete hooks and semantics contract), `meta` (required plain-JSON identity with name, description, and optional usage and phase guidance), and `args` (optional JSON object exposed as the `args` global; wrap a bare list in a field). The plugin also contributes a `tool:` system-prompt section carrying the usage policy — use the tool only on an explicit user ask for a workflow or large orchestration; prefer plain subagent calls for one or two delegations — per the convention that tool guidance ships with the tool plugin, never in the deployment persona. ## Lifecycle diff --git a/packages/workflow/workflow-workerthread/README.md b/packages/workflow/workflow-workerthread/README.md index 86d386350a..c6c253034b 100644 --- a/packages/workflow/workflow-workerthread/README.md +++ b/packages/workflow/workflow-workerthread/README.md @@ -2,6 +2,13 @@ The [`WorkflowService`](../workflow/README.md) implementation, on **`node:worker_threads`**: each run gets its OWN worker thread (one run = one worker, no pooling — a run is heavyweight, so the ~tens-of-ms thread spin-up is noise), the script executes in a vm context INSIDE that worker with the workflow hooks injected, and every `agent()` call bridges back over the message port to [`ctx.subagents`](../../subagent/README.md) on the host. Child agents are I/O-bound LLM loops and stay on the host event loop; the thread isolates the SCRIPT, the only part that can spin synchronously. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| Child-agent requests | Every script `agent()` call sends its prompt and optional model or structured-output schema to a subagent provider. Each child sees that provider's own context; phase and log narration stays on observer events. | Potentially many independent child contexts are paid, bounded by `maxConcurrentAgents`, `maxTotalAgents`, and `maxItemsPerCall`; they never join the parent history directly. | +| Parent tool result, indirectly | The parent sees only the script's materialized final JSON value, child count, or error through `dsh-tool-workflow`. Intermediate child outputs are available to the script but not the parent model. | Zero direct parent tokens from this engine. Final result size is capped by the tool consumer and retained until compaction. | + ## Trust premise: what the thread buys (and what it does not) Workflow scripts are **model-written** — the same trust level as the model's existing bash access — so this engine defends against **buggy** scripts, never hostile ones. A worker thread is NOT a security boundary: the vm context inside it is escapable by construction (`node:vm` shares object machinery with its surrounding realm, so a script can reach the `Function` constructor via `globalThis.constructor.constructor` and from it `process` and every Node builtin), and an escapee holds the same process privileges as the host — Node's permission model is process-wide. The absent globals are API surface that keeps honest scripts portable, not walls. What the thread concretely buys: diff --git a/packages/workflow/workflow/README.md b/packages/workflow/workflow/README.md index 5332243a85..361001f0b0 100644 --- a/packages/workflow/workflow/README.md +++ b/packages/workflow/workflow/README.md @@ -2,6 +2,12 @@ The **workflow seam** (`ctx.workflows`): an abstract service defining WHAT a workflow engine does — execute a model-written orchestration script that fans out subagents — without saying HOW. The bash-shaped third of the [workflow family](../README.md): implementations subclass `WorkflowService` and register as the `workflows` service (one per context); [`dsh-workflow-workerthread`](../workflow-workerthread/README.md) (one worker thread per run) is the implementation, and [`dsh-tool-workflow`](../tool-workflow/README.md) is the model-facing consumer. +## Model Experience + +| Context surface | What the model sees | Token effect | +|---|---|---| +| None directly | The service seam and `workflow/*` observer events register no prompt, schema, or message. `dsh-tool-workflow` renders the parent-facing contract and final value; an engine decides which child prompts run. | Zero direct tokens. Parent result and child contexts affect tokens only through the consumer and implementation. | + ## Service: `WorkflowService` (abstract) `start(request: WorkflowStartRequest): WorkflowRun` — parse and execute a script. Throws synchronously (`SCRIPT_PARSE`/`META_INVALID`) for a script that cannot begin; once a run is returned, its `result` NEVER rejects — every failure resolves with `stopReason: 'error'` (or `'cancelled'`) — and once the run is cancelled, `result` settles within the implementation's bounded grace even if the script itself never settles (a consumer awaiting `result` must never be wedged past a cancellation). `dispose()` must reach quiescence within a bounded grace (cancel → wait for the script to settle and its children to finish disposing → abandon), never hanging its caller. Runs are HOLDER-owned: the engine does not track its live runs, so disposing the engine's fiber mid-run leaves each run to its holder's teardown. diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index 2f98c74eea..8f87ab66f7 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -267,6 +267,7 @@ function docSyncLeafGates(): Gate[] { pnpmScript('markdown-links', 'verify-md-links', { label: 'markdown links' }), pnpmScript('doc-refs', 'verify-doc-refs', { label: 'doc refs' }), pnpmScript('package-paths', 'verify-package-paths', { label: 'package paths' }), + pnpmScript('package-readme-model-experience', 'verify-package-readme-model-experience', { label: 'package README model experience' }), pnpmScript('mermaid', 'verify-mermaid'), pnpmScript('rfc-classification', 'verify-rfc-classification', { label: 'rfc classification' }), pnpmScript('rfc-format', 'verify-rfc-format', { label: 'rfc format' }), diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts new file mode 100644 index 0000000000..84ba22ea88 --- /dev/null +++ b/scripts/verify-package-readme-model-experience.ts @@ -0,0 +1,81 @@ +/** + * Doc-sync gate: require every workspace package README to explain its exact + * model-visible context surface and token behavior in the canonical table. + * + * Run: `tsx scripts/verify-package-readme-model-experience.ts`. + */ + +import { existsSync, globSync, readFileSync } from 'node:fs' +import { relative, resolve } from 'node:path' + +const root = resolve(import.meta.dirname, '..') +const HEADING = '## Model Experience' +const HEADING_PATTERN = /^## Model Experience$/gm +const TABLE_HEADER = '| Context surface | What the model sees | Token effect |' +const TABLE_DIVIDER = '|---|---|---|' + +interface Failure { + path: string + message: string +} + +const failures: Failure[] = [] +const packageJsons = globSync('packages/*/*/package.json', { cwd: root }).sort() + +for (const packageJson of packageJsons) { + const readme = packageJson.replace(/package\.json$/, 'README.md') + const abs = resolve(root, readme) + if (!existsSync(abs)) { + failures.push({ path: readme, message: `missing package README; add one with ${HEADING}` }) + continue + } + + const source = readFileSync(abs, 'utf8') + const matches = [...source.matchAll(HEADING_PATTERN)] + if (matches.length !== 1) { + failures.push({ + path: readme, + message: matches.length === 0 ? `missing ${HEADING}` : `contains ${matches.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 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) { + failures.push({ path: readme, message: `must contain the exact table header ${TABLE_HEADER}` }) + continue + } + + const rows = lines.slice(headerIndex + 2).filter(line => line.startsWith('|')) + 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()) + if (cells.length !== 3 || cells.some(cell => cell.length === 0)) { + failures.push({ path: readme, message: `invalid three-column Model Experience row: ${row}` }) + } + } +} + +if (failures.length === 0) { + console.log(`verify-package-readme-model-experience: ${packageJsons.length} README(s) carry the canonical ${HEADING} table.`) + process.exit(0) +} + +console.error('verify-package-readme-model-experience failed:') +for (const failure of failures) { + console.error(` ${relative(root, resolve(root, failure.path))}: ${failure.message}`) +} +process.exit(1)