Merge remote-tracking branch 'origin/master' into structured-output-subagent-seam

# Conflicts:
#	packages/hooks/hooks-claude/tests/bridge.spec.ts
This commit is contained in:
Tianyi Cui
2026-07-07 09:41:14 +08:00
79 changed files with 2933 additions and 168 deletions

View File

@@ -17,7 +17,7 @@ Every fact has exactly one home — the tier whose job it is — and every other
| [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 |
| [development.md](development.md) | Human-facing setup and daily workflow; a bilingual pair under the [i18n contract](i18n/README.md) | 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/tools.md), [persistence-catalog](persistence-catalog/log-events.md), [module-graph.md](module-graph.md) | Exhaustive enumerations regenerated from source, freshness-gated | Hand edits of any kind |
| 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) |
Placement test: a story about a bug → postmortem. Why we chose X → RFC. How to do task Y → cookbook. What type Z looks like → core-data-structures. What package P promises → its README. A rule every agent must always obey → root AGENTS.md, one line, linking the home that holds the why.

761
docs/config-catalog.md Normal file
View File

@@ -0,0 +1,761 @@
<!-- Generated by scripts/gen-config-catalog.ts — do not edit by hand.
Run `pnpm run gen-config-catalog` to regenerate. -->
# Plugin Config Catalog
Every `config:` block a `cordis.yml` entry can set: for each loadable harness package, the verbatim config declaration (JSDoc included) its `apply` function or service constructor receives, with every referenced type pasted alongside (package-local types) or linked (everything else). The paste is the plugin's full declared config type — a field the runtime schema deliberately excludes is a runtime-only seam (its own JSDoc says so) and is not settable from `cordis.yml`. This is the **deployment**-axis reference — the wiring a plugin author works against is the cordis [events](cordis-catalog/events.md) + [services](cordis-catalog/services.md) catalogs, the model-facing tool schemas are the [tool catalog](tool-catalog.md), and [core-data-structures/](core-data-structures/core.md) documents the types these declarations reference.
This file is GENERATED from source (`scripts/gen-config-catalog.ts`) and verified fresh by `pnpm run verify-config-catalog` (part of `doc-sync`) — do not edit it by hand. Declaration blocks use a `ts config-catalog` fence (skipped by doc-typecheck, since a lone declaration referencing imports is not standalone-compilable). The generator also cross-checks the runtime schemastery schema against the pasted declaration — every schema-validated key, nested keys included, must be locatable on the declared config type — so the paste cannot hide a loader-accepted field.
A `Requires:` line lists the service keys the plugin `inject`s: its `cordis.yml` tree must also load providers for those services. Scope is the harness tier (`packages/`); the vendored cordis plugins a config tree may also load (`hmr`, the console logger, …) are pinned upstream source ([vendoring policy](../vendor/README.md)) and not catalogued here.
## `@deepseek-ai/dsh-acp`
Requires: `agents` · `sessions` · `sessionPersistence` · `tools`
```ts config-catalog
/** Plugin config: the agent template ACP sessions are created from. */
export interface AcpConfig {
/** Model name for created agents (must have a registered adapter). */
model?: string
/**
* Transport stream override. Production omits this (the plugin wires
* `process.stdin`/`process.stdout` via `ndJsonStream`). Tests inject an
* in-memory `Stream` (e.g. an `ndJsonStream` over a `Duplex` pair) to drive
* the bridge without a subprocess. Not part of the schemastery `Config` —
* it is a runtime-only seam, never set from a `cordis.yml`.
*/
stream?: Stream
}
```
Depends on: `Stream` (`@agentclientprotocol/sdk`)
Source: [`packages/ui/acp/src/index.ts:115`](../packages/ui/acp/src/index.ts)
## `@deepseek-ai/dsh-acp-agent`
```ts config-catalog
/**
* App config: the swappable per-deployment values. `model` configures the
* agent template the ACP bridge creates each session's agent from (NOT a
* pre-created agent — ACP creates agents at `session/new`); `persona` is the
* deployment persona (forwarded to the system-prompt plugin);
* `persistenceRoot` is the JSONL backend's directory.
*/
export interface Config {
/** Model name for ACP-created agents (must have a registered adapter). */
model: string
/** Deployment persona (the system-prompt plugin's `persona` config). */
persona?: string
/** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
persistenceRoot?: string
}
```
Source: [`packages/ui/acp-agent/src/index.ts:48`](../packages/ui/acp-agent/src/index.ts)
## `@deepseek-ai/dsh-agent-core`
```ts config-catalog
/**
* Bundle config: each field forwarded verbatim to the child that owns it —
* `agents` to the agent loop (an app that pre-creates no agents, like the ACP
* bridge, simply omits it), `persona` to the system-prompt plugin (the
* deployment's persona section). Both are optional INPUT here because each
* owner's schema supplies the default (`[]` / `''`); the schema is the
* INTERSECTION of the owners' own schemas, so validation and defaulting can
* never drift from them.
*/
export interface Config {
/** The agent-loop `agents` list (see dsh-agent-loop's `Config`). */
agents?: AgentLoopConfig['agents']
/** The deployment persona (see dsh-system-prompt's `Config`). */
persona?: SystemPromptConfig['persona']
}
```
Depends on: [`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`SystemPromptConfig`](#deepseek-aidsh-system-prompt)
Source: [`packages/core/agent-core/src/index.ts:68`](../packages/core/agent-core/src/index.ts)
## `@deepseek-ai/dsh-agent-loop`
Requires: `agents` · `sessions` · `llm` · `tools` · `systemPrompt`
```ts config-catalog
export interface Config {
/** Agents created from configuration at startup. */
agents: (AgentOptions & {
/** Agent id to register under; also seeds the fresh per-run session id (`${id}-session-<uuid>`). */
id: AgentId
/**
* If set, the config agent RESUMES this persisted session id instead of
* starting a fresh `${id}-session-<uuid>`. Sourced from an env var in
* cordis.yml (`resumeSessionId: !!js process.env.RESUME_SESSION_ID`), so a
* demo can continue a prior conversation without code changes. Requires a
* `dsh-session-persistence` backend; the resume is deferred until that
* service is available (via `ctx.inject`) and the loaded session's events
* seed the live session so history continues.
*
* The schema accepts a plain string at runtime (cordis.yml values are
* untyped); the brand is compile-time only — the config format is the
* boundary where an id enters, so the TYPE declares the brand here.
*/
resumeSessionId?: SessionId
})[]
}
```
Depends on: [`AgentId`](../packages/core/agent/src/index.ts) · [`AgentOptions`](../packages/core/agent/src/index.ts) · [`SessionId`](../packages/core/session/src/index.ts)
Source: [`packages/core/agent-loop/src/index.ts:32`](../packages/core/agent-loop/src/index.ts)
## `@deepseek-ai/dsh-bash-local`
```ts config-catalog
/** Plugin config (all optional — `static Config` supplies the defaults). */
export interface Config {
/** Default working directory for commands (default: process.cwd()). */
cwd?: string
/** Default foreground timeout in milliseconds. */
timeoutMs?: number
/** Upper bound for per-call timeout overrides. */
maxTimeoutMs?: number
/** Per-stream in-memory output cap; overflow spills to a temp file. */
maxOutputBytes?: number
/** Grace period between the SIGTERM and the SIGKILL escalation on a kill. */
graceMs?: number
}
```
Source: [`packages/bash/bash-local/src/index.ts:28`](../packages/bash/bash-local/src/index.ts)
## `@deepseek-ai/dsh-compact-basic`
Requires: `llm`
```ts config-catalog
/**
* Backend configuration. Every knob is REQUIRED except `auto` and
* `charsPerToken`: there is no concrete data yet to justify default
* thresholds/budgets, so a consumer must state each value explicitly rather
* than inherit a guessed default. `auto` alone defaults to `true`
* (auto-compaction is the intended posture), and `charsPerToken` defaults to
* the English-text heuristic its estimator was calibrated on.
*/
export interface BasicCompactConfig {
/** Context window size in tokens. */
contextWindow: number
/** Compact when estimated token usage exceeds this fraction of context window. */
thresholdRatio: number
/** Number of tokens of recent context to retain during compaction. */
retainTokens: number
/** Model to use for summarization (`''` — uses the agent's model). */
summarizationModel: string
/** Provider generation cap for the summarization call. */
maxTokens: number
/** Extra compaction attempts when the first compacted surface is still over threshold. */
compactionRetries: number
/** Enable automatic compaction on the `agent/pre-step` seam (default true). */
auto?: boolean
/**
* Text density for the token estimator: estimated tokens = chars /
* `charsPerToken`. Defaults to 4 (typical English text). A CJK-heavy
* deployment should set ~1-2 — CJK runs at roughly 1-2 chars per token, so
* the default UNDERestimates several-fold and compaction fires far too late.
* May be fractional.
*/
charsPerToken?: number
}
```
Source: [`packages/compact/compact-basic/src/types.ts:20`](../packages/compact/compact-basic/src/types.ts)
## `@deepseek-ai/dsh-fs-local`
```ts config-catalog
/** Configuration for the local filesystem backend. */
export interface Config {
/** Base directory for relative paths. Defaults to `process.cwd()`. */
cwd?: string
}
```
Source: [`packages/fs/fs-local/src/index.ts:58`](../packages/fs/fs-local/src/index.ts)
## `@deepseek-ai/dsh-hooks-claude`
Requires: `bash`
```ts config-catalog
/** Plugin config: where the CC hook config lives + substitution roots. */
export interface Config {
/**
* Path to a `hooks.json` or a settings file whose `hooks` key holds the config.
* PROCESS-LEVEL: read once at load, a relative path resolves against the process
* launch cwd, so one config applies to the whole process.
* TODO(per-session-hook-config): per-session discovery of a project-local
* `hooks.json` from each `session/new.cwd` is not yet implemented.
*/
configPath: string
/**
* Replaces `${CLAUDE_PLUGIN_ROOT}` in command strings (the plugin's root dir).
*/
pluginRoot?: string
/**
* Replaces `${CLAUDE_PROJECT_DIR}` in command strings AND is exported as the
* `CLAUDE_PROJECT_DIR` env var for hook processes. When omitted, the env var
* defaults per-run to the agent's session workspace (`session.header.cwd`, the
* same dir the hook runs in) — Claude Code always exports this var, and common
* unmodified hooks reference `$CLAUDE_PROJECT_DIR` for project-relative paths.
*/
projectDir?: string
/** Default per-hook timeout in ms when a hook sets none (CC default: 600000). */
defaultTimeoutMs?: number
/** Character cap for the `hook/result` event's persisted stderr summary. */
stderrSummaryMaxChars?: number
}
```
Source: [`packages/hooks/hooks-claude/src/index.ts:55`](../packages/hooks/hooks-claude/src/index.ts)
## `@deepseek-ai/dsh-hooks-codex`
Requires: `bash`
```ts config-catalog
/** Plugin config: where the Codex hooks.json lives + the model name for payloads. */
export interface Config {
/**
* Path to a Codex `hooks.json`. PROCESS-LEVEL: read once at load, a relative
* path resolves against the process launch cwd.
* TODO(per-session-hook-config): per-session project-local discovery from each
* `session/new.cwd` is not yet implemented.
*/
configPath: string
/** The model name stamped on every payload (Codex includes `model` on each event). */
model?: string
/** Default per-hook timeout in ms when a hook sets none (Codex default: 600000). */
defaultTimeoutMs?: number
/** Character cap for the `hook/result` event's persisted stderr summary. */
stderrSummaryMaxChars?: number
}
```
Source: [`packages/hooks/hooks-codex/src/index.ts:42`](../packages/hooks/hooks-codex/src/index.ts)
## `@deepseek-ai/dsh-invariants`
Requires: `sessions`
```ts config-catalog
/** Plugin config. */
export interface Config {
/**
* Deep-freeze logged session-event data so mutating a logged event throws.
* Default true — this plugin only runs in dev/test, where freezing is the
* point. Set false to assert the event contract without freezing.
*/
freeze?: boolean
}
```
Source: [`packages/support/invariants/src/index.ts:45`](../packages/support/invariants/src/index.ts)
## `@deepseek-ai/dsh-llm-deepseek`
Requires: `llm`
```ts config-catalog
export interface Config {
/** API key; falls back to $DEEPSEEK_API_KEY. Required one way or the other. */
apiKey?: string
/** Endpoint base; falls back to $DEEPSEEK_BASE_URL, then the public API. */
baseURL?: string
/** Model names to register (sent verbatim on the wire). */
models?: string[]
/** Thinking-mode default for every request (provider default: enabled). */
thinking?: 'enabled' | 'disabled'
/** Thinking effort (only meaningful with thinking enabled). */
reasoningEffort?: 'high' | 'max'
}
```
Source: [`packages/llm/llm-deepseek/src/index.ts:37`](../packages/llm/llm-deepseek/src/index.ts)
## `@deepseek-ai/dsh-llm-pi-ai`
Requires: `llm`
```ts config-catalog
export interface Config {
/** API key; falls back to $DEEPSEEK_API_KEY. Required one way or the other. */
apiKey?: string
/** Endpoint base; falls back to $DEEPSEEK_BASE_URL, then the public API. */
baseURL?: string
/** Model names to register (sent verbatim on the wire). */
models?: string[]
/**
* Thinking level for every request: 'off' disables thinking mode; 'high'
* and 'xhigh' (wire 'max') set the effort. Omitted = provider default
* (thinking enabled), matching llm-deepseek's omission semantics.
*/
reasoning?: PiAiReasoning
}
/** Reasoning levels surfaced by this adapter (DeepSeek wire: high|max). */
export type PiAiReasoning = 'off' | 'high' | 'xhigh'
```
Source: [`packages/llm/llm-pi-ai/src/index.ts:32`](../packages/llm/llm-pi-ai/src/index.ts)
## `@deepseek-ai/dsh-llm-replay`
Requires: `llm`
```ts config-catalog
export interface Config {
/** Override the fixture path; defaults to `$DSH_SNAPSHOT_FILE`. */
file?: string
/** Override the sidecar path; defaults to `$DSH_SNAPSHOT_OVERRIDE`. */
overrideFile?: string
/**
* Override the child-log paths; defaults to `$DSH_SNAPSHOT_CHILD_FILES` (a
* path-separator-delimited list). Each is a recorded subagent session log for
* a nested-agent scenario; absent/empty for a single-session scenario.
*/
childFiles?: string[]
}
```
Source: [`packages/support/llm-replay/src/index.ts:411`](../packages/support/llm-replay/src/index.ts)
## `@deepseek-ai/dsh-session-persistence-jsonl`
Requires: `sessions`
```ts config-catalog
export interface Config {
/**
* Root directory for all session files. Required (no default): a default of
* `process.cwd()` would scatter session files as the process's cwd changes
* (bash calls, subprocesses). Sessions group under per-cwd subdirectories.
*/
root: string
}
```
Source: [`packages/session-persistence/session-persistence-jsonl/src/index.ts:34`](../packages/session-persistence/session-persistence-jsonl/src/index.ts)
## `@deepseek-ai/dsh-session-persistence-sqlite`
Requires: `sessions`
```ts config-catalog
/** Plugin configuration. */
export interface Config {
/**
* Filesystem path to the SQLite database file. The special value `:memory:`
* opens an in-process database (tests); a file path is created (with parent
* dirs) on construction.
*/
path: string
/**
* SQLite `journal_mode` pragma. `wal` (the default) is the recorded
* durability model; pick a rollback-journal mode (`delete`/`truncate`/
* `persist`) on filesystems where WAL's shared-memory files do not work
* (network mounts). See {@link JournalMode}.
*/
journalMode?: JournalMode
}
/**
* Journal modes the backend will run under. `wal` is the default and the
* durability model the persistence ADR records; the rollback-journal modes
* (`delete`/`truncate`/`persist`) exist for filesystems where WAL's
* shared-memory files do not work (network mounts). `memory`/`off` are
* excluded: dropping journal durability silently contradicts what this
* backend promises.
*/
export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
```
Source: [`packages/session-persistence/session-persistence-sqlite/src/index.ts:50`](../packages/session-persistence/session-persistence-sqlite/src/index.ts)
## `@deepseek-ai/dsh-stdio-agent`
```ts config-catalog
/**
* App config: the swappable per-demo values, each routed to where the app wires
* it. `model`/`resumeSessionId` configure the pre-created `main` agent (through
* {@link @deepseek-ai/dsh-agent-core}'s forwarded `agents` list); `persona` is
* the deployment persona (forwarded to the system-prompt plugin);
* `persistenceRoot` is the JSONL backend's directory; `welcome` is the UI banner.
*/
export interface Config {
/** Model name for the `main` agent (must have a registered adapter). */
model: string
/** Deployment persona (the system-prompt plugin's `persona` config). */
persona?: string
/** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
persistenceRoot?: string
/** stdin-chat banner printed once on start. Defaults to `'ready.'`. */
welcome?: string
/**
* If set, the `main` agent RESUMES this persisted session id instead of
* starting fresh. Sourced from an env var in the leaf `cordis.yml`
* (`resumeSessionId: !!js process.env.RESUME_SESSION_ID`).
*/
resumeSessionId?: string
}
```
Source: [`packages/ui/stdio-agent/src/index.ts:59`](../packages/ui/stdio-agent/src/index.ts)
## `@deepseek-ai/dsh-subagent-acp`
Requires: `subagents`
```ts config-catalog
/** Config: how to spawn and drive the child ACP agent process. */
export interface Config {
/** Provider name on `ctx.subagents` (default `acp`). */
providerName: string
/** The executable to spawn for each run (the child ACP agent). */
command: string
/** Arguments passed to {@link command}. */
args: string[]
/**
* Working directory for the child process and its ACP session. Defaults to
* the parent process's cwd when omitted.
*/
cwd?: string
/**
* How to auto-answer the child's `session/request_permission` prompts:
* `reject` (default — decline every prompt) or `allow` (approve via the first
* allow-shaped option). The first cut surfaces no prompt to a human.
*/
permission: PermissionPolicy
/**
* Extra environment variables for the child process — e.g. the child
* harness's own `DEEPSEEK_API_KEY`. Forwarded on top of a credential-scrubbed
* copy of the parent env, so an explicit key here reaches the child while
* ambient secrets do not leak implicitly.
*/
env: Record<string, string>
/**
* Grace period (ms) for the child's EOF-driven quiesce on dispose — its
* window to flush persistence and tear down its own nested subprocesses
* before the parent escalates to a signal.
*/
disposeEofGraceMs?: number
/** Grace period (ms) between `SIGTERM` and the `SIGKILL` escalation on dispose. */
disposeGraceMs?: number
}
/**
* How the client answers a child's `session/request_permission`. The first cut
* does not surface permission prompts to a human, so every request is
* auto-answered by this fixed policy:
*
* - `reject` — decline every prompt (answer `cancelled`). Safe default: a child
* that asks before a side effect does not get to take it.
* - `allow` — approve every prompt by selecting its first `allow_*` option (or,
* if none is offered, `cancelled`). Use when the child is trusted to act.
*/
export type PermissionPolicy = 'allow' | 'reject'
```
Source: [`packages/subagent/subagent-acp/src/index.ts:30`](../packages/subagent/subagent-acp/src/index.ts)
## `@deepseek-ai/dsh-subagent-fork`
Requires: `subagents` · `agents`
```ts config-catalog
/** Config: the registry name to register the provider under. */
export interface Config {
/** Provider name on `ctx.subagents` (default `fork`). */
providerName: string
}
```
Source: [`packages/subagent/subagent-fork/src/index.ts:34`](../packages/subagent/subagent-fork/src/index.ts)
## `@deepseek-ai/dsh-subagent-mock`
Requires: `subagents`
```ts config-catalog
/** Config for the mock provider; all optional with test-friendly defaults. */
export interface Config {
/** Registry name to register under. */
name: string
/** The text the scripted child "returns" as its final answer. */
reply?: string
/** The stop reason the run settles with. */
stopReason?: SubagentStopReason
/** Which start-time capabilities to advertise (default: all `true`). */
capabilities?: Partial<SubagentCapabilities>
/**
* The context contract to declare ({@link SubagentProvider.inheritsParentContext});
* default `false` (spawn-like). Set `true` to exercise the fork-shaped tool
* wording in consumer tests.
*/
inheritsParentContext?: boolean
/**
* Structured value surfaced when a request carries an `outputSchema` and the
* `outputSchema` capability is on (default: `{ reply }`).
*/
structured?: unknown
}
```
Depends on: [`SubagentCapabilities`](../packages/subagent/subagent/src/index.ts) · [`SubagentStopReason`](../packages/subagent/subagent/src/index.ts)
Source: [`packages/support/subagent-mock/src/index.ts:84`](../packages/support/subagent-mock/src/index.ts)
## `@deepseek-ai/dsh-subagent-spawn`
Requires: `subagents` · `agents`
```ts config-catalog
/** Config: the registry name to register the provider under. */
export interface Config {
/** Provider name on `ctx.subagents` (default `spawn`). */
providerName: string
}
```
Source: [`packages/subagent/subagent-spawn/src/index.ts:26`](../packages/subagent/subagent-spawn/src/index.ts)
## `@deepseek-ai/dsh-system-prompt`
```ts config-catalog
export interface Config {
/**
* The deployment's persona — the ONE deployment-authored fragment of the
* system prompt, rendered as the order-0 `deployment:persona` section
* (after the harness identity, before all tool guidance). Every agent in
* the context shares it, subagents included. Template, not free-form text:
* every complete `{{…}}` group is interpreted strictly against the
* registered prompt variables (the shipped agent loop registers `{{model}}`
* and `{{cwd}}`), and there is no escape syntax for literal `{{…}}` prose
* yet (a deliberate deferral; see the prompt-variables RFC). Defaults to
* `''` — the empty section is dropped at render, so a persona-less
* deployment opens with the harness identity alone.
*/
persona?: string
}
```
Source: [`packages/core/system-prompt/src/index.ts:113`](../packages/core/system-prompt/src/index.ts)
## `@deepseek-ai/dsh-tool-fs`
Requires: `tools` · `fs` · `systemPrompt`
```ts config-catalog
/** Plugin config (all optional — `Config` supplies the defaults). */
export interface Config {
/** Default and maximum number of lines returned by one `read` call. */
readLimit?: number
/** Maximum characters returned for a single line before truncation. */
readMaxLineLength?: number
/** Maximum bytes returned for the selected lines of one `read` call. */
readMaxBytes?: number
/** Files at or above this size stream instead of loading whole into memory. */
readStreamMinSize?: number
}
```
Source: [`packages/fs/tool-fs/src/index.ts:48`](../packages/fs/tool-fs/src/index.ts)
## `@deepseek-ai/dsh-tool-subagent`
Requires: `tools` · `subagents`
```ts config-catalog
/** Config: which registered provider this tool delegates to, plus child defaults. */
export interface Config {
/** The `ctx.subagents` provider name to start runs on (e.g. `spawn`, `acp`). */
provider: string
/**
* The model-facing tool name to register (default `subagent`). To expose more
* than one transport, load this plugin once per provider — each load MUST set
* a distinct `toolName` (the tool registry rejects a duplicate name), e.g.
* `{ provider: 'spawn', toolName: 'subagent' }` and
* `{ provider: 'acp', toolName: 'subagent_acp' }`.
*/
toolName?: string
/**
* Default per-child agent options (model) applied to every spawned child.
* Omitted fields fall back to the child loop's own defaults. There is no
* per-child persona: the deployment persona (the system-prompt plugin's
* `persona` config) is a context-wide section every agent shares.
*/
agentOptions?: AgentOptions
}
```
Depends on: [`AgentOptions`](../packages/core/agent/src/index.ts)
Source: [`packages/subagent/tool-subagent/src/index.ts:44`](../packages/subagent/tool-subagent/src/index.ts)
## `@deepseek-ai/dsh-tool-web`
Requires: `tools` · `web` · `systemPrompt`
```ts config-catalog
export interface Config {
/** Register `web_search`. Defaults to true. */
search?: boolean
/** Register `web_fetch`. Defaults to true. */
fetch?: boolean
/** Upper bound on sources returned by one `web_search` call. */
searchMaxResults?: number
}
```
Source: [`packages/web/tool-web/src/index.ts:36`](../packages/web/tool-web/src/index.ts)
## `@deepseek-ai/dsh-web`
```ts config-catalog
/**
* Config for the web seam. `searchProvider` / `fetchProvider` pin which provider
* wins for each capability; both are optional (a single registered usable
* provider auto-selects). Operational overrides such as environment variables
* must feed these same fields rather than introduce a hidden priority chain.
*/
export interface WebServiceConfig {
/** Explicit search provider id. Omitted = auto-select when exactly one usable. */
readonly searchProvider?: string
/** Explicit fetch provider id. Omitted = auto-select when exactly one usable. */
readonly fetchProvider?: string
}
```
Source: [`packages/web/web/src/index.ts:68`](../packages/web/web/src/index.ts)
## `@deepseek-ai/dsh-web-fetch-local`
Requires: `web`
```ts config-catalog
export interface Config {
/** Maximum accepted request URL length. */
maxUrlLength?: number
/** Maximum response body size in bytes. */
maxResponseBytes?: number
/** Maximum decoded body length in characters. */
maxBodyChars?: number
/** Default fetch timeout in milliseconds. */
timeoutMs?: number
/** Upper bound for a per-request timeout override. */
maxTimeoutMs?: number
/** Maximum number of same-origin redirect hops to follow. */
maxRedirects?: number
/** `User-Agent` header sent on every request. */
userAgent?: string
}
```
Source: [`packages/web/web-fetch-local/src/index.ts:33`](../packages/web/web-fetch-local/src/index.ts)
## `@deepseek-ai/dsh-web-search-deepseek`
Requires: `web`
```ts config-catalog
export interface Config {
/** DeepSeek API key. Falls back to `$DEEPSEEK_API_KEY`. Empty → unavailable. */
apiKey?: string
/** Anthropic-compatible endpoint base; `/messages` is appended. */
baseURL?: string
/** Anthropic-format model name. Defaults to `deepseek-v4-flash`. */
model?: string
/** `anthropic-version` header value. Defaults to `2023-06-01`. */
apiVersion?: string
/** Upper bound on generated tokens for the Messages request. Defaults to 4096. */
maxTokens?: number
/** Maximum `web_search` server-tool uses per request. Defaults to 5. */
maxUses?: number
}
```
Source: [`packages/web/web-search-deepseek/src/index.ts:47`](../packages/web/web-search-deepseek/src/index.ts)
## `@deepseek-ai/dsh-web-search-exa`
Requires: `web`
```ts config-catalog
export interface Config {
/** Exa API key. Falls back to `$EXA_API_KEY`. Empty → provider unavailable. */
apiKey?: string
/** Endpoint base; `/search` is appended. Defaults to the public API. */
baseURL?: string
/** Retrieval mode sent as Exa's `type`. Defaults to `auto`. */
searchType?: 'auto' | 'keyword' | 'neural'
/** Default result count when a request carries no `maxResults`. Omitted = none. */
numResults?: number
/** Highlight sentences requested per result. Defaults to 1. */
highlightsPerResult?: number
}
```
Source: [`packages/web/web-search-exa/src/index.ts:38`](../packages/web/web-search-exa/src/index.ts)
## `@deepseek-ai/dsh-web-search-perplexity`
Requires: `web`
```ts config-catalog
export interface Config {
/** Perplexity API key. Falls back to `$PERPLEXITY_API_KEY`. Empty → unavailable. */
apiKey?: string
/** Endpoint base; `/chat/completions` is appended. Defaults to the public API. */
baseURL?: string
/** Search model name. Defaults to `sonar`. */
model?: string
/** Upper bound on generated answer tokens. Defaults to 1024. */
maxTokens?: number
/** Recency window sent as `search_recency_filter`. Omitted = no filter. */
searchRecency?: 'day' | 'week' | 'month' | 'year'
}
```
Source: [`packages/web/web-search-perplexity/src/index.ts:32`](../packages/web/web-search-perplexity/src/index.ts)
## Loadable plugins with no config
These load from a `cordis.yml` entry with no `config:` block; they declare no config surface.
- `@deepseek-ai/dsh-agent` ([`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts))
- `@deepseek-ai/dsh-fs-policy` ([`packages/fs/fs-policy/src/index.ts`](../packages/fs/fs-policy/src/index.ts))
- `@deepseek-ai/dsh-llm` ([`packages/llm/llm/src/index.ts`](../packages/llm/llm/src/index.ts))
- `@deepseek-ai/dsh-session` ([`packages/core/session/src/index.ts`](../packages/core/session/src/index.ts))
- `@deepseek-ai/dsh-subagent` ([`packages/subagent/subagent/src/index.ts`](../packages/subagent/subagent/src/index.ts))
- `@deepseek-ai/dsh-tool-bash` — requires `tools` · `bash` · `systemPrompt` ([`packages/bash/tool-bash/src/index.ts`](../packages/bash/tool-bash/src/index.ts))
- `@deepseek-ai/dsh-tool-todo` — requires `tools` ([`packages/todo/tool-todo/src/index.ts`](../packages/todo/tool-todo/src/index.ts))
- `@deepseek-ai/dsh-tools` — requires `systemPrompt` ([`packages/core/tools/src/index.ts`](../packages/core/tools/src/index.ts))
## Seam packages (not directly loadable)
Abstract service classes — a deployment loads a concrete implementation package instead ([capability seams](rfc/implemented/architecture/2026-06-13-capability-seams.md)).
- `@deepseek-ai/dsh-bash` — abstract `BashExecutor` ([`packages/bash/bash/src/index.ts`](../packages/bash/bash/src/index.ts))
- `@deepseek-ai/dsh-compact` — abstract `CompactService` ([`packages/compact/compact/src/index.ts`](../packages/compact/compact/src/index.ts))
- `@deepseek-ai/dsh-fs` — abstract `FileSystem` ([`packages/fs/fs/src/index.ts`](../packages/fs/fs/src/index.ts))
- `@deepseek-ai/dsh-session-persistence` — abstract `SessionPersistence` ([`packages/session-persistence/session-persistence/src/index.ts`](../packages/session-persistence/session-persistence/src/index.ts))
## Library packages (no plugin entry)
Imported as libraries by other packages; a `cordis.yml` cannot load them.
- `@deepseek-ai/dsh-app-boot` ([`packages/ui/app-boot/src/index.ts`](../packages/ui/app-boot/src/index.ts))
- `@deepseek-ai/dsh-brand` ([`packages/util/brand/src/index.ts`](../packages/util/brand/src/index.ts))
- `@deepseek-ai/dsh-hook-protocol` ([`packages/hooks/hook-protocol/src/index.ts`](../packages/hooks/hook-protocol/src/index.ts))
- `@deepseek-ai/dsh-subagent-inprocess` ([`packages/subagent/subagent-inprocess/src/index.ts`](../packages/subagent/subagent-inprocess/src/index.ts))

