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
* /
import { existsSync , globSync , 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-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-04 12:50:06 +08:00
const GROUP_ORDER = [
'util' ,
'llm' ,
'core' ,
'bash' ,
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' ,
'web' ,
'todo' ,
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-04 12:50:06 +08:00
'support' ,
'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.' ,
} ,
{
key : 'sessions' ,
pkg : 'session' ,
title : 'In-memory session store' ,
mode : 'core' ,
2026-07-10 16:51:19 +08:00
consumers : [ 'agent-loop' , 'agent' , 'session-persistence' , 'session-query' , 'subagent-inprocess' , 'invariants' ] ,
2026-07-03 01:13:52 +08:00
note : 'Owns append-only Session instances and emits the durable session event feed.' ,
} ,
{
key : 'sessionPersistence' ,
pkg : 'session-persistence' ,
title : 'Durable session persistence seam' ,
mode : 'seam' ,
implementations : [ 'session-persistence-jsonl' , 'session-persistence-sqlite' ] ,
2026-07-10 16:51:19 +08:00
consumers : [ 'agent-loop' , 'acp' , 'session-query' ] ,
2026-07-03 01:13:52 +08:00
note : 'Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time.' ,
} ,
2026-07-10 16:51:19 +08:00
{
key : 'sessionQuery' ,
pkg : 'session-query' ,
2026-07-11 12:20:35 +08:00
title : 'Exact session-history reads' ,
2026-07-10 16:51:19 +08:00
mode : 'seam' ,
2026-07-11 12:20:35 +08:00
note : 'Resolves live and optional persisted logs into one logical corpus for exact reads.' ,
2026-07-10 16:51:19 +08:00
} ,
2026-07-03 01:13:52 +08:00
{
key : 'systemPrompt' ,
pkg : 'system-prompt' ,
title : 'System prompt assembly registry' ,
mode : 'core' ,
2026-07-04 12:50:06 +08:00
consumers : [ 'agent-loop' , 'tools' , 'tool-fs' , '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-09 23:11:10 +08:00
consumers : [ 'agent-loop' , 'tool-ask-user' , 'tool-bash' , 'tool-cordis' , 'tool-fs' , 'tool-skill' , 'tool-subagent' , 'tool-todo' , 'tool-web' , 'acp' ] ,
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' ,
implementations : [ 'stdio-agent' , 'acp' ] ,
consumers : [ 'tool-ask-user' , 'stdio-agent' , 'acp' ] ,
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-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' ,
title : 'Agent registry' ,
mode : 'core' ,
consumers : [ 'agent-loop' , 'acp' , 'subagent-inprocess' , 'stdio-agent' , 'invariants' ] ,
note : 'Owns live Agent handles and the create/resume factory seam.' ,
} ,
{
key : 'agentLoop' ,
pkg : 'agent-loop' ,
title : 'Concrete loop driver' ,
mode : 'bundle' ,
consumers : [ 'agent-core' ] ,
note : 'The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package.' ,
} ,
{
key : 'bash' ,
pkg : 'bash' ,
title : 'Bash executor seam' ,
mode : 'seam' ,
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-09 15:42:37 +08:00
{
key : 'sandbox' ,
pkg : 'sandbox' ,
title : 'Process-sandbox seam' ,
mode : 'seam' ,
implementations : [ 'sandbox-local' ] ,
2026-07-09 16:05:44 +08:00
consumers : [ 'bash-sandbox' ] ,
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.' ,
} ,
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 : [ ] ,
consumers : [ 'acp' ] ,
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' ,
implementations : [ 'fs-local' ] ,
consumers : [ 'tool-fs' ] ,
companions : [ 'fs-policy' ] ,
note : 'tool-fs executes read/write/edit through ctx.fs; 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' ] ,
note : 'The basic backend currently consumes the pre-step event directly; a model-facing compact tool remains deferred.' ,
} ,
{
key : 'subagents' ,
pkg : 'subagent' ,
title : 'Subagent provider registry' ,
mode : 'seam' ,
implementations : [ 'subagent-spawn' , 'subagent-fork' , 'subagent-acp' , 'subagent-mock' ] ,
consumers : [ 'tool-subagent' ] ,
note : 'Providers implement transports; tool-subagent exposes one configured provider as a model-facing tool name.' ,
} ,
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-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-06 03:14:07 +08:00
consumers : [ 'tool-workflow' ] ,
2026-07-09 18:50:29 +08:00
note : 'One engine per context (bash shape, no named-provider registry); the worker-thread engine fans agent() calls out through ctx.subagents.' ,
2026-07-06 03:14:07 +08:00
} ,
2026-07-03 01:13:52 +08:00
]
const DYNAMIC_EVENT_DISPATCHERS : Array < { event : string ; pkg : string ; method : string } > = [
2026-07-12 05:13:17 +08:00
// Creation notifications preserve synchronous veto/rollback but observe
// returned promises explicitly so async listener rejection is not unhandled.
{ event : 'agent/created' , pkg : 'agent' , method : 'events.dispatch' } ,
2026-07-12 08:57:05 +08:00
// Registry disposal reuses the stable carrier captured before entry commit
// and contains each listener directly rather than rebuilding via agentEvents.
{ event : 'agent/disposed' , pkg : 'agent' , method : 'events.dispatch' } ,
2026-07-12 05:13:17 +08:00
{ event : 'session/created' , pkg : 'session' , method : 'events.dispatch' } ,
2026-07-12 18:57:42 +08:00
// Session event callbacks are likewise resolved before the log push, then
// invoked individually after commit so observer failures are contained.
{ event : 'session/event' , pkg : 'session' , method : 'events.dispatch' } ,
// Flush resolves the scoped callback set directly so internal instrumentation
// cannot substitute the accepted session before parallel invocation.
{ event : 'session/flush' , pkg : 'session' , method : 'events.dispatch' } ,
2026-07-12 05:13:17 +08:00
// Session disposal uses direct callback resolution so teardown contains each
// synchronous throw and returned-promise rejection independently.
{ event : 'session/disposed' , pkg : 'session' , method : 'events.dispatch' } ,
2026-07-13 11:58:55 +08:00
// tools/result uses ctx.events.dispatch directly so the registry can invoke
// every synchronous observer while containing each callback independently.
2026-07-11 22:55:26 +08:00
{ event : 'tools/result' , pkg : 'tools' , method : 'events.dispatch' } ,
2026-07-03 01:13:52 +08:00
// Subagent lifecycle events intentionally bypass ctx.emit and call
// ctx.events.dispatch directly so one throwing listener cannot starve later
// listeners or strand an already-started child run.
{ event : 'subagent/start' , pkg : 'subagent' , method : 'events.dispatch' } ,
{ event : 'subagent/end' , pkg : 'subagent' , method : 'events.dispatch' } ,
2026-07-09 12:36:18 +08:00
// provider-removed fires inside the provider registration's DISPOSER and
// routes through the same contained dispatch (see emitLifecycle in
// dsh-subagent), so the AST scan cannot attribute it either.
{ event : 'subagent/provider-removed' , pkg : 'subagent' , method : 'events.dispatch' } ,
2026-07-09 18:50:29 +08:00
// The workflow/* lifecycle events dispatch the same way, for the same
// per-listener-containment reason (WorkflowService.emitWorkflowEvent).
{ event : 'workflow/start' , pkg : 'workflow' , method : 'events.dispatch' } ,
{ event : 'workflow/phase' , pkg : 'workflow' , method : 'events.dispatch' } ,
{ event : 'workflow/log' , pkg : 'workflow' , method : 'events.dispatch' } ,
{ event : 'workflow/agent-start' , pkg : 'workflow' , method : 'events.dispatch' } ,
{ event : 'workflow/agent-end' , pkg : 'workflow' , method : 'events.dispatch' } ,
{ event : 'workflow/end' , pkg : 'workflow' , method : 'events.dispatch' } ,
2026-07-03 01:13:52 +08:00
]
2026-07-12 18:57:42 +08:00
const DYNAMIC_EVENT_LISTENERS : Array < { event : string ; pkg : string } > = [
// The invariants oracle marks the session started from its global
// internal/dispatch listener before product session-start callbacks run.
{ event : 'agent/session-start' , pkg : 'invariants' } ,
]
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 = [
{
id : 'echo' ,
2026-07-05 02:54:01 +08:00
rel : 'examples/echo-agent/composition.md' ,
2026-07-05 01:25:58 +08:00
title : 'Echo Agent App Composition' ,
label : 'examples/echo-agent' ,
config : 'examples/echo-agent/cordis.yml' ,
summary : 'The echo demo swaps in a local mock LLM and teaching echo tool, then loads the stdio app package for the shared spine and terminal front door.' ,
} ,
{
id : 'coding' ,
2026-07-05 02:54:01 +08:00
rel : 'examples/coding-agent/composition.md' ,
2026-07-05 01:25:58 +08:00
title : 'Coding Agent App Composition' ,
label : 'examples/coding-agent' ,
config : 'examples/coding-agent/cordis.yml' ,
summary : 'The coding REPL demo adds the real DeepSeek adapter, filesystem tools, todo_write, compaction, and both subagent transports on top of the stdio app package.' ,
} ,
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' ,
summary : 'The self-referential demo puts @deepseek-ai/dsh-tool-cordis on the coding spine, letting the agent inspect its own runtime and mount/unmount plugins into it.' ,
} ,
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-05 01:25:58 +08:00
title : 'ACP Agent App Composition' ,
label : 'examples/acp-agent' ,
config : 'examples/acp-agent/cordis.yml' ,
summary : 'The ACP demo exposes the same agent spine over JSON-RPC stdio, with no stdout logger and no pre-created agent; clients create sessions through the ACP bridge.' ,
} ,
]
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' )
lines . push ( ` ${ appNode } --> ${ agentCore } ["@deepseek-ai/dsh-agent-core"] ` )
lines . push ( ` ${ appNode } --> ${ jsonl } ["@deepseek-ai/dsh-session-persistence-jsonl"] ` )
if ( pluginName === '@deepseek-ai/dsh-stdio-agent' ) {
lines . push ( ` ${ appNode } --> ${ nodeId ( 'frontdoor' , 'stdio' ) } ["readline UI<br/>console logger<br/>pre-created main agent"] ` )
} else if ( pluginName === '@deepseek-ai/dsh-acp-agent' ) {
lines . push ( ` ${ appNode } --> ${ nodeId ( 'frontdoor' , 'acp' ) } ["@deepseek-ai/dsh-acp<br/>JSON-RPC stdio bridge<br/>sessions created by client"] ` )
}
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 } ` )
if ( plugin . name === '@deepseek-ai/dsh-stdio-agent' || plugin . name === '@deepseek-ai/dsh-acp-agent' ) {
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' )
}
function collectEventRelations ( ) : Map < string , EventRelation > {
const out = new Map < string , EventRelation > ( )
const ensure = ( event : string ) : EventRelation = > {
const existing = out . get ( event )
if ( existing ) return existing
const next = { dispatchers : new Map < string , Set < string > > ( ) , listeners : new Set < string > ( ) }
out . set ( event , next )
return next
}
for ( const rel of globSync ( 'packages/*/*/src/**/*.ts' , { cwd : root } ) . sort ( ) ) {
const [ , , leaf ] = rel . split ( '/' )
if ( leaf === undefined ) continue
const text = readFileSync ( resolve ( root , rel ) , 'utf8' )
const sf = ts . createSourceFile ( rel , text , ts . ScriptTarget . Latest , true )
const visit = ( node : ts.Node ) : void = > {
if ( ts . isCallExpression ( node ) && ts . isPropertyAccessExpression ( node . expression ) ) {
const method = node . expression . name . text
if ( ! isCordisContextReceiver ( node . expression , sf ) ) {
ts . forEachChild ( node , visit )
return
}
if ( method === 'on' ) {
const event = eventArg ( node . arguments , method )
if ( event ) ensure ( event ) . listeners . add ( leaf )
2026-07-12 22:49:46 +08:00
} else if ( method === 'emit' || method === 'parallel' || method === 'serial' || method === 'waterfall' ) {
2026-07-03 01:13:52 +08:00
const event = eventArg ( node . arguments , method )
if ( event ) {
const relation = ensure ( event )
const methods = relation . dispatchers . get ( leaf ) ? ? new Set < string > ( )
2026-07-12 22:49:46 +08:00
methods . add ( method )
2026-07-03 01:13:52 +08:00
relation . dispatchers . set ( leaf , methods )
}
}
}
ts . forEachChild ( node , visit )
}
visit ( sf )
}
for ( const entry of DYNAMIC_EVENT_DISPATCHERS ) {
const relation = ensure ( entry . event )
const methods = relation . dispatchers . get ( entry . pkg ) ? ? new Set < string > ( )
methods . add ( entry . method )
relation . dispatchers . set ( entry . pkg , methods )
}
2026-07-12 18:57:42 +08:00
for ( const entry of DYNAMIC_EVENT_LISTENERS ) {
ensure ( entry . event ) . listeners . add ( entry . pkg )
}
2026-07-03 01:13:52 +08:00
return out
}
function isCordisContextReceiver ( expr : ts.PropertyAccessExpression , sf : ts.SourceFile ) : boolean {
2026-07-09 06:06:17 +08:00
// The chained fused-dispatch spelling: `agentEvents(ctx, agent).emit(…)` —
// the receiver is a call expression, not an identifier.
if ( ts . isCallExpression ( expr . expression ) && expr . expression . expression . getText ( sf ) === 'agentEvents' ) {
return true
}
2026-07-03 01:13:52 +08:00
const target = expr . expression . getText ( sf )
2026-07-09 01:36:14 +08:00
if ( target === 'ctx' || target === 'this.ctx' ) return true
2026-07-13 23:27:00 +08:00
// Scoped-dispatch spellings are conventional names. Keep this list in sync
// with renames or the relationship matrix can silently lose an edge.
2026-07-11 22:55:26 +08:00
return target === 'events' || target === 'childCtx' || target === 'this.loopCtx' || target === 'emitCtx'
2026-07-03 01:13:52 +08:00
}
function eventArg ( args : ts.NodeArray < ts.Expression > , method : string ) : string | undefined {
if ( method === 'waterfall' ) {
const arg = args . find ( ts . isStringLiteralLike )
return arg ? . text
}
const first = args [ 0 ]
2026-07-09 01:36:14 +08:00
if ( first && ts . isStringLiteralLike ( first ) ) return first . text
// Scope-carrier dispatch: `emit(carrier, 'event/name', …)` puts the event
// name second. Accept a string literal in position 1 when position 0 is a
// non-literal expression (the carrier).
const second = args [ 1 ]
return second && ts . isStringLiteralLike ( second ) ? second.text : undefined
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-05 02:54:01 +08:00
const maintenance = 'hybrid generated: Cordis event declarations and most producer/listener edges are AST-scanned; dynamic dispatch sites are classified in `scripts/gen-doc-graphs.ts`'
const lines = generatedHeader ( 'Event Producer And Consumer Matrix' )
2026-07-03 01:13:52 +08:00
lines . push (
'This matrix shows which packages dispatch each harness-owned event and which packages listen to it. It is intentionally a table rather than one large graph: events are many-to-many, and dense relation data is easier to review in rows. Dynamic dispatch overrides cover sites that deliberately bypass `ctx.emit`, such as subagent lifecycle containment.' ,
'' ,
'| Event | Mode | Declared in | Dispatchers | Listeners |' ,
'| --- | --- | --- | --- | --- |' ,
)
for ( const event of [ . . . events ] . sort ( ( a , b ) = > a . name . localeCompare ( b . name ) ) ) {
const relation = relations . get ( event . name ) ? ? { dispatchers : new Map < string , Set < string > > ( ) , listeners : new Set < string > ( ) }
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
// unrecognized dispatch spelling. Listener-free extension points remain valid.
2026-07-09 12:36:18 +08:00
const undispatched = [ . . . events ]
. 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' : '' } `
+ ` ${ undispatched . map ( name = > ` " ${ name } " ` ) . join ( ', ' ) } — dead vocabulary, or a dispatch spelling the scan misses `
+ '(teach scripts/gen-doc-graphs.ts the spelling or add a DYNAMIC_EVENT_DISPATCHERS override)' ,
)
}
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' ,
' participant Persistence' ,
2026-07-04 12:50:06 +08:00
' participant SDK as UI or SDK listener' ,
2026-07-03 01:13:52 +08:00
' User->>Agent: send(content)' ,
2026-07-05 01:25:58 +08:00
` Agent-->>SDK: ${ mermaidCode ( 'agent/queued' ) } ` ,
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 ` ,
` Driver->>Session: ${ mermaidCode ( 'turn/start' ) } ` ,
` Driver->>Hooks: ${ mermaidCode ( 'agent/prompt-submit' ) } waterfall ` ,
2026-07-04 12:50:06 +08:00
' Hooks-->>Driver: allow, block, or add context' ,
2026-07-05 01:25:58 +08:00
` Driver->>Session: ${ mermaidCode ( 'user/message' ) } or rejected ${ mermaidCode ( 'turn/end' ) } ` ,
` Driver->>Prompt: ${ mermaidCode ( 'system-prompt/assemble' ) } waterfall ` ,
` Driver-->>Driver: ${ mermaidCode ( 'agent/pre-step' ) } serial checkpoint ` ,
` 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' ) } * ` ,
` Driver->>Hooks: ${ mermaidCode ( 'agent/step-result' ) } waterfall ` ,
` Driver->>Session: ${ mermaidCode ( 'assistant/message' ) } ` ,
` Driver->>Session: ${ mermaidCode ( 'tool/call' ) } ` ,
2026-07-04 12:50:06 +08:00
' Driver->>Tools: execute through pre and post waterfalls' ,
2026-07-03 01:13:52 +08:00
' Tools-->>Session: tool-owned events when applicable' ,
2026-07-05 01:25:58 +08:00
` Driver->>Session: ${ mermaidCode ( 'tool/result' ) } and ${ mermaidCode ( 'step/end' ) } ` ,
` Driver->>Hooks: ${ mermaidCode ( 'agent/turn-continuation' ) } waterfall ` ,
2026-07-11 22:55:26 +08:00
` Driver->>Hooks: ${ mermaidCode ( 'agent/turn-stop' ) } serial terminal checkpoint ` ,
2026-07-05 01:25:58 +08:00
` Driver->>Session: ${ mermaidCode ( 'turn/end' ) } ` ,
` Driver->>Persistence: ${ mermaidCode ( 'session/flush' ) } parallel checkpoint ` ,
` Driver-->>SDK: ${ mermaidCode ( 'agent/status' ) } idle ` ,
2026-07-03 01:13:52 +08:00
'```' ,
'' ,
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-11 22:55:26 +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 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-13 11:58:55 +08:00
` final[" ${ mermaidCode ( 'tools/result' ) } synchronous notification<br/>frozen authoritative outcome"] ` ,
2026-07-04 12:50:06 +08:00
' context["Buffered additionalContext<br/>context/message after all tool results"]' ,
2026-07-05 01:25:58 +08:00
` toolResult["Session event: ${ mermaidCode ( 'tool/result' ) } <br/>single model-facing outcome"] ` ,
2026-07-11 22:55:26 +08:00
' allResults["All calls in the step settled<br/>and tool/result events recorded"]' ,
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-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-04 12:50:06 +08:00
' denied --> post' ,
' 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-11 22:55:26 +08:00
' post --> final' ,
' 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-13 23:27:00 +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`, while `tools/result` observes the immutable outcome after transforms, lossless-JSON validation, and outer error normalization. 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 `additionalContext` 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' )
}
function renderSnapshotReplay ( ) : string {
2026-07-05 02:54:01 +08:00
const maintenance = 'curated Mermaid sequence based on the snapshot test harness'
2026-07-03 01:13:52 +08:00
return [
2026-07-05 02:54:01 +08:00
. . . generatedHeader ( 'ACP Snapshot Replay' ) ,
2026-07-04 12:50:06 +08:00
'This graph explains what a snapshot scenario proves: recorded real-model session logs are replayed keylessly, ACP stdout is normalized and diffed, and scenario workspaces preserve tool side effects that the UI stream alone cannot prove.' ,
2026-07-03 01:13:52 +08:00
'' ,
'```mermaid' ,
'sequenceDiagram' ,
' participant Recorder as Real API recording' ,
' participant Fixture as snapshot fixture' ,
2026-07-04 12:50:06 +08:00
' participant Workspace' ,
2026-07-03 01:13:52 +08:00
' participant Replay as llm-replay adapter' ,
' participant ACP as acp-agent subprocess' ,
' participant Golden as stdout golden' ,
' Recorder->>Fixture: session.jsonl + workspace inputs' ,
2026-07-04 12:50:06 +08:00
' Fixture->>Workspace: seed files and hook configs' ,
2026-07-03 01:13:52 +08:00
' Fixture->>Replay: recorded StreamChunk script' ,
2026-07-05 01:25:58 +08:00
` Replay->>ACP: deterministic ${ mermaidCode ( 'llm/stream' ) } chunks ` ,
2026-07-04 12:50:06 +08:00
' ACP->>Workspace: bash, fs, and hook side effects' ,
2026-07-03 01:13:52 +08:00
' ACP->>Golden: normalized sessionUpdate stream' ,
' Golden-->>ACP: diff must be empty' ,
'```' ,
'' ,
2026-07-04 12:50:06 +08:00
'The fs and hook snapshot matrix is valuable because it proves world state, hook decisions, and failed tool-card rendering, not just that replay returns text.' ,
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-06 00:52:06 +08:00
{ rel : 'packages/ui/acp/snapshot-replay.md' , content : renderSnapshotReplay ( ) } ,
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-05 02:54:01 +08:00
'examples/echo-agent/composition.md' : 'echo-agent app composition' ,
'examples/coding-agent/composition.md' : 'coding-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-06 00:52:06 +08:00
'packages/ui/acp/snapshot-replay.md' : 'ACP snapshot replay' ,
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-05 02:54:01 +08:00
'examples/echo-agent/composition.md' : 'hybrid generated' ,
'examples/coding-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-06 00:52:06 +08:00
'packages/ui/acp/snapshot-replay.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-05 01:25:58 +08:00
'The process decision behind this index is recorded in [the documentation graph RFC](rfc/implemented/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
}