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' ,
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' ,
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-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.' ,
},
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-23 13:56:56 +08:00
consumers : [ 'agent-loop' , 'tool-bash' , 'hooks-claude' , 'hooks-codex' , 'acp' , '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-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-10 16:51:19 +08:00
mode : 'seam' ,
2026-07-15 10:51:38 +08:00
implementations : [ 'session-query-sqlite' ],
2026-07-21 16:46:48 +08:00
consumers : [ 'session-reference' ],
2026-07-23 20:16:14 +08:00
note : 'The interface supplies exact reads, filters, and traces; its concrete backend adds full-text reconciliation, ranking, snippets, and cursor generations on the same service.' ,
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' ,
consumers : [ 'tui' , 'acp' ],
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-21 16:01:00 +08:00
consumers : [ 'agent-loop' , 'tool-ask-user' , 'tool-bash' , 'tool-cordis' , 'tool-fs' , 'tool-pty' , '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' ,
2026-07-20 19:26:04 +08:00
implementations : [ 'tui' , 'acp' ],
consumers : [ 'tool-ask-user' , 'tui' , 'acp' ],
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-20 22:13:59 +08:00
consumers : [ 'acp' ],
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' ,
consumers : [ 'tui' , 'acp' ],
2026-07-20 20:43:02 +08:00
note : 'Plugins register direct human commands; TUI and ACP consume the same 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.' ,
},
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.' ,
},
2026-07-14 20:05:57 +08:00
{
key : 'sandboxPolicy' ,
2026-07-20 13:59:18 +08:00
pkg : 'sandbox-policy' ,
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.' ,
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
},
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.' ,
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' ,
2026-07-08 11:00:06 +08:00
implementations : [ 'code-runtime-worker' ],
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' ,
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' ],
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
},
2026-07-09 21:22:54 +08:00
{
key : 'tasks' ,
pkg : 'tasks' ,
title : 'Background task registry' ,
mode : 'core' ,
2026-07-21 16:01:00 +08:00
consumers : [ 'tool-bash' , 'tool-pty' , 'tool-subagent' , 'tool-tasks' ],
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.' ,
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-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>`
}
function repoLink ( path : string , label : string , up = '..' ) : string {
return `[ ${ label } ]( ${ up } / ${ path } )`
}
function sourceLink ( source : string , up = '..' ) : string {
return repoLink ( source . split ( ':' )[ 0 ] ?? source , `\` ${ source } \`` , up )
}
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
}
function pkgList ( names : string [] | undefined , pkgsByShort : Map < string , Pkg >) : string {
if ( ! names || names . length === 0 ) return '-'
return names . map ( name => pkgLink ( pkgsByShort . get ( name ), name )). join ( ', ' )
}
function tableCell ( value : string ) : string {
return value . replace ( /\|/g , '\\|' ). replace ( /\n/g , '<br>' )
}
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-16 16:12:27 +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' ,
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 ]
2026-07-20 19:26:04 +08:00
function renderAppExpansion ( lines : string [], appNode : string , pluginName : string ) : void {
2026-07-05 01:25:58 +08:00
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-05 01:25:58 +08:00
lines . push ( ` ${ appNode } --> ${ nodeId ( 'frontdoor' , 'acp' ) } ["@deepseek-ai/dsh-acp<br/>JSON-RPC stdio bridge<br/>sessions created by client"]` )
}
2026-07-03 01:13:52 +08:00
lines . push (
2026-07-05 01:25:58 +08:00
` ${ 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-05 01:25:58 +08:00
lines . push (
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' ) {
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 ()
2026-07-03 01:13:52 +08:00
}
2026-07-14 23:14:51 +08:00
/** Return all event relations discovered from the Program. */
collect () : Map < string , EventRelation > {
for ( const source of this . sources ) this . visitSource ( source )
return this . relations
}
/** 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 )
}
/** 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 )
}
}
/** 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
}
return current
}
/** 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 ])
}
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 )
}
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 )
2026-07-03 01:13:52 +08:00
return out
}
2026-07-14 23:14:51 +08:00
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-14 23:14:51 +08:00
// unrecognized semantic dispatch shape. 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' : '' } `
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` ,
` Driver->>Session: ${ mermaidCode ( 'turn/start' ) } ` ,
` 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-05 01:25:58 +08:00
` Driver->>Session: ${ mermaidCode ( 'user/message' ) } or rejected ${ mermaidCode ( 'turn/end' ) } ` ,
` 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-24 21:18:48 +08:00
' Hooks-->>Driver: call agent.retry() 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-24 21:18:48 +08:00
` Driver->>Hooks: ${ mermaidCode ( 'agent/stopping' ) } serial terminal checkpoint` ,
2026-07-15 16:03:52 +08:00
' end' ,
2026-07-05 01:25:58 +08:00
` Driver->>Session: ${ mermaidCode ( 'turn/end' ) } ` ,
` 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"]` ,
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' )
}
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' ,
2026-07-19 17:45:49 +08:00
' participant Expected as stdout expected output' ,
2026-07-03 01:13:52 +08:00
' 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-19 17:45:49 +08:00
' ACP->>Expected: normalized sessionUpdate stream' ,
' Expected-->>ACP: diff must be empty' ,
2026-07-03 01:13:52 +08:00
'```' ,
'' ,
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-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-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-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-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-19 22:50:49 +08:00
'The process decision behind this index is recorded in [the documentation graph Agent Note](../.agents/notes/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
}