View File

@@ -21,7 +21,7 @@ createAgent(options: CreateAgentOptions): AgentHandle
async resume(options: ResumeAgentOptions): Promise<AgentHandle>
```
Source: [`packages/core/agent-loop/src/index.ts:63`](../../packages/core/agent-loop/src/index.ts)
Source: [`packages/core/agent-loop/src/index.ts:64`](../../packages/core/agent-loop/src/index.ts)
## `ctx.agents` — `AgentRegistry`

View File

@@ -1,6 +1,6 @@
# Session Persistence
The **durability seam** for the event log. [session.md](session.md) describes the in-memory `Session` — the append-only `SessionEvent` log that is the source of truth. This page describes how that log is made durable: the abstract `SessionPersistence` service, its backends, the flush checkpoint, crash recovery, and the metadata header that travels alongside the log. The event vocabulary the log carries is enumerated, member by member, in the generated [persistence log event catalog](../persistence-catalog/log-events.md).
The **durability seam** for the event log. [session.md](session.md) describes the in-memory `Session` — the append-only `SessionEvent` log that is the source of truth. This page describes how that log is made durable: the abstract `SessionPersistence` service, its backends, the flush checkpoint, crash recovery, and the metadata header that travels alongside the log. The event vocabulary the log carries is enumerated, member by member, in the generated [persistence log event catalog](../persistence-catalog.md).
The seam is a textbook [capability seam](../rfc/implemented/architecture/2026-06-13-capability-seams.md): one abstract service ([dsh-session-persistence](../../packages/session-persistence/session-persistence), `ctx.sessionPersistence`) defining create/append/load/list over the existing `SessionEvent`**no parallel persisted type** — and two interchangeable backends that pass the same `runPersistenceContract` suite. See the [session-persistence RFC](../rfc/implemented/architecture/2026-06-14-session-persistence.md).

View File

@@ -6,7 +6,7 @@ Source: [`packages/core/session/src/types.ts`](../../packages/core/session/src/t
## `SessionEventMap` — the event vocabulary
The append-only event types. Merge-extensible: a plugin declares extra event types via declaration merging — e.g. the [compaction seam](compaction.md) adds `compact/start` / `compact/summary` / `compact/end`, and `@deepseek-ai/dsh-hook-protocol` adds log-only `hook/invoked` / `hook/result` provenance for a hook bridge. Like `compact/*`, these are NOT `SurfaceEventType`s (no `surfaceOp`). The generated [persistence log event catalog](../persistence-catalog/log-events.md) enumerates every member — core and merged — with its payload, surface badge, and declaration site.
The append-only event types. Merge-extensible: a plugin declares extra event types via declaration merging — e.g. the [compaction seam](compaction.md) adds `compact/start` / `compact/summary` / `compact/end`, and `@deepseek-ai/dsh-hook-protocol` adds log-only `hook/invoked` / `hook/result` provenance for a hook bridge. Like `compact/*`, these are NOT `SurfaceEventType`s (no `surfaceOp`). The generated [persistence log event catalog](../persistence-catalog.md) enumerates every member — core and merged — with its payload, surface badge, and declaration site.
```ts type-equiv
interface SessionEventMap {
@@ -264,7 +264,7 @@ Every session event lives **inside** a turn (between a `turn/start` and its `tur
## Plugin-contributed log-only events
A plugin may declaration-merge extra `SessionEventMap` types. These are **log-only**: NOT `SurfaceEventType`s (they carry no `surfaceOp` and contribute nothing to derived history), but, like every event, they must sit inside an open turn. The full per-event enumeration — core and plugin-contributed alike, with payloads and provenance — is the generated [persistence log event catalog](../persistence-catalog/log-events.md); the compaction seam's `compact/*` semantics are discussed on [compaction.md](compaction.md).
A plugin may declaration-merge extra `SessionEventMap` types. These are **log-only**: NOT `SurfaceEventType`s (they carry no `surfaceOp` and contribute nothing to derived history), but, like every event, they must sit inside an open turn. The full per-event enumeration — core and plugin-contributed alike, with payloads and provenance — is the generated [persistence log event catalog](../persistence-catalog.md); the compaction seam's `compact/*` semantics are discussed on [compaction.md](compaction.md).
The hook bridges' `hook/invoked` / `hook/result` provenance pairs (from `@deepseek-ai/dsh-hook-protocol`) correlate by `handlerId`. The mid-turn hook points (`PreToolUse`/`PostToolUse`/`UserPromptSubmit`/`Stop`) fire inside the loop's open turn, so their `hook/*` records are turn-enclosed by construction. `SessionStart` gets no `hook/*` record — its injected `context/message` is the durable evidence — because it has no open turn to enclose one (see [the hook-bridges RFC](../rfc/implemented/feature/2026-06-30-hook-bridges.md)).

View File

@@ -3,14 +3,14 @@
# Documentation Graph Index
These diagrams are the relationship layer above the generated catalogs. Use them to navigate package topology, capability seams, event flow, model-facing tools, app composition, and runtime lifecycle paths. Exact signatures and type shapes still live in the generated [events](cordis-catalog/events.md) / [services](cordis-catalog/services.md) catalogs, [tool-catalog/](tool-catalog/tools.md), and [core-data-structures/](core-data-structures/core.md).
These diagrams are the relationship layer above the generated catalogs. Use them to navigate package topology, capability seams, event flow, model-facing tools, app composition, and runtime lifecycle paths. Exact signatures and type shapes still live in the generated [events](cordis-catalog/events.md) / [services](cordis-catalog/services.md) catalogs, [tool-catalog.md](tool-catalog.md), and [core-data-structures/](core-data-structures/core.md).
The process decision behind this index is recorded in [the documentation graph RFC](rfc/implemented/process/2026-07-03-documentation-graph-atlas.md).
| Graph | Mode |
| --- | --- |
| [module dependency graph](module-graph.md) | `generated` |
| [tool schema catalog and package map](tool-catalog/tools.md) | `generated` |
| [tool schema catalog and package map](tool-catalog.md) | `generated` |
| [capability seams and core services](capability-seams.md) | `hybrid generated` |
| [echo-agent app composition](../examples/echo-agent/composition.md) | `hybrid generated` |
| [coding-agent app composition](../examples/coding-agent/composition.md) | `hybrid generated` |

View File

@@ -3,11 +3,11 @@
# Persistence Log Event Catalog
Every event type that can appear in a session's durable event log: each member of the merge-extensible `SessionEventMap` — the owning vocabulary in `@deepseek-ai/dsh-session` plus every plugin declaration merge in this repo — with the payload it carries, its surface badge, and the declaration it comes from. It complements [session.md](../core-data-structures/session.md) (the `SessionEvent` envelope, surface list, and `deriveMessages()` projection), [persistence.md](../core-data-structures/persistence.md) (how the log is made durable), and the [cordis events catalog](../cordis-catalog/events.md) (the live bus wiring — a log event is NOT a cordis event; it reaches listeners via the single `session/event` emit).
Every event type that can appear in a session's durable event log: each member of the merge-extensible `SessionEventMap` — the owning vocabulary in `@deepseek-ai/dsh-session` plus every plugin declaration merge in this repo — with the payload it carries, its surface badge, and the declaration it comes from. It complements [session.md](core-data-structures/session.md) (the `SessionEvent` envelope, surface list, and `deriveMessages()` projection), [persistence.md](core-data-structures/persistence.md) (how the log is made durable), and the [cordis events catalog](cordis-catalog/events.md) (the live bus wiring — a log event is NOT a cordis event; it reaches listeners via the single `session/event` emit).
This file is GENERATED from source (`scripts/gen-persistence-catalog.ts`) and verified fresh by `pnpm run verify-persistence-catalog` (part of `doc-sync`) — do not edit it by hand. Payload blocks use a `ts persistence-catalog` fence (skipped by doc-typecheck, since a bare payload fragment is not standalone-compilable). Type names in a payload link to the page that documents them. See [the persistence-log-catalog RFC](../rfc/implemented/process/2026-07-04-persistence-log-catalog.md).
This file is GENERATED from source (`scripts/gen-persistence-catalog.ts`) and verified fresh by `pnpm run verify-persistence-catalog` (part of `doc-sync`) — do not edit it by hand. Payload blocks use a `ts persistence-catalog` fence (skipped by doc-typecheck, since a bare payload fragment is not standalone-compilable). Type names in a payload link to the page that documents them. See [the persistence-log-catalog RFC](rfc/implemented/process/2026-07-04-persistence-log-catalog.md).
The on-disk envelope around every payload is `SessionEvent``type`, monotonic `seq`, epoch-ms `time`, the `data` documented here, plus `surfaceOp`/`sourceEventSeqs` on **surface** events only ([envelope](../core-data-structures/session.md#sessioneventt--one-log-entry)). **surface** marks a `SurfaceEventType` member: it produces an LLM message and declares how it joins the surface list. **log-only** marks everything else: durable, replayable record with no derived-history contribution. Every payload is JSON-serializable (enforced at `Session.append`), and the whole format is pinned at `SESSION_FORMAT_VERSION = 0` — pre-release, no compatibility implied ([the version stance](../core-data-structures/persistence.md)). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction.
The on-disk envelope around every payload is `SessionEvent``type`, monotonic `seq`, epoch-ms `time`, the `data` documented here, plus `surfaceOp`/`sourceEventSeqs` on **surface** events only ([envelope](core-data-structures/session.md#sessioneventt--one-log-entry)). **surface** marks a `SurfaceEventType` member: it produces an LLM message and declares how it joins the surface list. **log-only** marks everything else: durable, replayable record with no derived-history contribution. Every payload is JSON-serializable (enforced at `Session.append`), and the whole format is pinned at `SESSION_FORMAT_VERSION = 0` — pre-release, no compatibility implied ([the version stance](core-data-structures/persistence.md)). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction.
## Events
@@ -21,9 +21,9 @@ Raw stream chunk — token-level replay fidelity.
'assistant/chunk': { turn: number; step: number; chunk: StreamChunk }
```
Types: [StreamChunk](../core-data-structures/llm-streaming.md)
Types: [StreamChunk](core-data-structures/llm-streaming.md)
Source: [`packages/core/session/src/types.ts:298`](../../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:298`](../packages/core/session/src/types.ts)
#### `assistant/message` — surface
@@ -33,9 +33,9 @@ Assembled assistant message for one step (derived history uses this). Carries th
'assistant/message': { turn: number; step: number; content: ContentBlock[]; usage?: TokenUsage }
```
Types: [ContentBlock](../core-data-structures/core.md) · [TokenUsage](../core-data-structures/llm-streaming.md)
Types: [ContentBlock](core-data-structures/core.md) · [TokenUsage](core-data-structures/llm-streaming.md)
Source: [`packages/core/session/src/types.ts:305`](../../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:305`](../packages/core/session/src/types.ts)
### `compact/*`
@@ -47,7 +47,7 @@ Marks the end of a compaction — log-only, releases the lock. `error` set if su
'compact/end': { turn: number; error?: string }
```
Source: [`packages/compact/compact/src/types.ts:46`](../../packages/compact/compact/src/types.ts)
Source: [`packages/compact/compact/src/types.ts:46`](../packages/compact/compact/src/types.ts)
#### `compact/start` — log-only
@@ -57,7 +57,7 @@ Marks the start of a compaction — log-only, holds the lock until `compact/end`
'compact/start': { turn: number }
```
Source: [`packages/compact/compact/src/types.ts:23`](../../packages/compact/compact/src/types.ts)
Source: [`packages/compact/compact/src/types.ts:23`](../packages/compact/compact/src/types.ts)
#### `compact/summary` — log-only
@@ -67,9 +67,9 @@ Provenance record of a completed summarization — log-only, no surfaceOp. The s
'compact/summary': { summary: ContentBlock[]; shadowedRange: { start: number; end: number }; shadowedSeqs: number[]; shadowedTokenCount: number; model: string; maxTokens?: number }
```
Types: [ContentBlock](../core-data-structures/core.md)
Types: [ContentBlock](core-data-structures/core.md)
Source: [`packages/compact/compact/src/types.ts:30`](../../packages/compact/compact/src/types.ts)
Source: [`packages/compact/compact/src/types.ts:30`](../packages/compact/compact/src/types.ts)
### `context/*`
@@ -81,9 +81,9 @@ In-session context injection (file-change notices, subdir AGENTS.md, skill conte
'context/message': { content: ContentBlock[]; source: MessageSource }
```
Types: [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md)
Types: [ContentBlock](core-data-structures/core.md) · [MessageSource](core-data-structures/core.md)
Source: [`packages/core/session/src/types.ts:296`](../../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:296`](../packages/core/session/src/types.ts)
### `hook/*`
@@ -95,7 +95,7 @@ A hook command was invoked at a hook point — log-only provenance (like `compac
'hook/invoked': { turn: number; point: string; dialect: HookDialect; matcher?: string; handlerId: string }
```
Source: [`packages/hooks/hook-protocol/src/types.ts:27`](../../packages/hooks/hook-protocol/src/types.ts)
Source: [`packages/hooks/hook-protocol/src/types.ts:27`](../packages/hooks/hook-protocol/src/types.ts)
#### `hook/result` — log-only
@@ -105,7 +105,7 @@ A hook command's outcome — log-only, paired with a prior `hook/invoked` (same
'hook/result': { turn: number; point: string; handlerId: string; decision: string; exitCode?: number; stderrSummary?: string; durationMs: number }
```
Source: [`packages/hooks/hook-protocol/src/types.ts:45`](../../packages/hooks/hook-protocol/src/types.ts)
Source: [`packages/hooks/hook-protocol/src/types.ts:45`](../packages/hooks/hook-protocol/src/types.ts)
### `prompt/*`
@@ -117,9 +117,9 @@ A queued prompt an `agent/prompt-submit` listener VETOED — the durable record
'prompt/blocked': { content: ContentBlock[]; source: MessageSource; reason: string }
```
Types: [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md)
Types: [ContentBlock](core-data-structures/core.md) · [MessageSource](core-data-structures/core.md)
Source: [`packages/core/session/src/types.ts:290`](../../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:290`](../packages/core/session/src/types.ts)
### `request/*`
@@ -131,7 +131,7 @@ Full snapshot of the EpochHeader the NEXT request is built under, with the Reque
'request/header': { header: EpochHeader; reason: RequestHeaderReason }
```
Source: [`packages/core/session/src/types.ts:350`](../../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:350`](../packages/core/session/src/types.ts)
#### `request/header-delta` — log-only
@@ -141,7 +141,7 @@ Amendment to the folded EpochHeader: at least one of a SystemDelta, a ToolsDelta
'request/header-delta': { system?: SystemDelta; tools?: ToolsDelta; config?: LlmCallConfig }
```
Source: [`packages/core/session/src/types.ts:361`](../../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:361`](../packages/core/session/src/types.ts)
### `steering/*`
@@ -153,9 +153,9 @@ Steering content injected between steps of a running turn.
'steering/message': { turn: number; content: ContentBlock[]; source: MessageSource }
```
Types: [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md)
Types: [ContentBlock](core-data-structures/core.md) · [MessageSource](core-data-structures/core.md)
Source: [`packages/core/session/src/types.ts:323`](../../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:323`](../packages/core/session/src/types.ts)
### `step/*`
@@ -167,7 +167,7 @@ Closes step `step` of turn `turn`.
'step/end': { turn: number; step: number }
```
Source: [`packages/core/session/src/types.ts:277`](../../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:277`](../packages/core/session/src/types.ts)
#### `step/start` — log-only
@@ -177,7 +177,7 @@ Opens step `step` of turn `turn` — one model call plus the tool executions it
'step/start': { turn: number; step: number }
```
Source: [`packages/core/session/src/types.ts:275`](../../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:275`](../packages/core/session/src/types.ts)
### `todo/*`
@@ -191,9 +191,9 @@ NOT a SurfaceEventType: it produces no LLM message and never reaches `deriveMess
'todo/write': { todos: TodoItem[] }
```
Types: [TodoItem](../core-data-structures/session.md)
Types: [TodoItem](core-data-structures/session.md)
Source: [`packages/core/session/src/types.ts:337`](../../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:337`](../packages/core/session/src/types.ts)
### `tool/*`
@@ -205,9 +205,9 @@ The model requested one tool invocation: `name` with the raw `arguments` JSON st
'tool/call': { turn: number; step: number; callId: CallId; name: string; arguments: string }
```
Types: [CallId](../core-data-structures/core.md)
Types: [CallId](core-data-structures/core.md)
Source: [`packages/core/session/src/types.ts:311`](../../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:311`](../packages/core/session/src/types.ts)
#### `tool/result` — surface
@@ -217,9 +217,9 @@ A completed tool call's model-facing result, plus an optional tool-private `meta
'tool/result': { turn: number; step: number; callId: CallId; content: ContentBlock[]; isError: boolean; error?: { name: string; code: string }; meta?: unknown }
```
Types: [CallId](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md)
Types: [CallId](core-data-structures/core.md) · [ContentBlock](core-data-structures/core.md)
Source: [`packages/core/session/src/types.ts:321`](../../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:321`](../packages/core/session/src/types.ts)
### `turn/*`
@@ -231,9 +231,9 @@ Closes turn `turn` with the TurnEndReason that ended it. The loop fires the awai
'turn/end': { turn: number; reason: TurnEndReason }
```
Types: [TurnEndReason](../core-data-structures/session.md)
Types: [TurnEndReason](core-data-structures/session.md)
Source: [`packages/core/session/src/types.ts:273`](../../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:273`](../packages/core/session/src/types.ts)
#### `turn/start` — log-only
@@ -243,9 +243,9 @@ Opens turn `turn`. `trigger` records what started it — a drained message batch
'turn/start': { turn: number; trigger: TurnTrigger }
```
Types: [TurnTrigger](../core-data-structures/session.md)
Types: [TurnTrigger](core-data-structures/session.md)
Source: [`packages/core/session/src/types.ts:267`](../../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:267`](../packages/core/session/src/types.ts)
### `user/*`
@@ -257,6 +257,6 @@ A user-visible prompt (queued message drained at turn start).
'user/message': { content: ContentBlock[]; source: MessageSource }
```
Types: [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md)
Types: [ContentBlock](core-data-structures/core.md) · [MessageSource](core-data-structures/core.md)
Source: [`packages/core/session/src/types.ts:279`](../../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:279`](../packages/core/session/src/types.ts)

View File

@@ -142,6 +142,7 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand;
| [Generate the RFC index tables](implemented/process/2026-07-04-generate-rfc-index-tables.md) | 2026-07-04 |
| [Generated persistence log event catalog](implemented/process/2026-07-04-persistence-log-catalog.md) | 2026-07-04 |
| [One gated in-file format for RFCs](implemented/process/2026-07-05-uniform-rfc-format.md) | 2026-07-05 |
| [Generated plugin config catalog](implemented/process/2026-07-06-generated-config-catalog.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 |
@@ -158,6 +159,7 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand;
| [Per-session snapshot replay for nested agents](implemented/testing/2026-06-22-subagent-snapshot-replay.md) | 2026-06-22 |
| [Hook snapshot matrix — end-to-end goldens for both bridges](implemented/testing/2026-07-04-hook-snapshot-matrix.md) | 2026-07-04 |
| [Single-source the acp-agent replay config](implemented/testing/2026-07-04-single-source-acp-replay-config.md) | 2026-07-04 |
| [Pin request-header content in one snapshot scenario](implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md) | 2026-07-06 |
## Rejected

View File

@@ -4,7 +4,7 @@ Status: implemented
## Problem
A reader — a plugin author, a prompt engineer, someone auditing what the agent can do — has no single place that lists the model-facing tools the harness ships. The `name` / `description` / JSON-Schema `parameters` a tool contributes are what the model actually receives (via `ctx.systemPrompt.tools()` off `ctx.tools.schemas()`), but they are scattered across each `defineTool` call in each `packages/*/tool-*` package, buried in string concatenation and runtime spreads. The cordis [events](../../../cordis-catalog/events.md) & [services](../../../cordis-catalog/services.md) catalogs ([their RFC](2026-06-20-generated-cordis-catalog.md)) document the *wiring* a plugin works against and the [core-data-structures catalog](../../../core-data-structures/core.md) documents the *vocabulary* those signatures move — but neither documents the *tools* the agent is offered. This RFC adds that third reference surface, `docs/tool-catalog/tools.md`, and a freshness gate so it cannot drift.
A reader — a plugin author, a prompt engineer, someone auditing what the agent can do — has no single place that lists the model-facing tools the harness ships. The `name` / `description` / JSON-Schema `parameters` a tool contributes are what the model actually receives (via `ctx.systemPrompt.tools()` off `ctx.tools.schemas()`), but they are scattered across each `defineTool` call in each `packages/*/tool-*` package, buried in string concatenation and runtime spreads. The cordis [events](../../../cordis-catalog/events.md) & [services](../../../cordis-catalog/services.md) catalogs ([their RFC](2026-06-20-generated-cordis-catalog.md)) document the *wiring* a plugin works against and the [core-data-structures catalog](../../../core-data-structures/core.md) documents the *vocabulary* those signatures move — but neither documents the *tools* the agent is offered. This RFC adds that third reference surface, `docs/tool-catalog.md`, and a freshness gate so it cannot drift.
## Decision

View File

@@ -4,7 +4,7 @@ Status: implemented
## Problem
The repo already had several high-trust documentation surfaces, each on a different axis: [module-graph.md](../../../module-graph.md) is generated from package `peerDependencies`, the generated [Cordis events](../../../cordis-catalog/events.md) and [services](../../../cordis-catalog/services.md) catalogs are generated from Cordis `Events` and `Context` declarations, [tool-catalog/tools.md](../../../tool-catalog/tools.md) is generated by booting shipped tool plugins, and [core-data-structures/](../../../core-data-structures/core.md) uses `ts type-equiv` blocks to keep pasted type definitions synchronized with source.
The repo already had several high-trust documentation surfaces, each on a different axis: [module-graph.md](../../../module-graph.md) is generated from package `peerDependencies`, the generated [Cordis events](../../../cordis-catalog/events.md) and [services](../../../cordis-catalog/services.md) catalogs are generated from Cordis `Events` and `Context` declarations, [tool-catalog.md](../../../tool-catalog.md) is generated by booting shipped tool plugins, and [core-data-structures/](../../../core-data-structures/core.md) uses `ts type-equiv` blocks to keep pasted type definitions synchronized with source.
Those references are accurate, but they are mostly catalogs. A maintainer still has to synthesize the relationships: which packages form a capability seam, which app bundles a concrete spine, which event is durable vs live, where a hook or policy plugin can intercept work, and which model-facing tool depends on which service. An SDK user has the same problem from another angle: "Which package do I install or load for the behavior I want, and which event/service/tool do I extend?"
@@ -31,7 +31,7 @@ The first index links ten relationship surfaces. Package topology and tool-packa
| Graph | Maintenance mode | Source of truth |
|---|---|---|
| [module dependency graph](../../../module-graph.md) | generated | `packages/*/*/package.json` peer dependencies plus package group paths |
| [tool schema catalog and package map](../../../tool-catalog/tools.md) | generated | boot-harvested tool schemas plus tool-package service/effect metadata |
| [tool schema catalog and package map](../../../tool-catalog.md) | generated | boot-harvested tool schemas plus tool-package service/effect metadata |
| [capability seams and core services](../../../capability-seams.md) | hybrid generated | Cordis service declarations plus a role manifest in `gen-doc-graphs.ts` |
| [echo-agent app composition](../../../../examples/echo-agent/composition.md) | hybrid generated | `examples/echo-agent/cordis.yml` plugin list plus curated app/bundle expansion |
| [coding-agent app composition](../../../../examples/coding-agent/composition.md) | hybrid generated | `examples/coding-agent/cordis.yml` plugin list plus curated app/bundle expansion |

View File

@@ -8,7 +8,7 @@ The session event log is the harness's on-disk contract: every `SessionEventMap`
## Decision
Generate `docs/persistence-catalog/log-events.md` from source, with a freshness gate, as the fourth reference surface: the *records* a persisted session log can contain, complementing the cordis catalog (wiring), core-data-structures (vocabulary), and the tool catalog (tools).
Generate `docs/persistence-catalog.md` from source, with a freshness gate, as the fourth reference surface: the *records* a persisted session log can contain, complementing the cordis catalog (wiring), core-data-structures (vocabulary), and the tool catalog (tools).
`scripts/gen-persistence-catalog.ts` is a pure TypeScript-AST pass, like `gen-cordis-catalog.ts` — log events ARE statically knowable: every member is a string-literal-named property with a static type annotation, so the AST is the whole truth. The walk collects every `interface SessionEventMap` declaration under `packages/*/*/src` — the owning top-level interface and every `declare module '@deepseek-ai/dsh-session'` merge — so a brand-new event, core or merged, appears in the next regenerate and an un-regenerated file fails `--check` (`verify-persistence-catalog`, a `doc-sync` member, so pre-push and CI both run it). Each entry renders the member's JSDoc prose, its payload (printed through the TypeScript printer, so a newline-separated multi-line type literal still yields a valid one-line fragment), a surface badge, cross-links into core-data-structures, and the declaration's source pointer, grouped by scope.

View File

@@ -0,0 +1,38 @@
# RFC: Generated plugin config catalog
Status: implemented
## Problem
The config surface — the exact set of fields a `cordis.yml` entry's `config:` block can set for each plugin, with types, defaults, and semantics — had no reference page. A deployment author assembling a config tree had to open every plugin's source (or trust its README) to learn what is settable. The per-package README `## Config` sections cover parts of it by hand, in formats that diverged package-by-package (a key/default table here, an annotated YAML snippet there) and with no gate tying them to source. Nothing enumerated which packages are loadable at all — plugin vs abstract seam vs plain library — and nothing verified that the runtime schemastery schema and the documented `Config` interface agree, so a schema-validated field could exist with no documentation anywhere.
## Decision
Generate the catalog from source: `scripts/gen-config-catalog.ts` emits [docs/config-catalog.md](../../../config-catalog.md), one section per configurable package containing the VERBATIM config declaration — the `export interface Config` (or equivalently named type) with its JSDoc, pasted as-is in a ` ```ts config-catalog ` fence — plus a `Requires:` line (the plugin's `inject`), a `Depends on:` line resolving every type name the paste references, and a source pointer. The paste is the plugin's full declared config type: a field the runtime schema deliberately excludes is a runtime-only seam, marked as such by its own JSDoc, not a `cordis.yml`-settable knob. Package-local referenced types are pasted transitively into the same fence; another plugin's config type links to that plugin's section; names in the cordis catalog's shared `LINK_MAP` link to core-data-structures; any other workspace type links to its source; an external type is named with its module. It mirrors the `gen-cordis-catalog` pattern exactly: `--write` regenerates, `--check` (`verify-config-catalog`, inside `doc-sync`) fails if the committed file is stale, output is deterministic, the file is a build artifact never hand-edited.
Pure AST generation is correct here for the same reason it is for the events/services catalog and NOT for the tool catalog: a config type is a static declaration and every schemastery schema in the repo is a static `z.object`/`z.intersect` literal, so the source is the whole truth — nothing about the config surface is runtime-composed.
Specific choices:
- **The config type is the second-parameter type.** What the catalog documents is the declared type of `apply(ctx, config)` / the service constructor's `(ctx, config)` — the value cordis actually passes — not a `Config` export located by naming convention. This is what makes the walk total: it works for interfaces named `AcpConfig` or `BasicCompactConfig`, for types declared in a sibling file, and for plugins with no validating schema at all.
- **Classification is total.** Every `packages/<group>/<pkg>` entry resolves, mirroring the Loader's `unwrapExports` (`exports.default ?? exports`), to a configurable plugin, a config-free plugin, an abstract seam class, or a library — each rendered in its own section — and an unclassifiable entry hard-errors. A new package cannot be silently undocumented.
- **Per-field JSDoc is enforced.** Every property of a pasted declaration (nested type literals included) needs non-empty JSDoc prose, or generation fails. The paste IS the documentation, so this is the same forcing function the events catalog applies via `@mode`: thin source docs fail the gate rather than yielding a thin catalog.
- **The schema is cross-checked, one-directionally, nested keys included.** When a plugin declares a schemastery schema (`export const Config` / `static Config`), the generator walks it statically — object-literal keys and their nested object/array compositions as key paths (`agents[].id`), chained refinements, and `z.intersect` composition across workspace packages — and every schema-validated key path must be locatable on the declared config type, resolving package-local and workspace-imported types (re-export chains included), intersections, unions, utility wrappers, and indexed access. So the paste cannot hide a loader-accepted field, top-level or nested. The check is presence-only and fails loud only on a definite miss: a path crossing a type the walk cannot enumerate (an external package's type) is skipped rather than mis-reported, and dynamic-key shapes (`z.dict`) or union alternatives contribute no nested paths. The reverse direction is deliberately unchecked: a declared field may be a runtime-only seam the schema excludes (the ACP bridge's test-injected `stream`).
- **A dedicated fence.** Pasted declarations use a ` ```ts config-catalog ` info string that `doc-typecheck` skips (a lone declaration referencing imported types is not standalone-compilable), excluded from the opt-out ratio — the same treatment the `cordis-catalog` and `persistence-catalog` fences get.
- **A single file at `docs/config-catalog.md`**, not a one-file directory: the page serves one audience (the `cordis.yml` author) with one axis, unlike `cordis-catalog/`, which holds two sibling pages.
The package README `## Config` sections stay. The overlap is accepted deliberately: the README is the curated per-package contract (config semantics in deployment context, alongside limitations and extension points), the catalog is the exhaustive generated enumeration. Because the catalog is generated, a disagreement between the two indicts the README, and the fix is a README edit — the catalog cannot drift.
## Alternatives considered
- **Synthesized per-field rendering** — a bullet list, table, or annotated-YAML snippet per field, assembled from parsed JSDoc plus schema metadata. Rejected for the verbatim paste: the interface with its JSDoc is already the authored contract in its authored form, and a synthesizing renderer re-formats prose it does not own, adding a rendering layer that can misrepresent it.
- **Runtime boot + schema introspection, as the tool catalog does** — rejected: nothing here is runtime-composed, and the schema alone under-documents the surface (prose-documented defaults, runtime-only fields, plugins with no schema at all). Booting would add fragility without adding truth.
- **Two-directional schema/interface equality** — rejected for the subset check: the declared type legitimately carries members the schema refuses to accept from config (runtime-only seams).
- **Retiring the README `## Config` sections in the same change** — rejected: the accepted duplication keeps the per-package contract readable in place, and a sweep would have to fold each README's extra facts into field JSDoc first — separable work the catalog does not depend on.
## Consequences
- The catalog cannot drift: a source change the committed file does not reflect fails `verify-config-catalog` in pre-push and CI. An undocumented config field, an unresolvable referenced type name, or a schema key missing from the config type fails the generator outright.
- Config prose now has a forcing function at the declaration: writing a new config field means writing its JSDoc, which becomes the catalog entry verbatim.
- The generator hard-errors on shapes it cannot walk statically — an aliased package-local config import, a schema built by anything other than `object`/`intersect` composition, an unlisted global type name. Introducing such a shape includes teaching the generator (or the shape stays out of the repo), which is the point: the catalog stays the whole truth.
- `gen-cordis-catalog.ts` exports its JSDoc/pointer helpers and `LINK_MAP` for reuse, so the two catalogs cross-link types identically and a link-map addition serves both.

