diff --git a/README.md b/README.md index 1ce5aa8960..2d97f103af 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/docs/architecture.md b/docs/architecture.md index 5cd56c6e91..903e58909c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 diff --git a/docs/development.md b/docs/development.md index 431d7b4dac..965f23d90f 100644 --- a/docs/development.md +++ b/docs/development.md @@ -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 diff --git a/docs/graphs/README.md b/docs/graphs/README.md new file mode 100644 index 0000000000..995481d160 --- /dev/null +++ b/docs/graphs/README.md @@ -0,0 +1,26 @@ + + +# 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`. diff --git a/docs/graphs/agent-lifecycle.md b/docs/graphs/agent-lifecycle.md new file mode 100644 index 0000000000..cc25e25694 --- /dev/null +++ b/docs/graphs/agent-lifecycle.md @@ -0,0 +1,41 @@ + + +# 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. diff --git a/docs/graphs/app-composition.md b/docs/graphs/app-composition.md new file mode 100644 index 0000000000..ce2bab32d8 --- /dev/null +++ b/docs/graphs/app-composition.md @@ -0,0 +1,90 @@ + + +# 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
@cordisjs/plugin-hmr"] + cfg_echo --> plugin_echo_hmr + plugin_echo_mock_llm["mock-llm
./src/mock-llm.ts"] + cfg_echo --> plugin_echo_mock_llm + plugin_echo_echo_tool["echo-tool
./src/echo-tool.ts"] + cfg_echo --> plugin_echo_echo_tool + plugin_echo_bash["bash
@deepseek-ai/dsh-bash-local"] + cfg_echo --> plugin_echo_bash + plugin_echo_stdio_agent["stdio-agent
@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
@cordisjs/plugin-hmr"] + cfg_coding --> plugin_coding_hmr + plugin_coding_llm_deepseek["llm-deepseek
@deepseek-ai/dsh-llm-deepseek"] + cfg_coding --> plugin_coding_llm_deepseek + plugin_coding_bash["bash
@deepseek-ai/dsh-bash-local"] + cfg_coding --> plugin_coding_bash + plugin_coding_stdio_agent["stdio-agent
@deepseek-ai/dsh-stdio-agent"] + cfg_coding --> plugin_coding_stdio_agent + plugin_coding_stdio_agent --> bundle_stdio + plugin_coding_compact_basic["compact-basic
@deepseek-ai/dsh-compact-basic"] + cfg_coding --> plugin_coding_compact_basic + plugin_coding_subagent["subagent
@deepseek-ai/dsh-subagent"] + cfg_coding --> plugin_coding_subagent + plugin_coding_subagent_spawn["subagent-spawn
@deepseek-ai/dsh-subagent-spawn"] + cfg_coding --> plugin_coding_subagent_spawn + plugin_coding_subagent_fork["subagent-fork
@deepseek-ai/dsh-subagent-fork"] + cfg_coding --> plugin_coding_subagent_fork + plugin_coding_tool_subagent["tool-subagent
@deepseek-ai/dsh-tool-subagent"] + cfg_coding --> plugin_coding_tool_subagent + plugin_coding_tool_subagent_fork["tool-subagent-fork
@deepseek-ai/dsh-tool-subagent"] + cfg_coding --> plugin_coding_tool_subagent_fork + plugin_coding_tool_todo["tool-todo
@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
@deepseek-ai/dsh-llm-deepseek"] + cfg_acp --> plugin_acp_llm_deepseek + plugin_acp_bash["bash
@deepseek-ai/dsh-bash-local"] + cfg_acp --> plugin_acp_bash + plugin_acp_acp_agent["acp-agent
@deepseek-ai/dsh-acp-agent"] + cfg_acp --> plugin_acp_acp_agent + plugin_acp_acp_agent --> bundle_acp_agent + plugin_acp_subagent["subagent
@deepseek-ai/dsh-subagent"] + cfg_acp --> plugin_acp_subagent + plugin_acp_subagent_spawn["subagent-spawn
@deepseek-ai/dsh-subagent-spawn"] + cfg_acp --> plugin_acp_subagent_spawn + plugin_acp_subagent_fork["subagent-fork
@deepseek-ai/dsh-subagent-fork"] + cfg_acp --> plugin_acp_subagent_fork + plugin_acp_tool_subagent["tool-subagent
@deepseek-ai/dsh-tool-subagent"] + cfg_acp --> plugin_acp_tool_subagent + plugin_acp_tool_subagent_fork["tool-subagent-fork
@deepseek-ai/dsh-tool-subagent"] + cfg_acp --> plugin_acp_tool_subagent_fork + plugin_acp_tool_todo["tool-todo
@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) | diff --git a/docs/graphs/capability-seams.md b/docs/graphs/capability-seams.md new file mode 100644 index 0000000000..d3b95b55f8 --- /dev/null +++ b/docs/graphs/capability-seams.md @@ -0,0 +1,110 @@ + + +# 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
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
In-memory session store"] + pkg_agent["agent"] + pkg_session_persistence["session-persistence"] + pkg_subagent_inprocess["subagent-inprocess"] + pkg_invariants["invariants"] + svc_sessionPersistence["ctx.sessionPersistence
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
System prompt assembly registry"] + pkg_tools["tools"] + svc_tools["ctx.tools
Tool registry and execution waterfall"] + pkg_tool_bash["tool-bash"] + pkg_tool_subagent["tool-subagent"] + pkg_tool_todo["tool-todo"] + svc_agents["ctx.agents
Agent registry"] + pkg_stdio_agent["stdio-agent"] + svc_agentLoop["ctx.agentLoop
Concrete loop driver"] + pkg_agent_core["agent-core"] + pkg_bash["bash"] + svc_bash["ctx.bash
Bash executor seam"] + pkg_bash_local["bash-local"] + pkg_compact["compact"] + svc_compact["ctx.compact
Compaction seam"] + pkg_subagent["subagent"] + svc_subagents["ctx.subagents
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. | diff --git a/docs/graphs/event-producer-consumer.md b/docs/graphs/event-producer-consumer.md new file mode 100644 index 0000000000..dfce3fe04a --- /dev/null +++ b/docs/graphs/event-producer-consumer.md @@ -0,0 +1,36 @@ + + +# 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`) | - | diff --git a/docs/graphs/hot-reload-disposal.md b/docs/graphs/hot-reload-disposal.md new file mode 100644 index 0000000000..ca98c25692 --- /dev/null +++ b/docs/graphs/hot-reload-disposal.md @@ -0,0 +1,24 @@ + + +# 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
events, tools, adapters, timers"] + reload["HMR / fiber.dispose()"] + disposers["Run disposers in owner fiber"] + quiescence["Owned AgentHandle.dispose()
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. diff --git a/docs/graphs/package-topology.md b/docs/graphs/package-topology.md new file mode 100644 index 0000000000..68d5a2442c --- /dev/null +++ b/docs/graphs/package-topology.md @@ -0,0 +1,189 @@ + + +# 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//` 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) | diff --git a/docs/graphs/session-surface.md b/docs/graphs/session-surface.md new file mode 100644 index 0000000000..b25f85b2d2 --- /dev/null +++ b/docs/graphs/session-surface.md @@ -0,0 +1,25 @@ + + +# 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
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. diff --git a/docs/graphs/snapshot-replay.md b/docs/graphs/snapshot-replay.md new file mode 100644 index 0000000000..d0ee1c9061 --- /dev/null +++ b/docs/graphs/snapshot-replay.md @@ -0,0 +1,24 @@ + + +# 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. diff --git a/docs/graphs/subagent-lineage.md b/docs/graphs/subagent-lineage.md new file mode 100644 index 0000000000..f687405782 --- /dev/null +++ b/docs/graphs/subagent-lineage.md @@ -0,0 +1,27 @@ + + +# 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
model-facing name"] + registry["ctx.subagents provider registry"] + spawn["spawn provider
fresh child session"] + fork["fork provider
seeded from completed-turn prefix"] + acp["ACP provider
out-of-process child"] + child["Child AgentHandle
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. diff --git a/docs/graphs/tool-affordance-map.md b/docs/graphs/tool-affordance-map.md new file mode 100644 index 0000000000..995dee7b08 --- /dev/null +++ b/docs/graphs/tool-affordance-map.md @@ -0,0 +1,35 @@ + + +# 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
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
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
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. | diff --git a/docs/graphs/tool-execution-pipeline.md b/docs/graphs/tool-execution-pipeline.md new file mode 100644 index 0000000000..7fde39af83 --- /dev/null +++ b/docs/graphs/tool-execution-pipeline.md @@ -0,0 +1,30 @@ + + +# 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()
tools/execute waterfall"] + policy["Policy / permission / hooks listener"] + body["Registered tool execute() body"] + owned["Tool-owned session events
todo/write, future fs policy facts"] + result["Session event: tool/result"] + ui["UI presentation
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. diff --git a/docs/rfc/README.md b/docs/rfc/README.md index 981710c4f5..8c5b5ebc9b 100644 --- a/docs/rfc/README.md +++ b/docs/rfc/README.md @@ -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 diff --git a/docs/rfc/implemented/process/2026-07-03-documentation-graph-atlas.md b/docs/rfc/implemented/process/2026-07-03-documentation-graph-atlas.md new file mode 100644 index 0000000000..e707786c14 --- /dev/null +++ b/docs/rfc/implemented/process/2026-07-03-documentation-graph-atlas.md @@ -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.` 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. diff --git a/package.json b/package.json index 7d82af4aad..3a35adf5e8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts new file mode 100644 index 0000000000..38a385fa62 --- /dev/null +++ b/scripts/gen-doc-graphs.ts @@ -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 +} + +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> + listeners: Set +} + +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 = { + '@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 [ + '', + '', + `# ${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() + 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 { + 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, '
') +} + +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//` 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() + const edges = new Set() + 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}
${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 = { + '@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)}
${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 { + const out = new Map() + const ensure = (event: string): EventRelation => { + const existing = out.get(event) + if (existing) return existing + const next = { dispatchers: new Map>(), listeners: new Set() } + 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() + 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() + 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, 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>, pkgsByShort: Map): 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, pkgsByShort: Map): 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>(), listeners: new Set() } + 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 { + 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() + 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, ''))}
${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()
tools/execute waterfall"]', + ' policy["Policy / permission / hooks listener"]', + ' body["Registered tool execute() body"]', + ' owned["Tool-owned session events
todo/write, future fs policy facts"]', + ' result["Session event: tool/result"]', + ' ui["UI presentation
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
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
model-facing name"]', + ' registry["ctx.subagents provider registry"]', + ' spawn["spawn provider
fresh child session"]', + ' fork["fork provider
seeded from completed-turn prefix"]', + ' acp["ACP provider
out-of-process child"]', + ' child["Child AgentHandle
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
events, tools, adapters, timers"]', + ' reload["HMR / fiber.dispose()"]', + ' disposers["Run disposers in owner fiber"]', + ' quiescence["Owned AgentHandle.dispose()
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 { + 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 = { + '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 = { + '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 { + 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() +}