docs(graphs): add generated documentation atlas

This commit is contained in:
Tianyi Cui
2026-07-03 01:13:52 +08:00
parent 33bc374038
commit 665c10ff19
19 changed files with 1584 additions and 5 deletions
+1 -1
View File
@@ -17,6 +17,6 @@ pnpm run demo:echo # runnable echo-agent example (no API key needed)
pnpm run demo:coding # the real DeepSeek coding agent (needs DEEPSEEK_API_KEY)
```
For humans, start with the [development guide](docs/development.md) for local setup, hooks, environment variables, and quality gates, then read the [architecture design](docs/architecture.md) before package work. Local context lives in [packages/](packages/) and [vendor/](vendor/).
For humans, start with the [development guide](docs/development.md) for local setup, hooks, environment variables, and quality gates, then read the [architecture design](docs/architecture.md) and [documentation graph atlas](docs/graphs/README.md) before package work. Local context lives in [packages/](packages/) and [vendor/](vendor/).
For agents, follow [AGENTS.md](AGENTS.md).
+2 -2
View File
@@ -8,9 +8,9 @@ The harness core is deliberately tiny: a handful of abstract services plus one c
Requirement context: [Coding Harness MVP 需求分析][mvp-doc].
For a catalog of the **data structures** this architecture moves around — the core vocabulary types, their literal shapes, and the seam types grouped by capability — see [core-data-structures/](core-data-structures/core.md). This document covers behavior; that one covers the types.
For a catalog of the **data structures** this architecture moves around — the core vocabulary types, their literal shapes, and the seam types grouped by capability — see [core-data-structures/](core-data-structures/core.md). For visual relationship maps across packages, seams, events, tools, lifecycle, and replay, see the [documentation graph atlas](graphs/README.md). This document covers behavior; those references cover types and topology.
**Contents:** [Layering](#layering) · [Service map](#service-map) · [Capability seams](#capability-seams-interface--implementation--consumer) · [The vocabulary (dsh-llm)](#the-vocabulary-dsh-llm) · [Event-sourced sessions](#event-sourced-sessions-dsh-session) · [Prompt assembly](#prompt-assembly-dsh-system-prompt) · [Tool pipeline](#tool-pipeline-dsh-tools) · [Agents and the loop](#agents-dsh-agent-and-the-loop-dsh-agent-loop) ([lifecycle](#loop-lifecycle-session--turn--step), [event taxonomy](#event-taxonomy), [waterfall semantics](#cordis-waterfall-semantics-important)) · [Plugin sanity checklist](#plugin-sanity-checklist) · [Extension cookbook](#extension-cookbook) · [Deferred work](#deferred-work-todo)
**Contents:** [Layering](#layering) · [Service map](#service-map) · [Capability seams](#capability-seams-interface--implementation--consumer) · [The vocabulary (dsh-llm)](#the-vocabulary-dsh-llm) · [Event-sourced sessions](#event-sourced-sessions-dsh-session) · [Prompt assembly](#prompt-assembly-dsh-system-prompt) · [Tool pipeline](#tool-pipeline-dsh-tools) · [Agents and the loop](#agents-dsh-agent-and-the-loop-dsh-agent-loop) ([lifecycle](#loop-lifecycle-session--turn--step), [event taxonomy](#event-taxonomy), [waterfall semantics](#cordis-waterfall-semantics-important)) · [Graph atlas](graphs/README.md) · [Plugin sanity checklist](#plugin-sanity-checklist) · [Extension cookbook](#extension-cookbook) · [Deferred work](#deferred-work-todo)
[microkernel-doc]: https://trtgsjkv6r.feishu.cn/wiki/VS9Lw1kQki6mDJk2UHocyuphnsc
[mvp-doc]: https://trtgsjkv6r.feishu.cn/wiki/ZwK6wfBE9i91V6kzMGYcgRGanxg
+3 -1
View File
@@ -96,9 +96,11 @@ pnpm run lint:fix # eslint . --fix
pnpm run doc-typecheck # compile checked TypeScript snippets in Markdown docs
pnpm run gen-cordis-catalog # regenerate docs/cordis-catalog/events-and-services.md from source
pnpm run verify-cordis-catalog # fail if the cordis events/services catalog is stale
pnpm run gen-doc-graphs # regenerate docs/graphs/*.md from source and curated graph definitions
pnpm run verify-doc-graphs # fail if docs/graphs/*.md is stale
pnpm run verify-md-wrap # fail on hard-wrapped prose paragraphs in docs/README markdown
pnpm run verify-type-equiv # fail if a ```ts type-equiv doc block drifts from its source type
pnpm run doc-sync # doc-typecheck, cordis-catalog freshness, markdown wrap/link, and type-equiv verification
pnpm run doc-sync # doc-typecheck, generated doc freshness, markdown wrap/link, and type-equiv verification
pnpm run gen-module-graph # regenerate docs/module-graph.md from package peerDeps
pnpm run verify-module-graph # fail if docs/module-graph.md is stale
pnpm run build # emit lib/types intermediates, then bundle lib/index.* runtime files
+26
View File
@@ -0,0 +1,26 @@
<!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand.
Run `pnpm run gen-doc-graphs` to regenerate. -->
# Documentation Graph Atlas
Maintenance mode: mixed: each linked page declares generated, hybrid, or curated mode.
The graph atlas is the relationship layer above the generated catalogs. Use it to navigate package topology, capability seams, event flow, model-facing tools, and runtime lifecycle paths. Exact signatures and type shapes still live in [cordis-catalog/](../cordis-catalog/events-and-services.md), [tool-catalog/](../tool-catalog/tools.md), and [core-data-structures/](../core-data-structures/core.md).
The process decision behind this atlas is recorded in [the documentation graph atlas RFC](../rfc/implemented/process/2026-07-03-documentation-graph-atlas.md).
| Graph | Mode |
| --- | --- |
| [package topology by group](package-topology.md) | `generated` |
| [capability seams and core services](capability-seams.md) | `hybrid generated` |
| [app composition](app-composition.md) | `hybrid generated` |
| [event producer/consumer matrix](event-producer-consumer.md) | `hybrid generated` |
| [tool affordance map](tool-affordance-map.md) | `hybrid generated` |
| [agent turn and step lifecycle](agent-lifecycle.md) | `curated` |
| [tool execution pipeline](tool-execution-pipeline.md) | `curated` |
| [session surface and message projection](session-surface.md) | `curated` |
| [subagent and session lineage](subagent-lineage.md) | `curated` |
| [plugin disposal and hot reload ownership](hot-reload-disposal.md) | `curated` |
| [ACP snapshot replay](snapshot-replay.md) | `curated` |
Regenerate with `pnpm run gen-doc-graphs`; verify freshness with `pnpm run verify-doc-graphs`.
+41
View File
@@ -0,0 +1,41 @@
<!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand.
Run `pnpm run gen-doc-graphs` to regenerate. -->
# Agent Turn And Step Lifecycle
Maintenance mode: curated Mermaid sequence; exact event signatures live in the generated Cordis catalog.
This sequence is the visual companion to [architecture.md](../architecture.md#loop-lifecycle-session--turn--step). It shows the durable session event path separately from live `agent/*` notifications.
```mermaid
sequenceDiagram
participant User
participant Agent
participant Loop
participant Prompt as ctx.systemPrompt
participant LLM as ctx.llm
participant Tools as ctx.tools
participant Session
participant Persistence
User->>Agent: send(content)
Agent->>Loop: queued work wakes driver
Loop->>Session: turn/start + user/message
Loop-->>User: agent/turn-start
Loop->>Prompt: system-prompt/assemble waterfall
Loop-->>Loop: agent/pre-step serial checkpoint
Loop->>Session: step/start
Loop->>LLM: agent/request waterfall, then llm/stream waterfall
LLM-->>Loop: StreamChunk*
Loop->>Session: assistant/chunk*
Loop-->>User: agent/stream-chunk* (master live mirror)
Loop->>Session: assistant/message
Loop->>Tools: tools/execute waterfall for each tool-call
Tools-->>Session: tool-owned events when applicable
Loop->>Session: tool/result
Loop-->>Loop: agent/turn-continuation waterfall
Loop->>Session: turn/end
Loop->>Persistence: session/flush parallel checkpoint
Loop-->>User: agent/status idle
```
Future pressure from the hooks stack: PR #129 removes the live `agent/stream-chunk` mirror and leaves durable `assistant/chunk` on `session/event` as the authoritative token stream. Consumers that need replayable transcript data should already treat `session/event` as the load-bearing path.
+90
View File
@@ -0,0 +1,90 @@
<!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand.
Run `pnpm run gen-doc-graphs` to regenerate. -->
# App Composition
Maintenance mode: hybrid: leaf plugin lists are parsed from `examples/*/cordis.yml`; bundle expansions are curated from app package source.
This graph is for SDK users asking which pieces a runnable agent loads. Leaf configs choose adapters and optional product tools; app packages provide the front door; `dsh-agent-core` bundles the providerless spine.
```mermaid
flowchart LR
subgraph example_echo["examples/echo-agent"]
cfg_echo["cordis.yml"]
plugin_echo_hmr["hmr<br/>@cordisjs/plugin-hmr"]
cfg_echo --> plugin_echo_hmr
plugin_echo_mock_llm["mock-llm<br/>./src/mock-llm.ts"]
cfg_echo --> plugin_echo_mock_llm
plugin_echo_echo_tool["echo-tool<br/>./src/echo-tool.ts"]
cfg_echo --> plugin_echo_echo_tool
plugin_echo_bash["bash<br/>@deepseek-ai/dsh-bash-local"]
cfg_echo --> plugin_echo_bash
plugin_echo_stdio_agent["stdio-agent<br/>@deepseek-ai/dsh-stdio-agent"]
cfg_echo --> plugin_echo_stdio_agent
plugin_echo_stdio_agent --> bundle_stdio
end
subgraph example_coding["examples/coding-agent"]
cfg_coding["cordis.yml"]
plugin_coding_hmr["hmr<br/>@cordisjs/plugin-hmr"]
cfg_coding --> plugin_coding_hmr
plugin_coding_llm_deepseek["llm-deepseek<br/>@deepseek-ai/dsh-llm-deepseek"]
cfg_coding --> plugin_coding_llm_deepseek
plugin_coding_bash["bash<br/>@deepseek-ai/dsh-bash-local"]
cfg_coding --> plugin_coding_bash
plugin_coding_stdio_agent["stdio-agent<br/>@deepseek-ai/dsh-stdio-agent"]
cfg_coding --> plugin_coding_stdio_agent
plugin_coding_stdio_agent --> bundle_stdio
plugin_coding_compact_basic["compact-basic<br/>@deepseek-ai/dsh-compact-basic"]
cfg_coding --> plugin_coding_compact_basic
plugin_coding_subagent["subagent<br/>@deepseek-ai/dsh-subagent"]
cfg_coding --> plugin_coding_subagent
plugin_coding_subagent_spawn["subagent-spawn<br/>@deepseek-ai/dsh-subagent-spawn"]
cfg_coding --> plugin_coding_subagent_spawn
plugin_coding_subagent_fork["subagent-fork<br/>@deepseek-ai/dsh-subagent-fork"]
cfg_coding --> plugin_coding_subagent_fork
plugin_coding_tool_subagent["tool-subagent<br/>@deepseek-ai/dsh-tool-subagent"]
cfg_coding --> plugin_coding_tool_subagent
plugin_coding_tool_subagent_fork["tool-subagent-fork<br/>@deepseek-ai/dsh-tool-subagent"]
cfg_coding --> plugin_coding_tool_subagent_fork
plugin_coding_tool_todo["tool-todo<br/>@deepseek-ai/dsh-tool-todo"]
cfg_coding --> plugin_coding_tool_todo
end
subgraph example_acp["examples/acp-agent"]
cfg_acp["cordis.yml"]
plugin_acp_llm_deepseek["llm-deepseek<br/>@deepseek-ai/dsh-llm-deepseek"]
cfg_acp --> plugin_acp_llm_deepseek
plugin_acp_bash["bash<br/>@deepseek-ai/dsh-bash-local"]
cfg_acp --> plugin_acp_bash
plugin_acp_acp_agent["acp-agent<br/>@deepseek-ai/dsh-acp-agent"]
cfg_acp --> plugin_acp_acp_agent
plugin_acp_acp_agent --> bundle_acp_agent
plugin_acp_subagent["subagent<br/>@deepseek-ai/dsh-subagent"]
cfg_acp --> plugin_acp_subagent
plugin_acp_subagent_spawn["subagent-spawn<br/>@deepseek-ai/dsh-subagent-spawn"]
cfg_acp --> plugin_acp_subagent_spawn
plugin_acp_subagent_fork["subagent-fork<br/>@deepseek-ai/dsh-subagent-fork"]
cfg_acp --> plugin_acp_subagent_fork
plugin_acp_tool_subagent["tool-subagent<br/>@deepseek-ai/dsh-tool-subagent"]
cfg_acp --> plugin_acp_tool_subagent
plugin_acp_tool_subagent_fork["tool-subagent-fork<br/>@deepseek-ai/dsh-tool-subagent"]
cfg_acp --> plugin_acp_tool_subagent_fork
plugin_acp_tool_todo["tool-todo<br/>@deepseek-ai/dsh-tool-todo"]
cfg_acp --> plugin_acp_tool_todo
end
bundle_stdio["@deepseek-ai/dsh-stdio-agent"] --> bundle_agent_core["@deepseek-ai/dsh-agent-core"]
bundle_stdio --> bundle_jsonl["@deepseek-ai/dsh-session-persistence-jsonl"]
bundle_stdio --> bundle_ui_stdio["@deepseek-ai/dsh-ui-stdio"]
bundle_acp_agent["@deepseek-ai/dsh-acp-agent"] --> bundle_agent_core
bundle_acp_agent --> bundle_jsonl
bundle_acp_agent --> bundle_acp["@deepseek-ai/dsh-acp"]
bundle_agent_core --> spine_llm["ctx.llm"]
bundle_agent_core --> spine_sessions["ctx.sessions"]
bundle_agent_core --> spine_tools["ctx.tools + tool-bash"]
bundle_agent_core --> spine_loop["ctx.agents + ctx.agentLoop"]
```
| Example | Parsed plugin ids | Config |
| --- | --- | --- |
| `examples/echo-agent` | `hmr`, `mock-llm`, `echo-tool`, `bash`, `stdio-agent` | [`examples/echo-agent/cordis.yml`](../../examples/echo-agent/cordis.yml) |
| `examples/coding-agent` | `hmr`, `llm-deepseek`, `bash`, `stdio-agent`, `compact-basic`, `subagent`, `subagent-spawn`, `subagent-fork`, `tool-subagent`, `tool-subagent-fork`, `tool-todo` | [`examples/coding-agent/cordis.yml`](../../examples/coding-agent/cordis.yml) |
| `examples/acp-agent` | `llm-deepseek`, `bash`, `acp-agent`, `subagent`, `subagent-spawn`, `subagent-fork`, `tool-subagent`, `tool-subagent-fork`, `tool-todo` | [`examples/acp-agent/cordis.yml`](../../examples/acp-agent/cordis.yml) |
+110
View File
@@ -0,0 +1,110 @@
<!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand.
Run `pnpm run gen-doc-graphs` to regenerate. -->
# Capability Seams And Core Services
Maintenance mode: hybrid: services are discovered from Cordis declarations; interface/implementation/consumer roles are classified in `scripts/gen-doc-graphs.ts` with a completeness guard.
A service can be a core spine service, a swappable capability seam, or a bundle/composition point. The graph shows the package that owns the service declaration, known implementation packages, and packages that consume the service directly.
```mermaid
flowchart LR
pkg_llm["llm"]
svc_llm["ctx.llm<br/>LLM adapter registry"]
pkg_llm_deepseek["llm-deepseek"]
pkg_llm_pi_ai["llm-pi-ai"]
pkg_llm_replay["llm-replay"]
pkg_agent_loop["agent-loop"]
pkg_compact_basic["compact-basic"]
pkg_session["session"]
svc_sessions["ctx.sessions<br/>In-memory session store"]
pkg_agent["agent"]
pkg_session_persistence["session-persistence"]
pkg_subagent_inprocess["subagent-inprocess"]
pkg_invariants["invariants"]
svc_sessionPersistence["ctx.sessionPersistence<br/>Durable session persistence seam"]
pkg_session_persistence_jsonl["session-persistence-jsonl"]
pkg_session_persistence_sqlite["session-persistence-sqlite"]
pkg_acp["acp"]
pkg_system_prompt["system-prompt"]
svc_systemPrompt["ctx.systemPrompt<br/>System prompt assembly registry"]
pkg_tools["tools"]
svc_tools["ctx.tools<br/>Tool registry and execution waterfall"]
pkg_tool_bash["tool-bash"]
pkg_tool_subagent["tool-subagent"]
pkg_tool_todo["tool-todo"]
svc_agents["ctx.agents<br/>Agent registry"]
pkg_stdio_agent["stdio-agent"]
svc_agentLoop["ctx.agentLoop<br/>Concrete loop driver"]
pkg_agent_core["agent-core"]
pkg_bash["bash"]
svc_bash["ctx.bash<br/>Bash executor seam"]
pkg_bash_local["bash-local"]
pkg_compact["compact"]
svc_compact["ctx.compact<br/>Compaction seam"]
pkg_subagent["subagent"]
svc_subagents["ctx.subagents<br/>Subagent provider registry"]
pkg_subagent_spawn["subagent-spawn"]
pkg_subagent_fork["subagent-fork"]
pkg_subagent_acp["subagent-acp"]
pkg_subagent_mock["subagent-mock"]
pkg_agent --> svc_agents
pkg_agent_loop --> svc_agentLoop
pkg_bash --> svc_bash
pkg_bash_local --> svc_bash
pkg_compact --> svc_compact
pkg_compact_basic --> svc_compact
pkg_llm --> svc_llm
pkg_llm_deepseek --> svc_llm
pkg_llm_pi_ai --> svc_llm
pkg_llm_replay --> svc_llm
pkg_session --> svc_sessions
pkg_session_persistence --> svc_sessionPersistence
pkg_session_persistence_jsonl --> svc_sessionPersistence
pkg_session_persistence_sqlite --> svc_sessionPersistence
pkg_subagent --> svc_subagents
pkg_subagent_acp --> svc_subagents
pkg_subagent_fork --> svc_subagents
pkg_subagent_mock --> svc_subagents
pkg_subagent_spawn --> svc_subagents
pkg_system_prompt --> svc_systemPrompt
pkg_tools --> svc_tools
svc_agentLoop --> pkg_agent_core
svc_agents --> pkg_acp
svc_agents --> pkg_agent_loop
svc_agents --> pkg_invariants
svc_agents --> pkg_stdio_agent
svc_agents --> pkg_subagent_inprocess
svc_bash --> pkg_tool_bash
svc_compact --> pkg_compact_basic
svc_llm --> pkg_agent_loop
svc_llm --> pkg_compact_basic
svc_sessionPersistence --> pkg_acp
svc_sessionPersistence --> pkg_agent_loop
svc_sessions --> pkg_agent
svc_sessions --> pkg_agent_loop
svc_sessions --> pkg_invariants
svc_sessions --> pkg_session_persistence
svc_sessions --> pkg_subagent_inprocess
svc_subagents --> pkg_tool_subagent
svc_systemPrompt --> pkg_agent_loop
svc_systemPrompt --> pkg_tools
svc_tools --> pkg_acp
svc_tools --> pkg_agent_loop
svc_tools --> pkg_tool_bash
svc_tools --> pkg_tool_subagent
svc_tools --> pkg_tool_todo
```
| ctx key | Role | Owner | Implementations | Direct consumers | Note |
| --- | --- | --- | --- | --- | --- |
| `ctx.llm` | `seam` | [`llm`](../../packages/llm/llm) | [`llm-deepseek`](../../packages/llm/llm-deepseek), [`llm-pi-ai`](../../packages/llm/llm-pi-ai), [`llm-replay`](../../packages/support/llm-replay) | [`agent-loop`](../../packages/core/agent-loop), [`compact-basic`](../../packages/compact/compact-basic) | Adapters register provider implementations; the loop and compaction call the provider-neutral stream service. |
| `ctx.sessions` | `core` | [`session`](../../packages/core/session) | - | [`agent-loop`](../../packages/core/agent-loop), [`agent`](../../packages/core/agent), [`session-persistence`](../../packages/session-persistence/session-persistence), [`subagent-inprocess`](../../packages/subagent/subagent-inprocess), [`invariants`](../../packages/support/invariants) | Owns append-only Session instances and emits the durable session event feed. |
| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../../packages/session-persistence/session-persistence) | [`session-persistence-jsonl`](../../packages/session-persistence/session-persistence-jsonl), [`session-persistence-sqlite`](../../packages/session-persistence/session-persistence-sqlite) | [`agent-loop`](../../packages/core/agent-loop), [`acp`](../../packages/ui/acp) | Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time. |
| `ctx.systemPrompt` | `core` | [`system-prompt`](../../packages/core/system-prompt) | - | [`agent-loop`](../../packages/core/agent-loop), [`tools`](../../packages/core/tools) | Collects prompt sections and model-facing tool schemas for each step. |
| `ctx.tools` | `core` | [`tools`](../../packages/core/tools) | - | [`agent-loop`](../../packages/core/agent-loop), [`tool-bash`](../../packages/bash/tool-bash), [`tool-subagent`](../../packages/subagent/tool-subagent), [`tool-todo`](../../packages/todo/tool-todo), [`acp`](../../packages/ui/acp) | Registers tool definitions, exposes schemas to the prompt, and routes calls through tools/execute. |
| `ctx.agents` | `core` | [`agent`](../../packages/core/agent) | - | [`agent-loop`](../../packages/core/agent-loop), [`acp`](../../packages/ui/acp), [`subagent-inprocess`](../../packages/subagent/subagent-inprocess), [`stdio-agent`](../../packages/ui/stdio-agent), [`invariants`](../../packages/support/invariants) | Owns live Agent handles and the create/resume factory seam. |
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../../packages/core/agent-loop) | - | [`agent-core`](../../packages/core/agent-core) | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. |
| `ctx.bash` | `seam` | [`bash`](../../packages/bash/bash) | [`bash-local`](../../packages/bash/bash-local) | [`tool-bash`](../../packages/bash/tool-bash) | The model-facing bash tools consume this seam; sandboxed or remote executors can replace bash-local. |
| `ctx.compact` | `seam` | [`compact`](../../packages/compact/compact) | [`compact-basic`](../../packages/compact/compact-basic) | [`compact-basic`](../../packages/compact/compact-basic) | The basic backend currently consumes the pre-step event directly; a model-facing compact tool remains deferred. |
| `ctx.subagents` | `seam` | [`subagent`](../../packages/subagent/subagent) | [`subagent-spawn`](../../packages/subagent/subagent-spawn), [`subagent-fork`](../../packages/subagent/subagent-fork), [`subagent-acp`](../../packages/subagent/subagent-acp), [`subagent-mock`](../../packages/support/subagent-mock) | [`tool-subagent`](../../packages/subagent/tool-subagent) | Providers implement transports; tool-subagent exposes one configured provider as a model-facing tool name. |
+36
View File
@@ -0,0 +1,36 @@
<!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand.
Run `pnpm run gen-doc-graphs` to regenerate. -->
# Event Producer And Consumer Matrix
Maintenance mode: hybrid generated: Cordis event declarations and most producer/listener edges are AST-scanned; dynamic dispatch sites are classified in `scripts/gen-doc-graphs.ts`.
This matrix shows which packages dispatch each harness-owned event and which packages listen to it. It is intentionally a table rather than one large graph: events are many-to-many, and dense relation data is easier to review in rows. Dynamic dispatch overrides cover sites that deliberately bypass `ctx.emit`, such as subagent lifecycle containment.
| Event | Mode | Declared in | Dispatchers | Listeners |
| --- | --- | --- | --- | --- |
| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:137`](../../packages/core/agent/src/types.ts) | [`agent`](../../packages/core/agent) (`emit`) | - |
| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:143`](../../packages/core/agent/src/types.ts) | [`agent`](../../packages/core/agent) (`emit`) | - |
| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:254`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`emit`) | - |
| `agent/pre-step` | `serial` | [`packages/core/agent/src/types.ts:214`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`serial`) | [`compact-basic`](../../packages/compact/compact-basic) |
| `agent/queued` | `emit` | [`packages/core/agent/src/types.ts:156`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`emit`) | - |
| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:223`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`waterfall`), [`compact-basic`](../../packages/compact/compact-basic) (`waterfall`) | - |
| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:150`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`emit`) | [`acp`](../../packages/ui/acp), [`invariants`](../../packages/support/invariants), [`ui-stdio`](../../packages/support/ui-stdio) |
| `agent/steering` | `emit` | [`packages/core/agent/src/types.ts:248`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`emit`) | - |
| `agent/step-end` | `emit` | [`packages/core/agent/src/types.ts:180`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`emit`) | - |
| `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:229`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`waterfall`) | - |
| `agent/step-start` | `emit` | [`packages/core/agent/src/types.ts:175`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`emit`) | - |
| `agent/stream-chunk` | `emit` | [`packages/core/agent/src/types.ts:243`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`emit`) | [`ui-stdio`](../../packages/support/ui-stdio) |
| `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:236`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`waterfall`) | - |
| `agent/turn-end` | `emit` | [`packages/core/agent/src/types.ts:169`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`emit`) | [`ui-stdio`](../../packages/support/ui-stdio) |
| `agent/turn-start` | `emit` | [`packages/core/agent/src/types.ts:163`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`emit`) | [`ui-stdio`](../../packages/support/ui-stdio) |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:31`](../../packages/llm/llm/src/index.ts) | [`llm`](../../packages/llm/llm) (`waterfall`) | [`llm-replay`](../../packages/support/llm-replay) |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:34`](../../packages/core/session/src/index.ts) | [`session`](../../packages/core/session) (`emit`) | [`invariants`](../../packages/support/invariants), [`session-persistence`](../../packages/session-persistence/session-persistence) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:40`](../../packages/core/session/src/index.ts) | [`session`](../../packages/core/session) (`emit`) | [`acp`](../../packages/ui/acp), [`invariants`](../../packages/support/invariants), [`session-persistence`](../../packages/session-persistence/session-persistence), [`ui-stdio`](../../packages/support/ui-stdio) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:49`](../../packages/core/session/src/index.ts) | [`agent-loop`](../../packages/core/agent-loop) (`parallel`) | [`session-persistence`](../../packages/session-persistence/session-persistence) |
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:65`](../../packages/subagent/subagent/src/index.ts) | [`subagent`](../../packages/subagent/subagent) (`events.dispatch`) | - |
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:59`](../../packages/subagent/subagent/src/index.ts) | [`subagent`](../../packages/subagent/subagent) (`events.dispatch`) | - |
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:24`](../../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../../packages/core/system-prompt) (`waterfall`) | - |
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:30`](../../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../../packages/core/system-prompt) (`emit`) | - |
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:48`](../../packages/core/tools/src/index.ts) | [`tools`](../../packages/core/tools) (`emit`) | - |
| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:43`](../../packages/core/tools/src/index.ts) | [`tools`](../../packages/core/tools) (`waterfall`) | - |
+24
View File
@@ -0,0 +1,24 @@
<!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand.
Run `pnpm run gen-doc-graphs` to regenerate. -->
# Plugin Disposal And Hot Reload Ownership
Maintenance mode: curated Mermaid flow based on Cordis fiber/effect conventions.
This graph is a maintainer checklist for plugin authors: registrations are effects, service injection gates activation, and owned handles must be disposed by their owner.
```mermaid
flowchart TD
plugin["ctx.plugin(plugin) creates fiber"]
inject["static inject gates activation"]
service["ctx.provide / Service constructor"]
effects["ctx.effect registrations<br/>events, tools, adapters, timers"]
reload["HMR / fiber.dispose()"]
disposers["Run disposers in owner fiber"]
quiescence["Owned AgentHandle.dispose()<br/>or service teardown awaits quiescence"]
plugin --> inject --> service
inject --> effects
reload --> disposers --> quiescence
```
Hook bridges and SDK plugins increase the number of long-lived listeners, so this ownership graph should stay small and visible.
+189
View File
@@ -0,0 +1,189 @@
<!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand.
Run `pnpm run gen-doc-graphs` to regenerate. -->
# Package Topology By Group
Maintenance mode: generated from `packages/*/*/package.json` peer dependencies plus package group paths.
This graph complements [module-graph.md](../module-graph.md): it keeps the same canonical peer-dependency edge source, but clusters packages by the `packages/<group>/<pkg>` hierarchy so layering and capability families are easier to scan.
```mermaid
flowchart TD
subgraph group_util["packages/util"]
pkg_brand["brand"]
end
subgraph group_llm["packages/llm"]
pkg_llm["llm"]
pkg_llm_deepseek["llm-deepseek"]
pkg_llm_pi_ai["llm-pi-ai"]
end
subgraph group_core["packages/core"]
pkg_agent["agent"]
pkg_agent_core["agent-core"]
pkg_agent_loop["agent-loop"]
pkg_session["session"]
pkg_system_prompt["system-prompt"]
pkg_tools["tools"]
end
subgraph group_bash["packages/bash"]
pkg_bash["bash"]
pkg_bash_local["bash-local"]
pkg_tool_bash["tool-bash"]
end
subgraph group_compact["packages/compact"]
pkg_compact["compact"]
pkg_compact_basic["compact-basic"]
end
subgraph group_subagent["packages/subagent"]
pkg_subagent["subagent"]
pkg_subagent_acp["subagent-acp"]
pkg_subagent_fork["subagent-fork"]
pkg_subagent_inprocess["subagent-inprocess"]
pkg_subagent_spawn["subagent-spawn"]
pkg_tool_subagent["tool-subagent"]
end
subgraph group_session_persistence["packages/session-persistence"]
pkg_session_persistence["session-persistence"]
pkg_session_persistence_jsonl["session-persistence-jsonl"]
pkg_session_persistence_sqlite["session-persistence-sqlite"]
end
subgraph group_todo["packages/todo"]
pkg_tool_todo["tool-todo"]
end
subgraph group_support["packages/support"]
pkg_invariants["invariants"]
pkg_llm_replay["llm-replay"]
pkg_subagent_mock["subagent-mock"]
pkg_ui_stdio["ui-stdio"]
end
subgraph group_ui["packages/ui"]
pkg_acp["acp"]
pkg_acp_agent["acp-agent"]
pkg_stdio_agent["stdio-agent"]
end
pkg_llm --> pkg_brand
pkg_bash --> pkg_brand
pkg_llm_deepseek --> pkg_llm
pkg_llm_pi_ai --> pkg_llm
pkg_session --> pkg_brand
pkg_session --> pkg_llm
pkg_system_prompt --> pkg_llm
pkg_bash_local --> pkg_bash
pkg_agent --> pkg_brand
pkg_agent --> pkg_llm
pkg_agent --> pkg_session
pkg_compact --> pkg_llm
pkg_compact --> pkg_session
pkg_session_persistence --> pkg_session
pkg_llm_replay --> pkg_llm
pkg_llm_replay --> pkg_session
pkg_tools --> pkg_agent
pkg_tools --> pkg_llm
pkg_tools --> pkg_system_prompt
pkg_compact_basic --> pkg_agent
pkg_compact_basic --> pkg_compact
pkg_compact_basic --> pkg_llm
pkg_compact_basic --> pkg_session
pkg_session_persistence_jsonl --> pkg_session
pkg_session_persistence_jsonl --> pkg_session_persistence
pkg_session_persistence_sqlite --> pkg_session
pkg_session_persistence_sqlite --> pkg_session_persistence
pkg_invariants --> pkg_agent
pkg_invariants --> pkg_llm
pkg_invariants --> pkg_session
pkg_ui_stdio --> pkg_agent
pkg_ui_stdio --> pkg_llm
pkg_ui_stdio --> pkg_session
pkg_agent_loop --> pkg_agent
pkg_agent_loop --> pkg_llm
pkg_agent_loop --> pkg_session
pkg_agent_loop --> pkg_session_persistence
pkg_agent_loop --> pkg_system_prompt
pkg_agent_loop --> pkg_tools
pkg_tool_bash --> pkg_agent
pkg_tool_bash --> pkg_bash
pkg_tool_bash --> pkg_llm
pkg_tool_bash --> pkg_tools
pkg_subagent --> pkg_agent
pkg_subagent --> pkg_llm
pkg_subagent --> pkg_tools
pkg_tool_todo --> pkg_agent
pkg_tool_todo --> pkg_session
pkg_tool_todo --> pkg_tools
pkg_acp --> pkg_agent
pkg_acp --> pkg_llm
pkg_acp --> pkg_session
pkg_acp --> pkg_session_persistence
pkg_acp --> pkg_tools
pkg_agent_core --> pkg_agent
pkg_agent_core --> pkg_agent_loop
pkg_agent_core --> pkg_invariants
pkg_agent_core --> pkg_llm
pkg_agent_core --> pkg_session
pkg_agent_core --> pkg_system_prompt
pkg_agent_core --> pkg_tool_bash
pkg_agent_core --> pkg_tools
pkg_subagent_acp --> pkg_agent
pkg_subagent_acp --> pkg_llm
pkg_subagent_acp --> pkg_subagent
pkg_subagent_inprocess --> pkg_agent
pkg_subagent_inprocess --> pkg_llm
pkg_subagent_inprocess --> pkg_session
pkg_subagent_inprocess --> pkg_subagent
pkg_tool_subagent --> pkg_agent
pkg_tool_subagent --> pkg_llm
pkg_tool_subagent --> pkg_subagent
pkg_tool_subagent --> pkg_tools
pkg_subagent_mock --> pkg_agent
pkg_subagent_mock --> pkg_llm
pkg_subagent_mock --> pkg_subagent
pkg_subagent_fork --> pkg_agent
pkg_subagent_fork --> pkg_session
pkg_subagent_fork --> pkg_subagent
pkg_subagent_fork --> pkg_subagent_inprocess
pkg_subagent_spawn --> pkg_subagent
pkg_subagent_spawn --> pkg_subagent_inprocess
pkg_acp_agent --> pkg_acp
pkg_acp_agent --> pkg_agent_core
pkg_acp_agent --> pkg_session_persistence_jsonl
pkg_stdio_agent --> pkg_agent
pkg_stdio_agent --> pkg_agent_core
pkg_stdio_agent --> pkg_session
pkg_stdio_agent --> pkg_session_persistence_jsonl
pkg_stdio_agent --> pkg_ui_stdio
```
| Package | Group | Depends on |
| --- | --- | --- |
| [`brand`](../../packages/util/brand) | `util` | - |
| [`llm`](../../packages/llm/llm) | `llm` | [`brand`](../../packages/util/brand) |
| [`bash`](../../packages/bash/bash) | `bash` | [`brand`](../../packages/util/brand) |
| [`llm-deepseek`](../../packages/llm/llm-deepseek) | `llm` | [`llm`](../../packages/llm/llm) |
| [`llm-pi-ai`](../../packages/llm/llm-pi-ai) | `llm` | [`llm`](../../packages/llm/llm) |
| [`session`](../../packages/core/session) | `core` | [`brand`](../../packages/util/brand), [`llm`](../../packages/llm/llm) |
| [`system-prompt`](../../packages/core/system-prompt) | `core` | [`llm`](../../packages/llm/llm) |
| [`bash-local`](../../packages/bash/bash-local) | `bash` | [`bash`](../../packages/bash/bash) |
| [`agent`](../../packages/core/agent) | `core` | [`brand`](../../packages/util/brand), [`llm`](../../packages/llm/llm), [`session`](../../packages/core/session) |
| [`compact`](../../packages/compact/compact) | `compact` | [`llm`](../../packages/llm/llm), [`session`](../../packages/core/session) |
| [`session-persistence`](../../packages/session-persistence/session-persistence) | `session-persistence` | [`session`](../../packages/core/session) |
| [`llm-replay`](../../packages/support/llm-replay) | `support` | [`llm`](../../packages/llm/llm), [`session`](../../packages/core/session) |
| [`tools`](../../packages/core/tools) | `core` | [`agent`](../../packages/core/agent), [`llm`](../../packages/llm/llm), [`system-prompt`](../../packages/core/system-prompt) |
| [`compact-basic`](../../packages/compact/compact-basic) | `compact` | [`agent`](../../packages/core/agent), [`compact`](../../packages/compact/compact), [`llm`](../../packages/llm/llm), [`session`](../../packages/core/session) |
| [`session-persistence-jsonl`](../../packages/session-persistence/session-persistence-jsonl) | `session-persistence` | [`session`](../../packages/core/session), [`session-persistence`](../../packages/session-persistence/session-persistence) |
| [`session-persistence-sqlite`](../../packages/session-persistence/session-persistence-sqlite) | `session-persistence` | [`session`](../../packages/core/session), [`session-persistence`](../../packages/session-persistence/session-persistence) |
| [`invariants`](../../packages/support/invariants) | `support` | [`agent`](../../packages/core/agent), [`llm`](../../packages/llm/llm), [`session`](../../packages/core/session) |
| [`ui-stdio`](../../packages/support/ui-stdio) | `support` | [`agent`](../../packages/core/agent), [`llm`](../../packages/llm/llm), [`session`](../../packages/core/session) |
| [`agent-loop`](../../packages/core/agent-loop) | `core` | [`agent`](../../packages/core/agent), [`llm`](../../packages/llm/llm), [`session`](../../packages/core/session), [`session-persistence`](../../packages/session-persistence/session-persistence), [`system-prompt`](../../packages/core/system-prompt), [`tools`](../../packages/core/tools) |
| [`tool-bash`](../../packages/bash/tool-bash) | `bash` | [`agent`](../../packages/core/agent), [`bash`](../../packages/bash/bash), [`llm`](../../packages/llm/llm), [`tools`](../../packages/core/tools) |
| [`subagent`](../../packages/subagent/subagent) | `subagent` | [`agent`](../../packages/core/agent), [`llm`](../../packages/llm/llm), [`tools`](../../packages/core/tools) |
| [`tool-todo`](../../packages/todo/tool-todo) | `todo` | [`agent`](../../packages/core/agent), [`session`](../../packages/core/session), [`tools`](../../packages/core/tools) |
| [`acp`](../../packages/ui/acp) | `ui` | [`agent`](../../packages/core/agent), [`llm`](../../packages/llm/llm), [`session`](../../packages/core/session), [`session-persistence`](../../packages/session-persistence/session-persistence), [`tools`](../../packages/core/tools) |
| [`agent-core`](../../packages/core/agent-core) | `core` | [`agent`](../../packages/core/agent), [`agent-loop`](../../packages/core/agent-loop), [`invariants`](../../packages/support/invariants), [`llm`](../../packages/llm/llm), [`session`](../../packages/core/session), [`system-prompt`](../../packages/core/system-prompt), [`tool-bash`](../../packages/bash/tool-bash), [`tools`](../../packages/core/tools) |
| [`subagent-acp`](../../packages/subagent/subagent-acp) | `subagent` | [`agent`](../../packages/core/agent), [`llm`](../../packages/llm/llm), [`subagent`](../../packages/subagent/subagent) |
| [`subagent-inprocess`](../../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../../packages/core/agent), [`llm`](../../packages/llm/llm), [`session`](../../packages/core/session), [`subagent`](../../packages/subagent/subagent) |
| [`tool-subagent`](../../packages/subagent/tool-subagent) | `subagent` | [`agent`](../../packages/core/agent), [`llm`](../../packages/llm/llm), [`subagent`](../../packages/subagent/subagent), [`tools`](../../packages/core/tools) |
| [`subagent-mock`](../../packages/support/subagent-mock) | `support` | [`agent`](../../packages/core/agent), [`llm`](../../packages/llm/llm), [`subagent`](../../packages/subagent/subagent) |
| [`subagent-fork`](../../packages/subagent/subagent-fork) | `subagent` | [`agent`](../../packages/core/agent), [`session`](../../packages/core/session), [`subagent`](../../packages/subagent/subagent), [`subagent-inprocess`](../../packages/subagent/subagent-inprocess) |
| [`subagent-spawn`](../../packages/subagent/subagent-spawn) | `subagent` | [`subagent`](../../packages/subagent/subagent), [`subagent-inprocess`](../../packages/subagent/subagent-inprocess) |
| [`acp-agent`](../../packages/ui/acp-agent) | `ui` | [`acp`](../../packages/ui/acp), [`agent-core`](../../packages/core/agent-core), [`session-persistence-jsonl`](../../packages/session-persistence/session-persistence-jsonl) |
| [`stdio-agent`](../../packages/ui/stdio-agent) | `ui` | [`agent`](../../packages/core/agent), [`agent-core`](../../packages/core/agent-core), [`session`](../../packages/core/session), [`session-persistence-jsonl`](../../packages/session-persistence/session-persistence-jsonl), [`ui-stdio`](../../packages/support/ui-stdio) |
+25
View File
@@ -0,0 +1,25 @@
<!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand.
Run `pnpm run gen-doc-graphs` to regenerate. -->
# Session Surface And Message Projection
Maintenance mode: curated Mermaid dataflow; exact event/type shapes live in core-data-structures.
This graph separates the append-only log from the derived message surface the next model request sees.
```mermaid
flowchart LR
append["Session.append(type, data)"]
log["Append-only SessionEvent log"]
surface["SurfaceManager linked list<br/>surfaceOp + sourceEventSeqs"]
derive["deriveMessages()"]
model["GenerateOptions.messages"]
persist["JSONL / SQLite persistence"]
replay["load / replay / fork seed"]
append --> log
log --> surface
surface --> derive --> model
log --> persist --> replay --> log
```
See [core-data-structures/session.md](../core-data-structures/session.md) for the full `SessionEventMap`, surface operations, and turn-enclosure invariant.
+24
View File
@@ -0,0 +1,24 @@
<!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand.
Run `pnpm run gen-doc-graphs` to regenerate. -->
# ACP Snapshot Replay
Maintenance mode: curated Mermaid sequence based on the snapshot test harness.
This graph explains what a snapshot scenario proves: recorded real-model session logs are replayed keylessly, then ACP stdout is normalized and diffed.
```mermaid
sequenceDiagram
participant Recorder as Real API recording
participant Fixture as snapshot fixture
participant Replay as llm-replay adapter
participant ACP as acp-agent subprocess
participant Golden as stdout golden
Recorder->>Fixture: session.jsonl + workspace inputs
Fixture->>Replay: recorded StreamChunk script
Replay->>ACP: deterministic llm/stream chunks
ACP->>Golden: normalized sessionUpdate stream
Golden-->>ACP: diff must be empty
```
Future pressure from the fs stack: policy rejection scenarios are valuable because they prove both world state and failed tool-card rendering, not just that replay returns text.
+27
View File
@@ -0,0 +1,27 @@
<!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand.
Run `pnpm run gen-doc-graphs` to regenerate. -->
# Subagent And Session Lineage
Maintenance mode: curated Mermaid flow; provider inventory is visible in the generated capability seam graph.
This graph keeps delegation semantics separate from hook observation. A subagent backend creates an ordinary child agent/session through the shared provider registry.
```mermaid
flowchart TD
parent["Parent Agent + Session"]
tool["tool-subagent<br/>model-facing name"]
registry["ctx.subagents provider registry"]
spawn["spawn provider<br/>fresh child session"]
fork["fork provider<br/>seeded from completed-turn prefix"]
acp["ACP provider<br/>out-of-process child"]
child["Child AgentHandle<br/>ordinary Agent lifecycle"]
result["SubagentResult returned to tool"]
parent --> tool --> registry
registry --> spawn --> child
registry --> fork --> child
registry --> acp --> child
child --> result --> parent
```
The hooks stack adds richer lifecycle observation around child runs; the core ownership rule stays the same: the provider owns the child handle and must dispose it.
+35
View File
@@ -0,0 +1,35 @@
<!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand.
Run `pnpm run gen-doc-graphs` to regenerate. -->
# Tool Affordance Map
Maintenance mode: hybrid: tool names/schemas are boot-harvested from shipped tool plugins; required services and shipped aliases are classified in `scripts/gen-doc-graphs.ts` with a completeness guard.
This page connects the model-visible tools to the plugin packages and service seams behind them. For exact JSON Schemas, see [tool-catalog/tools.md](../tool-catalog/tools.md).
```mermaid
flowchart LR
model["Model request tools[]"]
toolpkg__deepseek_ai_dsh_tool_bash["tool-bash<br/>bash, bash_kill, bash_output"]
model --> toolpkg__deepseek_ai_dsh_tool_bash
requires_ctx_tools["ctx.tools"]
toolpkg__deepseek_ai_dsh_tool_bash --> requires_ctx_tools
requires_ctx_bash["ctx.bash"]
toolpkg__deepseek_ai_dsh_tool_bash --> requires_ctx_bash
toolpkg__deepseek_ai_dsh_tool_subagent["tool-subagent<br/>subagent"]
model --> toolpkg__deepseek_ai_dsh_tool_subagent
toolpkg__deepseek_ai_dsh_tool_subagent --> requires_ctx_tools
requires_ctx_subagents["ctx.subagents"]
toolpkg__deepseek_ai_dsh_tool_subagent --> requires_ctx_subagents
toolpkg__deepseek_ai_dsh_tool_todo["tool-todo<br/>todo_write"]
model --> toolpkg__deepseek_ai_dsh_tool_todo
toolpkg__deepseek_ai_dsh_tool_todo --> requires_ctx_tools
requires_owning_Agent_session["owning Agent session"]
toolpkg__deepseek_ai_dsh_tool_todo --> requires_owning_Agent_session
```
| Tool package | Model-visible names | Requires | Writes / affects | Shipped aliases | Note |
| --- | --- | --- | --- | --- | --- |
| `@deepseek-ai/dsh-tool-bash` | `bash`, `bash_kill`, `bash_output` | `ctx.tools`, `ctx.bash` | `tool/call`, `tool/result`, `context/message via agent.inject() for background completion notices` | - | The bash/bash_output/bash_kill tools are model-facing consumers of the bash executor seam. |
| `@deepseek-ai/dsh-tool-subagent` | `subagent` | `ctx.tools`, `ctx.subagents` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The default package schema registers subagent; shipped coding/acp configs load it twice to expose spawn and fork backends. |
| `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`, `owning Agent session` | `tool/call`, `todo/write`, `tool/result` | - | todo_write is session-owned state; UIs render the latest todo/write event as a checklist or ACP plan. |
+30
View File
@@ -0,0 +1,30 @@
<!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand.
Run `pnpm run gen-doc-graphs` to regenerate. -->
# Tool Execution Pipeline
Maintenance mode: curated Mermaid flow; exact tool schemas and event signatures live in generated catalogs.
This graph shows where policy, hooks, sandboxing, and future filesystem guards fit without changing the loop. The key extension point is the `tools/execute` waterfall.
```mermaid
flowchart TD
model["Assistant message contains tool-call block"]
call["Session event: tool/call"]
waterfall["ctx.tools.execute()<br/>tools/execute waterfall"]
policy["Policy / permission / hooks listener"]
body["Registered tool execute() body"]
owned["Tool-owned session events<br/>todo/write, future fs policy facts"]
result["Session event: tool/result"]
ui["UI presentation<br/>presentCall / presentResult"]
model --> call --> waterfall
waterfall --> policy
policy -->|next()| body
policy -->|veto / throw| result
body --> owned
body --> result
call --> ui
result --> ui
```
Future pressure from the fs stack: PR #128 snapshots a policy rejection card. The graph keeps the veto path explicit because filesystem read-before-edit checks, permission prompts, and hook bridges all belong on this path.
+1
View File
@@ -136,6 +136,7 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r
| [Generated cordis events + services catalog](implemented/process/2026-06-20-generated-cordis-catalog.md) | 2026-06-20 |
| [Classify RFCs by kind via path-encoded subdirectories](implemented/process/2026-06-20-rfc-classification.md) | 2026-06-20 |
| [Generated tool-schema catalog (boot-and-harvest)](implemented/process/2026-07-02-tool-schema-catalog.md) | 2026-07-02 |
| [Documentation graph atlas for maintainers and SDK users](implemented/process/2026-07-03-documentation-graph-atlas.md) | 2026-07-03 |
### Testing
@@ -0,0 +1,66 @@
# RFC: Documentation graph atlas for maintainers and SDK users
Status: implemented (accepted 2026-07-03)
## Context
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`, [cordis-catalog/events-and-services.md](../../../cordis-catalog/events-and-services.md) is 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.
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?"
The pressure is already visible in the open stacks even though this implementation is based on `origin/master`: the hooks stack through PR #129 makes event producer/consumer topology and interception points much more important, while the filesystem stack through PR #128 makes capability seams, policy vetoes, tool presentation, and SDK assembly paths much more important. Graphs based only on today's small bash/todo/subagent surface would become obsolete as soon as those stacks land.
## Decision
Add a generated graph atlas under [docs/graphs/](../../../graphs/README.md), produced by `scripts/gen-doc-graphs.ts` and verified by `pnpm run verify-doc-graphs` as part of `doc-sync`.
The atlas is a relationship layer above the existing catalogs. It does not replace exact references; instead, it links to them and explains how their pieces fit together.
### Maintenance modes
Every graph page declares one maintenance mode:
- **Generated**: all nodes and edges are discovered from source; `--check` fails if the committed artifact is stale.
- **Hybrid generated**: source discovers the inventory, a small manifest classifies irreducible policy, and a completeness guard fails if discovered items are unclassified.
- **Curated**: the diagram explains design intent, temporal order, or ownership; it is emitted by the generator so the atlas remains a single regenerated unit, but the content is deliberately authored.
### First shipped atlas
The first atlas ships twelve files: the index plus eleven graph pages.
| Graph | Maintenance mode | Source of truth |
|---|---|---|
| [package topology by group](../../../graphs/package-topology.md) | generated | `packages/*/*/package.json` peer dependencies plus package group paths |
| [capability seams and core services](../../../graphs/capability-seams.md) | hybrid generated | Cordis service declarations plus a role manifest in `gen-doc-graphs.ts` |
| [app composition](../../../graphs/app-composition.md) | hybrid generated | `examples/*/cordis.yml` plugin lists plus curated app/bundle expansions |
| [event producer/consumer matrix](../../../graphs/event-producer-consumer.md) | hybrid generated | Cordis event declarations, AST-scanned `ctx.on/emit/parallel/serial/waterfall` sites, and explicit dynamic dispatch overrides |
| [tool affordance map](../../../graphs/tool-affordance-map.md) | hybrid generated | boot-harvested tool catalog plus a manifest of required services and shipped aliases |
| [agent turn and step lifecycle](../../../graphs/agent-lifecycle.md) | curated | architecture.md loop lifecycle, Cordis catalog links, and session event semantics |
| [tool execution pipeline](../../../graphs/tool-execution-pipeline.md) | curated | tool pipeline semantics and the `tools/execute` waterfall |
| [session surface and message projection](../../../graphs/session-surface.md) | curated | session surface/event-sourcing docs |
| [subagent and session lineage](../../../graphs/subagent-lineage.md) | curated | subagent seam docs and replay/fork semantics |
| [plugin disposal and hot reload ownership](../../../graphs/hot-reload-disposal.md) | curated | Cordis fiber/effect ownership conventions |
| [ACP snapshot replay](../../../graphs/snapshot-replay.md) | curated | snapshot harness behavior |
### Why one generator
Keeping the atlas behind one generator gives reviewers one freshness gate and keeps cross-page terminology synchronized. The tradeoff is that curated diagrams are edited in TypeScript string blocks rather than directly in Markdown. That is acceptable for this first cut because the user-facing artifact is still plain Markdown/Mermaid, and a future change can split the curated pages out if authorship ergonomics matter more than one-command regeneration.
### Completeness guards
The hybrid pages must fail loud when their manifests are stale:
- The capability seam graph imports the Cordis service collector and asserts every discovered harness `ctx.<key>` is classified in `SERVICE_ROLES`, and every classified key still exists.
- The tool affordance graph boot-harvests the shipped tool catalog and asserts every tool package has `TOOL_PACKAGE_META`.
- The event producer/consumer matrix labels itself hybrid because subagent lifecycle events deliberately use `ctx.events.dispatch` for per-listener containment; those dynamic edges are explicit overrides rather than invisible omissions.
## Format choices
Use Mermaid for committed diagrams because GitHub renders it in Markdown and it adds no new docs build dependency. Use Markdown tables for dense many-to-many data such as event producer/consumer relationships. Do not adopt PlantUML, hosted diagram services, or generated SVGs until Mermaid becomes the limiting factor.
## Consequences
- Maintainers get visual entry points for topology, seams, event flow, lifecycle, session replay, and snapshot behavior.
- SDK users get a path from use case to package composition instead of only bottom-up package references.
- `doc-sync` now includes `verify-doc-graphs`, so graph drift is caught with the other doc freshness gates.
- Future fs and hooks work has a concrete place to land new complexity: fs should expand the capability and tool graphs, while hooks should expand the event matrix and tool execution pipeline.
+3 -1
View File
@@ -36,10 +36,12 @@
"verify-cordis-catalog": "tsx scripts/gen-cordis-catalog.ts --check",
"gen-tool-catalog": "tsx scripts/gen-tool-catalog.ts",
"verify-tool-catalog": "tsx scripts/gen-tool-catalog.ts --check",
"gen-doc-graphs": "tsx scripts/gen-doc-graphs.ts",
"verify-doc-graphs": "tsx scripts/gen-doc-graphs.ts --check",
"gen-module-graph": "tsx scripts/gen-module-graph.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-tool-catalog && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-rfc-classification && pnpm run verify-type-equiv",
"doc-sync": "pnpm run doc-typecheck && pnpm run verify-cordis-catalog && pnpm run verify-tool-catalog && pnpm run verify-doc-graphs && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-rfc-classification && pnpm run verify-type-equiv",
"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:coding": "node --expose-internals --import tsx packages/ui/stdio-agent/src/bin.ts examples/coding-agent/cordis.yml",
+851
View File
@@ -0,0 +1,851 @@
/**
* Generate (and verify) the documentation graph atlas in docs/graphs/.
*
* This is the relationship layer above the existing catalogs:
* - module-graph.md answers "which packages depend on which packages?"
* - cordis-catalog/ answers "which events and services exist?"
* - tool-catalog/ answers "which tools does the model see?"
* - docs/graphs/ answers "how do those pieces fit together?"
*
* Generated pages discover the enumerable facts from source. Hybrid pages use
* discovered inventory plus small manifests for policy that source cannot infer
* (for example, whether a package is an implementation or consumer in a seam).
* Curated pages are still emitted here so the atlas is one regenerated unit,
* but their diagrams intentionally explain flow and ownership rather than
* pretending to enumerate every source edge.
*
* `tsx scripts/gen-doc-graphs.ts` -> write docs/graphs/*.md
* `tsx scripts/gen-doc-graphs.ts --check` -> exit 1 if any file is stale
*/
import { existsSync, globSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import ts from 'typescript'
import { collectEvents, collectServices } from './gen-cordis-catalog.ts'
import { collectToolCatalog } from './gen-tool-catalog.ts'
const root = resolve(import.meta.dirname, '..')
const OUT_DIR = 'docs/graphs'
const SCOPE = '@deepseek-ai/dsh-'
interface PkgJson {
name: string
peerDependencies?: Record<string, string>
}
interface Pkg {
short: string
name: string
group: string
rel: string
deps: string[]
}
interface GraphDoc {
rel: string
content: string
}
interface ServiceRole {
key: string
pkg: string
title: string
mode: 'core' | 'seam' | 'bundle'
implementations?: string[]
consumers?: string[]
note: string
}
interface ExamplePlugin {
id: string
name: string
}
interface EventRelation {
dispatchers: Map<string, Set<string>>
listeners: Set<string>
}
interface ToolPackageMeta {
requires: string[]
writes: string[]
shippedNames?: string[]
note: string
}
const GROUP_ORDER = ['util', 'llm', 'core', 'bash', 'compact', 'subagent', 'session-persistence', 'todo', 'support', 'ui']
const SERVICE_ROLES: ServiceRole[] = [
{
key: 'llm',
pkg: 'llm',
title: 'LLM adapter registry',
mode: 'seam',
implementations: ['llm-deepseek', 'llm-pi-ai', 'llm-replay'],
consumers: ['agent-loop', 'compact-basic'],
note: 'Adapters register provider implementations; the loop and compaction call the provider-neutral stream service.',
},
{
key: 'sessions',
pkg: 'session',
title: 'In-memory session store',
mode: 'core',
consumers: ['agent-loop', 'agent', 'session-persistence', 'subagent-inprocess', 'invariants'],
note: 'Owns append-only Session instances and emits the durable session event feed.',
},
{
key: 'sessionPersistence',
pkg: 'session-persistence',
title: 'Durable session persistence seam',
mode: 'seam',
implementations: ['session-persistence-jsonl', 'session-persistence-sqlite'],
consumers: ['agent-loop', 'acp'],
note: 'Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time.',
},
{
key: 'systemPrompt',
pkg: 'system-prompt',
title: 'System prompt assembly registry',
mode: 'core',
consumers: ['agent-loop', 'tools'],
note: 'Collects prompt sections and model-facing tool schemas for each step.',
},
{
key: 'tools',
pkg: 'tools',
title: 'Tool registry and execution waterfall',
mode: 'core',
consumers: ['agent-loop', 'tool-bash', 'tool-subagent', 'tool-todo', 'acp'],
note: 'Registers tool definitions, exposes schemas to the prompt, and routes calls through tools/execute.',
},
{
key: 'agents',
pkg: 'agent',
title: 'Agent registry',
mode: 'core',
consumers: ['agent-loop', 'acp', 'subagent-inprocess', 'stdio-agent', 'invariants'],
note: 'Owns live Agent handles and the create/resume factory seam.',
},
{
key: 'agentLoop',
pkg: 'agent-loop',
title: 'Concrete loop driver',
mode: 'bundle',
consumers: ['agent-core'],
note: 'The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package.',
},
{
key: 'bash',
pkg: 'bash',
title: 'Bash executor seam',
mode: 'seam',
implementations: ['bash-local'],
consumers: ['tool-bash'],
note: 'The model-facing bash tools consume this seam; sandboxed or remote executors can replace bash-local.',
},
{
key: 'compact',
pkg: 'compact',
title: 'Compaction seam',
mode: 'seam',
implementations: ['compact-basic'],
consumers: ['compact-basic'],
note: 'The basic backend currently consumes the pre-step event directly; a model-facing compact tool remains deferred.',
},
{
key: 'subagents',
pkg: 'subagent',
title: 'Subagent provider registry',
mode: 'seam',
implementations: ['subagent-spawn', 'subagent-fork', 'subagent-acp', 'subagent-mock'],
consumers: ['tool-subagent'],
note: 'Providers implement transports; tool-subagent exposes one configured provider as a model-facing tool name.',
},
]
const TOOL_PACKAGE_META: Record<string, ToolPackageMeta> = {
'@deepseek-ai/dsh-tool-bash': {
requires: ['ctx.tools', 'ctx.bash'],
writes: ['tool/call', 'tool/result', 'context/message via agent.inject() for background completion notices'],
note: 'The bash/bash_output/bash_kill tools are model-facing consumers of the bash executor seam.',
},
'@deepseek-ai/dsh-tool-subagent': {
requires: ['ctx.tools', 'ctx.subagents'],
writes: ['tool/call', 'tool/result', 'child session events through the chosen provider'],
shippedNames: ['subagent', 'subagent_fork'],
note: 'The default package schema registers subagent; shipped coding/acp configs load it twice to expose spawn and fork backends.',
},
'@deepseek-ai/dsh-tool-todo': {
requires: ['ctx.tools', 'owning Agent session'],
writes: ['tool/call', 'todo/write', 'tool/result'],
note: 'todo_write is session-owned state; UIs render the latest todo/write event as a checklist or ACP plan.',
},
}
const DYNAMIC_EVENT_DISPATCHERS: Array<{ event: string; pkg: string; method: string }> = [
// Subagent lifecycle events intentionally bypass ctx.emit and call
// ctx.events.dispatch directly so one throwing listener cannot starve later
// listeners or strand an already-started child run.
{ event: 'subagent/start', pkg: 'subagent', method: 'events.dispatch' },
{ event: 'subagent/end', pkg: 'subagent', method: 'events.dispatch' },
]
function generatedHeader(title: string, source: string): string[] {
return [
'<!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand.',
' Run `pnpm run gen-doc-graphs` to regenerate. -->',
'',
`# ${title}`,
'',
`Maintenance mode: ${source}.`,
'',
]
}
function collectPackages(): Pkg[] {
const pkgs: Pkg[] = []
for (const rel of globSync('packages/*/*/package.json', { cwd: root }).sort()) {
const json = JSON.parse(readFileSync(resolve(root, rel), 'utf8')) as PkgJson
if (!json.name.startsWith(SCOPE)) continue
const [, group, leaf] = rel.split('/')
if (group === undefined || leaf === undefined) throw new Error(`gen-doc-graphs: unexpected package path ${rel}`)
const deps = Object.keys(json.peerDependencies ?? {})
.filter(dep => dep.startsWith(SCOPE))
.map(dep => dep.slice(SCOPE.length))
.sort()
pkgs.push({
short: json.name.slice(SCOPE.length),
name: json.name,
group,
rel: dirname(rel),
deps,
})
}
return topoSort(pkgs)
}
function topoSort(pkgs: Pkg[]): Pkg[] {
const remaining = new Map(pkgs.map(p => [p.short, p]))
const placed = new Set<string>()
const out: Pkg[] = []
while (remaining.size > 0) {
const ready = [...remaining.values()]
.filter(pkg => pkg.deps.every(dep => placed.has(dep)))
.sort(comparePackages)
if (ready.length === 0) throw new Error(`gen-doc-graphs: dependency cycle among ${[...remaining.keys()].join(', ')}`)
for (const pkg of ready) {
out.push(pkg)
placed.add(pkg.short)
remaining.delete(pkg.short)
}
}
return out
}
function comparePackages(a: Pkg, b: Pkg): number {
const groupA = GROUP_ORDER.indexOf(a.group)
const groupB = GROUP_ORDER.indexOf(b.group)
const normA = groupA === -1 ? Number.MAX_SAFE_INTEGER : groupA
const normB = groupB === -1 ? Number.MAX_SAFE_INTEGER : groupB
return normA - normB || a.group.localeCompare(b.group) || a.short.localeCompare(b.short)
}
function nodeId(prefix: string, value: string): string {
return `${prefix}_${value.replace(/[^a-zA-Z0-9_]/g, '_')}`
}
function escLabel(value: string): string {
return value.replace(/"/g, '\\"')
}
function pkgLink(pkg: Pkg | undefined, fallback: string): string {
return pkg ? `[\`${pkg.short}\`](../../${pkg.rel})` : `\`${fallback}\``
}
function pkgList(names: string[] | undefined, pkgsByShort: Map<string, Pkg>): string {
if (!names || names.length === 0) return '-'
return names.map(name => pkgLink(pkgsByShort.get(name), name)).join(', ')
}
function codeList(values: string[]): string {
return values.length ? values.map(v => `\`${v}\``).join(', ') : '-'
}
function tableCell(value: string): string {
return value.replace(/\|/g, '\\|').replace(/\n/g, '<br>')
}
function renderMermaidPackageNode(pkg: Pkg): string {
return ` ${nodeId('pkg', pkg.short)}["${escLabel(pkg.short)}"]`
}
function renderPackageTopology(pkgs: Pkg[]): string {
const lines = generatedHeader('Package Topology By Group', 'generated from `packages/*/*/package.json` peer dependencies plus package group paths')
lines.push(
'This graph complements [module-graph.md](../module-graph.md): it keeps the same canonical peer-dependency edge source, but clusters packages by the `packages/<group>/<pkg>` hierarchy so layering and capability families are easier to scan.',
'',
'```mermaid',
'flowchart TD',
)
const groups = [...new Set(pkgs.map(pkg => pkg.group))].sort((a, b) => {
const ia = GROUP_ORDER.indexOf(a)
const ib = GROUP_ORDER.indexOf(b)
const na = ia === -1 ? Number.MAX_SAFE_INTEGER : ia
const nb = ib === -1 ? Number.MAX_SAFE_INTEGER : ib
return na - nb || a.localeCompare(b)
})
for (const group of groups) {
lines.push(` subgraph ${nodeId('group', group)}["packages/${escLabel(group)}"]`)
for (const pkg of pkgs.filter(p => p.group === group).sort((a, b) => a.short.localeCompare(b.short))) {
lines.push(renderMermaidPackageNode(pkg))
}
lines.push(' end')
}
for (const pkg of pkgs) {
for (const dep of pkg.deps) lines.push(` ${nodeId('pkg', pkg.short)} --> ${nodeId('pkg', dep)}`)
}
lines.push('```', '', '| Package | Group | Depends on |', '| --- | --- | --- |')
const byShort = new Map(pkgs.map(pkg => [pkg.short, pkg]))
for (const pkg of pkgs) {
lines.push(`| ${pkgLink(pkg, pkg.short)} | \`${pkg.group}\` | ${pkg.deps.length ? pkg.deps.map(dep => pkgLink(byShort.get(dep), dep)).join(', ') : '-'} |`)
}
lines.push('')
return lines.join('\n')
}
function assertServiceRolesComplete(): void {
const discovered = new Set(collectServices().map(service => service.key))
const classified = new Set(SERVICE_ROLES.map(role => role.key))
const missing = [...discovered].filter(key => !classified.has(key)).sort()
const stale = [...classified].filter(key => !discovered.has(key)).sort()
if (missing.length || stale.length) {
throw new Error([
missing.length ? `missing service role classification: ${missing.join(', ')}` : '',
stale.length ? `stale service role classification: ${stale.join(', ')}` : '',
].filter(Boolean).join('; '))
}
}
function renderCapabilitySeams(pkgs: Pkg[]): string {
assertServiceRolesComplete()
const pkgsByShort = new Map(pkgs.map(pkg => [pkg.short, pkg]))
const nodes = new Map<string, string>()
const edges = new Set<string>()
const addNode = (id: string, label: string): void => {
if (!nodes.has(id)) nodes.set(id, ` ${id}["${escLabel(label)}"]`)
}
const addEdge = (from: string, to: string): void => { edges.add(` ${from} --> ${to}`) }
const lines = generatedHeader('Capability Seams And Core Services', 'hybrid: services are discovered from Cordis declarations; interface/implementation/consumer roles are classified in `scripts/gen-doc-graphs.ts` with a completeness guard')
lines.push(
'A service can be a core spine service, a swappable capability seam, or a bundle/composition point. The graph shows the package that owns the service declaration, known implementation packages, and packages that consume the service directly.',
'',
'```mermaid',
'flowchart LR',
)
for (const role of SERVICE_ROLES) {
const svc = nodeId('svc', role.key)
const owner = nodeId('pkg', role.pkg)
addNode(owner, role.pkg)
addNode(svc, `ctx.${role.key}<br/>${role.title}`)
addEdge(owner, svc)
for (const impl of role.implementations ?? []) {
addNode(nodeId('pkg', impl), impl)
addEdge(nodeId('pkg', impl), svc)
}
for (const consumer of role.consumers ?? []) {
addNode(nodeId('pkg', consumer), consumer)
addEdge(svc, nodeId('pkg', consumer))
}
}
lines.push(...nodes.values(), ...[...edges].sort())
lines.push('```', '', '| ctx key | Role | Owner | Implementations | Direct consumers | Note |', '| --- | --- | --- | --- | --- | --- |')
for (const role of SERVICE_ROLES) {
lines.push(`| \`ctx.${role.key}\` | \`${role.mode}\` | ${pkgLink(pkgsByShort.get(role.pkg), role.pkg)} | ${pkgList(role.implementations, pkgsByShort)} | ${pkgList(role.consumers, pkgsByShort)} | ${tableCell(role.note)} |`)
}
lines.push('')
return lines.join('\n')
}
function parseExampleCordis(rel: string): ExamplePlugin[] {
const text = readFileSync(resolve(root, rel), 'utf8')
const plugins: ExamplePlugin[] = []
let current: { id: string; name?: string } | null = null
const flush = (): void => {
if (current?.name) plugins.push({ id: current.id, name: current.name })
}
for (const line of text.split('\n')) {
const id = /^-\s+id:\s+(.+?)\s*$/.exec(line)
if (id?.[1] !== undefined) {
flush()
current = { id: stripYamlScalar(id[1]) }
continue
}
const name = /^\s+name:\s+(.+?)\s*$/.exec(line)
if (name?.[1] !== undefined && current) current.name = stripYamlScalar(name[1])
}
flush()
return plugins
}
function stripYamlScalar(value: string): string {
return value.trim().replace(/^['"]|['"]$/g, '')
}
function renderAppComposition(): string {
const examples = [
{ id: 'echo', label: 'examples/echo-agent', config: 'examples/echo-agent/cordis.yml' },
{ id: 'coding', label: 'examples/coding-agent', config: 'examples/coding-agent/cordis.yml' },
{ id: 'acp', label: 'examples/acp-agent', config: 'examples/acp-agent/cordis.yml' },
]
const lines = generatedHeader('App Composition', 'hybrid: leaf plugin lists are parsed from `examples/*/cordis.yml`; bundle expansions are curated from app package source')
lines.push(
'This graph is for SDK users asking which pieces a runnable agent loads. Leaf configs choose adapters and optional product tools; app packages provide the front door; `dsh-agent-core` bundles the providerless spine.',
'',
'```mermaid',
'flowchart LR',
)
const bundleTargets: Record<string, string> = {
'@deepseek-ai/dsh-stdio-agent': nodeId('bundle', 'stdio'),
'@deepseek-ai/dsh-acp-agent': nodeId('bundle', 'acp_agent'),
}
for (const example of examples) {
lines.push(` subgraph ${nodeId('example', example.id)}["${escLabel(example.label)}"]`)
lines.push(` ${nodeId('cfg', example.id)}["cordis.yml"]`)
for (const plugin of parseExampleCordis(example.config)) {
const pluginNode = nodeId(`plugin_${example.id}`, plugin.id)
lines.push(` ${pluginNode}["${escLabel(plugin.id)}<br/>${escLabel(plugin.name)}"]`)
lines.push(` ${nodeId('cfg', example.id)} --> ${pluginNode}`)
const bundle = bundleTargets[plugin.name]
if (bundle !== undefined) lines.push(` ${pluginNode} --> ${bundle}`)
}
lines.push(' end')
}
lines.push(
` ${nodeId('bundle', 'stdio')}["@deepseek-ai/dsh-stdio-agent"] --> ${nodeId('bundle', 'agent_core')}["@deepseek-ai/dsh-agent-core"]`,
` ${nodeId('bundle', 'stdio')} --> ${nodeId('bundle', 'jsonl')}["@deepseek-ai/dsh-session-persistence-jsonl"]`,
` ${nodeId('bundle', 'stdio')} --> ${nodeId('bundle', 'ui_stdio')}["@deepseek-ai/dsh-ui-stdio"]`,
` ${nodeId('bundle', 'acp_agent')}["@deepseek-ai/dsh-acp-agent"] --> ${nodeId('bundle', 'agent_core')}`,
` ${nodeId('bundle', 'acp_agent')} --> ${nodeId('bundle', 'jsonl')}`,
` ${nodeId('bundle', 'acp_agent')} --> ${nodeId('bundle', 'acp')}["@deepseek-ai/dsh-acp"]`,
` ${nodeId('bundle', 'agent_core')} --> ${nodeId('spine', 'llm')}["ctx.llm"]`,
` ${nodeId('bundle', 'agent_core')} --> ${nodeId('spine', 'sessions')}["ctx.sessions"]`,
` ${nodeId('bundle', 'agent_core')} --> ${nodeId('spine', 'tools')}["ctx.tools + tool-bash"]`,
` ${nodeId('bundle', 'agent_core')} --> ${nodeId('spine', 'loop')}["ctx.agents + ctx.agentLoop"]`,
'```',
'',
'| Example | Parsed plugin ids | Config |',
'| --- | --- | --- |',
)
for (const example of examples) {
const plugins = parseExampleCordis(example.config)
lines.push(`| \`${example.label}\` | ${plugins.map(plugin => `\`${plugin.id}\``).join(', ')} | [\`${example.config}\`](../../${example.config}) |`)
}
lines.push('')
return lines.join('\n')
}
function collectEventRelations(): Map<string, EventRelation> {
const out = new Map<string, EventRelation>()
const ensure = (event: string): EventRelation => {
const existing = out.get(event)
if (existing) return existing
const next = { dispatchers: new Map<string, Set<string>>(), listeners: new Set<string>() }
out.set(event, next)
return next
}
for (const rel of globSync('packages/*/*/src/**/*.ts', { cwd: root }).sort()) {
const [, , leaf] = rel.split('/')
if (leaf === undefined) continue
const text = readFileSync(resolve(root, rel), 'utf8')
const sf = ts.createSourceFile(rel, text, ts.ScriptTarget.Latest, true)
const visit = (node: ts.Node): void => {
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
const method = node.expression.name.text
if (!isCordisContextReceiver(node.expression, sf)) {
ts.forEachChild(node, visit)
return
}
if (method === 'on') {
const event = eventArg(node.arguments, method)
if (event) ensure(event).listeners.add(leaf)
} else if (method === 'emit' || method === 'parallel' || method === 'serial' || method === 'waterfall') {
const event = eventArg(node.arguments, method)
if (event) {
const relation = ensure(event)
const methods = relation.dispatchers.get(leaf) ?? new Set<string>()
methods.add(method)
relation.dispatchers.set(leaf, methods)
}
}
}
ts.forEachChild(node, visit)
}
visit(sf)
}
for (const entry of DYNAMIC_EVENT_DISPATCHERS) {
const relation = ensure(entry.event)
const methods = relation.dispatchers.get(entry.pkg) ?? new Set<string>()
methods.add(entry.method)
relation.dispatchers.set(entry.pkg, methods)
}
return out
}
function isCordisContextReceiver(expr: ts.PropertyAccessExpression, sf: ts.SourceFile): boolean {
const target = expr.expression.getText(sf)
return target === 'ctx' || target === 'this.ctx'
}
function eventArg(args: ts.NodeArray<ts.Expression>, method: string): string | undefined {
if (method === 'waterfall') {
const arg = args.find(ts.isStringLiteralLike)
return arg?.text
}
const first = args[0]
return first && ts.isStringLiteralLike(first) ? first.text : undefined
}
function relationPackages(map: Map<string, Set<string>>, pkgsByShort: Map<string, Pkg>): string {
if (map.size === 0) return '-'
return [...map.entries()]
.sort(([a], [b]) => a.localeCompare(b))
.map(([pkg, methods]) => `${pkgLink(pkgsByShort.get(pkg), pkg)} (${[...methods].sort().map(m => `\`${m}\``).join(', ')})`)
.join(', ')
}
function listenerPackages(listeners: Set<string>, pkgsByShort: Map<string, Pkg>): string {
if (listeners.size === 0) return '-'
return [...listeners].sort().map(pkg => pkgLink(pkgsByShort.get(pkg), pkg)).join(', ')
}
function renderEventRelations(pkgs: Pkg[]): string {
const events = collectEvents()
const relations = collectEventRelations()
const pkgsByShort = new Map(pkgs.map(pkg => [pkg.short, pkg]))
const lines = generatedHeader('Event Producer And Consumer Matrix', 'hybrid generated: Cordis event declarations and most producer/listener edges are AST-scanned; dynamic dispatch sites are classified in `scripts/gen-doc-graphs.ts`')
lines.push(
'This matrix shows which packages dispatch each harness-owned event and which packages listen to it. It is intentionally a table rather than one large graph: events are many-to-many, and dense relation data is easier to review in rows. Dynamic dispatch overrides cover sites that deliberately bypass `ctx.emit`, such as subagent lifecycle containment.',
'',
'| Event | Mode | Declared in | Dispatchers | Listeners |',
'| --- | --- | --- | --- | --- |',
)
for (const event of [...events].sort((a, b) => a.name.localeCompare(b.name))) {
const relation = relations.get(event.name) ?? { dispatchers: new Map<string, Set<string>>(), listeners: new Set<string>() }
lines.push(`| \`${event.name}\` | \`${event.mode}\` | [\`${event.source}\`](../../${event.source.split(':')[0]}) | ${relationPackages(relation.dispatchers, pkgsByShort)} | ${listenerPackages(relation.listeners, pkgsByShort)} |`)
}
const declared = new Set(events.map(event => event.name))
const extra = [...relations.keys()].filter(event => !declared.has(event)).sort()
if (extra.length > 0) {
lines.push('', '## Non-harness or undeclared event strings seen in package source', '', '| Event string | Dispatchers | Listeners |', '| --- | --- | --- |')
for (const event of extra) {
const relation = relations.get(event)
if (!relation) continue
lines.push(`| \`${event}\` | ${relationPackages(relation.dispatchers, pkgsByShort)} | ${listenerPackages(relation.listeners, pkgsByShort)} |`)
}
}
lines.push('')
return lines.join('\n')
}
async function renderToolAffordance(): Promise<string> {
const catalog = await collectToolCatalog()
const lines = generatedHeader('Tool Affordance Map', 'hybrid: tool names/schemas are boot-harvested from shipped tool plugins; required services and shipped aliases are classified in `scripts/gen-doc-graphs.ts` with a completeness guard')
for (const entry of catalog) {
if (!TOOL_PACKAGE_META[entry.pkg]) {
throw new Error(`gen-doc-graphs: tool package ${entry.pkg} is missing TOOL_PACKAGE_META classification`)
}
}
lines.push(
'This page connects the model-visible tools to the plugin packages and service seams behind them. For exact JSON Schemas, see [tool-catalog/tools.md](../tool-catalog/tools.md).',
'',
'```mermaid',
'flowchart LR',
' model["Model request tools[]"]',
)
const requirementNodes = new Set<string>()
for (const entry of catalog) {
const meta = TOOL_PACKAGE_META[entry.pkg]
if (!meta) continue
const packageNode = nodeId('toolpkg', entry.pkg)
const names = entry.schemas.map(schema => schema.name).join(', ')
lines.push(` ${packageNode}["${escLabel(entry.pkg.replace(SCOPE, ''))}<br/>${escLabel(names)}"]`)
lines.push(` model --> ${packageNode}`)
for (const req of meta.requires) {
const reqNode = nodeId('requires', req)
if (!requirementNodes.has(reqNode)) {
lines.push(` ${reqNode}["${escLabel(req)}"]`)
requirementNodes.add(reqNode)
}
lines.push(` ${packageNode} --> ${reqNode}`)
}
}
lines.push('```', '', '| Tool package | Model-visible names | Requires | Writes / affects | Shipped aliases | Note |', '| --- | --- | --- | --- | --- | --- |')
for (const entry of catalog) {
const meta = TOOL_PACKAGE_META[entry.pkg]
if (!meta) continue
lines.push(`| \`${entry.pkg}\` | ${codeList(entry.schemas.map(schema => schema.name))} | ${codeList(meta.requires)} | ${codeList(meta.writes)} | ${codeList(meta.shippedNames ?? [])} | ${tableCell(meta.note)} |`)
}
lines.push('')
return lines.join('\n')
}
function renderLifecycle(): string {
return [
...generatedHeader('Agent Turn And Step Lifecycle', 'curated Mermaid sequence; exact event signatures live in the generated Cordis catalog'),
'This sequence is the visual companion to [architecture.md](../architecture.md#loop-lifecycle-session--turn--step). It shows the durable session event path separately from live `agent/*` notifications.',
'',
'```mermaid',
'sequenceDiagram',
' participant User',
' participant Agent',
' participant Loop',
' participant Prompt as ctx.systemPrompt',
' participant LLM as ctx.llm',
' participant Tools as ctx.tools',
' participant Session',
' participant Persistence',
' User->>Agent: send(content)',
' Agent->>Loop: queued work wakes driver',
' Loop->>Session: turn/start + user/message',
' Loop-->>User: agent/turn-start',
' Loop->>Prompt: system-prompt/assemble waterfall',
' Loop-->>Loop: agent/pre-step serial checkpoint',
' Loop->>Session: step/start',
' Loop->>LLM: agent/request waterfall, then llm/stream waterfall',
' LLM-->>Loop: StreamChunk*',
' Loop->>Session: assistant/chunk*',
' Loop-->>User: agent/stream-chunk* (master live mirror)',
' Loop->>Session: assistant/message',
' Loop->>Tools: tools/execute waterfall for each tool-call',
' Tools-->>Session: tool-owned events when applicable',
' Loop->>Session: tool/result',
' Loop-->>Loop: agent/turn-continuation waterfall',
' Loop->>Session: turn/end',
' Loop->>Persistence: session/flush parallel checkpoint',
' Loop-->>User: agent/status idle',
'```',
'',
'Future pressure from the hooks stack: PR #129 removes the live `agent/stream-chunk` mirror and leaves durable `assistant/chunk` on `session/event` as the authoritative token stream. Consumers that need replayable transcript data should already treat `session/event` as the load-bearing path.',
'',
].join('\n')
}
function renderToolPipeline(): string {
return [
...generatedHeader('Tool Execution Pipeline', 'curated Mermaid flow; exact tool schemas and event signatures live in generated catalogs'),
'This graph shows where policy, hooks, sandboxing, and future filesystem guards fit without changing the loop. The key extension point is the `tools/execute` waterfall.',
'',
'```mermaid',
'flowchart TD',
' model["Assistant message contains tool-call block"]',
' call["Session event: tool/call"]',
' waterfall["ctx.tools.execute()<br/>tools/execute waterfall"]',
' policy["Policy / permission / hooks listener"]',
' body["Registered tool execute() body"]',
' owned["Tool-owned session events<br/>todo/write, future fs policy facts"]',
' result["Session event: tool/result"]',
' ui["UI presentation<br/>presentCall / presentResult"]',
' model --> call --> waterfall',
' waterfall --> policy',
' policy -->|next()| body',
' policy -->|veto / throw| result',
' body --> owned',
' body --> result',
' call --> ui',
' result --> ui',
'```',
'',
'Future pressure from the fs stack: PR #128 snapshots a policy rejection card. The graph keeps the veto path explicit because filesystem read-before-edit checks, permission prompts, and hook bridges all belong on this path.',
'',
].join('\n')
}
function renderSessionSurface(): string {
return [
...generatedHeader('Session Surface And Message Projection', 'curated Mermaid dataflow; exact event/type shapes live in core-data-structures'),
'This graph separates the append-only log from the derived message surface the next model request sees.',
'',
'```mermaid',
'flowchart LR',
' append["Session.append(type, data)"]',
' log["Append-only SessionEvent log"]',
' surface["SurfaceManager linked list<br/>surfaceOp + sourceEventSeqs"]',
' derive["deriveMessages()"]',
' model["GenerateOptions.messages"]',
' persist["JSONL / SQLite persistence"]',
' replay["load / replay / fork seed"]',
' append --> log',
' log --> surface',
' surface --> derive --> model',
' log --> persist --> replay --> log',
'```',
'',
'See [core-data-structures/session.md](../core-data-structures/session.md) for the full `SessionEventMap`, surface operations, and turn-enclosure invariant.',
'',
].join('\n')
}
function renderSubagentLineage(): string {
return [
...generatedHeader('Subagent And Session Lineage', 'curated Mermaid flow; provider inventory is visible in the generated capability seam graph'),
'This graph keeps delegation semantics separate from hook observation. A subagent backend creates an ordinary child agent/session through the shared provider registry.',
'',
'```mermaid',
'flowchart TD',
' parent["Parent Agent + Session"]',
' tool["tool-subagent<br/>model-facing name"]',
' registry["ctx.subagents provider registry"]',
' spawn["spawn provider<br/>fresh child session"]',
' fork["fork provider<br/>seeded from completed-turn prefix"]',
' acp["ACP provider<br/>out-of-process child"]',
' child["Child AgentHandle<br/>ordinary Agent lifecycle"]',
' result["SubagentResult returned to tool"]',
' parent --> tool --> registry',
' registry --> spawn --> child',
' registry --> fork --> child',
' registry --> acp --> child',
' child --> result --> parent',
'```',
'',
'The hooks stack adds richer lifecycle observation around child runs; the core ownership rule stays the same: the provider owns the child handle and must dispose it.',
'',
].join('\n')
}
function renderHotReload(): string {
return [
...generatedHeader('Plugin Disposal And Hot Reload Ownership', 'curated Mermaid flow based on Cordis fiber/effect conventions'),
'This graph is a maintainer checklist for plugin authors: registrations are effects, service injection gates activation, and owned handles must be disposed by their owner.',
'',
'```mermaid',
'flowchart TD',
' plugin["ctx.plugin(plugin) creates fiber"]',
' inject["static inject gates activation"]',
' service["ctx.provide / Service constructor"]',
' effects["ctx.effect registrations<br/>events, tools, adapters, timers"]',
' reload["HMR / fiber.dispose()"]',
' disposers["Run disposers in owner fiber"]',
' quiescence["Owned AgentHandle.dispose()<br/>or service teardown awaits quiescence"]',
' plugin --> inject --> service',
' inject --> effects',
' reload --> disposers --> quiescence',
'```',
'',
'Hook bridges and SDK plugins increase the number of long-lived listeners, so this ownership graph should stay small and visible.',
'',
].join('\n')
}
function renderSnapshotReplay(): string {
return [
...generatedHeader('ACP Snapshot Replay', 'curated Mermaid sequence based on the snapshot test harness'),
'This graph explains what a snapshot scenario proves: recorded real-model session logs are replayed keylessly, then ACP stdout is normalized and diffed.',
'',
'```mermaid',
'sequenceDiagram',
' participant Recorder as Real API recording',
' participant Fixture as snapshot fixture',
' participant Replay as llm-replay adapter',
' participant ACP as acp-agent subprocess',
' participant Golden as stdout golden',
' Recorder->>Fixture: session.jsonl + workspace inputs',
' Fixture->>Replay: recorded StreamChunk script',
' Replay->>ACP: deterministic llm/stream chunks',
' ACP->>Golden: normalized sessionUpdate stream',
' Golden-->>ACP: diff must be empty',
'```',
'',
'Future pressure from the fs stack: policy rejection scenarios are valuable because they prove both world state and failed tool-card rendering, not just that replay returns text.',
'',
].join('\n')
}
async function renderDocs(): Promise<GraphDoc[]> {
const pkgs = collectPackages()
const docs: GraphDoc[] = [
{ rel: `${OUT_DIR}/package-topology.md`, content: renderPackageTopology(pkgs) },
{ rel: `${OUT_DIR}/capability-seams.md`, content: renderCapabilitySeams(pkgs) },
{ rel: `${OUT_DIR}/app-composition.md`, content: renderAppComposition() },
{ rel: `${OUT_DIR}/event-producer-consumer.md`, content: renderEventRelations(pkgs) },
{ rel: `${OUT_DIR}/tool-affordance-map.md`, content: await renderToolAffordance() },
{ rel: `${OUT_DIR}/agent-lifecycle.md`, content: renderLifecycle() },
{ rel: `${OUT_DIR}/tool-execution-pipeline.md`, content: renderToolPipeline() },
{ rel: `${OUT_DIR}/session-surface.md`, content: renderSessionSurface() },
{ rel: `${OUT_DIR}/subagent-lineage.md`, content: renderSubagentLineage() },
{ rel: `${OUT_DIR}/hot-reload-disposal.md`, content: renderHotReload() },
{ rel: `${OUT_DIR}/snapshot-replay.md`, content: renderSnapshotReplay() },
]
docs.unshift({ rel: `${OUT_DIR}/README.md`, content: renderIndex(docs) })
return docs
}
function renderIndex(docs: GraphDoc[]): string {
const labels: Record<string, string> = {
'package-topology.md': 'package topology by group',
'capability-seams.md': 'capability seams and core services',
'app-composition.md': 'app composition',
'event-producer-consumer.md': 'event producer/consumer matrix',
'tool-affordance-map.md': 'tool affordance map',
'agent-lifecycle.md': 'agent turn and step lifecycle',
'tool-execution-pipeline.md': 'tool execution pipeline',
'session-surface.md': 'session surface and message projection',
'subagent-lineage.md': 'subagent and session lineage',
'hot-reload-disposal.md': 'plugin disposal and hot reload ownership',
'snapshot-replay.md': 'ACP snapshot replay',
}
const modes: Record<string, string> = {
'package-topology.md': 'generated',
'capability-seams.md': 'hybrid generated',
'app-composition.md': 'hybrid generated',
'event-producer-consumer.md': 'hybrid generated',
'tool-affordance-map.md': 'hybrid generated',
'agent-lifecycle.md': 'curated',
'tool-execution-pipeline.md': 'curated',
'session-surface.md': 'curated',
'subagent-lineage.md': 'curated',
'hot-reload-disposal.md': 'curated',
'snapshot-replay.md': 'curated',
}
return [
...generatedHeader('Documentation Graph Atlas', 'mixed: each linked page declares generated, hybrid, or curated mode'),
'The graph atlas is the relationship layer above the generated catalogs. Use it to navigate package topology, capability seams, event flow, model-facing tools, and runtime lifecycle paths. Exact signatures and type shapes still live in [cordis-catalog/](../cordis-catalog/events-and-services.md), [tool-catalog/](../tool-catalog/tools.md), and [core-data-structures/](../core-data-structures/core.md).',
'',
'The process decision behind this atlas is recorded in [the documentation graph atlas RFC](../rfc/implemented/process/2026-07-03-documentation-graph-atlas.md).',
'',
'| Graph | Mode |',
'| --- | --- |',
...docs.map((doc) => {
const file = doc.rel.split('/').at(-1) ?? doc.rel
return `| [${labels[file] ?? file}](${file}) | \`${modes[file] ?? 'generated'}\` |`
}),
'',
'Regenerate with `pnpm run gen-doc-graphs`; verify freshness with `pnpm run verify-doc-graphs`.',
'',
].join('\n')
}
async function main(): Promise<void> {
const docs = await renderDocs()
if (process.argv.includes('--check')) {
const stale: string[] = []
for (const doc of docs) {
const abs = resolve(root, doc.rel)
const committed = existsSync(abs) ? readFileSync(abs, 'utf8') : null
if (committed !== doc.content) stale.push(doc.rel)
}
if (stale.length === 0) {
console.log(`gen-doc-graphs: ${docs.length} graph doc(s) are up to date.`)
return
}
console.error(`gen-doc-graphs: stale graph doc(s): ${stale.join(', ')}. Run \`pnpm run gen-doc-graphs\` and commit the result.`)
process.exit(1)
}
mkdirSync(resolve(root, OUT_DIR), { recursive: true })
for (const doc of docs) writeFileSync(resolve(root, doc.rel), doc.content)
console.log(`gen-doc-graphs: wrote ${docs.length} graph doc(s).`)
}
if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) {
await main()
}