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
This commit is contained in:
Tianyi Cui
2026-08-07 20:34:31 +08:00
parent e385c11e8e
commit 18ab9f6db2
35 changed files with 582 additions and 673 deletions

View File

@@ -48,9 +48,13 @@ interface EventRelation {
listeners: Set<string>
}
interface PackageSource {
/** One scanned package source file and its owning package short name. */
export interface PackageSource {
/** Repository-relative path. */
rel: string
/** Package short name from the `packages/<group>/<pkg>/src` path. */
pkg: string
/** The bound program source file. */
sourceFile: ts.SourceFile
}
@@ -120,7 +124,7 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'session',
title: 'In-memory session store',
mode: 'core',
consumers: ['agent-loop', 'agent', 'cli-demo', 'session-persistence', 'session-query', 'session-query-sqlite', 'subagent-inprocess', 'invariants'],
consumers: ['agent-loop', 'agent', 'session-persistence', 'session-query', 'session-query-sqlite', 'subagent-inprocess', 'invariants'],
note: 'Owns append-only Session instances and emits the durable session event feed.',
},
{
@@ -155,6 +159,24 @@ const SERVICE_ROLES: ServiceRole[] = [
consumers: ['agent-loop', 'tool-bash', 'hooks-claude', 'hooks-codex', 'session-query', 'session-query-sqlite'],
note: 'Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time.',
},
{
key: 'settings',
pkg: 'settings',
title: 'User-settings seam',
mode: 'seam',
implementations: ['settings-local'],
consumers: ['llm-deepseek', 'llm-pi-ai', 'apiproxy'],
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.',
},
{
key: 'credentials',
pkg: 'credentials',
title: 'Credential seam',
mode: 'seam',
implementations: ['credentials-local'],
consumers: ['llm-deepseek', 'llm-pi-ai', 'apiproxy'],
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.',
},
{
key: 'telemetry',
pkg: 'session-telemetry',
@@ -203,7 +225,6 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'session-reference',
title: 'Cross-session snapshot preparation',
mode: 'core',
consumers: ['tui'],
note: 'Projects bounded current-surface conversation snapshots into durable untrusted message context; host adapters own mention syntax.',
},
{
@@ -235,8 +256,7 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'user-interaction',
title: 'Human question/answer seam',
mode: 'seam',
implementations: ['tui'],
consumers: ['tool-ask-user', 'tui'],
consumers: ['tool-ask-user'],
note: 'UI front doors provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise.',
},
{
@@ -251,8 +271,7 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'commands',
title: 'Human command registry',
mode: 'core',
consumers: ['tui'],
note: 'Plugins register direct human commands; TUI consumes the effective per-agent catalog without sending invocations to the model.',
note: 'Plugins register direct human commands without sending invocations to the model.',
},
{
key: 'sessionProjections',
@@ -270,19 +289,12 @@ const SERVICE_ROLES: ServiceRole[] = [
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.',
},
{
key: 'tui',
pkg: 'tui',
title: 'Mounted-terminal interaction service',
mode: 'bundle',
note: 'One TUI front door provides a FIFO overlay host; injected plugins receive caller-fiber ownership without access to pi-tui or terminal lifecycle state.',
},
{
key: 'skills',
pkg: 'skill',
title: 'Skill provider registry',
mode: 'seam',
implementations: ['skill-local'],
implementations: ['skill-badge', 'skill-local'],
consumers: ['tool-skill'],
note: 'Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies.',
},
@@ -291,7 +303,7 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'agent',
title: 'Agent service',
mode: 'core',
consumers: ['agent-loop', 'acp', 'cli-demo', 'subagent-inprocess', 'tui-demo'],
consumers: ['agent-loop', 'acp', 'subagent-inprocess'],
note: 'Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation.',
},
{
@@ -315,24 +327,25 @@ const SERVICE_ROLES: ServiceRole[] = [
title: 'Subprocess seam',
mode: 'seam',
implementations: ['subprocess-local'],
consumers: ['bash-local', 'bash-sandbox', 'pty-local', 'lsp-local', 'subagent-acp'],
note: 'The bash executors, PTY shell backend, LSP host, and ACP subagent backend spawn through ctx.subprocess; the service owns process coordinates, tree/session lifetime, stdio dispositions, terminal mechanics, and kill escalation.',
consumers: ['bash-local', 'bash-sandbox', 'pty-local', 'lsp-local', 'subagent-acp', 'subagent-codex', 'subagent-claude-code'],
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.',
},
{
key: 'bash',
pkg: 'bash',
title: 'Bash executor seam',
mode: 'seam',
implementations: ['bash-local', 'bash-sandbox'],
consumers: ['tool-bash', 'hooks-claude', 'hooks-codex'],
note: 'The model-facing bash tools and hook bridges consume this seam; sandboxed or remote executors replace bash-local without touching them.',
implementations: ['bash-local', 'bash-sandbox', 'pwsh-local'],
consumers: ['tool-bash', 'tool-pwsh', 'hooks-claude', 'hooks-codex'],
note: 'The model-facing shell tools and hook bridges consume this seam; sandboxed, remote, or PowerShell executors replace bash-local without touching them.',
},
{
key: 'bashEnv',
pkg: 'tool-bash',
pkg: 'bash-env',
title: 'Managed bash environment registry',
mode: 'core',
note: 'Plugins declare effect-scoped DSH_* facts; tool-bash collects one trusted snapshot per execution and the executor rebuilds the namespace.',
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.',
},
{
key: 'pty',
@@ -409,11 +422,11 @@ const SERVICE_ROLES: ServiceRole[] = [
{
key: 'subagents',
pkg: 'subagent',
title: 'Subagent provider registry',
title: 'Subagent provider and continuation service',
mode: 'seam',
implementations: ['subagent-spawn', 'subagent-fork', 'subagent-acp'],
consumers: ['tool-subagent', 'tool-ralph'],
note: 'Providers implement transports; tool-subagent exposes configured delegation while tool-ralph requires one fresh structured-output route.',
implementations: ['subagent-spawn', 'subagent-fork', 'subagent-acp', 'subagent-codex', 'subagent-claude-code', 'subagent-dsh-sdk'],
consumers: ['tool-subagent', 'tool-subagent-control', 'tool-ralph'],
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.',
},
{
key: 'tasks',
@@ -592,7 +605,8 @@ function parseExampleCordis(rel: string): ExamplePlugin[] {
if (current?.name) plugins.push({ id: current.id, name: current.name })
}
for (const line of text.split('\n')) {
const id = /^-\s+id:\s+(.+?)\s*$/.exec(line)
// Top-level rows (`- id:`) and bundle-patch insert rows (` - id:`).
const id = /^\s*-\s+id:\s+(.+?)\s*$/.exec(line)
if (id?.[1] !== undefined) {
flush()
current = { id: stripYamlScalar(id[1]) }
@@ -611,28 +625,20 @@ function stripYamlScalar(value: string): string {
const APP_EXAMPLES = [
{
id: 'tui',
rel: 'examples/tui-agent/composition.md',
title: 'TUI Agent App Composition',
label: 'examples/tui-agent',
config: 'examples/tui-agent/cordis.yml',
summary: 'The TUI agent combines the real DeepSeek adapter, coding tools, compaction, subagents, and workflows with the full-screen terminal app package.',
id: 'dsh_base',
rel: 'apps/cli/composition.md',
title: 'DSH Base Composition',
label: 'packages/bundle/base/cordis.patch.yml',
config: 'packages/bundle/base/cordis.patch.yml',
summary: 'The dsh-base bundle patch every profile applies first; mode bundles (dsh-web-app, dsh-headless) and the user\'s profile layer patch over it.',
},
{
id: 'headless',
rel: 'examples/headless-agent/composition.md',
title: 'Headless Agent App Composition',
title: 'Headless Agent Snapshot Composition',
label: 'examples/headless-agent',
config: 'examples/headless-agent/cordis.yml',
summary: 'The headless demo combines the real DeepSeek adapter and coding capabilities with the one-shot app package, format-pure stdout, and one fresh persisted top-level session.',
},
{
id: 'cordis',
rel: 'examples/cordis-agent/composition.md',
title: 'Cordis Agent App Composition',
label: 'examples/cordis-agent',
config: 'examples/cordis-agent/cordis.yml',
summary: 'The self-referential demo puts @deepseek-ai/dsh-tool-cordis on the coding spine, letting the agent inspect its current-process runtime and mount or unmount in-memory temporary Plugins.',
summary: 'The headless snapshot composition combines the real DeepSeek adapter and coding capabilities with one explicitly configured persisted top-level agent; its JSONL driver is test-only.',
},
{
id: 'acp',
@@ -651,11 +657,7 @@ function renderAppExpansion(lines: string[], appNode: string, pluginName: string
const jsonl = nodeId('bundle', 'jsonl')
lines.push(` ${appNode} --> ${agentCore}["@deepseek-ai/dsh-agent-spine-demo"]`)
lines.push(` ${appNode} --> ${jsonl}["@deepseek-ai/dsh-session-persistence-jsonl"]`)
if (pluginName === '@deepseek-ai/dsh-tui-demo') {
lines.push(` ${appNode} --> ${nodeId('frontdoor', 'tui')}["@deepseek-ai/dsh-tui<br/>pre-created main agent"]`)
} else if (pluginName === '@deepseek-ai/dsh-cli-demo') {
lines.push(` ${appNode} --> ${nodeId('frontdoor', 'cli')}["one-shot driver<br/>format-pure stdout<br/>fresh top-level agent"]`)
} else if (pluginName === '@deepseek-ai/dsh-acp-demo') {
if (pluginName === '@deepseek-ai/dsh-acp-demo') {
lines.push(` ${appNode} --> ${nodeId('frontdoor', 'acp')}["@deepseek-ai/dsh-acp<br/>automation-only JSON-RPC stdio<br/>fresh sessions created by client"]`)
}
lines.push(
@@ -681,7 +683,7 @@ function renderAppComposition(example: AppExample): string {
const pluginNode = nodeId(`plugin_${example.id}`, plugin.id)
lines.push(` ${pluginNode}["${escLabel(plugin.id)}<br/>${escLabel(plugin.name)}"]`)
lines.push(` cfg --> ${pluginNode}`)
if (plugin.name === '@deepseek-ai/dsh-tui-demo' || plugin.name === '@deepseek-ai/dsh-cli-demo' || plugin.name === '@deepseek-ai/dsh-acp-demo') {
if (plugin.name === '@deepseek-ai/dsh-acp-demo') {
renderAppExpansion(lines, pluginNode, plugin.name)
}
}
@@ -698,13 +700,26 @@ function renderAppComposition(example: AppExample): string {
return lines.join('\n')
}
type CallSiteIndex = Map<ts.SignatureDeclaration | ts.JSDocSignature, ts.CallExpression[]>
/**
* 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.
*/
const EVENT_API_METHODS = new Set(['on', 'once', 'emit', 'parallel', 'serial', 'waterfall', 'dispatch'])
/** Collect event dispatch/listener relations from real cross-file receiver types. */
class EventRelationCollector {
export class EventRelationCollector {
private readonly relations = new Map<string, EventRelation>()
private readonly callSites = new Map<ts.SignatureDeclaration | ts.JSDocSignature, ts.CallExpression[]>()
private readonly fileCallSites = new Map<ts.SourceFile, CallSiteIndex>()
private readonly localCalleeProofs = new Map<ts.FunctionDeclaration, boolean>()
private globalCallSites: CallSiteIndex | null = null
private readonly contextType: ts.Type
private readonly agentDispatchType: ts.Type
private readonly eventsServiceType: ts.Type
private readonly packageSourceFiles: ReadonlySet<ts.SourceFile>
constructor(
private readonly project: TypeScriptProject,
@@ -713,7 +728,7 @@ class EventRelationCollector {
this.contextType = this.declaredType('vendor/cordis/src/context.ts', 'Context')
this.agentDispatchType = this.declaredType('packages/core/agent/src/dispatch.ts', 'AgentEventDispatch')
this.eventsServiceType = this.declaredType('vendor/cordis/src/events.ts', 'EventsService')
this.indexCallSites()
this.packageSourceFiles = new Set(sources.map(source => source.sourceFile))
}
/** Return all event relations discovered from the Program. */
@@ -733,20 +748,88 @@ class EventRelationCollector {
return this.project.checker.getDeclaredTypeOfSymbol(symbol)
}
/** Index resolved local function calls for narrow argument-flow recovery. */
private indexCallSites(): void {
/** 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()
const visit = (node: ts.Node): void => {
if (ts.isCallExpression(node)) {
const declaration = this.project.checker.getResolvedSignature(node)?.declaration
if (declaration) {
const calls = this.callSites.get(declaration) ?? []
const calls = index.get(declaration) ?? []
calls.push(node)
this.callSites.set(declaration, calls)
index.set(declaration, calls)
}
}
ts.forEachChild(node, visit)
}
for (const source of this.sources) visit(source.sourceFile)
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
* file, so only that file is indexed. Any other reference shape may alias
* 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) ?? []
}
/**
* 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.
*/
private provenLocalCallee(owner: ts.FunctionDeclaration): boolean {
const cached = this.localCalleeProofs.get(owner)
if (cached !== undefined) return cached
if (hasExportModifier(owner) || !ts.isExternalModule(owner.getSourceFile())) {
this.localCalleeProofs.set(owner, false)
return false
}
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
}
/** Walk one package source file and classify event API calls by receiver type. */
@@ -760,7 +843,7 @@ class EventRelationCollector {
this.addDispatcher(name, source.pkg, 'emitAgentEvent')
}
}
} else if (ts.isPropertyAccessExpression(node.expression)) {
} else if (ts.isPropertyAccessExpression(node.expression) && EVENT_API_METHODS.has(node.expression.name.text)) {
const receiverKind = this.receiverKind(node.expression.expression)
const method = node.expression.name.text
if (receiverKind === 'events-service' && method === 'dispatch') {
@@ -863,7 +946,7 @@ class EventRelationCollector {
const index = owner.parameters.indexOf(parameter)
if (index < 0) return new Set()
const events = new Set<string>()
for (const call of this.callSites.get(owner) ?? []) {
for (const call of this.callSitesFor(owner)) {
const argument = call.arguments[index]
if (argument) addAll(events, this.eventNamesFromArgumentList(argument, new Set(seen)))
}
@@ -910,6 +993,21 @@ class EventRelationCollector {
}
}
/** 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
}
/** Peel syntax-only wrappers that do not change an expression's runtime value. */
function unwrapExpression(expression: ts.Expression): ts.Expression {
let current = expression
@@ -965,14 +1063,22 @@ function unionSets<T>(left: ReadonlySet<T>, right: ReadonlySet<T>): Set<T> {
return out
}
function collectEventRelations(): Map<string, EventRelation> {
const project = new TypeScriptProject(root)
const sources = project.sourceFiles().flatMap((sourceFile): PackageSource[] => {
/**
* 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[] => {
const rel = project.relativePath(sourceFile)
const match = /^packages\/[^/]+\/([^/]+)\/src\/.+\.ts$/.exec(rel)
return match?.[1] ? [{ rel, pkg: match[1], sourceFile }] : []
}).sort((left, right) => left.rel.localeCompare(right.rel))
return new EventRelationCollector(project, sources).collect()
}
function collectEventRelations(): Map<string, EventRelation> {
const project = new TypeScriptProject(root)
return new EventRelationCollector(project, collectPackageSources(project)).collect()
}
function relationPackages(map: Map<string, Set<string>>, pkgsByShort: Map<string, Pkg>): string {
@@ -1053,20 +1159,22 @@ function renderLifecycle(): string {
' participant Session',
' participant SDK as UI or SDK listener',
' User->>Agent: followup(content)',
` Agent-->>SDK: ${mermaidCode('agent/inbox/enqueue')}`,
` Agent-->>SDK: ${mermaidCode('agent/inbox/spliced')}`,
` Agent-->>SDK: ${mermaidCode('agent/inbox/inserted')} { message }`,
' Agent->>Driver: queued work wakes driver',
` Driver-->>SDK: ${mermaidCode('agent/status')} running`,
' Note over Agent,Driver: next-step acceptance window opens',
` Driver->>Hooks: ${mermaidCode('agent/prompt-submit')} waterfall`,
' Hooks-->>Driver: authoritative allow, block, or add context',
' alt prompt blocked or admission failed',
' Driver-->>Driver: append context-only batch or keep steering boundary pending',
' else prompt allowed',
' 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',
' Driver-->>Driver: claimed batch stays removed, no turn opens',
' else enter proposed step',
` Driver->>Session: ${mermaidCode('turn/start')}`,
` Driver->>Session: ${mermaidCode('user/message')}`,
` Driver->>Prompt: ${mermaidCode('system-prompt/assemble')} waterfall`,
` Driver-->>Driver: ${mermaidCode('agent/step')} serial checkpoint`,
` Driver->>Session: ${mermaidCode('step/start')}`,
` Driver->>Session: ${mermaidCode('user/message')} per entered message`,
` Driver->>Prompt: ${mermaidCode('system-prompt/assemble')} waterfall`,
` Driver->>LLM: ${mermaidCode('agent/request')} waterfall, then ${mermaidCode('llm/stream')} waterfall`,
' LLM-->>Driver: StreamChunk*',
` Driver->>Session: ${mermaidCode('assistant/chunk')}*`,
@@ -1089,11 +1197,17 @@ function renderLifecycle(): string {
` Driver->>Session: ${mermaidCode('tool/result')}`,
' end',
' end',
' Driver->>Session: post-tool context and steering (no prompt-submit)',
` Driver->>Session: ${mermaidCode('step/end')}`,
` Driver->>Hooks: ${mermaidCode('agent/turn-stopping')} serial terminal checkpoint`,
' 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',
' end',
' Note over Agent,Driver: next-step acceptance window closes',
` Driver->>Session: ${mermaidCode('turn/end')}`,
' end',
` Driver-->>SDK: ${mermaidCode('agent/status')} idle`,
@@ -1101,9 +1215,9 @@ function renderLifecycle(): string {
'',
'The `assistant/message` edge records every successful provider call, including content-less and `max-tokens` finishes. Empty content stays out of derived history while the durable anchor retains usage and exact chunk provenance, including an explicit empty source set.',
'',
'`dsh-compact-basic` uses `agent/step` for pressure before request derivation and `agent/request-error` only for canonical context overflow. Once either trigger qualifies, optional tool-result pruning runs before summary selection. Recovery works between the closed failed step and failed turn close, and opens a fresh retry turn only when pruning or summarization advances the surface replacement generation; otherwise the original request error remains authoritative.',
'`dsh-compact-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.',
'',
'The returned `agent/prompt-submit` allow is authoritative; listeners wrapping `next()` preserve downstream content and additional contexts unless replacement is intentional. Steering bypasses that waterfall and joins at its durable checkpoint.',
'The returned `agent/pre-step` decision is authoritative; listeners wrapping `next()` preserve downstream messages unless replacement is intentional. Steering and injected context pass through the same waterfall after a later boundary claims their next-step batch.',
'',
'SDK users that need replayable transcript data should consume `session/event`; `agent/*` is the live coordination surface for queue/status, prompt interception, request shaping, steering, continuation, and errors.',
'',
@@ -1192,8 +1306,8 @@ function renderDocs(): GraphDoc[] {
function renderIndex(docs: GraphDoc[]): string {
const labels: Record<string, string> = {
'docs/capability-seams.md': 'capability seams and core services',
'apps/cli/composition.md': 'dsh shared base composition',
'examples/headless-agent/composition.md': 'headless-agent app composition',
'examples/tui-agent/composition.md': 'tui-agent app composition',
'examples/cordis-agent/composition.md': 'cordis-agent app composition',
'examples/acp-agent/composition.md': 'acp-agent app composition',
'docs/event-producer-consumer.md': 'event producer/consumer matrix',
@@ -1202,8 +1316,8 @@ function renderIndex(docs: GraphDoc[]): string {
}
const modes: Record<string, string> = {
'docs/capability-seams.md': 'hybrid generated',
'apps/cli/composition.md': 'hybrid generated',
'examples/headless-agent/composition.md': 'hybrid generated',
'examples/tui-agent/composition.md': 'hybrid generated',
'examples/cordis-agent/composition.md': 'hybrid generated',
'examples/acp-agent/composition.md': 'hybrid generated',
'docs/event-producer-consumer.md': 'hybrid generated',