2026-07-03 01:13:52 +08:00
/ * *
2026-07-13 23:27:00 +08:00
* Generate the relationship layer above the module , Cordis , and tool catalogs .
* Enumerable facts come from source ; hybrid graphs add manifests for policy the
* source cannot infer , while curated graphs explain flow and ownership .
* ` --check ` verifies the generated set .
2026-07-03 01:13:52 +08:00
* /
2026-07-14 23:14:51 +08:00
import { existsSync , mkdirSync , readFileSync , writeFileSync } from 'node:fs'
2026-07-05 02:54:01 +08:00
import { dirname , relative , resolve } from 'node:path'
2026-07-03 01:13:52 +08:00
import ts from 'typescript'
import { collectEvents , collectServices } from './gen-cordis-catalog.ts'
2026-07-14 00:24:04 +08:00
import {
collectPackageGraph ,
escapeMermaidLabel as escLabel ,
graphNodeId as nodeId ,
type PackageGraphNode ,
} from './package-graph.ts'
2026-07-14 23:14:51 +08:00
import { TypeScriptProject } from './ts-project.ts'
2026-07-03 01:13:52 +08:00
const root = resolve ( import . meta . dirname , '..' )
2026-07-14 00:24:04 +08:00
type Pkg = PackageGraphNode
2026-07-03 01:13:52 +08:00
interface GraphDoc {
rel : string
content : string
}
interface ServiceRole {
key : string
pkg : string
title : string
mode : 'core' | 'seam' | 'bundle'
implementations? : string [ ]
consumers? : string [ ]
2026-07-04 12:50:06 +08:00
companions? : string [ ]
2026-07-03 01:13:52 +08:00
note : string
}
interface ExamplePlugin {
id : string
name : string
}
interface EventRelation {
dispatchers : Map < string , Set < string > >
listeners : Set < string >
}
2026-07-14 23:14:51 +08:00
interface PackageSource {
rel : string
pkg : string
sourceFile : ts.SourceFile
}
type EventReceiverKind = 'context' | 'agent-dispatch' | 'events-service'
2026-07-04 12:50:06 +08:00
const GROUP_ORDER = [
'util' ,
'llm' ,
'core' ,
2026-07-19 18:47:34 +08:00
'goal' ,
refactor(process): split the process manager out of the bash executor
New process/ capability family: @deepseek-ai/dsh-process owns ctx.processes —
abstract ProcessManager.spawn(spec) over a fully-explicit ProcessSpawnSpec —
plus the shared DSH_* managed-environment and CollectedOutput vocabulary;
@deepseek-ai/dsh-process-local carries the former bash-local run.ts plumbing
(detached groups, tail-keep spill-backed output, credential scrub, kill
escalation, kill-and-join disposal) with no config of its own.
dsh-bash-local becomes a consumer: it keeps command defaulting, the fused
deadline timedOut/aborted classification, the model-friendly terminal env
(now merged through the ordinary env channel), and the [stderr]-marked
background read merge, and spawns through ctx.processes. Background-process
lifetime moves to the manager, so an executor reload no longer kills live
background work; a background spawn failure is injected once into the read
path instead of being buffered as fake stderr. dsh-bash re-exports the moved
vocabulary so bash consumers keep one import root; dsh-bash-sandbox only
redeclares the inherited inject.
Every composition loading a bash executor now loads dsh-process-local (CLI,
examples, python bundled runtime, create-sdk bash feature, inline test
configs).
2026-07-26 06:59:01 +08:00
'process' ,
2026-07-04 12:50:06 +08:00
'bash' ,
2026-07-21 16:01:00 +08:00
'pty' ,
2026-07-09 15:42:37 +08:00
'sandbox' ,
2026-07-04 12:50:06 +08:00
'fs' ,
2026-07-10 14:19:06 +08:00
'skill' ,
2026-07-04 12:50:06 +08:00
'compact' ,
'subagent' ,
feat(tasks): background task runtime, generic task_* control tools, bash/subagent producers
One shared ctx.tasks registry (branded <kind>-N ids, owner-fenced
read/kill/wait/list, attachSurface misconfiguration fence, reported-flag
notice dedup, atomic register) + dsh-tool-tasks (task_output/task_list/
task_kill, completion-notice injection, background prompt habit).
Producers opt in via their own enableRunInBackground config: bash
(stream kind; seam slimmed to resolve/run/start returning a BashProcess
handle, bash_output/bash_kill deleted) and subagent (final-output kind;
done settles after run.dispose()). Owner disposal drains tasks through
the new awaited ctx.agents.onCleanup seam in the loop's disposal chain.
Both RFCs moved to implemented/; docs, catalogs, snapshots re-pinned.
2026-07-09 21:22:54 +08:00
'tasks' ,
2026-07-10 10:13:13 +08:00
'workflow' ,
2026-07-04 12:50:06 +08:00
'web' ,
2026-07-08 19:20:50 +08:00
'spill' ,
2026-07-04 12:50:06 +08:00
'todo' ,
2026-07-22 16:57:23 +08:00
'plan' ,
2026-07-08 11:50:12 +08:00
'cordis' ,
2026-07-04 12:50:06 +08:00
'hooks' ,
'session-persistence' ,
2026-07-10 16:51:19 +08:00
'session-query' ,
2026-07-21 01:54:00 +08:00
'session-title' ,
2026-07-23 03:06:57 +08:00
'telemetry' ,
chore(storage,workspace): gates — coverage, catalogs, bilingual note
- Per-file 100% coverage across the five new packages (invariant
companion suites, failure-injection negatives, lifecycle and
malformed-medium branches).
- Canonical README Model Experience / Known Limitations sections; new
storage/ and workspace/ group READMEs; packages/README.md rows (budget
ceiling raised 760 → 790 for the two new groups).
- Cordis catalog/type-link registrations, service-role classification,
and regenerated catalogs/graphs for the new services and events.
- Agent Note: English body + i18n pairing record; design-sketch fences
opted out of doc-typecheck as ignore-check.
- Two exactOptionalPropertyTypes/discriminant fixes in new tests.
doc-sync (24 gates), typecheck, hygiene, and the five-package suite
(92 tests) all pass.
2026-07-24 22:49:57 +08:00
'storage' ,
'workspace' ,
2026-07-04 12:50:06 +08:00
'support' ,
2026-07-24 01:40:25 +08:00
'acp' ,
2026-07-04 12:50:06 +08:00
'ui' ,
]
2026-07-03 01:13:52 +08:00
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.' ,
} ,
2026-07-15 14:47:29 +08:00
{
key : 'tokenMeter' ,
pkg : 'token-meter' ,
title : 'Replay token measurement' ,
mode : 'core' ,
consumers : [ 'compact-basic' ] ,
2026-07-16 12:58:07 +08:00
note : 'Owns isolated per-session replay folds; pressure consumers share immutable revisioned measurements.' ,
2026-07-15 14:47:29 +08:00
} ,
2026-07-16 18:02:15 +08:00
{
key : 'toolResultPrune' ,
2026-07-16 18:51:26 +08:00
pkg : 'compact-tool-result-prune' ,
2026-07-16 18:02:15 +08:00
title : 'Model-free tool-result pruning' ,
mode : 'core' ,
consumers : [ 'compact-basic' ] ,
note : 'Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction.' ,
} ,
2026-07-03 01:13:52 +08:00
{
key : 'sessions' ,
pkg : 'session' ,
title : 'In-memory session store' ,
mode : 'core' ,
2026-07-23 13:56:56 +08:00
consumers : [ 'agent-loop' , 'agent' , 'cli-demo' , 'session-persistence' , 'session-query' , 'session-query-sqlite' , 'subagent-inprocess' , 'invariants' ] ,
2026-07-03 01:13:52 +08:00
note : 'Owns append-only Session instances and emits the durable session event feed.' ,
} ,
2026-07-19 19:19:57 +08:00
{
key : 'invariants' ,
pkg : 'invariants' ,
title : 'Package-owned invariant registry' ,
mode : 'core' ,
consumers : [ 'session' , 'agent' , 'scope' , 'agent-loop' ] ,
note : 'Companion subpaths register owner-local checks; the service owns selection, uniqueness, child fibers, and package-attributed failures.' ,
} ,
2026-07-03 01:13:52 +08:00
{
key : 'sessionPersistence' ,
pkg : 'session-persistence' ,
title : 'Durable session persistence seam' ,
mode : 'seam' ,
implementations : [ 'session-persistence-jsonl' , 'session-persistence-sqlite' ] ,
2026-07-24 01:40:25 +08:00
consumers : [ 'agent-loop' , 'tool-bash' , 'hooks-claude' , 'hooks-codex' , 'session-query' , 'session-query-sqlite' ] ,
2026-07-03 01:13:52 +08:00
note : 'Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time.' ,
} ,
2026-07-23 03:06:57 +08:00
{
key : 'telemetry' ,
pkg : 'session-telemetry' ,
title : 'Session telemetry seam' ,
mode : 'seam' ,
implementations : [ 'session-telemetry-otel' ] ,
consumers : [ ] ,
note : 'The seam captures, redacts, and hands session records to one backend; nothing else consumes the service — its output leaves the process.' ,
} ,
chore(storage,workspace): gates — coverage, catalogs, bilingual note
- Per-file 100% coverage across the five new packages (invariant
companion suites, failure-injection negatives, lifecycle and
malformed-medium branches).
- Canonical README Model Experience / Known Limitations sections; new
storage/ and workspace/ group READMEs; packages/README.md rows (budget
ceiling raised 760 → 790 for the two new groups).
- Cordis catalog/type-link registrations, service-role classification,
and regenerated catalogs/graphs for the new services and events.
- Agent Note: English body + i18n pairing record; design-sketch fences
opted out of doc-typecheck as ignore-check.
- Two exactOptionalPropertyTypes/discriminant fixes in new tests.
doc-sync (24 gates), typecheck, hygiene, and the five-package suite
(92 tests) all pass.
2026-07-24 22:49:57 +08:00
{
key : 'storage' ,
pkg : 'storage' ,
title : 'Non-session storage hub' ,
mode : 'seam' ,
implementations : [ 'storage-json' , 'storage-sqlite' ] ,
2026-07-25 16:04:48 +08:00
consumers : [ 'storage-domain' ] ,
chore(storage,workspace): gates — coverage, catalogs, bilingual note
- Per-file 100% coverage across the five new packages (invariant
companion suites, failure-injection negatives, lifecycle and
malformed-medium branches).
- Canonical README Model Experience / Known Limitations sections; new
storage/ and workspace/ group READMEs; packages/README.md rows (budget
ceiling raised 760 → 790 for the two new groups).
- Cordis catalog/type-link registrations, service-role classification,
and regenerated catalogs/graphs for the new services and events.
- Agent Note: English body + i18n pairing record; design-sketch fences
opted out of doc-typecheck as ignore-check.
- Two exactOptionalPropertyTypes/discriminant fixes in new tests.
doc-sync (24 gates), typecheck, hygiene, and the five-package suite
(92 tests) all pass.
2026-07-24 22:49:57 +08:00
note : 'Backends register side by side under names; data forms (domain first) mount on the hub and translate typed operations into opaque KV-unit primitives.' ,
} ,
2026-07-25 16:04:48 +08:00
{
key : 'storageDomain' ,
pkg : 'storage-domain' ,
title : 'Domain data facility' ,
mode : 'core' ,
consumers : [ 'workspace' ] ,
note : 'Waits for every configured backend, then publishes the domain form as one lifecycle-bound service for typed durable state.' ,
} ,
chore(storage,workspace): gates — coverage, catalogs, bilingual note
- Per-file 100% coverage across the five new packages (invariant
companion suites, failure-injection negatives, lifecycle and
malformed-medium branches).
- Canonical README Model Experience / Known Limitations sections; new
storage/ and workspace/ group READMEs; packages/README.md rows (budget
ceiling raised 760 → 790 for the two new groups).
- Cordis catalog/type-link registrations, service-role classification,
and regenerated catalogs/graphs for the new services and events.
- Agent Note: English body + i18n pairing record; design-sketch fences
opted out of doc-typecheck as ignore-check.
- Two exactOptionalPropertyTypes/discriminant fixes in new tests.
doc-sync (24 gates), typecheck, hygiene, and the five-package suite
(92 tests) all pass.
2026-07-24 22:49:57 +08:00
{
key : 'workspace' ,
pkg : 'workspace' ,
title : 'Workspace entity registry' ,
mode : 'core' ,
2026-07-25 16:04:48 +08:00
consumers : [ 'apiproxy' ] ,
note : 'Owns WorkspaceId-branded records over the domain facility; stable sessionIds accounts drive Host RPC and GUI projections.' ,
chore(storage,workspace): gates — coverage, catalogs, bilingual note
- Per-file 100% coverage across the five new packages (invariant
companion suites, failure-injection negatives, lifecycle and
malformed-medium branches).
- Canonical README Model Experience / Known Limitations sections; new
storage/ and workspace/ group READMEs; packages/README.md rows (budget
ceiling raised 760 → 790 for the two new groups).
- Cordis catalog/type-link registrations, service-role classification,
and regenerated catalogs/graphs for the new services and events.
- Agent Note: English body + i18n pairing record; design-sketch fences
opted out of doc-typecheck as ignore-check.
- Two exactOptionalPropertyTypes/discriminant fixes in new tests.
doc-sync (24 gates), typecheck, hygiene, and the five-package suite
(92 tests) all pass.
2026-07-24 22:49:57 +08:00
} ,
2026-07-10 16:51:19 +08:00
{
key : 'sessionQuery' ,
pkg : 'session-query' ,
2026-07-23 20:16:14 +08:00
title : 'Session reads, traces, filters, and search' ,
2026-07-15 10:51:38 +08:00
mode : 'seam' ,
implementations : [ 'session-query-sqlite' ] ,
2026-07-24 15:09:55 +08:00
consumers : [ 'session-reference' , 'tool-session-query' ] ,
note : 'The interface supplies exact reads, filters, and traces; its concrete backend adds full-text reconciliation, ranking, snippets, and cursor generations, while the model consumer owns workspace authority and cursor-free rendering.' ,
2026-07-10 16:51:19 +08:00
} ,
2026-07-21 16:46:48 +08:00
{
key : 'sessionReferences' ,
pkg : 'session-reference' ,
title : 'Cross-session snapshot preparation' ,
mode : 'core' ,
2026-07-24 01:40:25 +08:00
consumers : [ 'tui' ] ,
2026-07-21 16:46:48 +08:00
note : 'Projects bounded current-surface conversation snapshots into durable untrusted message context; host adapters own mention syntax.' ,
} ,
2026-07-21 01:54:00 +08:00
{
key : 'sessionTitle' ,
pkg : 'session-title' ,
title : 'Log-backed session titles' ,
mode : 'seam' ,
implementations : [ 'session-title-first-message-llm' , 'session-title-all-messages-llm' ] ,
note : 'Owns the deterministic fallback, latest-title fold, and sole optional asynchronous provider registration.' ,
} ,
2026-07-03 01:13:52 +08:00
{
key : 'systemPrompt' ,
pkg : 'system-prompt' ,
title : 'System prompt assembly registry' ,
mode : 'core' ,
2026-07-21 16:01:00 +08:00
consumers : [ 'agent-loop' , 'tools' , 'tool-fs' , 'tool-pty' , 'tool-web' ] ,
2026-07-03 01:13:52 +08:00
note : 'Collects prompt sections and model-facing tool schemas for each step.' ,
} ,
{
key : 'tools' ,
pkg : 'tools' ,
2026-07-11 22:55:26 +08:00
title : 'Tool registry and guarded execution pipeline' ,
2026-07-03 01:13:52 +08:00
mode : 'core' ,
2026-07-24 01:40:25 +08:00
consumers : [ 'agent-loop' , 'tool-ask-user' , 'tool-bash' , 'tool-cordis' , 'tool-fs' , 'tool-pty' , 'tool-skill' , 'tool-subagent' , 'tool-todo' , 'tool-web' ] ,
2026-07-11 22:55:26 +08:00
note : 'Registers capabilities, owns Code Mode transport, and routes calls through pre-policy, monotonic guards, around dispatch, post-policy, and final-result observation.' ,
2026-07-03 01:13:52 +08:00
} ,
2026-07-05 17:05:33 +08:00
{
key : 'userInteraction' ,
pkg : 'user-interaction' ,
title : 'Human question/answer seam' ,
mode : 'seam' ,
2026-07-24 01:40:25 +08:00
implementations : [ 'tui' ] ,
consumers : [ 'tool-ask-user' , 'tui' ] ,
2026-07-05 17:05:33 +08:00
note : 'UI front doors provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise.' ,
} ,
2026-07-10 01:38:39 +08:00
{
2026-07-22 16:57:23 +08:00
key : 'planMode' ,
pkg : 'plan-mode' ,
title : 'Plan collaboration state' ,
2026-07-10 01:38:39 +08:00
mode : 'core' ,
2026-07-22 16:57:23 +08:00
note : 'Folds logged plan/mode state, flushes user selections at turn boundaries, renders deployment-owned guidance, registers /plan, and keeps the plan-exit schema stable across transitions.' ,
2026-07-10 01:38:39 +08:00
} ,
2026-07-19 22:11:59 +08:00
{
key : 'commands' ,
pkg : 'commands' ,
title : 'Human command registry' ,
mode : 'core' ,
2026-07-24 01:40:25 +08:00
consumers : [ 'tui' ] ,
note : 'Plugins register direct human commands; TUI consumes the effective per-agent catalog without sending invocations to the model.' ,
2026-07-19 22:11:59 +08:00
} ,
2026-07-22 21:30:08 -07:00
{
key : 'tui' ,
pkg : 'tui' ,
title : 'Mounted-terminal interaction service' ,
mode : 'bundle' ,
note : 'One TUI front door provides a FIFO overlay host; injected plugins receive caller-fiber ownership without access to pi-tui or terminal lifecycle state.' ,
} ,
2026-07-05 16:50:29 +08:00
{
key : 'skills' ,
pkg : 'skill' ,
2026-07-08 15:50:38 +08:00
title : 'Skill provider registry' ,
2026-07-10 14:19:06 +08:00
mode : 'seam' ,
implementations : [ 'skill-local' ] ,
consumers : [ 'tool-skill' ] ,
note : 'Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies.' ,
2026-07-05 16:50:29 +08:00
} ,
2026-07-03 01:13:52 +08:00
{
key : 'agents' ,
pkg : 'agent' ,
2026-07-19 13:30:45 +08:00
title : 'Agent service' ,
2026-07-03 01:13:52 +08:00
mode : 'core' ,
2026-07-20 20:58:46 +08:00
consumers : [ 'agent-loop' , 'acp' , 'cli-demo' , 'subagent-inprocess' , 'tui-demo' ] ,
2026-07-19 13:30:45 +08:00
note : 'Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation.' ,
2026-07-03 01:13:52 +08:00
} ,
{
key : 'agentLoop' ,
pkg : 'agent-loop' ,
title : 'Concrete loop driver' ,
mode : 'bundle' ,
2026-07-15 15:57:57 +08:00
consumers : [ 'agent-spine-demo' ] ,
2026-07-03 01:13:52 +08:00
note : 'The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package.' ,
} ,
2026-07-19 18:47:34 +08:00
{
key : 'goals' ,
pkg : 'goal' ,
title : 'Same-session goal domain' ,
mode : 'core' ,
note : 'Folds revisioned objective state from the session log and keeps live continuation activation process-local.' ,
} ,
refactor(process): split the process manager out of the bash executor
New process/ capability family: @deepseek-ai/dsh-process owns ctx.processes —
abstract ProcessManager.spawn(spec) over a fully-explicit ProcessSpawnSpec —
plus the shared DSH_* managed-environment and CollectedOutput vocabulary;
@deepseek-ai/dsh-process-local carries the former bash-local run.ts plumbing
(detached groups, tail-keep spill-backed output, credential scrub, kill
escalation, kill-and-join disposal) with no config of its own.
dsh-bash-local becomes a consumer: it keeps command defaulting, the fused
deadline timedOut/aborted classification, the model-friendly terminal env
(now merged through the ordinary env channel), and the [stderr]-marked
background read merge, and spawns through ctx.processes. Background-process
lifetime moves to the manager, so an executor reload no longer kills live
background work; a background spawn failure is injected once into the read
path instead of being buffered as fake stderr. dsh-bash re-exports the moved
vocabulary so bash consumers keep one import root; dsh-bash-sandbox only
redeclares the inherited inject.
Every composition loading a bash executor now loads dsh-process-local (CLI,
examples, python bundled runtime, create-sdk bash feature, inline test
configs).
2026-07-26 06:59:01 +08:00
{
refactor(subprocess): rename the process seam to subprocess and address review
Review feedback (tianyicui): 'process' is a poor service name. The family is
now packages/subprocess/ — @deepseek-ai/dsh-subprocess (ctx.subprocess,
abstract SubprocessService, Subprocess* vocabulary) and
@deepseek-ai/dsh-subprocess-local (LocalSubprocessService) — renamed
throughout code, compositions, docs (en+zh, pairs re-recorded), catalogs,
and gates. 'subprocess' is the precise term for managed OS children (the
Python-stdlib sense), avoids colliding with Node's global process object,
and reads as one system beside dsh-subagent-subprocess.
ds-review-bot findings addressed:
- kill() on a settled handle is now a no-op (no signal to a possibly-reused
pgid, no referenced grace timer delaying exit); pinned by a spy test.
- The moved DshEnvironmentKey/DshEnvironment/CollectedOutput types get
drift-checked type-equiv blocks on the new subprocess.md page, restoring
their manifest registration.
- subprocess.md is registered in the core.md sub-page index (en+zh).
2026-07-26 12:43:14 +08:00
key : 'subprocess' ,
2026-07-26 14:10:46 +08:00
pkg : 'subprocess' ,
title : 'Subprocess seam' ,
refactor(process): split the process manager out of the bash executor
New process/ capability family: @deepseek-ai/dsh-process owns ctx.processes —
abstract ProcessManager.spawn(spec) over a fully-explicit ProcessSpawnSpec —
plus the shared DSH_* managed-environment and CollectedOutput vocabulary;
@deepseek-ai/dsh-process-local carries the former bash-local run.ts plumbing
(detached groups, tail-keep spill-backed output, credential scrub, kill
escalation, kill-and-join disposal) with no config of its own.
dsh-bash-local becomes a consumer: it keeps command defaulting, the fused
deadline timedOut/aborted classification, the model-friendly terminal env
(now merged through the ordinary env channel), and the [stderr]-marked
background read merge, and spawns through ctx.processes. Background-process
lifetime moves to the manager, so an executor reload no longer kills live
background work; a background spawn failure is injected once into the read
path instead of being buffered as fake stderr. dsh-bash re-exports the moved
vocabulary so bash consumers keep one import root; dsh-bash-sandbox only
redeclares the inherited inject.
Every composition loading a bash executor now loads dsh-process-local (CLI,
examples, python bundled runtime, create-sdk bash feature, inline test
configs).
2026-07-26 06:59:01 +08:00
mode : 'seam' ,
2026-07-26 14:10:46 +08:00
implementations : [ 'subprocess-local' ] ,
feat(subprocess): migrate lsp-local, subagent-acp, and the env scrubs onto the seam
Review direction (tianyicui, PR #660): in a stacked PR, change all other
process-running places to use the new service.
- lsp-local: LspConnection spawns through ctx.subprocess (piped protocol
streams + a no-spill collected stderr tail); its private process-tree
helpers (POSIX group signalling, Windows taskkill, liveness polling) are
deleted in favor of the seam's handle verbs, and its buildChildEnv now
rides scrubbedParentEnv (LSP children also stop inheriting stale DSH_*).
The plugin injects 'subprocess'; compositions/tests mount
dsh-subprocess-local.
- subagent-acp: the ACP child spawns through the seam (piped ndjson streams,
inherited stderr); spawn failure surfaces through done-rejection into the
same startup race; disposal is handle.dispose with the plugin's configured
graces. dsh-subagent-subprocess is DELETED — its dispose ladder and scrub
are the seam's, and the isolated-config-dir helper had no consumer.
- mcp-client, pty-local, sdk-helper: adopt scrubbedParentEnv as the one
scrub definition (their spawns stay put by ownership: the MCP SDK and
node-pty own those calls; the SDK wizard runs outside any composition).
- Coverage: per-file 100% over every touched src file, with each v8 ignore
carrying a platform or contract reason; new suites cover stdio
dispositions, the dispose ladder tiers, injected-win32 tree semantics,
waitForExit, settled-kill/terminate no-ops, and spawn-failure disposal.
- Docs: consumer-migration Agent Note (en; zh follows in this PR), seam note
updated in place, subprocess.md rewritten for the reshaped vocabulary
(type-equiv re-registered), READMEs and SERVICE_ROLES updated, taskkill
added to knip ignoreBinaries.
2026-07-26 15:27:59 +08:00
consumers : [ 'bash-local' , 'bash-sandbox' , 'lsp-local' , 'subagent-acp' ] ,
note : 'The bash executors, the LSP host, and the ACP subagent backend spawn their children through ctx.subprocess; the service owns tree lifetime, stdio dispositions (pipes, inherit, bounded spill-backed collection), and kill escalation.' ,
refactor(process): split the process manager out of the bash executor
New process/ capability family: @deepseek-ai/dsh-process owns ctx.processes —
abstract ProcessManager.spawn(spec) over a fully-explicit ProcessSpawnSpec —
plus the shared DSH_* managed-environment and CollectedOutput vocabulary;
@deepseek-ai/dsh-process-local carries the former bash-local run.ts plumbing
(detached groups, tail-keep spill-backed output, credential scrub, kill
escalation, kill-and-join disposal) with no config of its own.
dsh-bash-local becomes a consumer: it keeps command defaulting, the fused
deadline timedOut/aborted classification, the model-friendly terminal env
(now merged through the ordinary env channel), and the [stderr]-marked
background read merge, and spawns through ctx.processes. Background-process
lifetime moves to the manager, so an executor reload no longer kills live
background work; a background spawn failure is injected once into the read
path instead of being buffered as fake stderr. dsh-bash re-exports the moved
vocabulary so bash consumers keep one import root; dsh-bash-sandbox only
redeclares the inherited inject.
Every composition loading a bash executor now loads dsh-process-local (CLI,
examples, python bundled runtime, create-sdk bash feature, inline test
configs).
2026-07-26 06:59:01 +08:00
} ,
2026-07-03 01:13:52 +08:00
{
key : 'bash' ,
pkg : 'bash' ,
title : 'Bash executor seam' ,
mode : 'seam' ,
2026-07-09 16:05:44 +08:00
implementations : [ 'bash-local' , 'bash-sandbox' ] ,
2026-07-04 12:50:06 +08:00
consumers : [ 'tool-bash' , 'hooks-claude' , 'hooks-codex' ] ,
2026-07-09 16:05:44 +08:00
note : 'The model-facing bash tools and hook bridges consume this seam; sandboxed or remote executors replace bash-local without touching them.' ,
2026-07-04 12:50:06 +08:00
} ,
2026-07-12 15:41:42 +08:00
{
key : 'bashEnv' ,
pkg : 'tool-bash' ,
title : 'Managed bash environment registry' ,
mode : 'core' ,
note : 'Plugins declare effect-scoped DSH_* facts; tool-bash collects one trusted snapshot per execution and the executor rebuilds the namespace.' ,
} ,
2026-07-21 16:01:00 +08:00
{
key : 'pty' ,
pkg : 'pty' ,
title : 'Persistent PTY session registry' ,
mode : 'seam' ,
implementations : [ 'pty-local' ] ,
consumers : [ 'tool-pty' ] ,
note : 'The registry owns exact-Agent session identity and cleanup; backends own terminal mechanics, while tool-pty exposes the owner-scoped model surface.' ,
} ,
2026-07-09 15:42:37 +08:00
{
key : 'sandbox' ,
pkg : 'sandbox' ,
title : 'Process-sandbox seam' ,
mode : 'seam' ,
implementations : [ 'sandbox-local' ] ,
2026-07-21 16:01:00 +08:00
consumers : [ 'bash-sandbox' , 'pty-local' ] ,
2026-07-09 15:42:37 +08:00
note : 'Consumers hand over the exact argv they are about to spawn; same-world backends wrap it under a per-call policy and report enforcement.' ,
} ,
feat(sandbox): cross-family file sandbox — one policy home, sandboxed fs provider, fs escalation parity
Extend SandboxMode enforcement from bash to the filesystem tools, the sandbox
RFC's deferred cross-family phase.
- dsh-sandbox-policy (new, ctx.sandboxPolicy): the single home for the
deployment default mode + workspaceRoot and the per-session override event,
renamed bash/sandbox-mode -> sandbox/mode and moved here with its fold/setter.
Decouples the bash seam from dsh-session.
- dsh-fs-sandbox (new): SandboxedFileSystem extends LocalFileSystem and fences
write/edit by the per-call mode (read-only denies, workspace-write contains to
the workspace + temp roots via the shared writableRoots, danger passes
through); reads pass through. Structured FS_SANDBOX_DENIED; in-lock parent
re-canonicalization. A policy fence in trusted code, not a kernel boundary.
- dsh-sandbox: the shared escalation kit (writableRoots, the strictly-wider
ladder, denial/hint markers, approveEscalation) both tool families use;
approveEscalation takes a structural approver so dsh-sandbox gains no
approval/agent dependency, and both tools stay duplication-free.
- tool-fs: write/edit advertise sandbox_permissions/justification under a
confining ctx.fs, map FS_SANDBOX_DENIED to the shared [sandbox: ...] marker,
and resolve the same one-approved-wider retry.
- examples/acp-agent: composes sandbox-policy + fs-sandbox, drops the gating
that disabled the fs stack under confined modes.
RFC docs/rfc/implemented/feature/2026-07-14-cross-family-fs-sandbox.md; the old
sandbox RFC's In-process/deferred/FAQ sections updated to shipped fact.
2026-07-14 20:05:57 +08:00
{
key : 'sandboxPolicy' ,
2026-07-20 13:59:18 +08:00
pkg : 'sandbox-policy' ,
feat(sandbox): cross-family file sandbox — one policy home, sandboxed fs provider, fs escalation parity
Extend SandboxMode enforcement from bash to the filesystem tools, the sandbox
RFC's deferred cross-family phase.
- dsh-sandbox-policy (new, ctx.sandboxPolicy): the single home for the
deployment default mode + workspaceRoot and the per-session override event,
renamed bash/sandbox-mode -> sandbox/mode and moved here with its fold/setter.
Decouples the bash seam from dsh-session.
- dsh-fs-sandbox (new): SandboxedFileSystem extends LocalFileSystem and fences
write/edit by the per-call mode (read-only denies, workspace-write contains to
the workspace + temp roots via the shared writableRoots, danger passes
through); reads pass through. Structured FS_SANDBOX_DENIED; in-lock parent
re-canonicalization. A policy fence in trusted code, not a kernel boundary.
- dsh-sandbox: the shared escalation kit (writableRoots, the strictly-wider
ladder, denial/hint markers, approveEscalation) both tool families use;
approveEscalation takes a structural approver so dsh-sandbox gains no
approval/agent dependency, and both tools stay duplication-free.
- tool-fs: write/edit advertise sandbox_permissions/justification under a
confining ctx.fs, map FS_SANDBOX_DENIED to the shared [sandbox: ...] marker,
and resolve the same one-approved-wider retry.
- examples/acp-agent: composes sandbox-policy + fs-sandbox, drops the gating
that disabled the fs stack under confined modes.
RFC docs/rfc/implemented/feature/2026-07-14-cross-family-fs-sandbox.md; the old
sandbox RFC's In-process/deferred/FAQ sections updated to shipped fact.
2026-07-14 20:05:57 +08:00
title : 'Sandbox policy home' ,
mode : 'core' ,
implementations : [ ] ,
2026-07-21 16:01:00 +08:00
consumers : [ 'bash-sandbox' , 'fs-sandbox' , 'pty-local' ] ,
2026-07-20 13:59:18 +08:00
note : 'The one home for the deployment default mode + workspace root; only the sandboxed executor and provider read the service (the tool layers use the pure `sandbox/mode` fold it also exports). Both enforcing families read it so bash and fs cannot confine to different roots.' ,
feat(sandbox): cross-family file sandbox — one policy home, sandboxed fs provider, fs escalation parity
Extend SandboxMode enforcement from bash to the filesystem tools, the sandbox
RFC's deferred cross-family phase.
- dsh-sandbox-policy (new, ctx.sandboxPolicy): the single home for the
deployment default mode + workspaceRoot and the per-session override event,
renamed bash/sandbox-mode -> sandbox/mode and moved here with its fold/setter.
Decouples the bash seam from dsh-session.
- dsh-fs-sandbox (new): SandboxedFileSystem extends LocalFileSystem and fences
write/edit by the per-call mode (read-only denies, workspace-write contains to
the workspace + temp roots via the shared writableRoots, danger passes
through); reads pass through. Structured FS_SANDBOX_DENIED; in-lock parent
re-canonicalization. A policy fence in trusted code, not a kernel boundary.
- dsh-sandbox: the shared escalation kit (writableRoots, the strictly-wider
ladder, denial/hint markers, approveEscalation) both tool families use;
approveEscalation takes a structural approver so dsh-sandbox gains no
approval/agent dependency, and both tools stay duplication-free.
- tool-fs: write/edit advertise sandbox_permissions/justification under a
confining ctx.fs, map FS_SANDBOX_DENIED to the shared [sandbox: ...] marker,
and resolve the same one-approved-wider retry.
- examples/acp-agent: composes sandbox-policy + fs-sandbox, drops the gating
that disabled the fs stack under confined modes.
RFC docs/rfc/implemented/feature/2026-07-14-cross-family-fs-sandbox.md; the old
sandbox RFC's In-process/deferred/FAQ sections updated to shipped fact.
2026-07-14 20:05:57 +08:00
} ,
2026-07-09 15:25:18 +08:00
{
key : 'approval' ,
pkg : 'approval' ,
title : 'Approval seam' ,
mode : 'seam' ,
2026-07-09 15:36:08 +08:00
implementations : [ 'acp' ] ,
2026-07-09 16:37:10 +08:00
consumers : [ 'tools' , 'tool-bash' ] ,
2026-07-09 15:25:18 +08:00
note : 'One-shot permission decisions dispatched over the `approval/request` waterfall; answerers are listeners (the ACP bridge for its own agents), absence fails closed to `unavailable`.' ,
2026-07-04 12:50:06 +08:00
} ,
feat(permission): user-facing permission presets — one Permissions select over the two knobs
A preset names a bundle of the two mechanism knobs — request =
workspace-write + ask, yolo = danger-full-access + never — so the editor
shows ONE 'Permissions' select where the sandbox-mode and approval-policy
tiers stay orthogonal capabilities (the Codex /approvals shape: presets over
two dials). ctx.permission (dsh-permission) owns the config-defined table,
validates the default preset's bundle against the composed knob defaults at
load (fails loud), and writes a switch THROUGH: one log-only
permission/preset event (the audit fact reverse-mapping cannot recover —
the planned 'agent' preset shares request's knob values and differs only in
composed policy) plus each knob event via its own setter, deduped — a
net-zero switch appends nothing. Every knob consumer keeps reading its own
fold, untouched.
The current preset DERIVES from the effective knob values — the fold breaks
bundle ties, a knob state outside the table is the reserved 'custom' value
(a state, not an error: shown while it holds, switchable FROM, never a
target), and defaultPreset disappears (zero-event state reverse-maps from
the composition defaults).
The ACP bridge drops the two per-knob selects for the one preset select
(advertised only when ctx.permission is composed); pending/anchor/no-op
semantics carry over unchanged, with the no-op echo acknowledged before
vocabulary validation so a client re-pushing a derived 'custom' current
never errors. The sandbox variant example composes the
service with a workspace-write default; the permission-switching,
escalation-approved and escalation-rejected scenarios are re-recorded under
it (escalations now target an outside-workspace /tmp path under
danger-full-access, self-cleaning) and config-options is re-authored on the
single-select wire.
2026-07-12 21:03:41 +08:00
{
key : 'permission' ,
pkg : 'permission' ,
title : 'Permission presets' ,
mode : 'core' ,
implementations : [ ] ,
2026-07-14 01:09:44 +08:00
note : 'User-facing preset table (`workspace-write`/`danger-full-access`) bundling the sandbox-mode and approval-policy knobs; a switch writes one `permission/preset` event through to both knob events.' ,
feat(permission): user-facing permission presets — one Permissions select over the two knobs
A preset names a bundle of the two mechanism knobs — request =
workspace-write + ask, yolo = danger-full-access + never — so the editor
shows ONE 'Permissions' select where the sandbox-mode and approval-policy
tiers stay orthogonal capabilities (the Codex /approvals shape: presets over
two dials). ctx.permission (dsh-permission) owns the config-defined table,
validates the default preset's bundle against the composed knob defaults at
load (fails loud), and writes a switch THROUGH: one log-only
permission/preset event (the audit fact reverse-mapping cannot recover —
the planned 'agent' preset shares request's knob values and differs only in
composed policy) plus each knob event via its own setter, deduped — a
net-zero switch appends nothing. Every knob consumer keeps reading its own
fold, untouched.
The current preset DERIVES from the effective knob values — the fold breaks
bundle ties, a knob state outside the table is the reserved 'custom' value
(a state, not an error: shown while it holds, switchable FROM, never a
target), and defaultPreset disappears (zero-event state reverse-maps from
the composition defaults).
The ACP bridge drops the two per-knob selects for the one preset select
(advertised only when ctx.permission is composed); pending/anchor/no-op
semantics carry over unchanged, with the no-op echo acknowledged before
vocabulary validation so a client re-pushing a derived 'custom' current
never errors. The sandbox variant example composes the
service with a workspace-write default; the permission-switching,
escalation-approved and escalation-rejected scenarios are re-recorded under
it (escalations now target an outside-workspace /tmp path under
danger-full-access, self-cleaning) and config-options is re-authored on the
single-select wire.
2026-07-12 21:03:41 +08:00
} ,
2026-07-08 02:17:24 +08:00
{
key : 'codeRuntime' ,
pkg : 'code-runtime' ,
title : 'Code-execution seam' ,
mode : 'seam' ,
feat: add the worker-thread code runtime (dsh-code-runtime-worker)
The shipped backend of the code-execution seam, per the Code Mode RFC's
worker-thread section: one fresh Node worker per run, executing the
model's TypeScript after a host-side type-strip (wrapped in an
async-function shell so top-level return/await parse, sliced back out
position-preserved), bindings bridged over the message port under
hostile-peer rules (own-property name lookup, at-most-once replies,
post-settlement drops, null-prototype namespaces), logs streamed eagerly
with an in-band truncation marker, and two independent budgets — measured
event-loop busy time (computeMs) plus a never-pausing wall ceiling
(maxWallMs) — funneling into worker.terminate(). env: {} and execArgv: []
keep the isolate hermetic; disposal aborts in-flight runs and awaits
worker exits.
The worker entry loads unbuilt via Node's native type stripping
(src/worker.ts, erasable-only) and ships built as a sibling tsdown bundle
(lib/worker.js); tests/built-lib.e2e.ts pins the built load path under
plain node and joins the built-artifact smoke gate. Unit suites cover the
bootstrap in-process (fake port) and the runtime over real workers,
per-file 100%.
2026-07-08 11:00:06 +08:00
implementations : [ 'code-runtime-worker' ] ,
feat: Code Mode — the registry's mode config, the SDK codegen, and the run_code bridge
The dsh-tools half of the Code Mode RFC (its fourth, final change): the
registry gains its first config — mode: native | code | both — and OWNS how
its tools reach the model. 'code' contributes exactly one wire tool,
run_code, plus a lazy tools:sdk prompt section declaring every other tool
as a generated TypeScript API (jsonSchemaToTs: total over the defineTool
subset, unknown degradation, lexicographic byte-identical rendering);
'both' ships both representations; 'native' is byte-for-byte the old
behavior. Non-native modes fail every assembly loudly without a
typescript-language ctx.codeRuntime.
run_code's dispatch bridge: JSON-normalizes each binding argument before
dispatch (what dispatches is what the tool/code-dispatch event logs — the
append can never fail on payload shape; BigInt/circulars reject that one
call), serializes all program tool calls through a per-run queue (even
Promise.all — no concurrency-safety metadata yet), routes every sub-call
through tools/pre-execute → tools/post-execute (a deny rejects the
program-side promise), drops sub-call additionalContext (no safe outlet
mid-run; pinned), owns a run-scoped abort that follows the outer signal in
and fires on settlement (in-flight sub-dispatch aborted, queued abandoned,
queue drained before returning), and converts a failed run into
CodeRunFailedError → a structured isError carrying kind + captured logs.
tool/code-dispatch joins SessionEventMap by declaration merging (log-only;
deriveMessages ignores it).
The composed surface: the tools config forwards through agent-core and
both app packages; examples/code-agent + demo:code run the worker runtime
under mode code (keyless boot smoke + a with-key e2e proving the collapsed
[run_code] header, the dispatch events, and the file the program wrote);
two new snapshot scenarios (code-mode-turn, both-mode-turn) record the SDK
section, collapsed header, dispatch events, and result card — each its own
header-pinning class (the harness gains per-scenario config overlays and
per-class pins). Catalogs, graphs, cookbook, hooks-bridge notes, and the
RFC (moved to implemented/, restructured to decision-era headings) updated
in the same change.
2026-07-08 12:58:23 +08:00
consumers : [ 'tools' ] ,
note : 'Runs one model-written program against host-provided async bindings; backends differ by substrate and language (the tool registry consumes it for Code Mode).' ,
2026-07-08 02:17:24 +08:00
} ,
2026-07-04 12:50:06 +08:00
{
key : 'fs' ,
pkg : 'fs' ,
title : 'Filesystem provider seam' ,
mode : 'seam' ,
feat(sandbox): cross-family file sandbox — one policy home, sandboxed fs provider, fs escalation parity
Extend SandboxMode enforcement from bash to the filesystem tools, the sandbox
RFC's deferred cross-family phase.
- dsh-sandbox-policy (new, ctx.sandboxPolicy): the single home for the
deployment default mode + workspaceRoot and the per-session override event,
renamed bash/sandbox-mode -> sandbox/mode and moved here with its fold/setter.
Decouples the bash seam from dsh-session.
- dsh-fs-sandbox (new): SandboxedFileSystem extends LocalFileSystem and fences
write/edit by the per-call mode (read-only denies, workspace-write contains to
the workspace + temp roots via the shared writableRoots, danger passes
through); reads pass through. Structured FS_SANDBOX_DENIED; in-lock parent
re-canonicalization. A policy fence in trusted code, not a kernel boundary.
- dsh-sandbox: the shared escalation kit (writableRoots, the strictly-wider
ladder, denial/hint markers, approveEscalation) both tool families use;
approveEscalation takes a structural approver so dsh-sandbox gains no
approval/agent dependency, and both tools stay duplication-free.
- tool-fs: write/edit advertise sandbox_permissions/justification under a
confining ctx.fs, map FS_SANDBOX_DENIED to the shared [sandbox: ...] marker,
and resolve the same one-approved-wider retry.
- examples/acp-agent: composes sandbox-policy + fs-sandbox, drops the gating
that disabled the fs stack under confined modes.
RFC docs/rfc/implemented/feature/2026-07-14-cross-family-fs-sandbox.md; the old
sandbox RFC's In-process/deferred/FAQ sections updated to shipped fact.
2026-07-14 20:05:57 +08:00
implementations : [ 'fs-local' , 'fs-sandbox' ] ,
2026-07-04 12:50:06 +08:00
consumers : [ 'tool-fs' ] ,
companions : [ 'fs-policy' ] ,
feat(sandbox): cross-family file sandbox — one policy home, sandboxed fs provider, fs escalation parity
Extend SandboxMode enforcement from bash to the filesystem tools, the sandbox
RFC's deferred cross-family phase.
- dsh-sandbox-policy (new, ctx.sandboxPolicy): the single home for the
deployment default mode + workspaceRoot and the per-session override event,
renamed bash/sandbox-mode -> sandbox/mode and moved here with its fold/setter.
Decouples the bash seam from dsh-session.
- dsh-fs-sandbox (new): SandboxedFileSystem extends LocalFileSystem and fences
write/edit by the per-call mode (read-only denies, workspace-write contains to
the workspace + temp roots via the shared writableRoots, danger passes
through); reads pass through. Structured FS_SANDBOX_DENIED; in-lock parent
re-canonicalization. A policy fence in trusted code, not a kernel boundary.
- dsh-sandbox: the shared escalation kit (writableRoots, the strictly-wider
ladder, denial/hint markers, approveEscalation) both tool families use;
approveEscalation takes a structural approver so dsh-sandbox gains no
approval/agent dependency, and both tools stay duplication-free.
- tool-fs: write/edit advertise sandbox_permissions/justification under a
confining ctx.fs, map FS_SANDBOX_DENIED to the shared [sandbox: ...] marker,
and resolve the same one-approved-wider retry.
- examples/acp-agent: composes sandbox-policy + fs-sandbox, drops the gating
that disabled the fs stack under confined modes.
RFC docs/rfc/implemented/feature/2026-07-14-cross-family-fs-sandbox.md; the old
sandbox RFC's In-process/deferred/FAQ sections updated to shipped fact.
2026-07-14 20:05:57 +08:00
note : 'tool-fs executes read/write/edit through ctx.fs; fs-sandbox fences mutations by the shared sandbox mode; fs-policy contributes observed-state checks through the fs/* event gate.' ,
2026-07-03 01:13:52 +08:00
} ,
{
key : 'compact' ,
pkg : 'compact' ,
title : 'Compaction seam' ,
mode : 'seam' ,
implementations : [ 'compact-basic' ] ,
consumers : [ 'compact-basic' ] ,
2026-07-15 16:50:44 +08:00
note : 'The basic backend consumes post-step pressure and request-error recovery events; a model-facing compact tool remains deferred.' ,
2026-07-03 01:13:52 +08:00
} ,
{
key : 'subagents' ,
pkg : 'subagent' ,
title : 'Subagent provider registry' ,
mode : 'seam' ,
2026-07-19 11:54:37 +08:00
implementations : [ 'subagent-spawn' , 'subagent-fork' , 'subagent-acp' ] ,
2026-07-20 00:51:19 +08:00
consumers : [ 'tool-subagent' , 'tool-ralph' ] ,
note : 'Providers implement transports; tool-subagent exposes configured delegation while tool-ralph requires one fresh structured-output route.' ,
2026-07-03 01:13:52 +08:00
} ,
feat(tasks): background task runtime, generic task_* control tools, bash/subagent producers
One shared ctx.tasks registry (branded <kind>-N ids, owner-fenced
read/kill/wait/list, attachSurface misconfiguration fence, reported-flag
notice dedup, atomic register) + dsh-tool-tasks (task_output/task_list/
task_kill, completion-notice injection, background prompt habit).
Producers opt in via their own enableRunInBackground config: bash
(stream kind; seam slimmed to resolve/run/start returning a BashProcess
handle, bash_output/bash_kill deleted) and subagent (final-output kind;
done settles after run.dispose()). Owner disposal drains tasks through
the new awaited ctx.agents.onCleanup seam in the loop's disposal chain.
Both RFCs moved to implemented/; docs, catalogs, snapshots re-pinned.
2026-07-09 21:22:54 +08:00
{
key : 'tasks' ,
pkg : 'tasks' ,
title : 'Background task registry' ,
2026-07-26 05:13:39 +08:00
mode : 'seam' ,
implementations : [ 'tasks-local' ] ,
2026-07-21 16:01:00 +08:00
consumers : [ 'tool-bash' , 'tool-pty' , 'tool-subagent' , 'tool-tasks' ] ,
2026-07-26 05:13:39 +08:00
note : 'Producers (background bash, PTY sends, and subagent delegations) register running work; tool-tasks is the model-facing control surface that reads, lists, and kills it; tasks-local is the process-local registry.' ,
feat(tasks): background task runtime, generic task_* control tools, bash/subagent producers
One shared ctx.tasks registry (branded <kind>-N ids, owner-fenced
read/kill/wait/list, attachSurface misconfiguration fence, reported-flag
notice dedup, atomic register) + dsh-tool-tasks (task_output/task_list/
task_kill, completion-notice injection, background prompt habit).
Producers opt in via their own enableRunInBackground config: bash
(stream kind; seam slimmed to resolve/run/start returning a BashProcess
handle, bash_output/bash_kill deleted) and subagent (final-output kind;
done settles after run.dispose()). Owner disposal drains tasks through
the new awaited ctx.agents.onCleanup seam in the loop's disposal chain.
Both RFCs moved to implemented/; docs, catalogs, snapshots re-pinned.
2026-07-09 21:22:54 +08:00
} ,
2026-07-04 12:50:06 +08:00
{
key : 'web' ,
pkg : 'web' ,
title : 'Web access provider registry' ,
mode : 'seam' ,
implementations : [ 'web-search-exa' , 'web-search-perplexity' , 'web-search-deepseek' , 'web-fetch-local' ] ,
consumers : [ 'tool-web' ] ,
note : 'Search and fetch providers register into one ctx.web seam; tool-web owns the stable model-facing names.' ,
} ,
2026-07-08 19:20:50 +08:00
{
2026-07-13 11:07:27 +08:00
key : 'spillStore' ,
2026-07-08 19:20:50 +08:00
pkg : 'spill' ,
title : 'Spill storage seam' ,
mode : 'seam' ,
implementations : [ 'spill-local' ] ,
consumers : [ 'spill-policy' ] ,
2026-07-13 11:07:27 +08:00
note : 'The backend saves oversized tool text and returns a model-facing locator plus retrieval hint; spill-policy is the tools/post-execute consumer that decides when to spill.' ,
2026-07-08 19:20:50 +08:00
} ,
2026-07-25 02:15:58 +08:00
{
key : 'httpServer' ,
pkg : 'webserver' ,
title : 'HTTP route registration' ,
mode : 'core' ,
consumers : [ 'connection' , 'modules' , 'hmr' ] ,
note : 'Plain node:http carrier: named-route registry, index transform taps, and the static dist fallback; web-transport plugins register their own routes.' ,
} ,
{
key : 'clientModuleHost' ,
pkg : 'modules' ,
title : 'Client plugin graph host' ,
mode : 'core' ,
consumers : [ 'hmr' ] ,
note : 'Composes the __DSH_BOOT__ entry graph from an incremental dshClient scan, serves plugin bundles, and notifies rebuilt/graph-changed subscribers.' ,
} ,
2026-07-06 03:14:07 +08:00
{
key : 'workflows' ,
pkg : 'workflow' ,
title : 'Workflow script engine' ,
mode : 'seam' ,
2026-07-09 19:06:55 +08:00
implementations : [ 'workflow-workerthread' ] ,
2026-07-20 00:51:19 +08:00
consumers : [ 'tool-workflow' , 'tool-ralph' ] ,
note : 'One engine per context (bash shape, no named-provider registry); the general workflow and fixed Ralph consumers start runs whose agent() calls fan out through ctx.subagents.' ,
2026-07-06 03:14:07 +08:00
} ,
2026-07-03 01:13:52 +08:00
]
2026-07-05 02:54:01 +08:00
function generatedHeader ( title : string ) : string [ ] {
2026-07-03 01:13:52 +08:00
return [
'<!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand.' ,
' Run `pnpm run gen-doc-graphs` to regenerate. -->' ,
'' ,
` # ${ title } ` ,
'' ,
]
}
2026-07-05 02:54:01 +08:00
function maintenanceFooter ( source : string ) : string [ ] {
return [ ` Maintenance mode: ${ source } . ` , '' ]
}
function graphIndexLink ( rel : string ) : string {
return relative ( 'docs' , rel ) . replaceAll ( '\\' , '/' )
}
function linkFromDoc ( docRel : string , targetRel : string ) : string {
return relative ( dirname ( docRel ) , targetRel ) . replaceAll ( '\\' , '/' )
}
2026-07-05 01:25:58 +08:00
function mermaidCode ( value : string ) : string {
return ` <code> ${ value . replace ( /&/g , '&' ) . replace ( /</g , '<' ) . replace ( />/g , '>' ) } </code> `
2026-07-03 01:13:52 +08:00
}
2026-07-05 01:25:58 +08:00
function repoLink ( path : string , label : string , up = '..' ) : string {
return ` [ ${ label } ]( ${ up } / ${ path } ) `
2026-07-03 01:13:52 +08:00
}
2026-07-05 01:25:58 +08:00
function sourceLink ( source : string , up = '..' ) : string {
return repoLink ( source . split ( ':' ) [ 0 ] ? ? source , ` \` ${ source } \` ` , up )
2026-07-03 01:13:52 +08:00
}
2026-07-05 01:25:58 +08:00
function pkgLink ( pkg : Pkg | undefined , fallback : string , up = '..' ) : string {
return pkg ? repoLink ( pkg . rel , ` \` ${ pkg . short } \` ` , up ) : ` \` ${ fallback } \` `
2026-07-03 01:13:52 +08:00
}
2026-07-05 01:25:58 +08:00
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 ( ', ' )
2026-07-03 01:13:52 +08:00
}
2026-07-05 01:25:58 +08:00
function tableCell ( value : string ) : string {
return value . replace ( /\|/g , '\\|' ) . replace ( /\n/g , '<br>' )
2026-07-03 01:13:52 +08:00
}
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 ] ) )
2026-07-05 02:54:01 +08:00
const maintenance = 'hybrid: services are discovered from Cordis declarations; interface/implementation/consumer roles are classified in `scripts/gen-doc-graphs.ts` with a completeness guard'
2026-07-03 01:13:52 +08:00
const nodes = new Map < string , string > ( )
const edges = new Set < string > ( )
2026-07-04 12:50:06 +08:00
const companionEdges = new Set < string > ( )
2026-07-03 01:13:52 +08:00
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 } ` ) }
2026-07-05 02:54:01 +08:00
const lines = generatedHeader ( 'Capability Seams And Core Services' )
2026-07-03 01:13:52 +08:00
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 ) )
}
2026-07-04 12:50:06 +08:00
for ( const companion of role . companions ? ? [ ] ) {
addNode ( nodeId ( 'pkg' , companion ) , companion )
companionEdges . add ( ` ${ svc } -. event gate .-> ${ nodeId ( 'pkg' , companion ) } ` )
}
2026-07-03 01:13:52 +08:00
}
2026-07-04 12:50:06 +08:00
lines . push ( . . . nodes . values ( ) , . . . [ . . . edges ] . sort ( ) , . . . [ . . . companionEdges ] . sort ( ) )
lines . push ( '```' , '' , '| ctx key | Role | Owner | Implementations | Direct consumers | Companion plugins | Note |' , '| --- | --- | --- | --- | --- | --- | --- |' )
2026-07-03 01:13:52 +08:00
for ( const role of SERVICE_ROLES ) {
2026-07-04 12:50:06 +08:00
lines . push ( ` | \` ctx. ${ role . key } \` | \` ${ role . mode } \` | ${ pkgLink ( pkgsByShort . get ( role . pkg ) , role . pkg ) } | ${ pkgList ( role . implementations , pkgsByShort ) } | ${ pkgList ( role . consumers , pkgsByShort ) } | ${ pkgList ( role . companions , pkgsByShort ) } | ${ tableCell ( role . note ) } | ` )
2026-07-03 01:13:52 +08:00
}
2026-07-05 02:54:01 +08:00
lines . push ( '' , . . . maintenanceFooter ( maintenance ) )
2026-07-03 01:13:52 +08:00
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 , '' )
}
2026-07-05 01:25:58 +08:00
const APP_EXAMPLES = [
{
2026-07-19 01:09:20 +08:00
id : 'tui' ,
rel : 'examples/tui-agent/composition.md' ,
title : 'TUI Agent App Composition' ,
label : 'examples/tui-agent' ,
config : 'examples/tui-agent/cordis.yml' ,
2026-07-20 19:26:04 +08:00
summary : 'The TUI agent combines the real DeepSeek adapter, coding tools, compaction, subagents, and workflows with the full-screen terminal app package.' ,
2026-07-05 01:25:58 +08:00
} ,
{
2026-07-16 16:12:27 +08:00
id : 'headless' ,
rel : 'examples/headless-agent/composition.md' ,
title : 'Headless Agent App Composition' ,
label : 'examples/headless-agent' ,
config : 'examples/headless-agent/cordis.yml' ,
2026-07-19 13:25:30 +08:00
summary : 'The headless demo combines the real DeepSeek adapter and coding capabilities with the one-shot app package, format-pure stdout, and one fresh persisted top-level session.' ,
2026-07-05 01:25:58 +08:00
} ,
2026-07-08 11:50:12 +08:00
{
id : 'cordis' ,
rel : 'examples/cordis-agent/composition.md' ,
title : 'Cordis Agent App Composition' ,
label : 'examples/cordis-agent' ,
config : 'examples/cordis-agent/cordis.yml' ,
2026-07-27 21:20:06 +08:00
summary : 'The self-referential demo puts @deepseek-ai/dsh-tool-cordis on the coding spine, letting the agent inspect its current-process runtime and mount or unmount in-memory temporary Plugins.' ,
2026-07-08 11:50:12 +08:00
} ,
2026-07-05 01:25:58 +08:00
{
id : 'acp' ,
2026-07-05 02:54:01 +08:00
rel : 'examples/acp-agent/composition.md' ,
2026-07-24 01:40:25 +08:00
title : 'ACP Automation App Composition' ,
2026-07-05 01:25:58 +08:00
label : 'examples/acp-agent' ,
config : 'examples/acp-agent/cordis.yml' ,
2026-07-24 22:36:06 +08:00
summary : 'The ACP demo exposes fresh baseline-prompt agent sessions to programmatic clients over JSON-RPC stdio, with no stdout logger, human UI, or pre-created agent.' ,
2026-07-05 01:25:58 +08:00
} ,
]
type AppExample = typeof APP_EXAMPLES [ number ]
function renderAppExpansion ( lines : string [ ] , appNode : string , pluginName : string ) : void {
const agentCore = nodeId ( 'bundle' , 'agent_core' )
const jsonl = nodeId ( 'bundle' , 'jsonl' )
2026-07-15 15:57:57 +08:00
lines . push ( ` ${ appNode } --> ${ agentCore } ["@deepseek-ai/dsh-agent-spine-demo"] ` )
2026-07-05 01:25:58 +08:00
lines . push ( ` ${ appNode } --> ${ jsonl } ["@deepseek-ai/dsh-session-persistence-jsonl"] ` )
2026-07-20 19:26:04 +08:00
if ( pluginName === '@deepseek-ai/dsh-tui-demo' ) {
lines . push ( ` ${ appNode } --> ${ nodeId ( 'frontdoor' , 'tui' ) } ["@deepseek-ai/dsh-tui<br/>pre-created main agent"] ` )
2026-07-15 21:21:24 +08:00
} else if ( pluginName === '@deepseek-ai/dsh-cli-demo' ) {
2026-07-19 13:25:30 +08:00
lines . push ( ` ${ appNode } --> ${ nodeId ( 'frontdoor' , 'cli' ) } ["one-shot driver<br/>format-pure stdout<br/>fresh top-level agent"] ` )
2026-07-15 15:57:57 +08:00
} else if ( pluginName === '@deepseek-ai/dsh-acp-demo' ) {
2026-07-24 01:40:25 +08:00
lines . push ( ` ${ appNode } --> ${ nodeId ( 'frontdoor' , 'acp' ) } ["@deepseek-ai/dsh-acp<br/>automation-only JSON-RPC stdio<br/>fresh sessions created by client"] ` )
2026-07-05 01:25:58 +08:00
}
lines . push (
` ${ agentCore } --> ${ nodeId ( 'spine' , 'llm' ) } ["ctx.llm"] ` ,
` ${ agentCore } --> ${ nodeId ( 'spine' , 'sessions' ) } ["ctx.sessions"] ` ,
` ${ agentCore } --> ${ nodeId ( 'spine' , 'tools' ) } ["ctx.tools + tool-bash"] ` ,
` ${ agentCore } --> ${ nodeId ( 'spine' , 'loop' ) } ["ctx.agents + ctx.agentLoop"] ` ,
)
}
function renderAppComposition ( example : AppExample ) : string {
const plugins = parseExampleCordis ( example . config )
2026-07-05 02:54:01 +08:00
const maintenance = 'hybrid: the leaf plugin list is parsed from its `cordis.yml`; app package expansion is curated from package source'
const lines = generatedHeader ( example . title )
2026-07-03 01:13:52 +08:00
lines . push (
2026-07-05 01:25:58 +08:00
example . summary ,
2026-07-03 01:13:52 +08:00
'' ,
'```mermaid' ,
'flowchart LR' ,
2026-07-05 01:25:58 +08:00
` cfg[" ${ escLabel ( example . label ) } <br/>cordis.yml"] ` ,
2026-07-03 01:13:52 +08:00
)
2026-07-05 01:25:58 +08:00
for ( const plugin of plugins ) {
const pluginNode = nodeId ( ` plugin_ ${ example . id } ` , plugin . id )
lines . push ( ` ${ pluginNode } [" ${ escLabel ( plugin . id ) } <br/> ${ escLabel ( plugin . name ) } "] ` )
lines . push ( ` cfg --> ${ pluginNode } ` )
2026-07-20 19:26:04 +08:00
if ( plugin . name === '@deepseek-ai/dsh-tui-demo' || plugin . name === '@deepseek-ai/dsh-cli-demo' || plugin . name === '@deepseek-ai/dsh-acp-demo' ) {
2026-07-05 01:25:58 +08:00
renderAppExpansion ( lines , pluginNode , plugin . name )
2026-07-03 01:13:52 +08:00
}
}
lines . push (
'```' ,
'' ,
2026-07-05 01:25:58 +08:00
'| Plugin id | Package / module |' ,
'| --- | --- |' ,
. . . plugins . map ( plugin = > ` | \` ${ plugin . id } \` | \` ${ plugin . name } \` | ` ) ,
'' ,
2026-07-05 02:54:01 +08:00
` Source config: [ \` ${ example . config } \` ]( ${ linkFromDoc ( example . rel , example . config ) } ). ` ,
2026-07-03 01:13:52 +08:00
)
2026-07-05 02:54:01 +08:00
lines . push ( '' , . . . maintenanceFooter ( maintenance ) )
2026-07-03 01:13:52 +08:00
return lines . join ( '\n' )
}
2026-07-14 23:14:51 +08:00
/** Collect event dispatch/listener relations from real cross-file receiver types. */
class EventRelationCollector {
private readonly relations = new Map < string , EventRelation > ( )
private readonly callSites = new Map < ts.SignatureDeclaration | ts.JSDocSignature , ts.CallExpression [ ] > ( )
private readonly contextType : ts.Type
private readonly agentDispatchType : ts.Type
private readonly eventsServiceType : ts.Type
constructor (
private readonly project : TypeScriptProject ,
private readonly sources : readonly PackageSource [ ] ,
) {
this . contextType = this . declaredType ( 'vendor/cordis/src/context.ts' , 'Context' )
this . agentDispatchType = this . declaredType ( 'packages/core/agent/src/dispatch.ts' , 'AgentEventDispatch' )
this . eventsServiceType = this . declaredType ( 'vendor/cordis/src/events.ts' , 'EventsService' )
this . indexCallSites ( )
}
/** Return all event relations discovered from the Program. */
collect ( ) : Map < string , EventRelation > {
for ( const source of this . sources ) this . visitSource ( source )
return this . relations
2026-07-03 01:13:52 +08:00
}
2026-07-14 23:14:51 +08:00
/** Resolve one named class/interface declaration to its merged instance type. */
private declaredType ( relativePath : string , name : string ) : ts . Type {
const sourceFile = this . project . sourceFile ( relativePath )
const declaration = sourceFile . statements . find ( ( statement ) : statement is ts . ClassDeclaration | ts . InterfaceDeclaration = > {
return ( ts . isClassDeclaration ( statement ) || ts . isInterfaceDeclaration ( statement ) ) && statement . name ? . text === name
} )
const symbol = declaration ? . name && this . project . checker . getSymbolAtLocation ( declaration . name )
if ( ! symbol ) throw new Error ( ` cannot resolve TypeScript type ${ name } from ${ relativePath } ` )
return this . project . checker . getDeclaredTypeOfSymbol ( symbol )
2026-07-03 01:13:52 +08:00
}
2026-07-14 23:14:51 +08:00
/** Index resolved local function calls for narrow argument-flow recovery. */
private indexCallSites ( ) : void {
const visit = ( node : ts.Node ) : void = > {
if ( ts . isCallExpression ( node ) ) {
const declaration = this . project . checker . getResolvedSignature ( node ) ? . declaration
if ( declaration ) {
const calls = this . callSites . get ( declaration ) ? ? [ ]
calls . push ( node )
this . callSites . set ( declaration , calls )
}
}
ts . forEachChild ( node , visit )
}
for ( const source of this . sources ) visit ( source . sourceFile )
}
/** Walk one package source file and classify event API calls by receiver type. */
private visitSource ( source : PackageSource ) : void {
2026-07-03 01:13:52 +08:00
const visit = ( node : ts.Node ) : void = > {
2026-07-24 21:18:48 +08:00
if ( ts . isCallExpression ( node ) ) {
if ( this . isAgentEventEmitter ( node . expression ) ) {
const event = node . arguments [ 2 ]
if ( event ) {
for ( const name of this . finiteStringValues ( event ) ? ? [ ] ) {
this . addDispatcher ( name , source . pkg , 'emitAgentEvent' )
2026-07-14 23:14:51 +08:00
}
}
2026-07-24 21:18:48 +08:00
} else if ( ts . isPropertyAccessExpression ( node . expression ) ) {
const receiverKind = this . receiverKind ( node . expression . expression )
const method = node . expression . name . text
if ( receiverKind === 'events-service' && method === 'dispatch' ) {
const argumentList = node . arguments [ 1 ]
if ( argumentList ) {
for ( const event of this . eventNamesFromArgumentList ( argumentList , new Set ( ) ) ) {
this . addDispatcher ( event , source . pkg , 'events.dispatch' )
}
}
} else if ( receiverKind === 'context' || receiverKind === 'agent-dispatch' ) {
const eventNames = this . eventNamesFromCall ( node , receiverKind )
if ( method === 'on' || method === 'once' ) {
for ( const event of eventNames ) this . ensure ( event ) . listeners . add ( source . pkg )
} else if ( method === 'emit' || method === 'parallel' || method === 'serial' || method === 'waterfall' ) {
for ( const event of eventNames ) this . addDispatcher ( event , source . pkg , method )
}
2026-07-03 01:13:52 +08:00
}
}
}
ts . forEachChild ( node , visit )
}
2026-07-14 23:14:51 +08:00
visit ( source . sourceFile )
2026-07-03 01:13:52 +08:00
}
2026-07-14 23:14:51 +08:00
2026-07-24 21:18:48 +08:00
/** Match the exported contained-notification helper by declaration identity. */
private isAgentEventEmitter ( expression : ts.Expression ) : boolean {
if ( ! ts . isIdentifier ( expression ) ) return false
const local = this . project . checker . getSymbolAtLocation ( expression )
if ( ! local ) return false
const symbol = local . flags & ts . SymbolFlags . Alias
? this . project . checker . getAliasedSymbol ( local )
: local
const declarations = symbol . declarations ? ? [ ]
return declarations . some ( ( declaration ) = > {
return ts . isFunctionDeclaration ( declaration )
&& declaration . name ? . text === 'emitAgentEvent'
&& this . project . relativePath ( declaration . getSourceFile ( ) ) === 'packages/core/agent/src/dispatch.ts'
} )
}
2026-07-14 23:14:51 +08:00
/** Classify a receiver using assignability to the repository's actual event API types. */
private receiverKind ( receiver : ts.Expression ) : EventReceiverKind | undefined {
const type = this . project . checker . getTypeAtLocation ( receiver )
if ( type . flags & ( ts . TypeFlags . Any | ts . TypeFlags . Unknown | ts . TypeFlags . Never ) ) return undefined
if ( this . project . checker . isTypeAssignableTo ( type , this . eventsServiceType ) ) return 'events-service'
if ( this . project . checker . isTypeAssignableTo ( type , this . contextType ) ) return 'context'
if ( this . project . checker . isTypeAssignableTo ( type , this . agentDispatchType ) ) return 'agent-dispatch'
return undefined
2026-07-03 01:13:52 +08:00
}
2026-07-14 23:14:51 +08:00
/** Resolve the event-name argument for Context and fused agent dispatch calls. */
private eventNamesFromCall ( call : ts.CallExpression , receiverKind : Exclude < EventReceiverKind , 'events-service' > ) : Set < string > {
const candidates = receiverKind === 'context' ? call . arguments . slice ( 0 , 2 ) : call . arguments . slice ( 0 , 1 )
for ( const candidate of candidates ) {
const values = this . finiteStringValues ( candidate )
if ( values ) return values
}
return new Set ( )
2026-07-12 18:57:42 +08:00
}
2026-07-14 23:14:51 +08:00
/** Recover the event slot from the argument array handed to EventsService.dispatch(). */
private eventNamesFromArgumentList ( expression : ts.Expression , seen : Set < ts.Node > ) : Set < string > {
const current = unwrapExpression ( expression )
if ( seen . has ( current ) ) return new Set ( )
seen . add ( current )
if ( ts . isArrayLiteralExpression ( current ) ) {
for ( const element of current . elements . slice ( 0 , 2 ) ) {
if ( ts . isOmittedExpression ( element ) || ts . isSpreadElement ( element ) ) continue
const values = this . finiteStringValues ( element )
if ( values ) return values
}
return new Set ( )
}
if ( ts . isConditionalExpression ( current ) ) {
return unionSets (
this . eventNamesFromArgumentList ( current . whenTrue , new Set ( seen ) ) ,
this . eventNamesFromArgumentList ( current . whenFalse , new Set ( seen ) ) ,
)
}
if ( ! ts . isIdentifier ( current ) ) return new Set ( )
const symbol = this . project . checker . getSymbolAtLocation ( current )
if ( ! symbol ) return new Set ( )
const events = new Set < string > ( )
for ( const declaration of symbol . declarations ? ? [ ] ) {
if ( ts . isVariableDeclaration ( declaration ) && declaration . initializer && isConstDeclaration ( declaration ) ) {
addAll ( events , this . eventNamesFromArgumentList ( declaration . initializer , new Set ( seen ) ) )
} else if ( ts . isParameter ( declaration ) ) {
addAll ( events , this . eventNamesFromParameter ( declaration , seen ) )
}
}
return events
}
/** Follow a non-exported local helper parameter back to every resolved call site. */
private eventNamesFromParameter ( parameter : ts.ParameterDeclaration , seen : Set < ts.Node > ) : Set < string > {
const owner = parameter . parent
if ( ! ts . isFunctionDeclaration ( owner ) || hasExportModifier ( owner ) ) return new Set ( )
const index = owner . parameters . indexOf ( parameter )
if ( index < 0 ) return new Set ( )
const events = new Set < string > ( )
for ( const call of this . callSites . get ( owner ) ? ? [ ] ) {
const argument = call . arguments [ index ]
if ( argument ) addAll ( events , this . eventNamesFromArgumentList ( argument , new Set ( seen ) ) )
}
return events
}
/** Return a finite string-literal value set, rejecting widened and generic strings. */
private finiteStringValues ( expression : ts.Expression ) : Set < string > | undefined {
const current = unwrapExpression ( expression )
if ( ts . isStringLiteralLike ( current ) ) return new Set ( [ current . text ] )
if ( this . isForwardedAgentEventParameter ( current ) ) return undefined
return finiteStringTypeValues ( this . project . checker . getTypeAtLocation ( current ) )
}
/** Reject the contextual parameter inside the AgentEventDispatch forwarding object. */
private isForwardedAgentEventParameter ( expression : ts.Expression ) : boolean {
if ( ! ts . isIdentifier ( expression ) ) return false
const declarations = this . project . checker . getSymbolAtLocation ( expression ) ? . declarations ? ? [ ]
return declarations . some ( ( declaration ) = > {
if ( ! ts . isParameter ( declaration ) ) return false
const method = declaration . parent
if ( ! ts . isMethodDeclaration ( method ) || ! ts . isObjectLiteralExpression ( method . parent ) ) return false
const contextualType = this . project . checker . getContextualType ( method . parent )
return contextualType !== undefined
&& this . project . checker . isTypeAssignableTo ( contextualType , this . agentDispatchType )
} )
}
/** Get or create one relation row. */
private ensure ( event : string ) : EventRelation {
const existing = this . relations . get ( event )
if ( existing ) return existing
const relation = { dispatchers : new Map < string , Set < string > > ( ) , listeners : new Set < string > ( ) }
this . relations . set ( event , relation )
return relation
}
/** Add one dispatcher method without duplicating package/method labels. */
private addDispatcher ( event : string , pkg : string , method : string ) : void {
const relation = this . ensure ( event )
const methods = relation . dispatchers . get ( pkg ) ? ? new Set < string > ( )
methods . add ( method )
relation . dispatchers . set ( pkg , methods )
2026-07-12 18:57:42 +08:00
}
2026-07-03 01:13:52 +08:00
}
2026-07-14 23:14:51 +08:00
/** Peel syntax-only wrappers that do not change an expression's runtime value. */
function unwrapExpression ( expression : ts.Expression ) : ts . Expression {
let current = expression
while (
ts . isParenthesizedExpression ( current )
|| ts . isAsExpression ( current )
|| ts . isTypeAssertionExpression ( current )
|| ts . isNonNullExpression ( current )
|| ts . isSatisfiesExpression ( current )
) {
current = current . expression
2026-07-09 06:06:17 +08:00
}
2026-07-14 23:14:51 +08:00
return current
2026-07-03 01:13:52 +08:00
}
2026-07-14 23:14:51 +08:00
/** Return every value only when a type is a closed string-literal union. */
function finiteStringTypeValues ( type : ts . Type ) : Set < string > | undefined {
if ( type . flags & ts . TypeFlags . StringLiteral ) {
return new Set ( [ ( type as ts . StringLiteralType ) . value ] )
2026-07-09 06:06:17 +08:00
}
2026-07-14 23:14:51 +08:00
if ( type . flags & ts . TypeFlags . Never ) return new Set ( )
if ( ! type . isUnion ( ) ) return undefined
const values = new Set < string > ( )
for ( const member of type . types ) {
const memberValues = finiteStringTypeValues ( member )
if ( ! memberValues ) return undefined
addAll ( values , memberValues )
2026-07-03 01:13:52 +08:00
}
2026-07-14 23:14:51 +08:00
return values
}
/** Return whether a variable declaration belongs to a const declaration list. */
function isConstDeclaration ( declaration : ts.VariableDeclaration ) : boolean {
return ( declaration . parent . flags & ts . NodeFlags . Const ) !== 0
}
/** Return whether a declaration is visible to callers outside its source module. */
function hasExportModifier ( node : ts.Node ) : boolean {
return ts . canHaveModifiers ( node ) && ( ts . getModifiers ( node ) ? . some ( ( modifier ) = > {
return modifier . kind === ts . SyntaxKind . ExportKeyword || modifier . kind === ts . SyntaxKind . DefaultKeyword
} ) ? ? false )
}
/** Add every member of source to target. */
function addAll < T > ( target : Set < T > , source : ReadonlySet < T > ) : void {
for ( const value of source ) target . add ( value )
}
/** Return the union of two sets without mutating either input. */
function unionSets < T > ( left : ReadonlySet < T > , right : ReadonlySet < T > ) : Set < T > {
const out = new Set ( left )
addAll ( out , right )
return out
}
function collectEventRelations ( ) : Map < string , EventRelation > {
const project = new TypeScriptProject ( root )
const sources = project . sourceFiles ( ) . flatMap ( ( sourceFile ) : PackageSource [ ] = > {
const rel = project . relativePath ( sourceFile )
const match = /^packages\/[^/]+\/([^/]+)\/src\/.+\.ts$/ . exec ( rel )
return match ? . [ 1 ] ? [ { rel , pkg : match [ 1 ] , sourceFile } ] : [ ]
} ) . sort ( ( left , right ) = > left . rel . localeCompare ( right . rel ) )
return new EventRelationCollector ( project , sources ) . collect ( )
2026-07-03 01:13:52 +08:00
}
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 ] ) )
2026-07-14 23:14:51 +08:00
const maintenance = 'generated: Cordis event declarations and producer/listener edges are resolved from the repository TypeScript Program'
2026-07-05 02:54:01 +08:00
const lines = generatedHeader ( 'Event Producer And Consumer Matrix' )
2026-07-03 01:13:52 +08:00
lines . push (
2026-07-14 23:14:51 +08:00
'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. Receiver and event-name types also cover contained dispatch sites that deliberately bypass `ctx.emit`, such as subagent lifecycle containment.' ,
2026-07-03 01:13:52 +08:00
'' ,
'| 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 > ( ) }
2026-07-05 01:25:58 +08:00
lines . push ( ` | \` ${ event . name } \` | \` ${ event . mode } \` | ${ sourceLink ( event . source ) } | ${ relationPackages ( relation . dispatchers , pkgsByShort ) } | ${ listenerPackages ( relation . listeners , pkgsByShort ) } | ` )
2026-07-03 01:13:52 +08:00
}
2026-07-13 23:27:00 +08:00
// Every declared event needs a dispatcher: zero means dead vocabulary or an
2026-07-27 06:24:09 +08:00
// unrecognized semantic dispatch shape. Listener-free extension points remain
// valid. Client-declared events are exempt: the relation scan seeds the HOST
// aggregate program only (host+client cannot share one program — the cordis
// Context merges collide), so client dispatch sites are structurally
// invisible here; their rows stay in the table for the declarations' sake.
2026-07-09 12:36:18 +08:00
const undispatched = [ . . . events ]
2026-07-27 06:24:09 +08:00
. filter ( event = > ! event . source . startsWith ( 'packages/client/' ) )
2026-07-09 12:36:18 +08:00
. filter ( event = > ( relations . get ( event . name ) ? . dispatchers . size ? ? 0 ) === 0 )
. map ( event = > event . name )
. sort ( )
if ( undispatched . length > 0 ) {
throw new Error (
` event-producer-consumer matrix: no dispatcher found for declared event ${ undispatched . length > 1 ? 's' : '' } `
2026-07-14 23:14:51 +08:00
+ ` ${ undispatched . map ( name = > ` " ${ name } " ` ) . join ( ', ' ) } — dead vocabulary, or a dispatch shape the semantic scan misses `
+ '(teach scripts/gen-doc-graphs.ts the shape)' ,
2026-07-09 12:36:18 +08:00
)
}
2026-07-03 01:13:52 +08:00
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 ) } | ` )
}
}
2026-07-05 02:54:01 +08:00
lines . push ( '' , . . . maintenanceFooter ( maintenance ) )
2026-07-03 01:13:52 +08:00
return lines . join ( '\n' )
}
function renderLifecycle ( ) : string {
2026-07-05 02:54:01 +08:00
const maintenance = 'curated Mermaid sequence; exact event signatures live in the generated Cordis catalog'
2026-07-03 01:13:52 +08:00
return [
2026-07-05 02:54:01 +08:00
. . . generatedHeader ( 'Agent Turn And Step Lifecycle' ) ,
2026-07-05 01:25:58 +08:00
'This sequence is the visual companion to [architecture.md](architecture.md#loop-lifecycle-session--turn--step). It keeps durable replay facts on `session/event` and live control/status on `agent/*`.' ,
2026-07-03 01:13:52 +08:00
'' ,
'```mermaid' ,
'sequenceDiagram' ,
' participant User' ,
' participant Agent' ,
2026-07-03 01:32:01 +08:00
' participant Driver' ,
2026-07-04 12:50:06 +08:00
' participant Hooks as hook listeners' ,
2026-07-03 01:13:52 +08:00
' participant Prompt as ctx.systemPrompt' ,
' participant LLM as ctx.llm' ,
' participant Tools as ctx.tools' ,
' participant Session' ,
2026-07-04 12:50:06 +08:00
' participant SDK as UI or SDK listener' ,
2026-07-24 15:08:36 +08:00
' User->>Agent: followup(content)' ,
2026-07-23 19:15:45 +08:00
` Agent-->>SDK: ${ mermaidCode ( 'agent/inbox/enqueue' ) } ` ,
2026-07-03 01:32:01 +08:00
' Agent->>Driver: queued work wakes driver' ,
2026-07-05 01:25:58 +08:00
` Driver-->>SDK: ${ mermaidCode ( 'agent/status' ) } running ` ,
2026-07-27 18:07:51 +08:00
' Note over Agent,Driver: next-step acceptance window opens' ,
2026-07-05 01:25:58 +08:00
` Driver->>Hooks: ${ mermaidCode ( 'agent/prompt-submit' ) } waterfall ` ,
2026-07-21 16:46:48 +08:00
' Hooks-->>Driver: authoritative allow, block, or add context' ,
2026-07-27 18:07:51 +08:00
' alt prompt blocked or admission failed' ,
2026-07-27 18:32:07 +08:00
' Driver-->>Driver: append context-only batch or keep steering boundary pending' ,
2026-07-27 18:07:51 +08:00
' else prompt allowed' ,
` Driver->>Session: ${ mermaidCode ( 'turn/start' ) } ` ,
` Driver->>Session: ${ mermaidCode ( 'user/message' ) } ` ,
2026-07-05 01:25:58 +08:00
` Driver->>Prompt: ${ mermaidCode ( 'system-prompt/assemble' ) } waterfall ` ,
2026-07-24 21:18:48 +08:00
` Driver-->>Driver: ${ mermaidCode ( 'agent/step' ) } serial checkpoint ` ,
2026-07-05 01:25:58 +08:00
` Driver->>Session: ${ mermaidCode ( 'step/start' ) } ` ,
` Driver->>LLM: ${ mermaidCode ( 'agent/request' ) } waterfall, then ${ mermaidCode ( 'llm/stream' ) } waterfall ` ,
2026-07-03 01:32:01 +08:00
' LLM-->>Driver: StreamChunk*' ,
2026-07-05 01:25:58 +08:00
` Driver->>Session: ${ mermaidCode ( 'assistant/chunk' ) } * ` ,
` Session-->>SDK: ${ mermaidCode ( 'session/event' ) } ${ mermaidCode ( 'assistant/chunk' ) } * ` ,
2026-07-15 16:03:52 +08:00
' alt final adapter or terminal in-band request failure' ,
` Driver->>Session: ${ mermaidCode ( 'step/end' ) } ` ,
` Driver->>Hooks: ${ mermaidCode ( 'agent/request-error' ) } waterfall ` ,
2026-07-27 21:17:49 +08:00
' Hooks-->>Driver: return retry action or preserve the original error' ,
2026-07-15 16:03:52 +08:00
' else model request succeeded' ,
2026-07-05 01:25:58 +08:00
` Driver->>Session: ${ mermaidCode ( 'assistant/message' ) } ` ,
2026-07-18 14:59:26 +08:00
' Driver->>Tools: classify pending call by executionMode' ,
' loop barriers and bounded rolling pool, reclassify before start' ,
' opt call starts' ,
` Driver->>Session: ${ mermaidCode ( 'tool/call' ) } ` ,
' Driver->>Tools: ordered pre, concurrent execute' ,
2026-07-16 15:52:35 +08:00
' Tools-->>Session: tool-owned events when applicable' ,
' end' ,
2026-07-18 14:59:26 +08:00
' opt next model-order result ready' ,
2026-07-16 15:52:35 +08:00
' Driver->>Tools: ordered post' ,
` Driver->>Session: ${ mermaidCode ( 'tool/result' ) } ` ,
' end' ,
2026-07-13 11:02:21 +08:00
' end' ,
2026-07-21 16:46:48 +08:00
' Driver->>Session: post-tool context and steering (no prompt-submit)' ,
2026-07-15 16:03:52 +08:00
` Driver->>Session: ${ mermaidCode ( 'step/end' ) } ` ,
2026-07-27 17:38:42 +08:00
` Driver->>Hooks: ${ mermaidCode ( 'agent/turn-stopping' ) } serial terminal checkpoint ` ,
2026-07-15 16:03:52 +08:00
' end' ,
2026-07-27 18:07:51 +08:00
' Note over Agent,Driver: next-step acceptance window closes' ,
2026-07-05 01:25:58 +08:00
` Driver->>Session: ${ mermaidCode ( 'turn/end' ) } ` ,
2026-07-27 18:07:51 +08:00
' end' ,
2026-07-05 01:25:58 +08:00
` Driver-->>SDK: ${ mermaidCode ( 'agent/status' ) } idle ` ,
2026-07-03 01:13:52 +08:00
'```' ,
'' ,
2026-07-15 14:47:29 +08:00
'The `assistant/message` edge records every successful provider call, including content-less and `max-tokens` finishes. Empty content stays out of derived history while the durable anchor retains usage and exact chunk provenance, including an explicit empty source set.' ,
'' ,
2026-07-24 21:18:48 +08:00
'`dsh-compact-basic` uses `agent/step` for pressure before request derivation and `agent/request-error` only for canonical context overflow. Once either trigger qualifies, optional tool-result pruning runs before summary selection. Recovery works between the closed failed step and failed turn close, and opens a fresh retry turn only when pruning or summarization advances the surface replacement generation; otherwise the original request error remains authoritative.' ,
2026-07-15 16:50:44 +08:00
'' ,
2026-07-21 16:46:48 +08:00
'The returned `agent/prompt-submit` allow is authoritative; listeners wrapping `next()` preserve downstream content and additional contexts unless replacement is intentional. Steering bypasses that waterfall and joins at its durable checkpoint.' ,
'' ,
2026-07-04 12:50:06 +08:00
'SDK users that need replayable transcript data should consume `session/event`; `agent/*` is the live coordination surface for queue/status, prompt interception, request shaping, steering, continuation, and errors.' ,
2026-07-03 01:13:52 +08:00
'' ,
2026-07-05 02:54:01 +08:00
. . . maintenanceFooter ( maintenance ) ,
2026-07-03 01:13:52 +08:00
] . join ( '\n' )
}
function renderToolPipeline ( ) : string {
2026-07-05 02:54:01 +08:00
const maintenance = 'curated Mermaid flow; exact tool schemas and event signatures live in generated catalogs'
2026-07-03 01:13:52 +08:00
return [
2026-07-05 02:54:01 +08:00
. . . generatedHeader ( 'Tool Execution Pipeline' ) ,
2026-07-23 02:53:43 +08:00
'This graph shows where policy, hooks, sandboxing, filesystem guards, result rewriting, final-outcome observation, and UI rendering fit without changing the loop. The transformable extension points are the `tools/pre-execute`, `tools/execute`, and `tools/post-execute` waterfalls; monotonic guards, definition-owned `finalizeContent`, and `tools/result` are the owner-enforced boundaries around them.' ,
2026-07-03 01:13:52 +08:00
'' ,
'```mermaid' ,
'flowchart TD' ,
' model["Assistant message contains tool-call block"]' ,
2026-07-05 01:25:58 +08:00
` toolCall["Session event: ${ mermaidCode ( 'tool/call' ) } <br/>logged before execution"] ` ,
2026-07-04 12:50:06 +08:00
' presentCall["UI pending card<br/>presentCall(args)"]' ,
2026-07-05 01:25:58 +08:00
` pre[" ${ mermaidCode ( 'tools/pre-execute' ) } waterfall<br/>hooks, permission, sandbox"] ` ,
2026-07-11 22:55:26 +08:00
' guards["Registered monotonic guards<br/>deny or abstain; identity protected"]' ,
2026-07-11 23:14:09 +08:00
' denied["denied or approval refused<br/>tool body skipped"]' ,
2026-07-09 15:25:18 +08:00
` approval[" ${ mermaidCode ( 'ctx.approval' ) } one-shot prompt<br/>absent or unanswerable: deny"] ` ,
2026-07-08 10:06:07 +08:00
` around[" ${ mermaidCode ( 'tools/execute' ) } waterfall<br/>timeout, retry, metrics (around dispatch)"] ` ,
2026-07-03 01:32:01 +08:00
' toolBody["Registered tool execute() body"]' ,
2026-07-05 01:25:58 +08:00
` fsGate[" ${ mermaidCode ( 'fs/write-intent' ) } or ${ mermaidCode ( 'fs/edit-intent' ) } <br/>tool-fs mutations only"] ` ,
feat: Code Mode — the registry's mode config, the SDK codegen, and the run_code bridge
The dsh-tools half of the Code Mode RFC (its fourth, final change): the
registry gains its first config — mode: native | code | both — and OWNS how
its tools reach the model. 'code' contributes exactly one wire tool,
run_code, plus a lazy tools:sdk prompt section declaring every other tool
as a generated TypeScript API (jsonSchemaToTs: total over the defineTool
subset, unknown degradation, lexicographic byte-identical rendering);
'both' ships both representations; 'native' is byte-for-byte the old
behavior. Non-native modes fail every assembly loudly without a
typescript-language ctx.codeRuntime.
run_code's dispatch bridge: JSON-normalizes each binding argument before
dispatch (what dispatches is what the tool/code-dispatch event logs — the
append can never fail on payload shape; BigInt/circulars reject that one
call), serializes all program tool calls through a per-run queue (even
Promise.all — no concurrency-safety metadata yet), routes every sub-call
through tools/pre-execute → tools/post-execute (a deny rejects the
program-side promise), drops sub-call additionalContext (no safe outlet
mid-run; pinned), owns a run-scoped abort that follows the outer signal in
and fires on settlement (in-flight sub-dispatch aborted, queued abandoned,
queue drained before returning), and converts a failed run into
CodeRunFailedError → a structured isError carrying kind + captured logs.
tool/code-dispatch joins SessionEventMap by declaration merging (log-only;
deriveMessages ignores it).
The composed surface: the tools config forwards through agent-core and
both app packages; examples/code-agent + demo:code run the worker runtime
under mode code (keyless boot smoke + a with-key e2e proving the collapsed
[run_code] header, the dispatch events, and the file the program wrote);
two new snapshot scenarios (code-mode-turn, both-mode-turn) record the SDK
section, collapsed header, dispatch events, and result card — each its own
header-pinning class (the harness gains per-scenario config overlays and
per-class pins). Catalogs, graphs, cookbook, hooks-bridge notes, and the
RFC (moved to implemented/, restructured to decision-era headings) updated
in the same change.
2026-07-08 12:58:23 +08:00
` owned["Tool-owned session events<br/> ${ mermaidCode ( 'todo/write' ) } , ${ mermaidCode ( 'fs/observed' ) } , ${ mermaidCode ( 'hook/invoked' ) } , ${ mermaidCode ( 'hook/result' ) } , ${ mermaidCode ( 'tool/code-dispatch' ) } "] ` ,
2026-07-05 01:25:58 +08:00
` post[" ${ mermaidCode ( 'tools/post-execute' ) } waterfall<br/>accept, block, replace, add context"] ` ,
2026-07-23 03:15:15 +08:00
' normalized["Registry outer normalization<br/>pipeline/result snapshot throws become isError"]' ,
2026-07-23 02:53:43 +08:00
' finalize["ToolDefinition.finalizeContent<br/>last content-only invariant"]' ,
2026-07-13 11:58:55 +08:00
` final[" ${ mermaidCode ( 'tools/result' ) } synchronous notification<br/>frozen authoritative outcome"] ` ,
2026-07-23 19:15:45 +08:00
' context["Active-batch additionalContexts FIFO<br/>injected user/message after recorded tool results"]' ,
2026-07-05 01:25:58 +08:00
` toolResult["Session event: ${ mermaidCode ( 'tool/result' ) } <br/>single model-facing outcome"] ` ,
2026-07-15 12:49:50 +08:00
' allResults["Tool batch settled<br/>recorded tool/result events complete"]' ,
2026-07-04 12:50:06 +08:00
' presentResult["UI completed card<br/>presentResult(args, result)"]' ,
' model --> toolCall' ,
' toolCall --> presentCall' ,
' toolCall --> pre' ,
2026-07-11 22:55:26 +08:00
' pre -->|allow| guards' ,
' guards -->|allow| around' ,
' guards -->|deny| denied' ,
2026-07-23 02:53:43 +08:00
' guards -.->|throw| normalized' ,
2026-07-08 10:06:07 +08:00
' around --> toolBody' ,
2026-07-09 15:25:18 +08:00
' pre -->|deny| denied' ,
' pre -->|ask| approval' ,
2026-07-11 23:14:09 +08:00
' approval -->|allowed-once| guards' ,
2026-07-09 15:25:18 +08:00
' approval -->|rejected, cancelled, unavailable| denied' ,
2026-07-23 02:53:43 +08:00
' approval -.->|throw| normalized' ,
2026-07-04 12:50:06 +08:00
' denied --> post' ,
2026-07-23 02:53:43 +08:00
' pre -.->|throw| normalized' ,
2026-07-04 12:50:06 +08:00
' toolBody --> fsGate' ,
' fsGate --> toolBody' ,
2026-07-03 01:32:01 +08:00
' toolBody --> owned' ,
2026-07-08 10:06:07 +08:00
' toolBody --> around' ,
' around --> post' ,
2026-07-23 02:53:43 +08:00
' around -.->|wrapper throws| normalized' ,
' post -.->|throw| normalized' ,
' post --> finalize' ,
' normalized --> finalize' ,
' finalize --> final' ,
2026-07-11 22:55:26 +08:00
' final --> toolResult' ,
2026-07-04 12:50:06 +08:00
' toolResult --> presentResult' ,
2026-07-11 22:55:26 +08:00
' toolResult --> allResults' ,
' allResults --> context' ,
2026-07-03 01:13:52 +08:00
'```' ,
'' ,
2026-07-23 03:15:15 +08:00
'Filesystem read-before-edit checks stay below `tool-fs` on `fs/*` events. Generic pre/post waterfalls host hooks and approval policy; `ctx.approval` resolves asks before monotonic guards, and owner policy that must not be reordered remains a registered guard. Around-dispatch concerns such as timeouts wrap `tools/execute`. The registry losslessly snapshots the candidate result and normalizes a snapshot failure before the visible definition\'s snapshotted `finalizeContent` callback enforces its synchronous content-only invariant. `tools/result` then observes the immutable, lossless-JSON outcome. This lets hooks span tool families without coupling the tools to one policy service. Code Mode sends both the reserved `run_code` transport and its serialized sub-calls through the pipeline; sub-calls carry the parent token, log `tool/code-dispatch`, surface denials as binding rejections, and omit `additionalContexts` to preserve call/result adjacency.' ,
2026-07-03 01:13:52 +08:00
'' ,
2026-07-05 02:54:01 +08:00
. . . maintenanceFooter ( maintenance ) ,
2026-07-03 01:13:52 +08:00
] . join ( '\n' )
}
2026-07-05 01:25:58 +08:00
function renderDocs ( ) : GraphDoc [ ] {
2026-07-14 00:24:04 +08:00
const pkgs = collectPackageGraph ( root , GROUP_ORDER , 'gen-doc-graphs' )
2026-07-03 01:13:52 +08:00
const docs : GraphDoc [ ] = [
2026-07-05 01:25:58 +08:00
{ rel : 'docs/capability-seams.md' , content : renderCapabilitySeams ( pkgs ) } ,
. . . APP_EXAMPLES . map ( example = > ( { rel : example.rel , content : renderAppComposition ( example ) } ) ) ,
{ rel : 'docs/event-producer-consumer.md' , content : renderEventRelations ( pkgs ) } ,
{ rel : 'docs/agent-lifecycle.md' , content : renderLifecycle ( ) } ,
{ rel : 'docs/tool-execution-pipeline.md' , content : renderToolPipeline ( ) } ,
2026-07-03 01:13:52 +08:00
]
2026-07-05 01:25:58 +08:00
docs . unshift ( { rel : 'docs/graph-atlas.md' , content : renderIndex ( docs ) } )
2026-07-03 01:13:52 +08:00
return docs
}
function renderIndex ( docs : GraphDoc [ ] ) : string {
const labels : Record < string , string > = {
2026-07-05 01:25:58 +08:00
'docs/capability-seams.md' : 'capability seams and core services' ,
2026-07-16 16:12:27 +08:00
'examples/headless-agent/composition.md' : 'headless-agent app composition' ,
2026-07-19 01:09:20 +08:00
'examples/tui-agent/composition.md' : 'tui-agent app composition' ,
2026-07-08 11:50:12 +08:00
'examples/cordis-agent/composition.md' : 'cordis-agent app composition' ,
2026-07-05 02:54:01 +08:00
'examples/acp-agent/composition.md' : 'acp-agent app composition' ,
2026-07-05 01:25:58 +08:00
'docs/event-producer-consumer.md' : 'event producer/consumer matrix' ,
'docs/agent-lifecycle.md' : 'agent turn and step lifecycle' ,
'docs/tool-execution-pipeline.md' : 'tool execution pipeline' ,
2026-07-03 01:13:52 +08:00
}
const modes : Record < string , string > = {
2026-07-05 01:25:58 +08:00
'docs/capability-seams.md' : 'hybrid generated' ,
2026-07-16 16:12:27 +08:00
'examples/headless-agent/composition.md' : 'hybrid generated' ,
2026-07-19 01:09:20 +08:00
'examples/tui-agent/composition.md' : 'hybrid generated' ,
2026-07-08 11:50:12 +08:00
'examples/cordis-agent/composition.md' : 'hybrid generated' ,
2026-07-05 02:54:01 +08:00
'examples/acp-agent/composition.md' : 'hybrid generated' ,
2026-07-05 01:25:58 +08:00
'docs/event-producer-consumer.md' : 'hybrid generated' ,
'docs/agent-lifecycle.md' : 'curated' ,
'docs/tool-execution-pipeline.md' : 'curated' ,
2026-07-03 01:13:52 +08:00
}
2026-07-05 01:25:58 +08:00
const rows = [
'| [module dependency graph](module-graph.md) | `generated` |' ,
2026-07-06 22:26:06 +08:00
'| [tool schema catalog and package map](tool-catalog.md) | `generated` |' ,
2026-07-05 01:25:58 +08:00
. . . docs . map ( ( doc ) = > {
2026-07-05 02:54:01 +08:00
const link = graphIndexLink ( doc . rel )
2026-07-05 01:25:58 +08:00
return ` | [ ${ labels [ doc . rel ] ? ? link } ]( ${ link } ) | \` ${ modes [ doc . rel ] ? ? 'generated' } \` | `
} ) ,
]
2026-07-05 02:54:01 +08:00
const maintenance = 'mixed: each linked page declares generated, hybrid, or curated mode'
2026-07-03 01:13:52 +08:00
return [
2026-07-05 02:54:01 +08:00
. . . generatedHeader ( 'Documentation Graph Index' ) ,
2026-07-06 22:26:06 +08:00
'These diagrams are the relationship layer above the generated catalogs. Use them to navigate package topology, capability seams, event flow, model-facing tools, app composition, and runtime lifecycle paths. Exact signatures and type shapes still live in the generated [events](cordis-catalog/events.md) / [services](cordis-catalog/services.md) catalogs, [tool-catalog.md](tool-catalog.md), and [core-data-structures/](core-data-structures/core.md).' ,
2026-07-03 01:13:52 +08:00
'' ,
2026-07-26 23:08:47 +08:00
'The process decision behind this index is recorded in [the documentation graph Agent Note](../.agents/notes/archived/process/2026-07-03-documentation-graph-atlas.md).' ,
2026-07-03 01:13:52 +08:00
'' ,
'| Graph | Mode |' ,
'| --- | --- |' ,
2026-07-05 01:25:58 +08:00
. . . rows ,
2026-07-03 01:13:52 +08:00
'' ,
'Regenerate with `pnpm run gen-doc-graphs`; verify freshness with `pnpm run verify-doc-graphs`.' ,
'' ,
2026-07-05 02:54:01 +08:00
. . . maintenanceFooter ( maintenance ) ,
2026-07-03 01:13:52 +08:00
] . join ( '\n' )
}
2026-07-05 01:25:58 +08:00
function main ( ) : void {
const docs = renderDocs ( )
2026-07-03 01:13:52 +08:00
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 )
}
2026-07-05 01:25:58 +08:00
for ( const doc of docs ) {
mkdirSync ( dirname ( resolve ( root , doc . rel ) ) , { recursive : true } )
writeFileSync ( resolve ( root , doc . rel ) , doc . content )
}
2026-07-03 01:13:52 +08:00
console . log ( ` gen-doc-graphs: wrote ${ docs . length } graph doc(s). ` )
}
if ( process . argv [ 1 ] && import . meta . filename === resolve ( process . argv [ 1 ] ) ) {
2026-07-05 01:25:58 +08:00
main ( )
2026-07-03 01:13:52 +08:00
}