View File

@@ -51,7 +51,7 @@ The ACP server app loads `@deepseek-ai/dsh-llm-deepseek`, whose `apply` throws w
A snapshot run asserts **two** normalized surfaces, because the harness's external surfaces are distinct:
1. The **stdout transcript** — the framed `session/update` JSON-RPC the editor sees. Catches regressions in the ACP bridge's event→update translation (`streamSessionEventUpdate`). Compared against a committed `stdout.golden.jsonl`.
2. The **re-persisted session JSONL** — the log the replay run itself persists, compared against the scenario's `session.jsonl`. Catches regressions in the loop, tool dispatch, and turn/step structure that never surface on stdout. There is no separate session golden: `session.jsonl` is BOTH the replay source (recorded scenarios) and the expected produced log. Both sides pass through `normalizeSessionLog` before comparing — the fixture is raw-harvested (its own real session id / cwd / timestamps) and the replay output has fresh ones, so each is scrubbed against ITS OWN volatile values (the fixture's read from its header line) and the comparison is on normalized form. For an authored override scenario the same `session.jsonl` holds the expected produced log; `replay.override.json` drives the model, and `llm-replay` ignores the fixture for model chunks when an override exists, so committing the expected log there does not affect replay.
2. The **re-persisted session JSONL** — the log the replay run itself persists, compared against the scenario's `session.jsonl`. Catches regressions in the loop, tool dispatch, and turn/step structure that never surface on stdout. There is no separate session golden: `session.jsonl` is BOTH the replay source (recorded scenarios) and the expected produced log. Both sides pass through `normalizeSessionLog` before comparing — the fixture is raw-harvested (its own real session id / cwd / timestamps) and the replay output has fresh ones, so each is scrubbed against ITS OWN volatile values (the fixture's read from its header line) and the comparison is on normalized form. Request-header CONTENT (the composed system prompt + tool schemas) is additionally scrubbed to `{{system}}`/`{{tools}}` tokens on both sides — in the stored fixtures too — for every scenario except the one that pins it ([pinned-header RFC](2026-07-06-pin-request-header-content-in-one-scenario.md)). For an authored override scenario the same `session.jsonl` holds the expected produced log; `replay.override.json` drives the model, and `llm-replay` ignores the fixture for model chunks when an override exists, so committing the expected log there does not affect replay.
The two are genuinely additive: stdout is the bridge's *lossy projection* of the log (it drops `assistant/message.usage`, `step/*`, exact `seq`/`time`, and renders tool I/O differently), so a loop/tool/turn-structure regression can change the JSONL while leaving the stdout projection identical, and a bridge-translation regression can change stdout while the JSONL is untouched. Asserting the JSONL equality also echoes the proposed [universal replay fixture](../../proposed/testing/2026-06-11-deterministic-and-stress-testing.md) idea.

