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'
2026-07-28 23:48:35 +08:00
import { projectCordisCatalog } from '@deepseek-ai/dsh-typert-generator'
import { CORDIS_CATALOG_POLICY } from './gen-cordis-catalog.ts'
import type { EventEntry , ServiceEntry } from '@deepseek-ai/dsh-typert-generator'
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-30 11:20:49 +08:00
/** One scanned package source file and its owning package short name. */
export interface PackageSource {
/** Repository-relative path. */
2026-07-14 23:14:51 +08:00
rel : string
2026-07-30 11:20:49 +08:00
/** Package short name from the `packages/<group>/<pkg>/src` path. */
2026-07-14 23:14:51 +08:00
pkg : string
2026-07-30 11:20:49 +08:00
/** The bound program source file. */
2026-07-14 23:14:51 +08:00
sourceFile : ts.SourceFile
}
type EventReceiverKind = 'context' | 'agent-dispatch' | 'events-service'
2026-07-04 12:50:06 +08:00
const GROUP_ORDER = [
'util' ,
2026-07-23 15:20:47 +08:00
'attachment' ,
2026-07-04 12:50:06 +08:00
'llm' ,
'core' ,
2026-07-28 23:48:35 +08:00
'typert' ,
2026-07-19 18:47:34 +08:00
'goal' ,
2026-08-18 11:11:34 +08:00
'experimental' ,
refactor(process): split the process manager out of the bash executor
New process/ capability family: @deepseek-ai/dsh-process owns ctx.processes —
abstract ProcessManager.spawn(spec) over a fully-explicit ProcessSpawnSpec —
plus the shared DSH_* managed-environment and CollectedOutput vocabulary;
@deepseek-ai/dsh-process-local carries the former bash-local run.ts plumbing
(detached groups, tail-keep spill-backed output, credential scrub, kill
escalation, kill-and-join disposal) with no config of its own.
dsh-bash-local becomes a consumer: it keeps command defaulting, the fused
deadline timedOut/aborted classification, the model-friendly terminal env
(now merged through the ordinary env channel), and the [stderr]-marked
background read merge, and spawns through ctx.processes. Background-process
lifetime moves to the manager, so an executor reload no longer kills live
background work; a background spawn failure is injected once into the read
path instead of being buffered as fake stderr. dsh-bash re-exports the moved
vocabulary so bash consumers keep one import root; dsh-bash-sandbox only
redeclares the inherited inject.
Every composition loading a bash executor now loads dsh-process-local (CLI,
examples, python bundled runtime, create-sdk bash feature, inline test
configs).
2026-07-26 06:59:01 +08:00
'process' ,
2026-07-04 12:50:06 +08:00
'bash' ,
2026-07-21 16:01:00 +08:00
'pty' ,
2026-07-09 15:42:37 +08:00
'sandbox' ,
2026-08-07 21:04:33 +08:00
'e2b' ,
2026-07-04 12:50:06 +08:00
'fs' ,
2026-07-10 14:19:06 +08:00
'skill' ,
2026-07-04 12:50:06 +08:00
'compact' ,
'subagent' ,
feat(tasks): background task runtime, generic task_* control tools, bash/subagent producers
One shared ctx.tasks registry (branded <kind>-N ids, owner-fenced
read/kill/wait/list, attachSurface misconfiguration fence, reported-flag
notice dedup, atomic register) + dsh-tool-tasks (task_output/task_list/
task_kill, completion-notice injection, background prompt habit).
Producers opt in via their own enableRunInBackground config: bash
(stream kind; seam slimmed to resolve/run/start returning a BashProcess
handle, bash_output/bash_kill deleted) and subagent (final-output kind;
done settles after run.dispose()). Owner disposal drains tasks through
the new awaited ctx.agents.onCleanup seam in the loop's disposal chain.
Both RFCs moved to implemented/; docs, catalogs, snapshots re-pinned.
2026-07-09 21:22:54 +08:00
'tasks' ,
2026-07-10 10:13:13 +08:00
'workflow' ,
2026-07-04 12:50:06 +08:00
'web' ,
2026-08-22 23:44:56 +08:00
'webhook' ,
2026-07-08 19:20:50 +08:00
'spill' ,
2026-07-04 12:50:06 +08:00
'todo' ,
2026-07-22 16:57:23 +08:00
'plan' ,
2026-07-08 11:50:12 +08:00
'cordis' ,
2026-07-04 12:50:06 +08:00
'hooks' ,
'session-persistence' ,
2026-07-10 16:51:19 +08:00
'session-query' ,
2026-07-21 01:54:00 +08:00
'session-title' ,
2026-07-23 03:06:57 +08:00
'telemetry' ,
chore(storage,workspace): gates — coverage, catalogs, bilingual note
- Per-file 100% coverage across the five new packages (invariant
companion suites, failure-injection negatives, lifecycle and
malformed-medium branches).
- Canonical README Model Experience / Known Limitations sections; new
storage/ and workspace/ group READMEs; packages/README.md rows (budget
ceiling raised 760 → 790 for the two new groups).
- Cordis catalog/type-link registrations, service-role classification,
and regenerated catalogs/graphs for the new services and events.
- Agent Note: English body + i18n pairing record; design-sketch fences
opted out of doc-typecheck as ignore-check.
- Two exactOptionalPropertyTypes/discriminant fixes in new tests.
doc-sync (24 gates), typecheck, hygiene, and the five-package suite
(92 tests) all pass.
2026-07-24 22:49:57 +08:00
'storage' ,
'workspace' ,
2026-07-04 12:50:06 +08:00
'support' ,
2026-07-24 01:40:25 +08:00
'acp' ,
2026-07-04 12:50:06 +08:00
'ui' ,
]
2026-07-03 01:13:52 +08:00
const SERVICE_ROLES : ServiceRole [ ] = [
2026-07-23 15:20:47 +08:00
{
key : 'attachments' ,
pkg : 'attachment' ,
title : 'Durable binary attachment storage' ,
mode : 'seam' ,
implementations : [ 'attachment-local' ] ,
2026-08-25 23:47:20 +08:00
consumers : [ 'api-session-controller' , 'host-apiproxy' , 'tool-fs' , 'llm-pi-ai' , 'llm-deepseek' ] ,
2026-07-23 15:20:47 +08:00
note : 'The host commits accepted images before session events; provider adapters resolve authorized durable references into provider-native content.' ,
} ,
2026-07-03 01:13:52 +08:00
{
key : 'llm' ,
pkg : 'llm' ,
title : 'LLM adapter registry' ,
mode : 'seam' ,
implementations : [ 'llm-deepseek' , 'llm-pi-ai' , 'llm-replay' ] ,
2026-08-13 00:36:22 +08:00
consumers : [ 'agent-loop' , 'compaction-basic' ] ,
2026-07-03 01:13:52 +08:00
note : 'Adapters register provider implementations; the loop and compaction call the provider-neutral stream service.' ,
} ,
2026-08-22 20:03:23 +08:00
{
key : 'deepseekLlmApiExtensions' ,
pkg : 'deepseek-llm-api-extensions' ,
title : 'Official DeepSeek request extensions' ,
mode : 'seam' ,
2026-08-22 02:02:38 +08:00
implementations : [ 'session-log-deepseek' , 'plugin-package-inventory-deepseek' ] ,
2026-08-22 20:03:23 +08:00
consumers : [ 'llm-deepseek' ] ,
note : 'Plugins prepare independent top-level fields; the official adapter merges them and commits their delivery state after HTTP acceptance.' ,
} ,
2026-07-15 14:47:29 +08:00
{
key : 'tokenMeter' ,
pkg : 'token-meter' ,
title : 'Replay token measurement' ,
mode : 'core' ,
2026-08-13 00:36:22 +08:00
consumers : [ 'compaction-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
{
2026-08-13 00:36:22 +08:00
key : 'toolResultPruner' ,
pkg : 'compaction-tool-result-pruner' ,
2026-07-16 18:02:15 +08:00
title : 'Model-free tool-result pruning' ,
mode : 'core' ,
2026-08-13 00:36:22 +08:00
consumers : [ 'compaction-basic' ] ,
2026-07-16 18:02:15 +08:00
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-08-25 23:47:20 +08:00
consumers : [ 'agent-loop' , 'agent' , 'session-persistence' , 'session-query' , 'session-query-sqlite' , 'subagent-in-process-driver' , 'invariants' , 'message-feedback' ] ,
2026-07-03 01:13:52 +08:00
note : 'Owns append-only Session instances and emits the durable session event feed.' ,
} ,
2026-08-22 21:13:53 +08:00
{
key : 'sessionController' ,
pkg : 'api-session-controller' ,
title : 'Host Session Remote controller' ,
mode : 'core' ,
2026-08-25 23:47:20 +08:00
consumers : [ 'host-apiproxy' ] ,
2026-08-22 21:13:53 +08:00
note : 'Owns Session commands, cold reads, durable-event following, live control state, and Agent activation policy; apiProxy reuses its inspection and Agent-resolution operations for Session-aware domains.' ,
} ,
2026-08-23 06:14:32 +08:00
{
key : 'workspaceController' ,
pkg : 'api-workspace-controller' ,
title : 'Host Workspace Remote controller' ,
mode : 'core' ,
note : 'Owns Workspace commands and reconnect-safe Workspace state delivery through the generated Remote namespace.' ,
} ,
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-28 23:48:35 +08:00
{
key : 'typert' ,
pkg : 'typert-registry' ,
title : 'Runtime type registry' ,
mode : 'core' ,
2026-08-05 11:17:47 +08:00
consumers : [ 'typert-loader' , 'api-gateway' ] ,
note : 'Plugins register live zod contributions directly or through dsh-typert-loader; the API gateway consumes invocation descriptors and providers, while other runtime consumers query schemas and reflection metadata at their own edges.' ,
} ,
{
key : 'typertGateway' ,
pkg : 'api-gateway' ,
2026-08-13 00:36:22 +08:00
title : 'Typert Host invocation gateway' ,
2026-08-05 11:17:47 +08:00
mode : 'core' ,
note : 'Associates generated Remote descriptors with live Cordis services, resolves registered identities, and exposes unary calls through the shared Connection RPC carrier.' ,
2026-07-28 23:48:35 +08:00
} ,
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-08-13 00:36:22 +08:00
consumers : [ 'agent-loop' , 'tool-bash' , 'hooks-claude-code' , 'hooks-codex' , 'session-query' , 'session-query-sqlite' , 'message-feedback' ] ,
2026-07-03 01:13:52 +08:00
note : 'Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time.' ,
} ,
feat(settings): add user-settings seam (ctx.settings) + file provider
Two-package capability family mirroring session-persistence/:
- dsh-settings: abstract Settings service — namespace registry with
caller-fiber effect registrations, layered resolution (schema defaults
< composition base < user document), schemastery validation,
per-namespace deep-equal commit detection, and the settings/updated
event. Boot/registration validation fails loud; provider publishes
keep last-good per namespace.
- dsh-settings-local: settings.yaml/.json provider — resolveSpec
defaulting to $DSH_HOME/settings.yaml, chokidar hot reload,
content-equality self-write suppression, atomic 0600 tmp+rename
writes, comment-preserving YAML namespace patching.
Consumers register inside ctx.inject(['settings'], …), so every
composition works unchanged without a mounted provider. Real Loader +
Include composition test proves cordis.yml boot and external-edit hot
propagation; HMR disposal test proves registry cleanup. Both packages
hold per-file 100% coverage.
Doc budgets rise 1705→1710 (AGENTS.md) and 835→845 (packages/README.md):
one structural line per file for the new package group.
Agent Note: .agents/notes/implemented/architecture/2026-07-28-user-settings-seam.md
2026-07-28 17:30:12 +08:00
{
key : 'settings' ,
pkg : 'settings' ,
title : 'User-settings seam' ,
mode : 'seam' ,
2026-08-13 00:36:22 +08:00
implementations : [ 'settings-file' ] ,
2026-08-25 23:47:20 +08:00
consumers : [ 'llm-deepseek' , 'llm-pi-ai' , 'host-apiproxy' ] ,
2026-07-30 10:53:39 +08:00
note : 'Plugins register namespace schemas and resolve layered values; providers store the raw document. The LLM adapters register their entry config as the composition base under the user section; the web gateway serves redacted layered descriptors and writes the user layer.' ,
docs: bilingual credentials/settings-consumer documentation, catalogs, and gates
New credentials data-structure page (type-equiv manifested), group README,
rewritten llm-deepseek/llm-pi-ai READMEs (dynamic configuration, dict
profiles, credential chain), capability-seams/service-role registration,
Agent Note (bilingual), demo compositions mounting settings-local +
credentials-local with no inline key plumbing, installSettingsSection
consumer helper on the settings seam (deduplicating both adapters' wiring),
jscpd symmetry markers for the provider twins, runtime-closure additions for
python/sdk-runtime, and doc-budget ceilings AGENTS.md 1750→1755 /
packages/README.md 850→865 for the structural one-line group rows.
2026-07-29 14:20:06 +08:00
} ,
2026-08-24 18:23:42 +08:00
{
key : 'subagentModelSelection' ,
pkg : 'tool-subagent' ,
title : 'Subagent model-selection preference' ,
mode : 'core' ,
consumers : [ 'tool-subagent' ] ,
note : 'Owns the default-off settings namespace that Agent-scoped delegation tools sample when composing a new top-level Session.' ,
} ,
docs: bilingual credentials/settings-consumer documentation, catalogs, and gates
New credentials data-structure page (type-equiv manifested), group README,
rewritten llm-deepseek/llm-pi-ai READMEs (dynamic configuration, dict
profiles, credential chain), capability-seams/service-role registration,
Agent Note (bilingual), demo compositions mounting settings-local +
credentials-local with no inline key plumbing, installSettingsSection
consumer helper on the settings seam (deduplicating both adapters' wiring),
jscpd symmetry markers for the provider twins, runtime-closure additions for
python/sdk-runtime, and doc-budget ceilings AGENTS.md 1750→1755 /
packages/README.md 850→865 for the structural one-line group rows.
2026-07-29 14:20:06 +08:00
{
key : 'credentials' ,
pkg : 'credentials' ,
title : 'Credential seam' ,
mode : 'seam' ,
implementations : [ 'credentials-local' ] ,
2026-08-25 23:47:20 +08:00
consumers : [ 'llm-deepseek' , 'llm-pi-ai' , 'host-apiproxy' ] ,
2026-07-30 10:53:39 +08:00
note : 'Configuration carries references to secrets; providers own the values. Consumers resolve per operation, so a rotated credential reaches the very next request; the web gateway exposes value-free views and write-only storage.' ,
feat(settings): add user-settings seam (ctx.settings) + file provider
Two-package capability family mirroring session-persistence/:
- dsh-settings: abstract Settings service — namespace registry with
caller-fiber effect registrations, layered resolution (schema defaults
< composition base < user document), schemastery validation,
per-namespace deep-equal commit detection, and the settings/updated
event. Boot/registration validation fails loud; provider publishes
keep last-good per namespace.
- dsh-settings-local: settings.yaml/.json provider — resolveSpec
defaulting to $DSH_HOME/settings.yaml, chokidar hot reload,
content-equality self-write suppression, atomic 0600 tmp+rename
writes, comment-preserving YAML namespace patching.
Consumers register inside ctx.inject(['settings'], …), so every
composition works unchanged without a mounted provider. Real Loader +
Include composition test proves cordis.yml boot and external-edit hot
propagation; HMR disposal test proves registry cleanup. Both packages
hold per-file 100% coverage.
Doc budgets rise 1705→1710 (AGENTS.md) and 835→845 (packages/README.md):
one structural line per file for the new package group.
Agent Note: .agents/notes/implemented/architecture/2026-07-28-user-settings-seam.md
2026-07-28 17:30:12 +08:00
} ,
2026-08-13 15:33:29 +08:00
{
key : 'authorization' ,
pkg : 'authorization' ,
title : 'Authorization flow registry' ,
mode : 'seam' ,
implementations : [ ] ,
consumers : [ 'llm-pi-ai' ] ,
note : 'Flows are registered by the plugin that knows how to obtain one credential and keyed by the record they write; the seam owns the conversation and the one-attempt-per-key lifecycle, never the protocol.' ,
} ,
2026-07-23 03:06:57 +08:00
{
2026-08-13 00:36:22 +08:00
key : 'sessionTelemetry' ,
2026-07-23 03:06:57 +08:00
pkg : 'session-telemetry' ,
title : 'Session telemetry seam' ,
mode : 'seam' ,
implementations : [ 'session-telemetry-otel' ] ,
consumers : [ ] ,
note : 'The seam captures, redacts, and hands session records to one backend; nothing else consumes the service — its output leaves the process.' ,
} ,
chore(storage,workspace): gates — coverage, catalogs, bilingual note
- Per-file 100% coverage across the five new packages (invariant
companion suites, failure-injection negatives, lifecycle and
malformed-medium branches).
- Canonical README Model Experience / Known Limitations sections; new
storage/ and workspace/ group READMEs; packages/README.md rows (budget
ceiling raised 760 → 790 for the two new groups).
- Cordis catalog/type-link registrations, service-role classification,
and regenerated catalogs/graphs for the new services and events.
- Agent Note: English body + i18n pairing record; design-sketch fences
opted out of doc-typecheck as ignore-check.
- Two exactOptionalPropertyTypes/discriminant fixes in new tests.
doc-sync (24 gates), typecheck, hygiene, and the five-package suite
(92 tests) all pass.
2026-07-24 22:49:57 +08:00
{
key : 'storage' ,
pkg : 'storage' ,
title : 'Non-session storage hub' ,
mode : 'seam' ,
implementations : [ 'storage-json' , 'storage-sqlite' ] ,
2026-07-25 16:04:48 +08:00
consumers : [ 'storage-domain' ] ,
chore(storage,workspace): gates — coverage, catalogs, bilingual note
- Per-file 100% coverage across the five new packages (invariant
companion suites, failure-injection negatives, lifecycle and
malformed-medium branches).
- Canonical README Model Experience / Known Limitations sections; new
storage/ and workspace/ group READMEs; packages/README.md rows (budget
ceiling raised 760 → 790 for the two new groups).
- Cordis catalog/type-link registrations, service-role classification,
and regenerated catalogs/graphs for the new services and events.
- Agent Note: English body + i18n pairing record; design-sketch fences
opted out of doc-typecheck as ignore-check.
- Two exactOptionalPropertyTypes/discriminant fixes in new tests.
doc-sync (24 gates), typecheck, hygiene, and the five-package suite
(92 tests) all pass.
2026-07-24 22:49:57 +08:00
note : 'Backends register side by side under names; data forms (domain first) mount on the hub and translate typed operations into opaque KV-unit primitives.' ,
} ,
2026-07-25 16:04:48 +08:00
{
key : 'storageDomain' ,
pkg : 'storage-domain' ,
title : 'Domain data facility' ,
mode : 'core' ,
2026-08-10 11:28:38 -07:00
consumers : [ 'workspace' , 'message-feedback' ] ,
2026-07-25 16:04:48 +08:00
note : 'Waits for every configured backend, then publishes the domain form as one lifecycle-bound service for typed durable state.' ,
} ,
chore(storage,workspace): gates — coverage, catalogs, bilingual note
- Per-file 100% coverage across the five new packages (invariant
companion suites, failure-injection negatives, lifecycle and
malformed-medium branches).
- Canonical README Model Experience / Known Limitations sections; new
storage/ and workspace/ group READMEs; packages/README.md rows (budget
ceiling raised 760 → 790 for the two new groups).
- Cordis catalog/type-link registrations, service-role classification,
and regenerated catalogs/graphs for the new services and events.
- Agent Note: English body + i18n pairing record; design-sketch fences
opted out of doc-typecheck as ignore-check.
- Two exactOptionalPropertyTypes/discriminant fixes in new tests.
doc-sync (24 gates), typecheck, hygiene, and the five-package suite
(92 tests) all pass.
2026-07-24 22:49:57 +08:00
{
2026-08-10 11:28:38 -07:00
key : 'messageFeedback' ,
pkg : 'message-feedback' ,
title : 'Lifecycle-bound message feedback' ,
mode : 'core' ,
note : 'Owns local per-assistant-message feedback, lifecycle and target validation, per-item compare-and-set, and the Host unary Remote contract without entering Session history or telemetry.' ,
} ,
chore(storage,workspace): gates — coverage, catalogs, bilingual note
- Per-file 100% coverage across the five new packages (invariant
companion suites, failure-injection negatives, lifecycle and
malformed-medium branches).
- Canonical README Model Experience / Known Limitations sections; new
storage/ and workspace/ group READMEs; packages/README.md rows (budget
ceiling raised 760 → 790 for the two new groups).
- Cordis catalog/type-link registrations, service-role classification,
and regenerated catalogs/graphs for the new services and events.
- Agent Note: English body + i18n pairing record; design-sketch fences
opted out of doc-typecheck as ignore-check.
- Two exactOptionalPropertyTypes/discriminant fixes in new tests.
doc-sync (24 gates), typecheck, hygiene, and the five-package suite
(92 tests) all pass.
2026-07-24 22:49:57 +08:00
{
2026-08-13 00:36:22 +08:00
key : 'workspaceRegistry' ,
chore(storage,workspace): gates — coverage, catalogs, bilingual note
- Per-file 100% coverage across the five new packages (invariant
companion suites, failure-injection negatives, lifecycle and
malformed-medium branches).
- Canonical README Model Experience / Known Limitations sections; new
storage/ and workspace/ group READMEs; packages/README.md rows (budget
ceiling raised 760 → 790 for the two new groups).
- Cordis catalog/type-link registrations, service-role classification,
and regenerated catalogs/graphs for the new services and events.
- Agent Note: English body + i18n pairing record; design-sketch fences
opted out of doc-typecheck as ignore-check.
- Two exactOptionalPropertyTypes/discriminant fixes in new tests.
doc-sync (24 gates), typecheck, hygiene, and the five-package suite
(92 tests) all pass.
2026-07-24 22:49:57 +08:00
pkg : 'workspace' ,
title : 'Workspace entity registry' ,
mode : 'core' ,
2026-08-25 23:47:20 +08:00
consumers : [ 'api-workspace-controller' , 'api-session-controller' ] ,
2026-07-25 16:04:48 +08:00
note : 'Owns WorkspaceId-branded records over the domain facility; stable sessionIds accounts drive Host RPC and GUI projections.' ,
chore(storage,workspace): gates — coverage, catalogs, bilingual note
- Per-file 100% coverage across the five new packages (invariant
companion suites, failure-injection negatives, lifecycle and
malformed-medium branches).
- Canonical README Model Experience / Known Limitations sections; new
storage/ and workspace/ group READMEs; packages/README.md rows (budget
ceiling raised 760 → 790 for the two new groups).
- Cordis catalog/type-link registrations, service-role classification,
and regenerated catalogs/graphs for the new services and events.
- Agent Note: English body + i18n pairing record; design-sketch fences
opted out of doc-typecheck as ignore-check.
- Two exactOptionalPropertyTypes/discriminant fixes in new tests.
doc-sync (24 gates), typecheck, hygiene, and the five-package suite
(92 tests) all pass.
2026-07-24 22:49:57 +08:00
} ,
2026-07-10 16:51:19 +08:00
{
key : 'sessionQuery' ,
pkg : 'session-query' ,
2026-07-23 20:16:14 +08:00
title : 'Session reads, traces, filters, and search' ,
2026-07-15 10:51:38 +08:00
mode : 'seam' ,
implementations : [ 'session-query-sqlite' ] ,
2026-07-24 15:09:55 +08:00
consumers : [ 'session-reference' , 'tool-session-query' ] ,
note : 'The interface supplies exact reads, filters, and traces; its concrete backend adds full-text reconciliation, ranking, snippets, and cursor generations, while the model consumer owns workspace authority and cursor-free rendering.' ,
2026-07-10 16:51:19 +08:00
} ,
2026-07-27 15:25:33 +08:00
{
key : 'fileReferences' ,
pkg : 'file-reference' ,
2026-08-14 16:18:40 +08:00
title : 'File reference discovery' ,
2026-07-27 15:25:33 +08:00
mode : 'seam' ,
implementations : [ 'file-reference-local' ] ,
refactor(reference): serve discovery through typert Remote faces
Replace the legacy reference.* API Proxy domain with @Remote methods on the
owning services, following the typert gateway design master adopted on
2026-08-02 (message-feedback and plugin-inventory precedents):
- FileReferenceService and SessionReferenceResolver extend TypertRemoteService;
fileReferences/list and sessionReferenceResolver/candidates are unary Remote
methods cancelled through the reserved trailing signal, and the candidates
face attaches each candidate's canonical mention under the configured limit
- move the wire types to type-only ./types subpaths (FileReferenceCandidate,
SessionReferenceMentionCandidate) and export ./typert plus ./remote artifacts
- mount both contributions in the api-remotes client assembly; ui-reference
consumes ctx.remote instead of connection.api.references and registers zh/en
locale dictionaries for its sections and labels
- delete the reference.* routes, schemas, map rows, client stubs, and fixtures;
the connection fixture serves the Remote endpoints instead
- release deliverPrompt admission listeners when the agent is disposed with the
prepared prompt still pending, and cover the reference-* RpcError codes in
the schema spec
- add the missing tsconfig paths for the /grammar and /types subpaths (clean-
tree vitest could not resolve @deepseek-ai/dsh-file-reference/grammar)
- regenerate the cordis catalog, capability seams, and event matrix; update the
owning bilingual READMEs, Agent Notes, and the reference-composer golden
2026-08-17 18:35:04 +08:00
note : 'The interface returns path-only completion candidates within the addressed Agent cwd through its unary Remote contract; providers own namespace access and ranking without reading file contents.' ,
2026-07-27 15:25:33 +08:00
} ,
2026-07-21 16:46:48 +08:00
{
2026-08-13 00:36:22 +08:00
key : 'sessionReferenceResolver' ,
2026-07-21 16:46:48 +08:00
pkg : 'session-reference' ,
title : 'Cross-session snapshot preparation' ,
mode : 'core' ,
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' ,
2026-08-13 00:36:22 +08:00
implementations : [ 'session-title-first-prompt-llm' , 'session-title-all-prompts-llm' ] ,
2026-07-21 01:54:00 +08:00
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-08-13 00:36:22 +08:00
consumers : [ 'agent-loop' , 'tools' , 'tool-fs' , 'tool-terminal' , '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-08-13 00:36:22 +08:00
consumers : [ 'agent-loop' , 'tool-ask-user' , 'tool-bash' , 'tool-cordis' , 'tool-fs' , 'tool-terminal' , 'tool-skill' , 'tool-subagent' , 'tool-todo' , 'tool-web' ] ,
2026-07-11 22:55:26 +08:00
note : 'Registers capabilities, owns Code Mode transport, and routes calls through pre-policy, monotonic guards, around dispatch, post-policy, and final-result observation.' ,
2026-07-03 01:13:52 +08:00
} ,
2026-07-05 17:05:33 +08:00
{
2026-08-13 00:36:22 +08:00
key : 'userQuestions' ,
pkg : 'user-questions' ,
2026-07-05 17:05:33 +08:00
title : 'Human question/answer seam' ,
mode : 'seam' ,
2026-08-04 10:07:17 +08:00
consumers : [ 'tool-ask-user' ] ,
2026-07-22 15:13:12 +08:00
note : 'UI front ends provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise.' ,
2026-07-05 17:05:33 +08:00
} ,
2026-07-10 01:38:39 +08:00
{
2026-07-22 16:57:23 +08:00
key : 'planMode' ,
pkg : 'plan-mode' ,
title : 'Plan collaboration state' ,
2026-07-10 01:38:39 +08:00
mode : 'core' ,
2026-07-22 16:57:23 +08:00
note : 'Folds logged plan/mode state, flushes user selections at turn boundaries, renders deployment-owned guidance, registers /plan, and keeps the plan-exit schema stable across transitions.' ,
2026-07-10 01:38:39 +08:00
} ,
2026-08-03 20:30:33 +08:00
{
key : 'agentPresets' ,
pkg : 'agent-presets' ,
title : 'Per-session agent composition' ,
mode : 'core' ,
note : 'Discovers preset directories over trusted and user-authored roots and mounts one preset cordis.yml under an agent scope during creation, rejecting a row that never activates or that publishes into the root service realm.' ,
} ,
2026-07-19 22:11:59 +08:00
{
key : 'commands' ,
pkg : 'commands' ,
title : 'Human command registry' ,
mode : 'core' ,
2026-08-04 10:07:17 +08:00
note : 'Plugins register direct human commands without sending invocations to the model.' ,
2026-07-19 22:11:59 +08:00
} ,
2026-07-28 01:09:55 +08:00
{
key : 'sessionProjections' ,
pkg : 'session-projection' ,
title : 'Session projection units' ,
mode : 'core' ,
consumers : [ 'tool-todo' , 'session-title' , 'host-apiproxy' ] ,
note : 'Domains register state-driven fold units; the eager drive keeps per-session watermark states and api-proxy serves baselines and pushes changed values.' ,
} ,
2026-07-28 02:11:31 +08:00
{
key : 'sessionProjectionCache' ,
pkg : 'session-projection-cache' ,
title : 'Persisted projection cache' ,
mode : 'core' ,
consumers : [ 'host-apiproxy' ] ,
note : 'Durably checkpoints projection unit states per session (throttled + turn/end/detach mandatory points) and serves the cold-read ladder: cache row + persistence tail replay, so listings never load full logs.' ,
} ,
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' ,
2026-08-13 00:36:22 +08:00
implementations : [ 'skill-badge' , 'skill-filesystem' ] ,
2026-07-10 14:19:06 +08:00
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-08-25 23:47:20 +08:00
consumers : [ 'agent-loop' , 'acp' , 'subagent-in-process-driver' ] ,
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
} ,
2026-08-09 12:13:58 +08:00
{
key : 'agentDefaultModel' ,
pkg : 'agent-default-model' ,
title : 'Default Agent model selection' ,
mode : 'core' ,
consumers : [ 'headless' , 'host-apiproxy' ] ,
2026-07-22 15:13:12 +08:00
note : 'Layers the default ModelSelection through settings so direct and Host-backed Agent entry points share one state owner.' ,
2026-08-09 12:13:58 +08:00
} ,
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-08-07 21:04:33 +08:00
{
key : 'e2b' ,
pkg : 'e2b' ,
title : 'E2B sandbox lifecycle owner' ,
mode : 'core' ,
consumers : [ 'fs-e2b' , 'subprocess-e2b' ] ,
note : 'Owns one shared E2B SDK handle, remote working directory, and final sandbox disposition so both fundamental E2B providers inhabit the same Linux runtime.' ,
} ,
refactor(process): split the process manager out of the bash executor
New process/ capability family: @deepseek-ai/dsh-process owns ctx.processes —
abstract ProcessManager.spawn(spec) over a fully-explicit ProcessSpawnSpec —
plus the shared DSH_* managed-environment and CollectedOutput vocabulary;
@deepseek-ai/dsh-process-local carries the former bash-local run.ts plumbing
(detached groups, tail-keep spill-backed output, credential scrub, kill
escalation, kill-and-join disposal) with no config of its own.
dsh-bash-local becomes a consumer: it keeps command defaulting, the fused
deadline timedOut/aborted classification, the model-friendly terminal env
(now merged through the ordinary env channel), and the [stderr]-marked
background read merge, and spawns through ctx.processes. Background-process
lifetime moves to the manager, so an executor reload no longer kills live
background work; a background spawn failure is injected once into the read
path instead of being buffered as fake stderr. dsh-bash re-exports the moved
vocabulary so bash consumers keep one import root; dsh-bash-sandbox only
redeclares the inherited inject.
Every composition loading a bash executor now loads dsh-process-local (CLI,
examples, python bundled runtime, create-sdk bash feature, inline test
configs).
2026-07-26 06:59:01 +08:00
{
refactor(subprocess): rename the process seam to subprocess and address review
Review feedback (tianyicui): 'process' is a poor service name. The family is
now packages/subprocess/ — @deepseek-ai/dsh-subprocess (ctx.subprocess,
abstract SubprocessService, Subprocess* vocabulary) and
@deepseek-ai/dsh-subprocess-local (LocalSubprocessService) — renamed
throughout code, compositions, docs (en+zh, pairs re-recorded), catalogs,
and gates. 'subprocess' is the precise term for managed OS children (the
Python-stdlib sense), avoids colliding with Node's global process object,
and reads as one system beside dsh-subagent-subprocess.
ds-review-bot findings addressed:
- kill() on a settled handle is now a no-op (no signal to a possibly-reused
pgid, no referenced grace timer delaying exit); pinned by a spy test.
- The moved DshEnvironmentKey/DshEnvironment/CollectedOutput types get
drift-checked type-equiv blocks on the new subprocess.md page, restoring
their manifest registration.
- subprocess.md is registered in the core.md sub-page index (en+zh).
2026-07-26 12:43:14 +08:00
key : 'subprocess' ,
2026-07-26 14:10:46 +08:00
pkg : 'subprocess' ,
title : 'Subprocess seam' ,
refactor(process): split the process manager out of the bash executor
New process/ capability family: @deepseek-ai/dsh-process owns ctx.processes —
abstract ProcessManager.spawn(spec) over a fully-explicit ProcessSpawnSpec —
plus the shared DSH_* managed-environment and CollectedOutput vocabulary;
@deepseek-ai/dsh-process-local carries the former bash-local run.ts plumbing
(detached groups, tail-keep spill-backed output, credential scrub, kill
escalation, kill-and-join disposal) with no config of its own.
dsh-bash-local becomes a consumer: it keeps command defaulting, the fused
deadline timedOut/aborted classification, the model-friendly terminal env
(now merged through the ordinary env channel), and the [stderr]-marked
background read merge, and spawns through ctx.processes. Background-process
lifetime moves to the manager, so an executor reload no longer kills live
background work; a background spawn failure is injected once into the read
path instead of being buffered as fake stderr. dsh-bash re-exports the moved
vocabulary so bash consumers keep one import root; dsh-bash-sandbox only
redeclares the inherited inject.
Every composition loading a bash executor now loads dsh-process-local (CLI,
examples, python bundled runtime, create-sdk bash feature, inline test
configs).
2026-07-26 06:59:01 +08:00
mode : 'seam' ,
2026-08-07 21:04:33 +08:00
implementations : [ 'subprocess-local' , 'subprocess-e2b' ] ,
2026-08-13 00:36:22 +08:00
consumers : [ 'bash-local' , 'bash-sandbox' , 'terminal-bash' , 'lsp-stdio' , 'subagent-acp' , 'subagent-codex' , 'subagent-claude-code' ] ,
fix(rebase): migrate the replayed stack onto current master APIs
The linear replay carried each commit's own lineage, so this checkpoint
restores the master-owned surfaces the conflicted regions clobbered and
migrates branch-owned code to master's post-rebase APIs:
- rebuild subprocess-local spawn.ts on master's tree-exit-observer
machinery, keeping the branch's win32 childEnv key semantics and the
Linux zombie-quiescence probe; the zombie test reaps its survivor
directly since a confirmed-absent verdict is a permanent
no-more-signals boundary
- migrate pty-local test stubs to the Inbox-model Agent interface,
Session.create, runnerFailureRules, and the new turn/start payload
- implement the seam's resolveExecutable/spawnTerminal abstracts in the
new pwsh-local and tool-fs-search test fakes
- restore code-runtime, atomic-write, pwsh-local, and app-boot to
master's exact content (the net-zero code-runtime churn is pruned
from this history) and drop rename-detection graft debris
- re-apply the PR's architecture rows and execution-world paragraph,
re-record bilingual pairings, regenerate catalogs, and reconcile the
lockfile
2026-08-07 20:34:31 +08:00
note : 'The bash executors, the PTY shell backend, the LSP host, and the out-of-process ACP, Codex, and Claude Code subagent backends spawn through ctx.subprocess; the service owns process coordinates, tree/session lifetime, stdio dispositions, terminal mechanics, and kill escalation.' ,
refactor(process): split the process manager out of the bash executor
New process/ capability family: @deepseek-ai/dsh-process owns ctx.processes —
abstract ProcessManager.spawn(spec) over a fully-explicit ProcessSpawnSpec —
plus the shared DSH_* managed-environment and CollectedOutput vocabulary;
@deepseek-ai/dsh-process-local carries the former bash-local run.ts plumbing
(detached groups, tail-keep spill-backed output, credential scrub, kill
escalation, kill-and-join disposal) with no config of its own.
dsh-bash-local becomes a consumer: it keeps command defaulting, the fused
deadline timedOut/aborted classification, the model-friendly terminal env
(now merged through the ordinary env channel), and the [stderr]-marked
background read merge, and spawns through ctx.processes. Background-process
lifetime moves to the manager, so an executor reload no longer kills live
background work; a background spawn failure is injected once into the read
path instead of being buffered as fake stderr. dsh-bash re-exports the moved
vocabulary so bash consumers keep one import root; dsh-bash-sandbox only
redeclares the inherited inject.
Every composition loading a bash executor now loads dsh-process-local (CLI,
examples, python bundled runtime, create-sdk bash feature, inline test
configs).
2026-07-26 06:59:01 +08:00
} ,
2026-07-03 01:13:52 +08:00
{
2026-08-13 00:36:22 +08:00
key : 'shell' ,
pkg : 'shell' ,
2026-07-03 01:13:52 +08:00
title : 'Bash executor seam' ,
mode : 'seam' ,
2026-08-02 21:46:53 +08:00
implementations : [ 'bash-local' , 'bash-sandbox' , 'pwsh-local' ] ,
2026-08-13 00:36:22 +08:00
consumers : [ 'tool-bash' , 'tool-pwsh' , 'hooks-claude-code' , 'hooks-codex' ] ,
2026-08-02 21:46:53 +08:00
note : 'The model-facing shell tools and hook bridges consume this seam; sandboxed, remote, or PowerShell executors replace bash-local without touching them.' ,
2026-07-04 12:50:06 +08:00
} ,
2026-07-12 15:41:42 +08:00
{
2026-08-13 00:36:22 +08:00
key : 'shellEnv' ,
pkg : 'shell-env' ,
2026-07-12 15:41:42 +08:00
title : 'Managed bash environment registry' ,
mode : 'core' ,
2026-08-02 21:46:53 +08:00
consumers : [ 'tool-bash' , 'tool-pwsh' ] ,
note : 'Plugins declare effect-scoped DSH_* facts; each shell tool collects one trusted snapshot per execution and its executor rebuilds the namespace.' ,
2026-07-12 15:41:42 +08:00
} ,
2026-07-21 16:01:00 +08:00
{
2026-08-13 00:36:22 +08:00
key : 'terminals' ,
pkg : 'terminal' ,
2026-07-21 16:01:00 +08:00
title : 'Persistent PTY session registry' ,
mode : 'seam' ,
2026-08-13 00:36:22 +08:00
implementations : [ 'terminal-bash' ] ,
consumers : [ 'tool-terminal' ] ,
note : 'The registry owns exact-Agent session identity and cleanup; backends own terminal mechanics, while tool-terminal exposes the owner-scoped model tools.' ,
2026-07-21 16:01:00 +08:00
} ,
2026-07-09 15:42:37 +08:00
{
key : 'sandbox' ,
pkg : 'sandbox' ,
title : 'Process-sandbox seam' ,
mode : 'seam' ,
implementations : [ 'sandbox-local' ] ,
2026-08-13 00:36:22 +08:00
consumers : [ 'bash-sandbox' , 'terminal-bash' ] ,
2026-07-09 15:42:37 +08:00
note : 'Consumers hand over the exact argv they are about to spawn; same-world backends wrap it under a per-call policy and report enforcement.' ,
} ,
feat(sandbox): cross-family file sandbox — one policy home, sandboxed fs provider, fs escalation parity
Extend SandboxMode enforcement from bash to the filesystem tools, the sandbox
RFC's deferred cross-family phase.
- dsh-sandbox-policy (new, ctx.sandboxPolicy): the single home for the
deployment default mode + workspaceRoot and the per-session override event,
renamed bash/sandbox-mode -> sandbox/mode and moved here with its fold/setter.
Decouples the bash seam from dsh-session.
- dsh-fs-sandbox (new): SandboxedFileSystem extends LocalFileSystem and fences
write/edit by the per-call mode (read-only denies, workspace-write contains to
the workspace + temp roots via the shared writableRoots, danger passes
through); reads pass through. Structured FS_SANDBOX_DENIED; in-lock parent
re-canonicalization. A policy fence in trusted code, not a kernel boundary.
- dsh-sandbox: the shared escalation kit (writableRoots, the strictly-wider
ladder, denial/hint markers, approveEscalation) both tool families use;
approveEscalation takes a structural approver so dsh-sandbox gains no
approval/agent dependency, and both tools stay duplication-free.
- tool-fs: write/edit advertise sandbox_permissions/justification under a
confining ctx.fs, map FS_SANDBOX_DENIED to the shared [sandbox: ...] marker,
and resolve the same one-approved-wider retry.
- examples/acp-agent: composes sandbox-policy + fs-sandbox, drops the gating
that disabled the fs stack under confined modes.
RFC docs/rfc/implemented/feature/2026-07-14-cross-family-fs-sandbox.md; the old
sandbox RFC's In-process/deferred/FAQ sections updated to shipped fact.
2026-07-14 20:05:57 +08:00
{
key : 'sandboxPolicy' ,
2026-07-20 13:59:18 +08:00
pkg : 'sandbox-policy' ,
feat(sandbox): cross-family file sandbox — one policy home, sandboxed fs provider, fs escalation parity
Extend SandboxMode enforcement from bash to the filesystem tools, the sandbox
RFC's deferred cross-family phase.
- dsh-sandbox-policy (new, ctx.sandboxPolicy): the single home for the
deployment default mode + workspaceRoot and the per-session override event,
renamed bash/sandbox-mode -> sandbox/mode and moved here with its fold/setter.
Decouples the bash seam from dsh-session.
- dsh-fs-sandbox (new): SandboxedFileSystem extends LocalFileSystem and fences
write/edit by the per-call mode (read-only denies, workspace-write contains to
the workspace + temp roots via the shared writableRoots, danger passes
through); reads pass through. Structured FS_SANDBOX_DENIED; in-lock parent
re-canonicalization. A policy fence in trusted code, not a kernel boundary.
- dsh-sandbox: the shared escalation kit (writableRoots, the strictly-wider
ladder, denial/hint markers, approveEscalation) both tool families use;
approveEscalation takes a structural approver so dsh-sandbox gains no
approval/agent dependency, and both tools stay duplication-free.
- tool-fs: write/edit advertise sandbox_permissions/justification under a
confining ctx.fs, map FS_SANDBOX_DENIED to the shared [sandbox: ...] marker,
and resolve the same one-approved-wider retry.
- examples/acp-agent: composes sandbox-policy + fs-sandbox, drops the gating
that disabled the fs stack under confined modes.
RFC docs/rfc/implemented/feature/2026-07-14-cross-family-fs-sandbox.md; the old
sandbox RFC's In-process/deferred/FAQ sections updated to shipped fact.
2026-07-14 20:05:57 +08:00
title : 'Sandbox policy home' ,
mode : 'core' ,
implementations : [ ] ,
2026-08-13 00:36:22 +08:00
consumers : [ 'bash-sandbox' , 'fs-sandbox' , 'terminal-bash' ] ,
2026-07-20 13:59:18 +08:00
note : 'The one home for the deployment default mode + workspace root; only the sandboxed executor and provider read the service (the tool layers use the pure `sandbox/mode` fold it also exports). Both enforcing families read it so bash and fs cannot confine to different roots.' ,
feat(sandbox): cross-family file sandbox — one policy home, sandboxed fs provider, fs escalation parity
Extend SandboxMode enforcement from bash to the filesystem tools, the sandbox
RFC's deferred cross-family phase.
- dsh-sandbox-policy (new, ctx.sandboxPolicy): the single home for the
deployment default mode + workspaceRoot and the per-session override event,
renamed bash/sandbox-mode -> sandbox/mode and moved here with its fold/setter.
Decouples the bash seam from dsh-session.
- dsh-fs-sandbox (new): SandboxedFileSystem extends LocalFileSystem and fences
write/edit by the per-call mode (read-only denies, workspace-write contains to
the workspace + temp roots via the shared writableRoots, danger passes
through); reads pass through. Structured FS_SANDBOX_DENIED; in-lock parent
re-canonicalization. A policy fence in trusted code, not a kernel boundary.
- dsh-sandbox: the shared escalation kit (writableRoots, the strictly-wider
ladder, denial/hint markers, approveEscalation) both tool families use;
approveEscalation takes a structural approver so dsh-sandbox gains no
approval/agent dependency, and both tools stay duplication-free.
- tool-fs: write/edit advertise sandbox_permissions/justification under a
confining ctx.fs, map FS_SANDBOX_DENIED to the shared [sandbox: ...] marker,
and resolve the same one-approved-wider retry.
- examples/acp-agent: composes sandbox-policy + fs-sandbox, drops the gating
that disabled the fs stack under confined modes.
RFC docs/rfc/implemented/feature/2026-07-14-cross-family-fs-sandbox.md; the old
sandbox RFC's In-process/deferred/FAQ sections updated to shipped fact.
2026-07-14 20:05:57 +08:00
} ,
2026-07-09 15:25:18 +08:00
{
key : 'approval' ,
2026-08-25 23:47:20 +08:00
pkg : 'user-approval' ,
2026-07-09 15:25:18 +08:00
title : 'Approval seam' ,
mode : 'seam' ,
2026-08-25 23:47:20 +08:00
implementations : [ ] ,
consumers : [ 'tools' , 'tool-bash' , 'acp' ] ,
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
{
2026-08-13 00:36:22 +08:00
key : 'permissionPresets' ,
pkg : 'permission-presets' ,
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
title : 'Permission presets' ,
mode : 'core' ,
implementations : [ ] ,
2026-07-14 01:09:44 +08:00
note : 'User-facing preset table (`workspace-write`/`danger-full-access`) bundling the sandbox-mode and approval-policy knobs; a switch writes one `permission/preset` event through to both knob events.' ,
feat(permission): user-facing permission presets — one Permissions select over the two knobs
A preset names a bundle of the two mechanism knobs — request =
workspace-write + ask, yolo = danger-full-access + never — so the editor
shows ONE 'Permissions' select where the sandbox-mode and approval-policy
tiers stay orthogonal capabilities (the Codex /approvals shape: presets over
two dials). ctx.permission (dsh-permission) owns the config-defined table,
validates the default preset's bundle against the composed knob defaults at
load (fails loud), and writes a switch THROUGH: one log-only
permission/preset event (the audit fact reverse-mapping cannot recover —
the planned 'agent' preset shares request's knob values and differs only in
composed policy) plus each knob event via its own setter, deduped — a
net-zero switch appends nothing. Every knob consumer keeps reading its own
fold, untouched.
The current preset DERIVES from the effective knob values — the fold breaks
bundle ties, a knob state outside the table is the reserved 'custom' value
(a state, not an error: shown while it holds, switchable FROM, never a
target), and defaultPreset disappears (zero-event state reverse-maps from
the composition defaults).
The ACP bridge drops the two per-knob selects for the one preset select
(advertised only when ctx.permission is composed); pending/anchor/no-op
semantics carry over unchanged, with the no-op echo acknowledged before
vocabulary validation so a client re-pushing a derived 'custom' current
never errors. The sandbox variant example composes the
service with a workspace-write default; the permission-switching,
escalation-approved and escalation-rejected scenarios are re-recorded under
it (escalations now target an outside-workspace /tmp path under
danger-full-access, self-cleaning) and config-options is re-authored on the
single-select wire.
2026-07-12 21:03:41 +08:00
} ,
2026-07-08 02:17:24 +08:00
{
key : 'codeRuntime' ,
pkg : 'code-runtime' ,
title : 'Code-execution seam' ,
mode : 'seam' ,
2026-08-25 23:47:20 +08:00
implementations : [ 'code-runtime-worker-thread' ] ,
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' ,
2026-08-07 21:04:33 +08:00
implementations : [ 'fs-local' , 'fs-sandbox' , 'fs-e2b' ] ,
2026-07-04 12:50:06 +08:00
consumers : [ 'tool-fs' ] ,
2026-08-13 00:36:22 +08:00
companions : [ 'fs-observation-policy' ] ,
note : 'tool-fs executes read/write/edit through ctx.fs; fs-sandbox fences mutations by the shared sandbox mode; fs-observation-policy contributes observed-state checks through the fs/* event gate.' ,
2026-07-03 01:13:52 +08:00
} ,
{
2026-08-13 00:36:22 +08:00
key : 'compaction' ,
pkg : 'compaction' ,
2026-07-03 01:13:52 +08:00
title : 'Compaction seam' ,
mode : 'seam' ,
2026-08-13 00:36:22 +08:00
implementations : [ 'compaction-basic' ] ,
consumers : [ 'compaction-basic' ] ,
docs: purge chain-of-thought leakage from prose
Delete design-session citations (decision/audit/plan ordinals, stack
positions), change narration, review choreography, and reviewer-addressed
justification from comments, JSDoc, docs, READMEs, Agent Notes, tests, and
generator templates; restate every affected fact as current-state contract
prose. Fix generated docs at their sources and regenerate the catalogs and
cordis-surface regions; re-paste type-equiv blocks; update every bilingual
counterpart and re-record the pairs. Record the citation rule in the
committed-artifact-citations Agent Note.
2026-08-09 15:09:19 +08:00
note : 'The basic backend consumes post-step pressure and request-error recovery events; there is no model-facing compact tool.' ,
2026-07-03 01:13:52 +08:00
} ,
{
key : 'subagents' ,
pkg : 'subagent' ,
2026-07-27 00:00:14 +08:00
title : 'Subagent provider and continuation service' ,
2026-07-03 01:13:52 +08:00
mode : 'seam' ,
2026-08-13 00:36:22 +08:00
implementations : [ 'subagent-spawn-in-process' , 'subagent-fork-in-process' , 'subagent-acp' , 'subagent-codex' , 'subagent-claude-code' , 'subagent-dsh-sdk' ] ,
2026-07-27 00:00:14 +08:00
consumers : [ 'tool-subagent' , 'tool-subagent-control' , 'tool-ralph' ] ,
2026-07-30 21:21:14 +08:00
note : 'Providers implement transports; the service also owns optional Activation-based continuation orchestration, tool-subagent selects one-shot or continuable delegation, tool-subagent-control delivers follow-ups, and tool-ralph requires one fresh structured-output route.' ,
2026-07-03 01:13:52 +08:00
} ,
2026-08-14 12:48:53 +08:00
{
2026-08-19 22:28:18 +08:00
key : 'agentTeams' ,
2026-08-25 23:47:20 +08:00
pkg : 'experimental-agent-team' ,
2026-08-14 12:48:53 +08:00
title : 'Agent Teams coordination domain' ,
mode : 'core' ,
2026-08-19 21:51:15 +08:00
consumers : [ 'tool-agent-team' , 'client-ui-agent-team' ] ,
note : 'Owns the implicit-root roster, durable peer mailbox, shared task DAG, continuable-child lifecycle, and generated Team Remote methods; tool-agent-team contributes model controls and client-ui-agent-team mounts the browser contribution.' ,
2026-08-19 14:45:40 +08:00
} ,
feat(tasks): background task runtime, generic task_* control tools, bash/subagent producers
One shared ctx.tasks registry (branded <kind>-N ids, owner-fenced
read/kill/wait/list, attachSurface misconfiguration fence, reported-flag
notice dedup, atomic register) + dsh-tool-tasks (task_output/task_list/
task_kill, completion-notice injection, background prompt habit).
Producers opt in via their own enableRunInBackground config: bash
(stream kind; seam slimmed to resolve/run/start returning a BashProcess
handle, bash_output/bash_kill deleted) and subagent (final-output kind;
done settles after run.dispose()). Owner disposal drains tasks through
the new awaited ctx.agents.onCleanup seam in the loop's disposal chain.
Both RFCs moved to implemented/; docs, catalogs, snapshots re-pinned.
2026-07-09 21:22:54 +08:00
{
2026-08-13 00:36:22 +08:00
key : 'jobs' ,
pkg : 'jobs' ,
title : 'Background job registry' ,
2026-07-26 05:13:39 +08:00
mode : 'seam' ,
2026-08-13 00:36:22 +08:00
implementations : [ 'jobs-local' ] ,
consumers : [ 'tool-bash' , 'tool-terminal' , 'tool-subagent' , 'tool-jobs' ] ,
note : 'Producers (background bash, PTY sends, and subagent delegations) register running work; tool-jobs is the model-facing controller that reads, lists, and kills it; jobs-local is the process-local registry.' ,
feat(tasks): background task runtime, generic task_* control tools, bash/subagent producers
One shared ctx.tasks registry (branded <kind>-N ids, owner-fenced
read/kill/wait/list, attachSurface misconfiguration fence, reported-flag
notice dedup, atomic register) + dsh-tool-tasks (task_output/task_list/
task_kill, completion-notice injection, background prompt habit).
Producers opt in via their own enableRunInBackground config: bash
(stream kind; seam slimmed to resolve/run/start returning a BashProcess
handle, bash_output/bash_kill deleted) and subagent (final-output kind;
done settles after run.dispose()). Owner disposal drains tasks through
the new awaited ctx.agents.onCleanup seam in the loop's disposal chain.
Both RFCs moved to implemented/; docs, catalogs, snapshots re-pinned.
2026-07-09 21:22:54 +08:00
} ,
2026-07-04 12:50:06 +08:00
{
key : 'web' ,
pkg : 'web' ,
title : 'Web access provider registry' ,
mode : 'seam' ,
2026-08-13 00:36:22 +08:00
implementations : [ 'web-search-exa' , 'web-search-perplexity' , 'web-search-deepseek' , 'web-fetch-http' ] ,
2026-07-04 12:50:06 +08:00
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-28 15:44:53 +08:00
{
key : 'directoryPicker' ,
2026-08-25 23:47:20 +08:00
pkg : 'host-directory-picker' ,
2026-07-28 15:44:53 +08:00
title : 'Workspace-directory picking seam' ,
mode : 'seam' ,
2026-08-25 23:47:20 +08:00
implementations : [ 'host-directory-picker-native' , 'host-directory-picker-browse' ] ,
consumers : [ 'host-apiproxy' ] ,
2026-07-29 02:11:31 +08:00
note : 'Discriminated interaction capability: the native backend opens one OS chooser on the host display, the browse backend serves listing/creation primitives for the in-app browser; dual-face backends fill ui-workspace directory-flow slots from their browser halves (no wire advertisement).' ,
2026-07-28 15:44:53 +08:00
} ,
2026-07-25 02:15:58 +08:00
{
2026-08-13 00:36:22 +08:00
key : 'webServer' ,
2026-08-25 23:47:20 +08:00
pkg : 'host-webserver' ,
2026-07-25 02:15:58 +08:00
title : 'HTTP route registration' ,
mode : 'core' ,
2026-08-25 23:47:20 +08:00
consumers : [ 'client-connection' , 'client-modules' , 'client-hmr' ] ,
2026-07-25 02:15:58 +08:00
note : 'Plain node:http carrier: named-route registry, index transform taps, and the static dist fallback; web-transport plugins register their own routes.' ,
} ,
{
2026-08-13 00:36:22 +08:00
key : 'clientModules' ,
2026-08-25 23:47:20 +08:00
pkg : 'client-modules' ,
2026-07-25 02:15:58 +08:00
title : 'Client plugin graph host' ,
mode : 'core' ,
2026-08-25 23:47:20 +08:00
consumers : [ 'client-hmr' ] ,
2026-08-10 20:38:57 +08:00
note : 'Composes the __DSH_BOOT__ entry graph from an incremental dsh.client scan, serves plugin bundles, and notifies rebuilt/graph-changed subscribers.' ,
2026-07-25 02:15:58 +08:00
} ,
2026-07-06 03:14:07 +08:00
{
2026-08-13 00:36:22 +08:00
key : 'workflowEngine' ,
2026-07-06 03:14:07 +08:00
pkg : 'workflow' ,
title : 'Workflow script engine' ,
mode : 'seam' ,
2026-08-13 00:36:22 +08:00
implementations : [ 'workflow-worker-thread' ] ,
2026-07-20 00:51:19 +08:00
consumers : [ 'tool-workflow' , 'tool-ralph' ] ,
2026-08-09 15:27:21 +08:00
note : 'One engine per context, as in bash, with 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-08-22 23:44:56 +08:00
{
key : 'webhookRuntime' ,
pkg : 'webhook' ,
title : 'Webhook rule runtime' ,
mode : 'core' ,
consumers : [ 'webhook-github' ] ,
note : 'Provider adapters dispatch authenticated deliveries; trusted plugins register independent process-local rules, and the runtime turns non-null results into ordinary Workspace-backed Sessions without delivery or completion state.' ,
} ,
2026-08-12 23:51:31 +08:00
{
key : 'lsp' ,
pkg : 'lsp' ,
title : 'Language-server navigation seam' ,
mode : 'seam' ,
2026-08-25 23:47:20 +08:00
implementations : [ 'lsp-stdio' ] ,
2026-08-12 23:51:31 +08:00
consumers : [ 'tool-lsp' ] ,
note : 'Provider registration and selection plus normalized query execution over exactly four operations; the seam offers no protocol escape hatch, so a backend translates into the normalized request and result.' ,
} ,
{
key : 'apiProxy' ,
2026-08-25 23:47:20 +08:00
pkg : 'host-apiproxy' ,
2026-08-12 23:51:31 +08:00
title : 'Host API dispatch' ,
mode : 'core' ,
2026-08-25 23:47:20 +08:00
consumers : [ 'client-connection' ] ,
2026-08-12 23:51:31 +08:00
note : 'The transport-agnostic host gateway face: it dispatches browser API calls, and each open host stream subscribes to the events it forwards rather than being pushed to through a broadcast verb.' ,
} ,
{
key : 'dynamicCordisRunner' ,
pkg : 'cordis-host-runner' ,
title : 'Dynamic Cordis package host runner' ,
mode : 'core' ,
consumers : [ 'tool-cordis' ] ,
note : 'Owns the in-memory definition registry, the vm sandbox for host halves, and the request-run round trip; browser pages reach the same service over the wire through its remote namespace.' ,
} ,
2026-08-13 00:19:15 +08:00
{
key : 'cordisInspect' ,
pkg : 'cordis-host-runner' ,
title : 'Dynamic Cordis inspect registry' ,
mode : 'core' ,
consumers : [ 'tool-cordis' ] ,
note : 'Registers host inspect providers, mirrors the client provider manifest, and routes client queries through the dynamic Cordis transport.' ,
2026-07-06 03:14:07 +08:00
} ,
2026-07-03 01:13:52 +08:00
]
2026-07-05 02:54:01 +08:00
function generatedHeader ( title : string ) : string [ ] {
2026-07-03 01:13:52 +08:00
return [
'<!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand.' ,
' Run `pnpm run gen-doc-graphs` to regenerate. -->' ,
'' ,
` # ${ title } ` ,
'' ,
]
}
2026-07-05 02:54:01 +08:00
function maintenanceFooter ( source : string ) : string [ ] {
return [ ` Maintenance mode: ${ source } . ` , '' ]
}
function graphIndexLink ( rel : string ) : string {
return relative ( 'docs' , rel ) . replaceAll ( '\\' , '/' )
}
function linkFromDoc ( docRel : string , targetRel : string ) : string {
return relative ( dirname ( docRel ) , targetRel ) . replaceAll ( '\\' , '/' )
}
2026-07-05 01:25:58 +08:00
function mermaidCode ( value : string ) : string {
return ` <code> ${ value . replace ( /&/g , '&' ) . replace ( /</g , '<' ) . replace ( />/g , '>' ) } </code> `
2026-07-03 01:13:52 +08:00
}
2026-07-05 01:25:58 +08:00
function repoLink ( path : string , label : string , up = '..' ) : string {
return ` [ ${ label } ]( ${ up } / ${ path } ) `
2026-07-03 01:13:52 +08:00
}
2026-07-05 01:25:58 +08:00
function sourceLink ( source : string , up = '..' ) : string {
return repoLink ( source . split ( ':' ) [ 0 ] ? ? source , ` \` ${ source } \` ` , up )
2026-07-03 01:13:52 +08:00
}
2026-07-05 01:25:58 +08:00
function pkgLink ( pkg : Pkg | undefined , fallback : string , up = '..' ) : string {
return pkg ? repoLink ( pkg . rel , ` \` ${ pkg . short } \` ` , up ) : ` \` ${ fallback } \` `
2026-07-03 01:13:52 +08:00
}
2026-07-05 01:25:58 +08:00
function pkgList ( names : string [ ] | undefined , pkgsByShort : Map < string , Pkg > ) : string {
if ( ! names || names . length === 0 ) return '-'
return names . map ( name = > pkgLink ( pkgsByShort . get ( name ) , name ) ) . join ( ', ' )
2026-07-03 01:13:52 +08:00
}
2026-07-05 01:25:58 +08:00
function tableCell ( value : string ) : string {
return value . replace ( /\|/g , '\\|' ) . replace ( /\n/g , '<br>' )
2026-07-03 01:13:52 +08:00
}
2026-07-28 23:48:35 +08:00
function assertServiceRolesComplete ( services : readonly ServiceEntry [ ] ) : void {
const discovered = new Set ( services . map ( service = > service . key ) )
2026-07-03 01:13:52 +08:00
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 ( '; ' ) )
}
}
2026-07-28 23:48:35 +08:00
function renderCapabilitySeams ( pkgs : Pkg [ ] , services : readonly ServiceEntry [ ] ) : string {
assertServiceRolesComplete ( services )
2026-07-03 01:13:52 +08:00
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' ) ) {
2026-08-06 04:40:32 +08:00
// Top-level rows (`- id:`) and bundle-patch insert rows (` - id:`).
const id = /^\s*-\s+id:\s+(.+?)\s*$/ . exec ( line )
2026-07-03 01:13:52 +08:00
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-08-04 10:07:17 +08:00
id : 'dsh_base' ,
2026-07-30 10:34:21 +08:00
rel : 'apps/cli/composition.md' ,
2026-08-04 10:07:17 +08:00
title : 'DSH Base Composition' ,
2026-08-06 04:40:32 +08:00
label : 'packages/bundle/base/cordis.patch.yml' ,
config : 'packages/bundle/base/cordis.patch.yml' ,
docs(python): define the standalone minimal profile
Record sdk-minimal as the narrow repository-owned exception to base-first profile composition: callers still launch only dsh and cannot provide an arbitrary Cordis tree, while the shipped bundle may own a complete explicit roster. Cross-link the launcher, profile-bundle, Python-runtime, minimal-agent, snapshot, and telemetry decisions; the supersession audit keeps each older note active because its remaining rationale is independent.
Update the CLI, architecture, Python tutorial/reference, example, runtime-wheel reference, and bundle documentation. The docs distinguish the full sdk profile from sdk-minimal, explain explicit-home/plugin/patch customization, state the minimal permission and persistence choices, and retain the separately packaged web profile and frontend assets for direct dsh use.
Correct dsh-base descriptions to cover base-backed profiles, make SDK startup configuration visible in the generated config catalog, add sdk-minimal to the module graph, and regenerate the base-composition graph. English and Chinese pairs are re-recorded at the exact reviewed contents.
2026-08-24 15:39:43 +08:00
summary : 'The dsh-base bundle patch shared by the web, headless, sdk, and acp profiles; their mode bundles and user layers patch over it, while sdk-minimal owns a separate standalone tree.' ,
2026-07-05 01:25:58 +08:00
} ,
]
type AppExample = typeof APP_EXAMPLES [ number ]
function renderAppComposition ( example : AppExample ) : string {
const plugins = parseExampleCordis ( example . config )
2026-08-24 10:00:15 +08:00
const maintenance = 'hybrid: the patch row list is parsed from its `cordis.yml`; app package expansion is curated from package source'
2026-07-05 02:54:01 +08:00
const lines = generatedHeader ( example . title )
2026-07-03 01:13:52 +08:00
lines . push (
2026-07-05 01:25:58 +08:00
example . summary ,
2026-07-03 01:13:52 +08:00
'' ,
'```mermaid' ,
'flowchart LR' ,
2026-07-05 01:25:58 +08:00
` cfg[" ${ escLabel ( example . label ) } <br/>cordis.yml"] ` ,
2026-07-03 01:13:52 +08:00
)
2026-07-05 01:25:58 +08:00
for ( const plugin of plugins ) {
const pluginNode = nodeId ( ` plugin_ ${ example . id } ` , plugin . id )
lines . push ( ` ${ pluginNode } [" ${ escLabel ( plugin . id ) } <br/> ${ escLabel ( plugin . name ) } "] ` )
lines . push ( ` cfg --> ${ pluginNode } ` )
2026-07-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-30 01:03:22 +08:00
type CallSiteIndex = Map < ts.SignatureDeclaration | ts.JSDocSignature , ts.CallExpression [ ] >
2026-07-30 11:20:49 +08:00
/ * *
* The only method names visitSource classifies ; receiver typing runs on these
* alone . Obligation : every method name matched by a branch inside visitSource
* must appear here — the prefilter drops non - members before any branch runs ,
* so a branch for an unlisted name is silently dead .
* /
2026-07-30 01:03:22 +08:00
const EVENT_API_METHODS = new Set ( [ 'on' , 'once' , 'emit' , 'parallel' , 'serial' , 'waterfall' , 'dispatch' ] )
2026-08-10 15:16:53 +08:00
/ * *
* Collect event dispatch / listener relations from real cross - file receiver types .
*
* TODO : the program is seeded from the host aggregate alone ( ts - project . ts
* documents why : one program cannot hold both faces ' Context merges ) , so a
* Client package enters only when a host file imports it . Client - face
* listeners on client - face events are therefore under - reported —
2026-08-11 16:50:03 +08:00
* ` connection/reset ` omits ` ui-skill ` / ` ui-agent-preset ` . Closing it needs a
* second Client program whose relations merge into these , not a wider seed .
2026-08-10 15:16:53 +08:00
* /
2026-07-30 11:20:49 +08:00
export class EventRelationCollector {
2026-07-14 23:14:51 +08:00
private readonly relations = new Map < string , EventRelation > ( )
2026-07-30 01:03:22 +08:00
private readonly fileCallSites = new Map < ts.SourceFile , CallSiteIndex > ( )
private readonly localCalleeProofs = new Map < ts.FunctionDeclaration , boolean > ( )
private globalCallSites : CallSiteIndex | null = null
2026-07-14 23:14:51 +08:00
private readonly contextType : ts.Type
private readonly agentDispatchType : ts.Type
private readonly eventsServiceType : ts.Type
2026-07-30 01:03:22 +08:00
private readonly packageSourceFiles : ReadonlySet < ts.SourceFile >
2026-07-14 23:14:51 +08:00
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' )
2026-07-30 01:03:22 +08:00
this . packageSourceFiles = new Set ( sources . map ( source = > source . sourceFile ) )
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
2026-07-03 01:13:52 +08:00
}
2026-07-14 23:14:51 +08:00
/** Resolve one named class/interface declaration to its merged instance type. */
private declaredType ( relativePath : string , name : string ) : ts . Type {
const sourceFile = this . project . sourceFile ( relativePath )
const declaration = sourceFile . statements . find ( ( statement ) : statement is ts . ClassDeclaration | ts . InterfaceDeclaration = > {
return ( ts . isClassDeclaration ( statement ) || ts . isInterfaceDeclaration ( statement ) ) && statement . name ? . text === name
} )
const symbol = declaration ? . name && this . project . checker . getSymbolAtLocation ( declaration . name )
if ( ! symbol ) throw new Error ( ` cannot resolve TypeScript type ${ name } from ${ relativePath } ` )
return this . project . checker . getDeclaredTypeOfSymbol ( symbol )
2026-07-03 01:13:52 +08:00
}
2026-07-14 23:14:51 +08:00
2026-07-30 01:03:22 +08:00
/** Index resolved function calls in the given files for narrow argument-flow recovery. */
private buildCallSiteIndex ( files : Iterable < ts.SourceFile > ) : CallSiteIndex {
const index : CallSiteIndex = new Map ( )
2026-07-14 23:14:51 +08:00
const visit = ( node : ts.Node ) : void = > {
if ( ts . isCallExpression ( node ) ) {
const declaration = this . project . checker . getResolvedSignature ( node ) ? . declaration
if ( declaration ) {
2026-07-30 01:03:22 +08:00
const calls = index . get ( declaration ) ? ? [ ]
2026-07-14 23:14:51 +08:00
calls . push ( node )
2026-07-30 01:03:22 +08:00
index . set ( declaration , calls )
2026-07-14 23:14:51 +08:00
}
}
ts . forEachChild ( node , visit )
}
2026-07-30 01:03:22 +08:00
for ( const file of files ) visit ( file )
return index
}
/ * *
* Return every indexed call resolving to one local helper declaration .
* Fast path : when every same - file reference to the non - exported helper is
* provably a direct callee , module scoping confines all of its calls to that
2026-08-09 15:27:21 +08:00
* file , so only that file is indexed . Any other reference form may alias
2026-07-30 01:03:22 +08:00
* the function value outward , so the original full package - source index
* decides instead .
* /
private callSitesFor ( owner : ts.FunctionDeclaration ) : ts . CallExpression [ ] {
if ( ! this . globalCallSites && ! this . provenLocalCallee ( owner ) ) {
this . globalCallSites = this . buildCallSiteIndex ( this . packageSourceFiles )
}
if ( this . globalCallSites ) return this . globalCallSites . get ( owner ) ? ? [ ]
const file = owner . getSourceFile ( )
let index = this . fileCallSites . get ( file )
if ( ! index ) {
index = this . buildCallSiteIndex ( [ file ] )
this . fileCallSites . set ( file , index )
}
return index . get ( owner ) ? ? [ ]
}
/ * *
2026-07-30 11:20:49 +08:00
* Prove every same - file reference to one helper is a direct callee . The
* proof owns its premises : an exported helper or a helper in a global
* script file ( no import / export means program - wide scope , callable from
* another file with no same - file reference at all ) fails immediately .
* Alias escapes ( re - export statements , default exports , value reads )
* resolve back to the owner symbol at a non - callee position and fail the
* proof , as does anything the scan cannot positively classify .
2026-07-30 01:03:22 +08:00
* /
private provenLocalCallee ( owner : ts.FunctionDeclaration ) : boolean {
const cached = this . localCalleeProofs . get ( owner )
if ( cached !== undefined ) return cached
2026-07-30 11:20:49 +08:00
if ( hasExportModifier ( owner ) || ! ts . isExternalModule ( owner . getSourceFile ( ) ) ) {
this . localCalleeProofs . set ( owner , false )
return false
}
2026-07-30 01:03:22 +08:00
const name = owner . name
const ownerSymbol = name && this . project . checker . getSymbolAtLocation ( name )
let proven = ! ! ownerSymbol
const refersToOwner = ( identifier : ts.Identifier ) : boolean = > {
// Shorthand properties resolve to the property symbol; ask for the value side.
const local = ts . isShorthandPropertyAssignment ( identifier . parent )
? this . project . checker . getShorthandAssignmentValueSymbol ( identifier . parent )
: this . project . checker . getSymbolAtLocation ( identifier )
if ( ! local ) return false
const symbol = local . flags & ts . SymbolFlags . Alias
? this . project . checker . getAliasedSymbol ( local )
: local
return symbol === ownerSymbol
}
const visit = ( node : ts.Node ) : void = > {
if ( ! proven ) return
if ( ts . isIdentifier ( node ) && node !== name && node . text === name ? . text
&& ! isDirectCallee ( node ) && refersToOwner ( node ) ) {
proven = false
return
}
ts . forEachChild ( node , visit )
}
visit ( owner . getSourceFile ( ) )
this . localCalleeProofs . set ( owner , proven )
return proven
2026-07-14 23:14:51 +08:00
}
/** 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-30 01:03:22 +08:00
} else if ( ts . isPropertyAccessExpression ( node . expression ) && EVENT_API_METHODS . has ( node . expression . name . text ) ) {
2026-07-24 21:18:48 +08:00
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 > ( )
2026-07-30 01:03:22 +08:00
for ( const call of this . callSitesFor ( owner ) ) {
2026-07-14 23:14:51 +08:00
const argument = call . arguments [ index ]
if ( argument ) addAll ( events , this . eventNamesFromArgumentList ( argument , new Set ( seen ) ) )
}
return events
}
/** Return a finite string-literal value set, rejecting widened and generic strings. */
private finiteStringValues ( expression : ts.Expression ) : Set < string > | undefined {
const current = unwrapExpression ( expression )
if ( ts . isStringLiteralLike ( current ) ) return new Set ( [ current . text ] )
if ( this . isForwardedAgentEventParameter ( current ) ) return undefined
return finiteStringTypeValues ( this . project . checker . getTypeAtLocation ( current ) )
}
/** Reject the contextual parameter inside the AgentEventDispatch forwarding object. */
private isForwardedAgentEventParameter ( expression : ts.Expression ) : boolean {
if ( ! ts . isIdentifier ( expression ) ) return false
const declarations = this . project . checker . getSymbolAtLocation ( expression ) ? . declarations ? ? [ ]
return declarations . some ( ( declaration ) = > {
if ( ! ts . isParameter ( declaration ) ) return false
const method = declaration . parent
if ( ! ts . isMethodDeclaration ( method ) || ! ts . isObjectLiteralExpression ( method . parent ) ) return false
const contextualType = this . project . checker . getContextualType ( method . parent )
return contextualType !== undefined
&& this . project . checker . isTypeAssignableTo ( contextualType , this . agentDispatchType )
} )
}
/** Get or create one relation row. */
private ensure ( event : string ) : EventRelation {
const existing = this . relations . get ( event )
if ( existing ) return existing
const relation = { dispatchers : new Map < string , Set < string > > ( ) , listeners : new Set < string > ( ) }
this . relations . set ( event , relation )
return relation
}
/** Add one dispatcher method without duplicating package/method labels. */
private addDispatcher ( event : string , pkg : string , method : string ) : void {
const relation = this . ensure ( event )
const methods = relation . dispatchers . get ( pkg ) ? ? new Set < string > ( )
methods . add ( method )
relation . dispatchers . set ( pkg , methods )
2026-07-12 18:57:42 +08:00
}
2026-07-03 01:13:52 +08:00
}
2026-07-30 01:03:22 +08:00
/** Return whether an identifier is the callee of a call, seen through value-preserving wrappers. */
function isDirectCallee ( identifier : ts.Identifier ) : boolean {
let current : ts.Node = identifier
while (
ts . isParenthesizedExpression ( current . parent )
|| ts . isAsExpression ( current . parent )
|| ts . isTypeAssertionExpression ( current . parent )
|| ts . isNonNullExpression ( current . parent )
|| ts . isSatisfiesExpression ( current . parent )
) {
current = current . parent
}
return ts . isCallExpression ( current . parent ) && current . parent . expression === current
}
2026-07-14 23:14:51 +08:00
/** Peel syntax-only wrappers that do not change an expression's runtime value. */
function unwrapExpression ( expression : ts.Expression ) : ts . Expression {
let current = expression
while (
ts . isParenthesizedExpression ( current )
|| ts . isAsExpression ( current )
|| ts . isTypeAssertionExpression ( current )
|| ts . isNonNullExpression ( current )
|| ts . isSatisfiesExpression ( current )
) {
current = current . expression
2026-07-09 06:06:17 +08:00
}
2026-07-14 23:14:51 +08:00
return current
2026-07-03 01:13:52 +08:00
}
2026-07-14 23:14:51 +08:00
/** Return every value only when a type is a closed string-literal union. */
function finiteStringTypeValues ( type : ts . Type ) : Set < string > | undefined {
if ( type . flags & ts . TypeFlags . StringLiteral ) {
return new Set ( [ ( type as ts . StringLiteralType ) . value ] )
2026-07-09 06:06:17 +08:00
}
2026-07-14 23:14:51 +08:00
if ( type . flags & ts . TypeFlags . Never ) return new Set ( )
if ( ! type . isUnion ( ) ) return undefined
const values = new Set < string > ( )
for ( const member of type . types ) {
const memberValues = finiteStringTypeValues ( member )
if ( ! memberValues ) return undefined
addAll ( values , memberValues )
2026-07-03 01:13:52 +08:00
}
2026-07-14 23:14:51 +08:00
return values
}
/** Return whether a variable declaration belongs to a const declaration list. */
function isConstDeclaration ( declaration : ts.VariableDeclaration ) : boolean {
return ( declaration . parent . flags & ts . NodeFlags . Const ) !== 0
}
/** Return whether a declaration is visible to callers outside its source module. */
function hasExportModifier ( node : ts.Node ) : boolean {
return ts . canHaveModifiers ( node ) && ( ts . getModifiers ( node ) ? . some ( ( modifier ) = > {
return modifier . kind === ts . SyntaxKind . ExportKeyword || modifier . kind === ts . SyntaxKind . DefaultKeyword
} ) ? ? false )
}
/** Add every member of source to target. */
function addAll < T > ( target : Set < T > , source : ReadonlySet < T > ) : void {
for ( const value of source ) target . add ( value )
}
/** Return the union of two sets without mutating either input. */
function unionSets < T > ( left : ReadonlySet < T > , right : ReadonlySet < T > ) : Set < T > {
const out = new Set ( left )
addAll ( out , right )
return out
}
2026-07-30 11:33:35 +08:00
/ * *
* Select the package source files of one project in deterministic order .
* @param project - the loaded repository TypeScript project .
* @returns ` packages/<group>/<pkg>/src ` files tagged with their package name .
* /
export function collectPackageSources ( project : TypeScriptProject ) : PackageSource [ ] {
return project . sourceFiles ( ) . flatMap ( ( sourceFile ) : PackageSource [ ] = > {
2026-07-14 23:14:51 +08:00
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 ) )
2026-07-30 11:33:35 +08:00
}
function collectEventRelations ( ) : Map < string , EventRelation > {
const project = new TypeScriptProject ( root )
return new EventRelationCollector ( project , collectPackageSources ( project ) ) . 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 ( ', ' )
}
2026-07-28 23:48:35 +08:00
function renderEventRelations ( pkgs : Pkg [ ] , events : readonly EventEntry [ ] ) : string {
2026-07-03 01:13:52 +08:00
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 (
docs: purge chain-of-thought leakage from prose
Delete design-session citations (decision/audit/plan ordinals, stack
positions), change narration, review choreography, and reviewer-addressed
justification from comments, JSDoc, docs, READMEs, Agent Notes, tests, and
generator templates; restate every affected fact as current-state contract
prose. Fix generated docs at their sources and regenerate the catalogs and
cordis-surface regions; re-paste type-equiv blocks; update every bilingual
counterpart and re-record the pairs. Record the citation rule in the
committed-artifact-citations Agent Note.
2026-08-09 15:09:19 +08:00
'This matrix shows which packages dispatch each harness-owned event and which packages listen to it. Events are many-to-many, so the dense relation data is presented as a table rather than one large graph. 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-08-09 15:27:21 +08:00
// unrecognized semantic dispatch form. Listener-free extension points remain
2026-07-27 06:24:09 +08:00
// valid. Client-declared events are exempt: the relation scan seeds the HOST
// aggregate program only (host+client cannot share one program — the cordis
// Context merges collide), so client dispatch sites are structurally
// invisible here; their rows stay in the table for the declarations' sake.
2026-07-09 12:36:18 +08:00
const undispatched = [ . . . events ]
2026-07-27 06:24:09 +08:00
. filter ( event = > ! event . source . startsWith ( 'packages/client/' ) )
2026-07-09 12:36:18 +08:00
. filter ( event = > ( relations . get ( event . name ) ? . dispatchers . size ? ? 0 ) === 0 )
. map ( event = > event . name )
. sort ( )
if ( undispatched . length > 0 ) {
throw new Error (
` event-producer-consumer matrix: no dispatcher found for declared event ${ undispatched . length > 1 ? 's' : '' } `
2026-08-09 15:27:21 +08:00
+ ` ${ undispatched . map ( name = > ` " ${ name } " ` ) . join ( ', ' ) } — dead vocabulary, or a dispatch form the semantic scan misses `
+ '(teach scripts/gen-doc-graphs.ts that form)' ,
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-08-12 16:23:22 +08:00
'This sequence is the visual companion to [architecture.md](architecture.md#turn-flow). 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-31 19:21:16 +08:00
` Agent-->>SDK: ${ mermaidCode ( 'agent/inbox/spliced' ) } ` ,
` Agent-->>SDK: ${ mermaidCode ( 'agent/inbox/inserted' ) } { message } ` ,
2026-07-03 01:32:01 +08:00
' Agent->>Driver: queued work wakes driver' ,
2026-07-05 01:25:58 +08:00
` Driver-->>SDK: ${ mermaidCode ( 'agent/status' ) } running ` ,
2026-08-12 19:52:04 +08:00
` Driver->>Session: ${ mermaidCode ( 'turn/start' ) } ` ,
2026-07-31 19:21:16 +08:00
' Note over Agent,Driver: claim pending next-step input plus one queued prompt' ,
` Driver-->>SDK: ${ mermaidCode ( 'agent/inbox/spliced' ) } pure deletion ` ,
` Driver-->>SDK: ${ mermaidCode ( 'agent/inbox/claimed' ) } { message, turn } per message ` ,
` Driver->>Hooks: ${ mermaidCode ( 'agent/pre-step' ) } waterfall ` ,
' Hooks-->>Driver: authoritative reject or enter(messages)' ,
' alt proposed step rejected or pre-step failed' ,
2026-08-12 19:52:04 +08:00
' Driver-->>Driver: claimed batch stays removed, the open turn spends no step' ,
2026-07-31 19:21:16 +08:00
' else enter proposed step' ,
2026-07-05 01:25:58 +08:00
` Driver->>Session: ${ mermaidCode ( 'step/start' ) } ` ,
2026-07-31 19:21:16 +08:00
` Driver->>Session: ${ mermaidCode ( 'user/message' ) } per entered message ` ,
` Driver->>Prompt: ${ mermaidCode ( 'system-prompt/assemble' ) } waterfall ` ,
2026-07-05 01:25:58 +08:00
` Driver->>LLM: ${ mermaidCode ( 'agent/request' ) } waterfall, then ${ mermaidCode ( 'llm/stream' ) } waterfall ` ,
2026-07-03 01:32:01 +08:00
' LLM-->>Driver: StreamChunk*' ,
2026-07-05 01:25:58 +08:00
` Driver->>Session: ${ mermaidCode ( 'assistant/chunk' ) } * ` ,
` Session-->>SDK: ${ mermaidCode ( 'session/event' ) } ${ mermaidCode ( 'assistant/chunk' ) } * ` ,
2026-07-15 16:03:52 +08:00
' alt final adapter or terminal in-band request failure' ,
` Driver->>Session: ${ mermaidCode ( 'step/end' ) } ` ,
` Driver->>Hooks: ${ mermaidCode ( 'agent/request-error' ) } waterfall ` ,
2026-07-27 21:17:49 +08:00
' Hooks-->>Driver: return retry action or preserve the original error' ,
2026-07-15 16:03:52 +08:00
' else model request succeeded' ,
2026-07-05 01:25:58 +08:00
` Driver->>Session: ${ mermaidCode ( 'assistant/message' ) } ` ,
2026-07-18 14:59:26 +08:00
' Driver->>Tools: classify pending call by executionMode' ,
' loop barriers and bounded rolling pool, reclassify before start' ,
' opt call starts' ,
` Driver->>Session: ${ mermaidCode ( 'tool/call' ) } ` ,
' Driver->>Tools: ordered pre, concurrent execute' ,
2026-07-16 15:52:35 +08:00
' Tools-->>Session: tool-owned events when applicable' ,
' end' ,
2026-07-18 14:59:26 +08:00
' opt next model-order result ready' ,
2026-07-16 15:52:35 +08:00
' Driver->>Tools: ordered post' ,
` Driver->>Session: ${ mermaidCode ( 'tool/result' ) } ` ,
' end' ,
2026-07-13 11:02:21 +08:00
' end' ,
2026-07-15 16:03:52 +08:00
` Driver->>Session: ${ mermaidCode ( 'step/end' ) } ` ,
2026-07-31 19:21:16 +08:00
' opt natural stop and next-step inbox empty' ,
` Driver->>Hooks: ${ mermaidCode ( 'agent/turn-stopping' ) } serial terminal checkpoint ` ,
' end' ,
' opt next-step input is pending' ,
' Driver-->>Driver: claim pending next-step input' ,
` Driver-->>SDK: ${ mermaidCode ( 'agent/inbox/claimed' ) } { message, turn } per message ` ,
` Driver->>Hooks: ${ mermaidCode ( 'agent/pre-step' ) } waterfall ` ,
' Hooks-->>Driver: authoritative reject or enter(messages)' ,
' end' ,
2026-07-15 16:03:52 +08:00
' end' ,
2026-07-27 18:07:51 +08:00
' end' ,
2026-08-12 19:52:04 +08:00
` Driver->>Session: ${ mermaidCode ( 'turn/end' ) } ` ,
2026-07-05 01:25:58 +08:00
` Driver-->>SDK: ${ mermaidCode ( 'agent/status' ) } idle ` ,
2026-07-03 01:13:52 +08:00
'```' ,
'' ,
2026-08-09 15:35:02 +08:00
'The `assistant/message` event records every successful provider call, including content-less and `max-tokens` finishes. Empty content stays out of derived history, while the durable event keeps usage and `sourceEventSeqs` listing the exact `assistant/chunk` events, including an explicit empty list.' ,
2026-07-15 14:47:29 +08:00
'' ,
2026-08-13 00:36:22 +08:00
'`dsh-compaction-basic` uses `agent/pre-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-08-25 11:50:49 +08:00
'The returned `agent/pre-step` decision is authoritative; listeners wrapping `next()` preserve downstream messages and `startsRequestSeries` unless replacement is intentional. Steering and injected context pass through the same waterfall after a later claim operation takes their next-step batch.' ,
2026-07-21 16:46:48 +08:00
'' ,
2026-07-24 19:54:25 +08:00
'SDK users that need replayable transcript data should consume `session/event`; `agent/*` is the live coordination API for queue/status, prompt interception, request construction, 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-08-09 15:27:21 +08:00
'This graph shows where policy, hooks, sandboxing, filesystem guards, result rewriting, final-outcome observation, and UI rendering run without changing the loop. The `tools/pre-execute` waterfall runs first, monotonic guards run next, and the `tools/execute` and `tools/post-execute` waterfalls follow; the three waterfalls may transform a call. Definition-owned `finalizeContent` and `tools/result` run afterward.' ,
2026-07-03 01:13:52 +08:00
'' ,
'```mermaid' ,
'flowchart TD' ,
' model["Assistant message contains tool-call block"]' ,
2026-07-05 01:25:58 +08:00
` toolCall["Session event: ${ mermaidCode ( 'tool/call' ) } <br/>logged before execution"] ` ,
2026-07-04 12:50:06 +08:00
' presentCall["UI pending card<br/>presentCall(args)"]' ,
2026-07-05 01:25:58 +08:00
` pre[" ${ mermaidCode ( 'tools/pre-execute' ) } waterfall<br/>hooks, permission, sandbox"] ` ,
2026-07-11 22:55:26 +08:00
' guards["Registered monotonic guards<br/>deny or abstain; identity protected"]' ,
2026-07-11 23:14:09 +08:00
' denied["denied or approval refused<br/>tool body skipped"]' ,
2026-07-09 15:25:18 +08:00
` approval[" ${ mermaidCode ( 'ctx.approval' ) } one-shot prompt<br/>absent or unanswerable: deny"] ` ,
2026-07-08 10:06:07 +08:00
` around[" ${ mermaidCode ( 'tools/execute' ) } waterfall<br/>timeout, retry, metrics (around dispatch)"] ` ,
2026-07-03 01:32:01 +08:00
' toolBody["Registered tool execute() body"]' ,
2026-07-05 01:25:58 +08:00
` fsGate[" ${ mermaidCode ( 'fs/write-intent' ) } or ${ mermaidCode ( 'fs/edit-intent' ) } <br/>tool-fs mutations only"] ` ,
feat: Code Mode — the registry's mode config, the SDK codegen, and the run_code bridge
The dsh-tools half of the Code Mode RFC (its fourth, final change): the
registry gains its first config — mode: native | code | both — and OWNS how
its tools reach the model. 'code' contributes exactly one wire tool,
run_code, plus a lazy tools:sdk prompt section declaring every other tool
as a generated TypeScript API (jsonSchemaToTs: total over the defineTool
subset, unknown degradation, lexicographic byte-identical rendering);
'both' ships both representations; 'native' is byte-for-byte the old
behavior. Non-native modes fail every assembly loudly without a
typescript-language ctx.codeRuntime.
run_code's dispatch bridge: JSON-normalizes each binding argument before
dispatch (what dispatches is what the tool/code-dispatch event logs — the
append can never fail on payload shape; BigInt/circulars reject that one
call), serializes all program tool calls through a per-run queue (even
Promise.all — no concurrency-safety metadata yet), routes every sub-call
through tools/pre-execute → tools/post-execute (a deny rejects the
program-side promise), drops sub-call additionalContext (no safe outlet
mid-run; pinned), owns a run-scoped abort that follows the outer signal in
and fires on settlement (in-flight sub-dispatch aborted, queued abandoned,
queue drained before returning), and converts a failed run into
CodeRunFailedError → a structured isError carrying kind + captured logs.
tool/code-dispatch joins SessionEventMap by declaration merging (log-only;
deriveMessages ignores it).
The composed surface: the tools config forwards through agent-core and
both app packages; examples/code-agent + demo:code run the worker runtime
under mode code (keyless boot smoke + a with-key e2e proving the collapsed
[run_code] header, the dispatch events, and the file the program wrote);
two new snapshot scenarios (code-mode-turn, both-mode-turn) record the SDK
section, collapsed header, dispatch events, and result card — each its own
header-pinning class (the harness gains per-scenario config overlays and
per-class pins). Catalogs, graphs, cookbook, hooks-bridge notes, and the
RFC (moved to implemented/, restructured to decision-era headings) updated
in the same change.
2026-07-08 12:58:23 +08:00
` owned["Tool-owned session events<br/> ${ mermaidCode ( 'todo/write' ) } , ${ mermaidCode ( 'fs/observed' ) } , ${ mermaidCode ( 'hook/invoked' ) } , ${ mermaidCode ( 'hook/result' ) } , ${ mermaidCode ( 'tool/code-dispatch' ) } "] ` ,
2026-07-05 01:25:58 +08:00
` post[" ${ mermaidCode ( 'tools/post-execute' ) } waterfall<br/>accept, block, replace, add context"] ` ,
2026-07-23 03:15:15 +08:00
' normalized["Registry outer normalization<br/>pipeline/result snapshot throws become isError"]' ,
2026-07-23 02:53:43 +08:00
' finalize["ToolDefinition.finalizeContent<br/>last content-only invariant"]' ,
2026-07-13 11:58:55 +08:00
` final[" ${ mermaidCode ( 'tools/result' ) } synchronous notification<br/>frozen authoritative outcome"] ` ,
2026-07-23 19:15:45 +08:00
' context["Active-batch additionalContexts FIFO<br/>injected user/message after recorded tool results"]' ,
2026-07-05 01:25:58 +08:00
` toolResult["Session event: ${ mermaidCode ( 'tool/result' ) } <br/>single model-facing outcome"] ` ,
2026-07-15 12:49:50 +08:00
' allResults["Tool batch settled<br/>recorded tool/result events complete"]' ,
2026-07-04 12:50:06 +08:00
' presentResult["UI completed card<br/>presentResult(args, result)"]' ,
' model --> toolCall' ,
' toolCall --> presentCall' ,
' toolCall --> pre' ,
2026-07-11 22:55:26 +08:00
' pre -->|allow| guards' ,
' guards -->|allow| around' ,
' guards -->|deny| denied' ,
2026-07-23 02:53:43 +08:00
' guards -.->|throw| normalized' ,
2026-07-08 10:06:07 +08:00
' around --> toolBody' ,
2026-07-09 15:25:18 +08:00
' pre -->|deny| denied' ,
' pre -->|ask| approval' ,
2026-07-11 23:14:09 +08:00
' approval -->|allowed-once| guards' ,
2026-07-09 15:25:18 +08:00
' approval -->|rejected, cancelled, unavailable| denied' ,
2026-07-23 02:53:43 +08:00
' approval -.->|throw| normalized' ,
2026-07-04 12:50:06 +08:00
' denied --> post' ,
2026-07-23 02:53:43 +08:00
' pre -.->|throw| normalized' ,
2026-07-04 12:50:06 +08:00
' toolBody --> fsGate' ,
' fsGate --> toolBody' ,
2026-07-03 01:32:01 +08:00
' toolBody --> owned' ,
2026-07-08 10:06:07 +08:00
' toolBody --> around' ,
' around --> post' ,
2026-07-23 02:53:43 +08:00
' around -.->|wrapper throws| normalized' ,
' post -.->|throw| normalized' ,
' post --> finalize' ,
' normalized --> finalize' ,
' finalize --> final' ,
2026-07-11 22:55:26 +08:00
' final --> toolResult' ,
2026-07-04 12:50:06 +08:00
' toolResult --> presentResult' ,
2026-07-11 22:55:26 +08:00
' toolResult --> allResults' ,
' allResults --> context' ,
2026-07-03 01:13:52 +08:00
'```' ,
'' ,
2026-07-24 19:54:25 +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`, return denials as binding rejections, and omit `additionalContexts` to preserve call/result adjacency.' ,
2026-07-03 01:13:52 +08:00
'' ,
2026-07-05 02:54:01 +08:00
. . . maintenanceFooter ( maintenance ) ,
2026-07-03 01:13:52 +08:00
] . join ( '\n' )
}
2026-07-05 01:25:58 +08:00
function renderDocs ( ) : GraphDoc [ ] {
2026-07-14 00:24:04 +08:00
const pkgs = collectPackageGraph ( root , GROUP_ORDER , 'gen-doc-graphs' )
2026-07-28 23:48:35 +08:00
const { model } = projectCordisCatalog ( root , CORDIS_CATALOG_POLICY )
2026-07-03 01:13:52 +08:00
const docs : GraphDoc [ ] = [
2026-07-28 23:48:35 +08:00
{ rel : 'docs/capability-seams.md' , content : renderCapabilitySeams ( pkgs , model . services ) } ,
2026-07-05 01:25:58 +08:00
. . . APP_EXAMPLES . map ( example = > ( { rel : example.rel , content : renderAppComposition ( example ) } ) ) ,
2026-07-28 23:48:35 +08:00
{ rel : 'docs/event-producer-consumer.md' , content : renderEventRelations ( pkgs , model . events ) } ,
2026-07-05 01:25:58 +08:00
{ rel : 'docs/agent-lifecycle.md' , content : renderLifecycle ( ) } ,
{ rel : 'docs/tool-execution-pipeline.md' , content : renderToolPipeline ( ) } ,
2026-07-03 01:13:52 +08:00
]
2026-07-05 01:25:58 +08:00
docs . unshift ( { rel : 'docs/graph-atlas.md' , content : renderIndex ( docs ) } )
2026-07-03 01:13:52 +08:00
return docs
}
function renderIndex ( docs : GraphDoc [ ] ) : string {
const labels : Record < string , string > = {
2026-07-05 01:25:58 +08:00
'docs/capability-seams.md' : 'capability seams and core services' ,
2026-08-04 10:07:17 +08:00
'apps/cli/composition.md' : 'dsh shared base composition' ,
2026-07-05 01:25:58 +08:00
'docs/event-producer-consumer.md' : 'event producer/consumer matrix' ,
'docs/agent-lifecycle.md' : 'agent turn and step lifecycle' ,
'docs/tool-execution-pipeline.md' : 'tool execution pipeline' ,
2026-07-03 01:13:52 +08:00
}
const modes : Record < string , string > = {
2026-07-05 01:25:58 +08:00
'docs/capability-seams.md' : 'hybrid generated' ,
2026-08-04 10:07:17 +08:00
'apps/cli/composition.md' : 'hybrid generated' ,
2026-07-05 01:25:58 +08:00
'docs/event-producer-consumer.md' : 'hybrid generated' ,
'docs/agent-lifecycle.md' : 'curated' ,
'docs/tool-execution-pipeline.md' : 'curated' ,
2026-07-03 01:13:52 +08:00
}
2026-07-05 01:25:58 +08:00
const rows = [
'| [module dependency graph](module-graph.md) | `generated` |' ,
2026-07-06 22:26:06 +08:00
'| [tool schema catalog and package map](tool-catalog.md) | `generated` |' ,
2026-07-05 01:25:58 +08:00
. . . docs . map ( ( doc ) = > {
2026-07-05 02:54:01 +08:00
const link = graphIndexLink ( doc . rel )
2026-07-05 01:25:58 +08:00
return ` | [ ${ labels [ doc . rel ] ? ? link } ]( ${ link } ) | \` ${ modes [ doc . rel ] ? ? 'generated' } \` | `
} ) ,
]
2026-07-05 02:54:01 +08:00
const maintenance = 'mixed: each linked page declares generated, hybrid, or curated mode'
2026-07-03 01:13:52 +08:00
return [
2026-07-05 02:54:01 +08:00
. . . generatedHeader ( 'Documentation Graph Index' ) ,
2026-07-24 19:54:25 +08:00
'These diagrams show relationships that the generated catalogs do not. Use them to find package relationships, capability seams, event flow, model-facing tools, app composition, and runtime lifecycle paths. Exact signatures and type definitions still live in the [subsystem pages](subsystems/core.md) (types + the generated Cordis API regions) and [tool-catalog.md](tool-catalog.md).' ,
2026-07-03 01:13:52 +08:00
'' ,
2026-07-26 23:08:47 +08:00
'The process decision behind this index is recorded in [the documentation graph Agent Note](../.agents/notes/archived/process/2026-07-03-documentation-graph-atlas.md).' ,
2026-07-03 01:13:52 +08:00
'' ,
'| Graph | Mode |' ,
'| --- | --- |' ,
2026-07-05 01:25:58 +08:00
. . . rows ,
2026-07-03 01:13:52 +08:00
'' ,
'Regenerate with `pnpm run gen-doc-graphs`; verify freshness with `pnpm run verify-doc-graphs`.' ,
'' ,
2026-07-05 02:54:01 +08:00
. . . maintenanceFooter ( maintenance ) ,
2026-07-03 01:13:52 +08:00
] . join ( '\n' )
}
2026-07-05 01:25:58 +08:00
function main ( ) : void {
const docs = renderDocs ( )
2026-07-03 01:13:52 +08:00
if ( process . argv . includes ( '--check' ) ) {
const stale : string [ ] = [ ]
for ( const doc of docs ) {
const abs = resolve ( root , doc . rel )
const committed = existsSync ( abs ) ? readFileSync ( abs , 'utf8' ) : null
if ( committed !== doc . content ) stale . push ( doc . rel )
}
if ( stale . length === 0 ) {
console . log ( ` gen-doc-graphs: ${ docs . length } graph doc(s) are up to date. ` )
return
}
console . error ( ` gen-doc-graphs: stale graph doc(s): ${ stale . join ( ', ' ) } . Run \` pnpm run gen-doc-graphs \` and commit the result. ` )
process . exit ( 1 )
}
2026-07-05 01:25:58 +08:00
for ( const doc of docs ) {
mkdirSync ( dirname ( resolve ( root , doc . rel ) ) , { recursive : true } )
writeFileSync ( resolve ( root , doc . rel ) , doc . content )
}
2026-07-03 01:13:52 +08:00
console . log ( ` gen-doc-graphs: wrote ${ docs . length } graph doc(s). ` )
}
if ( process . argv [ 1 ] && import . meta . filename === resolve ( process . argv [ 1 ] ) ) {
2026-07-05 01:25:58 +08:00
main ( )
2026-07-03 01:13:52 +08:00
}