View File

@@ -0,0 +1,32 @@
# RFC: Pin request-header content in one snapshot scenario
Status: implemented
## Problem
Every model-driving ACP snapshot fixture (`session.jsonl`) embedded the full composed system prompt and the complete tool-schema list in its `request/header` event — roughly 8 KB on one line, per fixture. That content is identical across the suite (byte-identical tool list everywhere, including subagent children; identical prompt modulo each recording's temp cwd), so any change touching a tool description or a system-prompt line had to update every fixture: re-record everything against the live API (churning model responses and stdout goldens along the way) or hand-edit ~35 giant header lines. Introducing the dynamic-workflows feature — one new tool plus one prompt paragraph — rewrote every snapshot fixture in the repo, burying the behavioral diff a reviewer should be reading.
## Decision
Exactly one scenario — `text-turn`, flagged `pinsHeader` in `acp.snapshot.ts` — commits and compares the full request-header content. Every other fixture stores and compares that content as stable tokens via the pure normalizer `scrubRequestHeaders` in `snapshot-normalize.ts`: a `request/header` event's `header.system` becomes `"{{system}}"` and `header.tools` becomes `"{{tools}}"`; a `request/header-delta` keeps its structural facts — the system delta's `keepStart`/`keepEnd` line positions with one `{{system}}` token per inserted line, the tools delta's added/removed/changed tool names — and tokenizes only the bulk (prompt text, schema bodies), so two different deltas still compare different. The scrub is composed in front of `normalizeSessionLog` on BOTH sides of a non-pinning scenario's log compare and applied to the harvested logs record mode writes, so a re-record cannot smuggle the content back. Absent fields stay absent — WHETHER a header carried a prompt or tools is behavior and stays visible — and `config`/`reason` stay verbatim: a model swap churns every fixture by design (it invalidates the recorded responses), while a prompt or schema edit churns none of them (replay derives model behavior exclusively from `assistant/chunk` events and never reads header content — see `dsh-llm-replay`).
A system-prompt or tool-schema change therefore lands as exactly one committed-fixture diff — the pinned `text-turn` header line — updated by hand or by re-recording that one scenario (`pnpm run test:snapshot:record` with `-t text-turn`).
Guards make the split self-enforcing. On disk (fixture meta-tests): every non-pinning `session*.jsonl` must be a fixed point of `scrubRequestHeaders` (unscrubbed content crept in — apply the scrub), the pinning scenario's fixture must NOT be one (the pin lost its content), and exactly one scenario must pin. Live (every non-pinning scenario run): each `request/header` the run produces — parent, spawn child, fork child, initial or resume — must equal the pinned fixture's header after both sides normalize their own volatile values, and no `request/header-delta` may appear at all (a mid-run header change diverges from the pin by construction, and its content would be invisible under the scrub), so the single-pin premise is asserted rather than assumed.
One pin covers the whole suite because every session — parent, spawn child, fork child — composes the identical tool list and the identical prompt modulo cwd, and the uniformity guard fails the suite the moment that stops holding. If header composition ever becomes session-dependent by design (a restricted subagent toolset, say), the divergent shape gets its own pinning scenario.
## Alternatives considered
- **Re-record or hand-edit every fixture per change** — the status quo; the churn this RFC removes.
- **Scrub at compare time only, keeping fixtures raw** — the compares go green without fixture edits, but every committed fixture then carries a permanently stale copy of the prompt and schemas: dead weight that misleads readers and still rewrites wholesale on the next re-record. Storing the tokens keeps the fixture honest about what it does and does not pin.
- **Scrub everywhere, pin nowhere** — loses the only end-to-end record of the composed header as actually sent (prompt assembly, registered-tool order, full schemas). The generated tool catalog documents each tool in isolation; only a real fixture pins the composed set.
- **Slim the session log itself (log a content digest, store the header elsewhere)** — violates the reconstructability contract: the product log must reproduce each request bit-for-bit ([reconstructable-requests RFC](../architecture/2026-07-05-reconstructable-requests.md)). Header bulk is a test-artifact concern, solved in test normalization; the live log is untouched.
## Verification
All 37 snapshot scenarios replay green with the scrubbed fixtures (the committed fixtures were rewritten once through `scrubRequestHeaders` itself; `text-turn` untouched). The fixed-point, pin-retains-content, exactly-one-pin, live header-uniformity, and no-unpinned-delta guards run inside the suite, and `scrubRequestHeaders` has unit coverage for both header event types, delta structure preservation (line positions, insert arity, tool names), absent-field preservation, config/reason retention, byte-for-byte pass-through of other lines, and idempotence.
## Consequences
A tool-description or system-prompt change churns one committed fixture line instead of every fixture in the suite, so snapshot diffs read as behavior again, and ~270 KB of duplicated header bytes leave the repo. The cost: non-pinning fixtures no longer display header content, so reading one shows tokens where the prompt and schemas were — the pinned `text-turn` fixture is the place to look, and the live uniformity guard guarantees it speaks for every session in the suite. A header change surfaces as a suite-wide test failure whose fix is the one pinned line, rather than as ~35 fixture rewrites.

View File

@@ -7,7 +7,7 @@ How this repo tests, tier by tier, and the rules that keep a green suite meaning
- **Unit** (`pnpm run test`): vitest over `packages|examples/*/tests/**/*.spec.ts`, colocated with what they test. Every registry gets an HMR-safety test (dispose the contributing fiber, assert cleanup). Excessive tests are welcome — err toward covering edge cases, error paths, event ordering, and concurrency races; review findings get regression tests (see `packages/core/agent-loop/tests/review-fixes.spec.ts`).
- **Coverage gate** (`pnpm run test:coverage`): the gating run, per-file 100% on `packages/*/*/src`. An uncovered line is often dead code the gate is correctly flagging for deletion, not a missing test to bolt on. Line coverage is necessary, never sufficient — it proves lines ran, not that the feature works as shipped.
- **Real-API e2e** (`pnpm run test:e2e`): with-key tests against live provider APIs — the DeepSeek model plus provider-specific smokes that gate on their own keys (`EXA_API_KEY`, `PERPLEXITY_API_KEY`, …); each suite self-skips without its key so keyless CI stays green ([real-API e2e RFC](rfc/implemented/testing/2026-06-19-real-api-e2e-ci.md)).
- **Snapshot** (`pnpm run test:snapshot`): boots the real example subprocess, replays a recorded session keyless, diffs normalized stdout + the re-persisted log against committed goldens ([snapshot RFC](rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md)). Re-record with `pnpm run test:snapshot:record`; reviewing the golden diff is part of the review.
- **Snapshot** (`pnpm run test:snapshot`): boots the real example subprocess, replays a recorded session keyless, diffs normalized stdout + the re-persisted log against committed goldens ([snapshot RFC](rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md)). Re-record with `pnpm run test:snapshot:record`; reviewing the golden diff is part of the review. System-prompt/tool-schema content is pinned by ONE scenario (`text-turn`) and tokenized in every other fixture, so a prompt or schema edit churns one committed line ([pinned-header RFC](rfc/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)).
## The with-key policy: inference is cheap here

View File

@@ -3,9 +3,9 @@
# Tool Schema Catalog
Every model-facing tool a shipped plugin contributes to `ctx.tools`: the `name`, `description`, and JSON-Schema `parameters` the model receives via the system-prompt assembly. It complements the cordis [events](../cordis-catalog/events.md) & [services](../cordis-catalog/services.md) catalogs (the wiring a plugin listens to and calls) and [core-data-structures/](../core-data-structures/core.md) (the types those signatures move) — this page is the *tools* the agent is offered.
Every model-facing tool a shipped plugin contributes to `ctx.tools`: the `name`, `description`, and JSON-Schema `parameters` the model receives via the system-prompt assembly. It complements the cordis [events](cordis-catalog/events.md) & [services](cordis-catalog/services.md) catalogs (the wiring a plugin listens to and calls) and [core-data-structures/](core-data-structures/core.md) (the types those signatures move) — this page is the *tools* the agent is offered.
This file is GENERATED and verified fresh by `pnpm run verify-tool-catalog` (part of `doc-sync`) — do not edit it by hand. Unlike the cordis catalog (a pure source-AST pass), this generator BOOTS each tool plugin on a real context and reads `ctx.tools.schemas()`, because a tool schema is not statically knowable (runtime-spread enums, concatenated descriptions, config-driven names, raw-JSON-Schema MCP tools). A completeness guard globs `packages/*/tool-*` and fails if any package is missing from the generator's boot manifest, so a new tool cannot be silently undocumented. See [the tool-schema-catalog RFC](../rfc/implemented/process/2026-07-02-tool-schema-catalog.md).
This file is GENERATED and verified fresh by `pnpm run verify-tool-catalog` (part of `doc-sync`) — do not edit it by hand. Unlike the cordis catalog (a pure source-AST pass), this generator BOOTS each tool plugin on a real context and reads `ctx.tools.schemas()`, because a tool schema is not statically knowable (runtime-spread enums, concatenated descriptions, config-driven names, raw-JSON-Schema MCP tools). A completeness guard globs `packages/*/tool-*` and fails if any package is missing from the generator's boot manifest, so a new tool cannot be silently undocumented. See [the tool-schema-catalog RFC](rfc/implemented/process/2026-07-02-tool-schema-catalog.md).
Scope: shipped product tools under `packages/*/tool-*`, each booted with its DEFAULT config. The registered tool NAME can be a load-time config (e.g. `tool-subagent`'s `toolName`), so a deployment may surface a package under a different or additional name — a per-package note records those shipped aliases where they exist. The `examples/` demo tools (e.g. `echo`) are excluded, matching the cordis catalog's packages-only scope.
@@ -59,7 +59,7 @@ Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs
}
```
Source: [`packages/bash/tool-bash/src/index.ts`](../../packages/bash/tool-bash/src/index.ts)
Source: [`packages/bash/tool-bash/src/index.ts`](../packages/bash/tool-bash/src/index.ts)
### `bash_kill`
@@ -80,7 +80,7 @@ Ask the executor to kill a running background bash task by task id.
}
```
Source: [`packages/bash/tool-bash/src/index.ts`](../../packages/bash/tool-bash/src/index.ts)
Source: [`packages/bash/tool-bash/src/index.ts`](../packages/bash/tool-bash/src/index.ts)
### `bash_output`
@@ -101,7 +101,7 @@ Read new output from a background bash task started with `bash` + `run_in_backgr
}
```
Source: [`packages/bash/tool-bash/src/index.ts`](../../packages/bash/tool-bash/src/index.ts)
Source: [`packages/bash/tool-bash/src/index.ts`](../packages/bash/tool-bash/src/index.ts)
The bash/bash_output/bash_kill tools are model-facing consumers of the bash executor seam.
@@ -140,7 +140,7 @@ Edit an existing UTF-8 text file by replacing literal text.
}
```
Source: [`packages/fs/tool-fs/src/index.ts`](../../packages/fs/tool-fs/src/index.ts)
Source: [`packages/fs/tool-fs/src/index.ts`](../packages/fs/tool-fs/src/index.ts)
### `read`
@@ -169,7 +169,7 @@ Read a UTF-8 text file and return line-numbered content.
}
```
Source: [`packages/fs/tool-fs/src/index.ts`](../../packages/fs/tool-fs/src/index.ts)
Source: [`packages/fs/tool-fs/src/index.ts`](../packages/fs/tool-fs/src/index.ts)
### `write`
@@ -195,7 +195,7 @@ Create or fully replace a UTF-8 text file.
}
```
Source: [`packages/fs/tool-fs/src/index.ts`](../../packages/fs/tool-fs/src/index.ts)
Source: [`packages/fs/tool-fs/src/index.ts`](../packages/fs/tool-fs/src/index.ts)
The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. The tool schemas above are identical with or without the policy plugin.
@@ -225,7 +225,7 @@ Delegate a self-contained task to a subagent (a separate agent that works in its
}
```
Source: [`packages/subagent/tool-subagent/src/index.ts`](../../packages/subagent/tool-subagent/src/index.ts)
Source: [`packages/subagent/tool-subagent/src/index.ts`](../packages/subagent/tool-subagent/src/index.ts)
The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped example agents load this package once per subagent backend, so the model additionally sees `subagent_fork` (bound to the fork backend) with an identical schema — see `examples/coding-agent/cordis.yml` and `examples/acp-agent/cordis.yml`.
@@ -272,7 +272,7 @@ Record and update a structured task list for the current work. Send the ENTIRE l
}
```
Source: [`packages/todo/tool-todo/src/index.ts`](../../packages/todo/tool-todo/src/index.ts)
Source: [`packages/todo/tool-todo/src/index.ts`](../packages/todo/tool-todo/src/index.ts)
todo_write is session-owned state; UIs render the latest todo/write event as a checklist or ACP plan.
@@ -301,7 +301,7 @@ Fetch the content of a specific HTTP(S) URL and return it decoded to text.
}
```
Source: [`packages/web/tool-web/src/index.ts`](../../packages/web/tool-web/src/index.ts)
Source: [`packages/web/tool-web/src/index.ts`](../packages/web/tool-web/src/index.ts)
### `web_search`
@@ -322,6 +322,6 @@ Search the web for current information. Returns an optional summary answer and a
}
```
Source: [`packages/web/tool-web/src/index.ts`](../../packages/web/tool-web/src/index.ts)
Source: [`packages/web/tool-web/src/index.ts`](../packages/web/tool-web/src/index.ts)
web_search and web_fetch keep provider selection behind ctx.web so model-visible schemas stay stable across backend swaps.