Merge remote-tracking branch 'origin/master' into codex/truncated-design

# Conflicts:
#	docs/capability-seams.md
#	docs/config-catalog.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/core.md
#	docs/event-producer-consumer.md
#	docs/module-graph.md
#	docs/rfc/INDEX.md
#	docs/tool-catalog.md
#	examples/acp-agent/README.md
#	packages/README.md
#	packages/bash/bash/README.md
#	packages/core/tools/tests/gen-tool-catalog.spec.ts
#	packages/support/acp-snapshot/src/harness.ts
#	pnpm-lock.yaml
#	scripts/gen-doc-graphs.ts
#	scripts/gen-tool-catalog.ts
#	scripts/type-equiv.manifest.json
This commit is contained in:
Dudu-0223
2026-07-13 09:49:46 +08:00
424 changed files with 35979 additions and 6313 deletions

View File

@@ -105,12 +105,6 @@ const dshBinPackageFiles = [
'src',
] as const
// Packages that ship a worker-thread entry as a sibling runtime bundle
// (lib/worker.js, its own tsdown entry): the bootstrap is loaded via
// `new Worker(new URL('./worker.js', import.meta.url))`, so it cannot live
// inside the index bundle and must be published alongside it.
const workerEntryPackages = new Set(['@deepseek-ai/dsh-code-runtime-worker'])
const dshWorkerPackageFiles = [
'lib/index.js',
'lib/worker.js',
@@ -124,8 +118,12 @@ function sameStringList(actual: readonly string[] | undefined, expected: readonl
}
function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
if (manifest.name && workerEntryPackages.has(manifest.name)) return dshWorkerPackageFiles
return manifest.bin ? dshBinPackageFiles : dshPackageFiles
if (manifest.bin) return dshBinPackageFiles
// A declared "./worker" subpath export sanctions the one extra runtime
// bundle a worker-thread entry needs (and NodeNext/publint then validate
// that subpath's targets like any other export).
if (manifest.exports?.['./worker']) return dshWorkerPackageFiles
return dshPackageFiles
}
function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {

View File

@@ -0,0 +1,29 @@
/**
* Boot the Code Mode demo under the UI named on the command line:
* `pnpm run demo:code-mode [repl|acp]`, default `repl`. Code Mode is the
* point — the UI is just the surface it happens to wear: each UI boots its
* base example through that example's `code-mode.cordis.yml` overlay
* (include ./cordis.yml, flip `tools.mode` to `code`, insert the
* worker-thread code runtime). Both need DEEPSEEK_API_KEY (repo-root .env
* works). Anything else on the command line is a misconfiguration and
* fails loud with usage.
*/
import { spawn } from 'node:child_process'
// Each UI's node invocation, verbatim what its base demo script runs plus
// the overlay config (the stdio bin keeps --expose-internals for the cordis
// Loader's HMR path).
const UIS = new Map([
['repl', ['--expose-internals', '--import', 'tsx', 'packages/ui/stdio-agent/src/bin.ts', 'examples/coding-agent/code-mode.cordis.yml']],
['acp', ['--import', 'tsx', 'packages/ui/acp-agent/src/bin.ts', 'examples/acp-agent/code-mode.cordis.yml']],
])
const ui = process.argv[2] ?? 'repl'
const args = UIS.get(ui)
if (!args || process.argv.length > 3) {
console.error('usage: pnpm run demo:code-mode [repl|acp]')
process.exit(2)
}
const child = spawn(process.execPath, args, { stdio: 'inherit' })
child.on('exit', (code, signal) => { process.exit(signal !== null ? 1 : code ?? 1) })

View File

@@ -1,11 +1,11 @@
{
"AGENTS.md": 1691,
"AGENTS.md": 1802,
"docs/AGENTS.md": 1315,
"docs/architecture.md": 1640,
"docs/architecture.md": 1750,
"docs/cordis-primer.md": 550,
"docs/defensive-patterns.md": 550,
"docs/testing.md": 800,
"examples/AGENTS.md": 610,
"examples/AGENTS.md": 705,
"packages/AGENTS.md": 450,
"packages/README.md": 610
"packages/README.md": 710
}

View File

@@ -633,11 +633,17 @@ export function collectConfigCatalog(scanRoot: string = root): CatalogEntry[] {
const manifests: { dir: string; pkg: string }[] = []
for (const manifestRel of globSync('packages/*/*/package.json', { cwd: scanRoot }).sort()) {
const dir = manifestRel.slice(0, -'/package.json'.length)
const pkg = (JSON.parse(readFileSync(resolve(scanRoot, manifestRel), 'utf8')) as { name?: string }).name
const manifest = JSON.parse(readFileSync(resolve(scanRoot, manifestRel), 'utf8')) as { name?: string; os?: string[]; cpu?: string[] }
const pkg = manifest.name
if (!pkg) {
violations.push(`${manifestRel} has no "name".`)
continue
}
if (manifest.os !== undefined && manifest.cpu !== undefined) {
// A per-platform native-binary package (npm os/cpu selection) ships no
// JavaScript at all — nothing to classify, no Config to catalog.
continue
}
pkgDirByName.set(pkg, dir)
manifests.push({ dir, pkg })
}

247
scripts/gen-cordis-api.ts Normal file
View File

@@ -0,0 +1,247 @@
/**
* Generate (and verify) the runtime cordis API catalog the `cordis_inspect`
* tool serves to the model: packages/cordis/tool-cordis/src/api-catalog.ts.
*
* The artifact is the machine-readable sibling of docs/cordis-catalog: it
* reuses `collectServices` / `collectEvents` from `gen-cordis-catalog.ts` (the
* same JSDoc-completeness-enforcing AST walk), so the API the model reads at
* runtime and the API the docs render cannot diverge. Emitted as a typed
* TypeScript data module (not JSON): it compiles under the package tsconfig,
* passes lint and the export-JSDoc gate, and is trivially covered by import.
*
* The data is trimmed for a model-facing text surface: per service the
* `ctx.<key>` name, the first sentence of the class doc, and the raw method
* signatures; per event the name, `@mode`, signature, and first sentence of
* doc; the SHAPES of every exported interface/type-alias the service
* signatures reference (transitively — so a model can see that e.g. a
* `BashRunResult.stdout` is `{ text, truncated }`, not a string); plus the
* curated inherited `ctx` surface shared with the docs catalog. Source
* pointers are dropped (a `file:line` means nothing to the model) and entries
* are sorted deterministically.
*
* `tsx scripts/gen-cordis-api.ts` → write the artifact
* `tsx scripts/gen-cordis-api.ts --check` → exit 1 if the committed file is
* stale (CI / pre-push gate)
*/
import { globSync, readFileSync, writeFileSync } from 'node:fs'
import { resolve } from 'node:path'
import ts from 'typescript'
import { collectEvents, collectServices, INHERITED_SERVICES } from './gen-cordis-catalog.ts'
const root = resolve(import.meta.dirname, '..')
const OUT = 'packages/cordis/tool-cordis/src/api-catalog.ts'
/** Declarations longer than this render as a truncated stub — a shape the model cannot skim teaches nothing. */
const MAX_DECL_CHARS = 1500
/** The first sentence of a (possibly multi-line) JSDoc prose block. */
function firstSentence(doc: string): string {
const line = doc.split('\n', 1)[0] ?? ''
const match = /^(.*?[.!?])(?:\s|$)/.exec(line)
return (match?.[1] ?? line).trim()
}
/** Render a string as a single-quoted, lint-clean TS literal. */
function quote(value: string): string {
return `'${value.replace(/\\/g, '\\\\').replace(/'/g, '\\\'').replace(/\n/g, '\\n')}'`
}
/**
* Every exported `interface` / `type` declaration under `packages/<group>/<pkg>/src`,
* printed without comments, keyed by name. A name declared in more than one
* package (e.g. each plugin's `Config`) is ambiguous and dropped entirely —
* serving the wrong package's shape is worse than serving none.
*/
function collectTypeDecls(scanRoot: string = root): Map<string, string> {
const printer = ts.createPrinter({ removeComments: true })
const decls = new Map<string, string>()
const ambiguous = new Set<string>()
for (const rel of globSync('packages/*/*/src/*.ts', { cwd: scanRoot }).sort()) {
const abs = resolve(scanRoot, rel)
const sf = ts.createSourceFile(abs, readFileSync(abs, 'utf8'), ts.ScriptTarget.Latest, true)
for (const stmt of sf.statements) {
if (!ts.isInterfaceDeclaration(stmt) && !ts.isTypeAliasDeclaration(stmt)) continue
if (!(stmt.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) ?? false)) continue
const name = stmt.name.text
if (decls.has(name)) {
ambiguous.add(name)
continue
}
const printed = printer.printNode(ts.EmitHint.Unspecified, stmt, sf).replace(/\r/g, '')
decls.set(name, printed.length > MAX_DECL_CHARS
? `${printed.slice(0, MAX_DECL_CHARS)} /* …truncated — full shape in source */`
: printed)
}
}
for (const name of ambiguous) decls.delete(name)
return decls
}
/**
* The transitive closure of type names referenced by the seed texts: every
* collected declaration whose name appears (word-bounded) in a seed or in an
* already-included declaration, sorted by name.
*/
function referencedTypes(seeds: string[], decls: Map<string, string>): { name: string; declaration: string }[] {
const included = new Map<string, string>()
let frontier = seeds
while (frontier.length > 0) {
const next: string[] = []
for (const [name, declaration] of decls) {
if (included.has(name)) continue
const pattern = new RegExp(`\\b${name}\\b`)
if (frontier.some(text => pattern.test(text))) {
included.set(name, declaration)
next.push(declaration)
}
}
frontier = next
}
return [...included].map(([name, declaration]) => ({ name, declaration })).sort((a, b) => a.name.localeCompare(b.name))
}
/** Render the whole generated module (pure, deterministic given sorted collector output). */
function render(): string {
const services = collectServices()
const events = collectEvents().sort((a, b) => a.name.localeCompare(b.name))
const types = referencedTypes(services.flatMap(service => service.methods), collectTypeDecls())
const lines: string[] = [
'/**',
' * Generated by scripts/gen-cordis-api.ts — do not edit by hand; run',
' * `pnpm run gen-cordis-api` to regenerate (freshness-gated by',
' * `pnpm run verify-cordis-api` in doc-sync).',
' *',
' * The machine-readable cordis API catalog `cordis_inspect` serves to the',
' * model: harness services (summary + public method signatures), harness',
' * events (mode + signature), and the inherited `ctx` surface. Produced by',
' * the same AST walk as docs/cordis-catalog, so this data and the rendered',
' * docs cannot diverge.',
' *',
' * @module @deepseek-ai/dsh-tool-cordis/api-catalog',
' */',
'',
'/** One harness `ctx.<key>` service: its one-line summary and public method signatures. */',
'export interface ServiceApiEntry {',
' /** The `ctx.<key>` name, e.g. `tools`. */',
' key: string',
' /** First sentence of the service class JSDoc. */',
' summary: string',
' /** Public method signatures, bodies stripped, in source order. */',
' methods: readonly string[]',
'}',
'',
'/** One harness event: its dispatch mode, exact signature, and one-line summary. */',
'export interface EventApiEntry {',
' /** The scoped event name, e.g. `agent/status`. */',
' name: string',
' /** The dispatch mode from the declaration\'s `@mode` tag. */',
' mode: string',
' /** The exact listener signature, whitespace-normalized. */',
' signature: string',
' /** First sentence of the event JSDoc. */',
' summary: string',
'}',
'',
'/** One inherited (cordis core + loader/hmr/timer) `ctx` member group with its summary. */',
'export interface InheritedApiEntry {',
' /** The `ctx` member name(s), e.g. `ctx.on / ctx.once`. */',
' name: string',
' /** One-line summary of what the member does. */',
' summary: string',
'}',
'',
'/** One named type shape the service signatures reference. */',
'export interface TypeApiEntry {',
' /** The exported type/interface name, e.g. `BashRunResult`. */',
' name: string',
' /** The full declaration text, comments stripped. */',
' declaration: string',
'}',
'',
'/** Every harness `ctx.<key>` service, sorted by key. */',
'export const SERVICE_API: readonly ServiceApiEntry[] = [',
]
for (const service of services) {
lines.push(' {')
lines.push(` key: ${quote(service.key)},`)
lines.push(` summary: ${quote(firstSentence(service.doc))},`)
if (service.methods.length === 0) {
lines.push(' methods: [],')
} else {
lines.push(' methods: [')
for (const method of service.methods) lines.push(` ${quote(method)},`)
lines.push(' ],')
}
lines.push(' },')
}
lines.push(
']',
'',
'/** Every harness event, sorted by name. */',
'export const EVENT_API: readonly EventApiEntry[] = [',
)
for (const event of events) {
lines.push(' {')
lines.push(` name: ${quote(event.name)},`)
lines.push(` mode: ${quote(event.mode)},`)
lines.push(` signature: ${quote(event.signature)},`)
lines.push(` summary: ${quote(firstSentence(event.doc))},`)
lines.push(' },')
}
lines.push(
']',
'',
'/** Shapes of every exported type the SERVICE_API signatures reference (transitively), sorted by name. */',
'export const TYPE_API: readonly TypeApiEntry[] = [',
)
for (const type of types) {
lines.push(' {')
lines.push(` name: ${quote(type.name)},`)
lines.push(` declaration: ${quote(type.declaration)},`)
lines.push(' },')
}
lines.push(
']',
'',
'/** The inherited `ctx` surface (cordis core + loader/hmr/timer), in curated order. */',
'export const INHERITED_CTX_API: readonly InheritedApiEntry[] = [',
)
for (const inherited of INHERITED_SERVICES) {
lines.push(` { name: ${quote(inherited.name)}, summary: ${quote(inherited.summary)} },`)
}
lines.push(']', '')
return lines.join('\n')
}
/** CLI entry: default writes the artifact, `--check` fails if the committed
* copy is stale. Guarded behind an entry-point check so importing this module
* for tests neither regenerates the committed file nor calls process.exit. */
function main(): void {
const content = render()
if (process.argv.includes('--check')) {
let committed: string | null = null
try {
committed = readFileSync(resolve(root, OUT), 'utf8')
} catch {
// Only ENOENT (not yet generated) is expected; a present-but-unreadable
// file is not a state this repo produces. Either way the remedy is the
// same — regenerate — so treat a read failure as "stale".
committed = null
}
if (committed === content) {
console.log(`gen-cordis-api: ${OUT} is up to date.`)
process.exit(0)
}
console.error(`gen-cordis-api: ${OUT} is stale. Run \`pnpm run gen-cordis-api\` and commit ${OUT}.`)
process.exit(1)
}
writeFileSync(resolve(root, OUT), content)
console.log(`gen-cordis-api: wrote ${OUT}.`)
}
// Run only when invoked as a script, not when imported by a test.
if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) {
main()
}

View File

@@ -92,11 +92,17 @@ export const LINK_MAP: Record<string, string> = {
ToolDefinition: 'tools.md',
ToolExecution: 'tools.md',
ToolExecutionResult: 'tools.md',
ApprovalOutcome: 'approval.md',
ApprovalPolicy: 'approval.md',
ApprovalRequest: 'approval.md',
BashExecRequest: 'bash.md',
BashExecSpec: 'bash.md',
BashRunResult: 'bash.md',
BashTask: 'bash.md',
BashTaskRead: 'bash.md',
ConfinedArgv: 'sandbox.md',
SandboxMode: 'sandbox.md',
SandboxPolicy: 'sandbox.md',
CodeRunRequest: 'code-runtime.md',
CodeRunResult: 'code-runtime.md',
FsEditOutcome: 'filesystem.md',
@@ -327,7 +333,7 @@ const INHERITED_EVENTS: InheritedEntry[] = [
{ name: 'loader/patch-context', summary: 'A context is being patched during a reload.', source: 'vendor/loader/src/index.ts:27' },
]
const INHERITED_SERVICES: InheritedEntry[] = [
export const INHERITED_SERVICES: InheritedEntry[] = [
{ name: 'ctx.on / ctx.once', summary: 'Register an event listener (disposable).', source: 'vendor/cordis/src/events.ts:29' },
{ name: 'ctx.emit / ctx.parallel / ctx.serial / ctx.bail / ctx.waterfall', summary: 'Dispatch an event (sync / awaited / first-bail / veto-chain).', source: 'vendor/cordis/src/events.ts:29' },
{ name: 'ctx.plugin / ctx.inject', summary: 'Load a plugin / declare required services.', source: 'vendor/cordis/src/registry.ts:144' },

View File

@@ -70,12 +70,15 @@ const GROUP_ORDER = [
'llm',
'core',
'bash',
'sandbox',
'fs',
'skill',
'compact',
'subagent',
'web',
'spill',
'todo',
'cordis',
'hooks',
'session-persistence',
'support',
@@ -122,9 +125,27 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'tools',
title: 'Tool registry and execution waterfall',
mode: 'core',
consumers: ['agent-loop', 'tool-bash', 'tool-fs', 'tool-subagent', 'tool-todo', 'tool-web', 'acp'],
consumers: ['agent-loop', 'tool-ask-user', 'tool-bash', 'tool-cordis', 'tool-fs', 'tool-skill', 'tool-subagent', 'tool-todo', 'tool-web', 'acp'],
note: 'Registers tool definitions, exposes schemas to the prompt, and routes calls through tools/pre-execute and tools/post-execute.',
},
{
key: 'userInteraction',
pkg: 'user-interaction',
title: 'Human question/answer seam',
mode: 'seam',
implementations: ['stdio-agent', 'acp'],
consumers: ['tool-ask-user', 'stdio-agent', 'acp'],
note: 'UI front doors provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise.',
},
{
key: 'skills',
pkg: 'skill',
title: 'Skill provider registry',
mode: 'seam',
implementations: ['skill-local'],
consumers: ['tool-skill'],
note: 'Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies.',
},
{
key: 'agents',
pkg: 'agent',
@@ -146,9 +167,27 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'bash',
title: 'Bash executor seam',
mode: 'seam',
implementations: ['bash-local'],
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 can replace bash-local.',
note: 'The model-facing bash tools and hook bridges consume this seam; sandboxed or remote executors replace bash-local without touching them.',
},
{
key: 'sandbox',
pkg: 'sandbox',
title: 'Process-sandbox seam',
mode: 'seam',
implementations: ['sandbox-local'],
consumers: ['bash-sandbox'],
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.',
},
{
key: 'approval',
pkg: 'approval',
title: 'Approval seam',
mode: 'seam',
implementations: ['acp'],
consumers: ['tools', 'tool-bash'],
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`.',
},
{
key: 'codeRuntime',
@@ -156,8 +195,8 @@ const SERVICE_ROLES: ServiceRole[] = [
title: 'Code-execution seam',
mode: 'seam',
implementations: ['code-runtime-worker'],
consumers: [],
note: 'Runs one model-written program against host-provided async bindings; backends differ by substrate and language (the Code Mode RFC specifies the worker-thread backend and the tool-registry consumer).',
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).',
},
{
key: 'fs',
@@ -205,6 +244,15 @@ const SERVICE_ROLES: ServiceRole[] = [
consumers: ['spill-policy'],
note: 'The backend saves oversized tool text to a session-scoped path; spill-policy is the tools/post-execute consumer that decides when to spill.',
},
{
key: 'workflows',
pkg: 'workflow',
title: 'Workflow script engine',
mode: 'seam',
implementations: ['workflow-workerthread'],
consumers: ['tool-workflow'],
note: 'One engine per context (bash shape, no named-provider registry); the worker-thread engine fans agent() calls out through ctx.subagents.',
},
]
const DYNAMIC_EVENT_DISPATCHERS: Array<{ event: string; pkg: string; method: string }> = [
@@ -213,6 +261,14 @@ const DYNAMIC_EVENT_DISPATCHERS: Array<{ event: string; pkg: string; method: str
// listeners or strand an already-started child run.
{ event: 'subagent/start', pkg: 'subagent', method: 'events.dispatch' },
{ event: 'subagent/end', pkg: 'subagent', method: 'events.dispatch' },
// The workflow/* lifecycle events dispatch the same way, for the same
// per-listener-containment reason (WorkflowService.emitWorkflowEvent).
{ event: 'workflow/start', pkg: 'workflow', method: 'events.dispatch' },
{ event: 'workflow/phase', pkg: 'workflow', method: 'events.dispatch' },
{ event: 'workflow/log', pkg: 'workflow', method: 'events.dispatch' },
{ event: 'workflow/agent-start', pkg: 'workflow', method: 'events.dispatch' },
{ event: 'workflow/agent-end', pkg: 'workflow', method: 'events.dispatch' },
{ event: 'workflow/end', pkg: 'workflow', method: 'events.dispatch' },
]
function generatedHeader(title: string): string[] {
@@ -419,6 +475,14 @@ const APP_EXAMPLES = [
config: 'examples/coding-agent/cordis.yml',
summary: 'The coding REPL demo adds the real DeepSeek adapter, filesystem tools, todo_write, compaction, and both subagent transports on top of the stdio app package.',
},
{
id: 'cordis',
rel: 'examples/cordis-agent/composition.md',
title: 'Cordis Agent App Composition',
label: 'examples/cordis-agent',
config: 'examples/cordis-agent/cordis.yml',
summary: 'The self-referential demo puts @deepseek-ai/dsh-tool-cordis on the coding spine, letting the agent inspect its own runtime and mount/unmount plugins into it.',
},
{
id: 'acp',
rel: 'examples/acp-agent/composition.md',
@@ -648,11 +712,12 @@ function renderToolPipeline(): string {
` toolCall["Session event: ${mermaidCode('tool/call')}<br/>logged before execution"]`,
' presentCall["UI pending card<br/>presentCall(args)"]',
` pre["${mermaidCode('tools/pre-execute')} waterfall<br/>hooks, permission, sandbox"]`,
' denied["deny or ask<br/>tool body skipped"]',
' denied["denied<br/>tool body skipped"]',
` approval["${mermaidCode('ctx.approval')} one-shot prompt<br/>absent or unanswerable: deny"]`,
` around["${mermaidCode('tools/execute')} waterfall<br/>timeout, retry, metrics (around dispatch)"]`,
' toolBody["Registered tool execute() body"]',
` fsGate["${mermaidCode('fs/write-intent')} or ${mermaidCode('fs/edit-intent')}<br/>tool-fs mutations only"]`,
` owned["Tool-owned session events<br/>${mermaidCode('todo/write')}, ${mermaidCode('fs/observed')}, ${mermaidCode('hook/invoked')}, ${mermaidCode('hook/result')}"]`,
` owned["Tool-owned session events<br/>${mermaidCode('todo/write')}, ${mermaidCode('fs/observed')}, ${mermaidCode('hook/invoked')}, ${mermaidCode('hook/result')}, ${mermaidCode('tool/code-dispatch')}"]`,
` post["${mermaidCode('tools/post-execute')} waterfall<br/>accept, block, replace, add context"]`,
' context["Buffered additionalContext<br/>context/message after all tool results"]',
` toolResult["Session event: ${mermaidCode('tool/result')}<br/>single model-facing outcome"]`,
@@ -662,7 +727,10 @@ function renderToolPipeline(): string {
' toolCall --> pre',
' pre -->|allow| around',
' around --> toolBody',
' pre -->|deny or ask| denied',
' pre -->|deny| denied',
' pre -->|ask| approval',
' approval -->|allowed-once| around',
' approval -->|rejected, cancelled, unavailable| denied',
' denied --> post',
' toolBody --> fsGate',
' fsGate --> toolBody',
@@ -674,7 +742,7 @@ function renderToolPipeline(): string {
' toolResult --> presentResult',
'```',
'',
'Filesystem read-before-edit checks live below `tool-fs` on the `fs/*` event gate; hook bridges and future permission prompts live on the generic pre/post tool waterfalls; and around-dispatch concerns like the tool-call timeout policy (`@deepseek-ai/dsh-timeout-policy`) wrap core dispatch on `tools/execute`. That split lets the same hooks observe bash, fs, web, todo, and subagent calls without coupling those tools to one policy service.',
'Filesystem read-before-edit checks live below `tool-fs` on the `fs/*` event gate; hook bridges and the approval seam\'s permission prompts live on the generic pre/post tool waterfalls; and around-dispatch concerns like the tool-call timeout policy (`@deepseek-ai/dsh-timeout-policy`) wrap core dispatch on `tools/execute`. That split lets the same hooks observe bash, fs, web, todo, and subagent calls without coupling those tools to one policy service.',
'',
...maintenanceFooter(maintenance),
].join('\n')
@@ -728,6 +796,7 @@ function renderIndex(docs: GraphDoc[]): string {
'docs/capability-seams.md': 'capability seams and core services',
'examples/echo-agent/composition.md': 'echo-agent app composition',
'examples/coding-agent/composition.md': 'coding-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',
'docs/agent-lifecycle.md': 'agent turn and step lifecycle',
@@ -738,6 +807,7 @@ function renderIndex(docs: GraphDoc[]): string {
'docs/capability-seams.md': 'hybrid generated',
'examples/echo-agent/composition.md': 'hybrid generated',
'examples/coding-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',
'docs/agent-lifecycle.md': 'curated',

View File

@@ -42,12 +42,14 @@ const GROUP_ORDER = [
'core',
'bash',
'fs',
'skill',
'compact',
'subagent',
'web',
'spill',
'timeout',
'todo',
'cordis',
'hooks',
'session-persistence',
'support',

View File

@@ -38,20 +38,28 @@ import { basename, resolve } from 'node:path'
import { Context } from 'cordis'
import type { ToolSchema } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import ToolRegistry, { type Config as ToolsConfig } from '@deepseek-ai/dsh-tools'
import LocalBashExecutor from '@deepseek-ai/dsh-bash-local'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
import WebService from '@deepseek-ai/dsh-web'
import * as WebSearchExa from '@deepseek-ai/dsh-web-search-exa'
import * as WebFetchLocal from '@deepseek-ai/dsh-web-fetch-local'
import SubagentService from '@deepseek-ai/dsh-subagent'
import * as SubagentMock from '@deepseek-ai/dsh-subagent-mock'
import SkillService from '@deepseek-ai/dsh-skill'
import * as SkillLocal from '@deepseek-ai/dsh-skill-local'
import * as ToolAskUser from '@deepseek-ai/dsh-tool-ask-user'
import * as ToolBash from '@deepseek-ai/dsh-tool-bash'
import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
import * as ToolFsSearch from '@deepseek-ai/dsh-tool-fs-search'
import * as ToolSkill from '@deepseek-ai/dsh-tool-skill'
import * as ToolTodo from '@deepseek-ai/dsh-tool-todo'
import * as ToolSubagent from '@deepseek-ai/dsh-tool-subagent'
import * as ToolWeb from '@deepseek-ai/dsh-tool-web'
import VmWorkflowEngine from '@deepseek-ai/dsh-workflow-workerthread'
import * as ToolWorkflow from '@deepseek-ai/dsh-tool-workflow'
const root = resolve(import.meta.dirname, '..')
const OUT = 'docs/tool-catalog.md'
@@ -85,6 +93,13 @@ interface ToolPackage {
/** Plug the injected seams + the tool plugin onto a context that already
* carries `systemPrompt` + `tools`. */
mount: (ctx: Context) => Promise<void>
/**
* Config for the caller's `ToolRegistry` mount. The registry itself ships a
* model-facing tool (`run_code`, registered under a non-native `mode`), so
* ITS catalog entry boots the registry in the mode that surfaces it;
* every other entry uses the default (native) registry.
*/
toolsConfig?: ToolsConfig
/**
* A deployment note rendered after the package's tools, for a fact that
* booting the package alone cannot show. The registered tool NAME can be a
@@ -101,6 +116,33 @@ interface ToolPackage {
* guard proves it is exhaustive against the on-disk glob.
*/
const TOOL_PACKAGES: ToolPackage[] = [
{
pkg: '@deepseek-ai/dsh-tool-ask-user',
dir: 'tool-ask-user',
source: 'packages/ui/tool-ask-user/src/index.ts',
requires: ['ctx.tools', 'ctx.userInteraction'],
writes: ['tool/call', 'tool/result after a UI/provider answers the question'],
async mount(ctx) {
await ctx.plugin(UserInteractionService)
await ctx.plugin(ToolAskUser)
},
note:
'ask_user_question pauses the tool call until the active UI provider returns a human answer.',
},
{
pkg: '@deepseek-ai/dsh-tools',
dir: 'tools',
source: 'packages/core/tools/src/code-mode.ts',
requires: ['ctx.tools', 'ctx.codeRuntime (execution time)', 'ctx.systemPrompt'],
writes: ['tool/call', 'one tool/code-dispatch per bridged sub-call', 'tool/result'],
// The registry's OWN tool: run_code exists only under a non-native mode
// (the registry registers it in its constructor; the code runtime is read
// at assembly/execution time, so the schema harvest needs none mounted).
toolsConfig: { mode: 'code' },
async mount() {},
note:
'Registered by the tool registry itself under `mode: code` / `mode: both` (see the Code Mode RFC). Under `code` it is the ONLY wire tool; the other registered tools are declared to the model as a generated TypeScript SDK prompt section instead, and a program calls them through port-bridged bindings that dispatch through the ordinary tools/pre-execute → tools/post-execute pipeline, one at a time.',
},
{
pkg: '@deepseek-ai/dsh-tool-bash',
dir: 'tool-bash',
@@ -114,6 +156,18 @@ const TOOL_PACKAGES: ToolPackage[] = [
note:
'The bash/bash_output/bash_kill tools are model-facing consumers of the bash executor seam.',
},
{
pkg: '@deepseek-ai/dsh-tool-cordis',
dir: 'tool-cordis',
source: 'packages/cordis/tool-cordis/src/index.ts',
requires: ['ctx.tools'],
writes: ['tool/call', 'tool/result', 'live plugin-tree mutations (mount/unmount)'],
async mount(ctx) {
await ctx.plugin(ToolCordis)
},
note:
'Ships in examples/cordis-agent only (a deliberate opt-in — mounted code gets the real ctx, see docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). Plugins the model mounts may register ADDITIONAL model-visible tools at runtime; the request-header ToolsDelta logs those tool-set changes.',
},
{
pkg: '@deepseek-ai/dsh-tool-fs',
dir: 'tool-fs',
@@ -147,6 +201,21 @@ const TOOL_PACKAGES: ToolPackage[] = [
note:
'glob and grep are bash-backed discovery tools: they run fixed ripgrep commands through ctx.bash as ordinary foreground calls (never background tasks). Capped results save the complete formatted list through the optional ctx.spillFiles backend; returned paths are follow-up-readable in co-located bash/filesystem deployments.',
},
{
pkg: '@deepseek-ai/dsh-tool-skill',
dir: 'tool-skill',
source: 'packages/skill/tool-skill/src/index.ts',
requires: ['ctx.tools', 'ctx.skills'],
writes: ['tool/call', 'tool/result'],
async mount(ctx) {
await ctx.plugin(SkillService)
await ctx.plugin(SkillLocal, {
dshHome: resolve(root, '.tmp/tool-catalog/.dsh'),
agentsHome: resolve(root, '.tmp/tool-catalog/.agents'),
})
await ctx.plugin(ToolSkill)
},
},
{
pkg: '@deepseek-ai/dsh-tool-subagent',
dir: 'tool-subagent',
@@ -175,6 +244,22 @@ const TOOL_PACKAGES: ToolPackage[] = [
note:
'todo_write is session-owned state; UIs render the latest todo/write event as a checklist or ACP plan.',
},
{
pkg: '@deepseek-ai/dsh-tool-workflow',
dir: 'tool-workflow',
source: 'packages/workflow/tool-workflow/src/index.ts',
requires: ['ctx.tools', 'ctx.workflows', 'ctx.systemPrompt', 'a calling Agent (exec.agent parents the script children)'],
writes: ['tool/call', 'tool/result'],
async mount(ctx) {
// The tool injects `workflows`; boot the vm engine over a scripted
// subagent provider to satisfy it. The schema does not depend on which
// provider backs the engine.
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentMock, { name: 'mock' })
await ctx.plugin(VmWorkflowEngine, { provider: 'mock' })
await ctx.plugin(ToolWorkflow)
},
},
{
pkg: '@deepseek-ai/dsh-tool-web',
dir: 'tool-web',
@@ -249,7 +334,7 @@ export async function collectToolCatalog(packages: ToolPackage[] = TOOL_PACKAGES
// fiber) — the repo's "dispose must reach quiescence" rule.
try {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRegistry, entry.toolsConfig ?? {})
await entry.mount(ctx)
const schemas = ctx.tools.schemas().sort((a, b) => a.name.localeCompare(b.name))
catalog.push({

View File

@@ -286,18 +286,28 @@ function demoSmokeGate(options: { needs?: string[] } = {}): Gate {
...dependencyOptions,
verify: async (result) => {
const output = result.stdout + result.stderr
if (!output.includes('[tool call] echo({"text":"ci smoke"})')) {
throw new Error('demo smoke did not show the echo tool call.')
const sessionsRoot = join(root, '.sessions')
try {
if (!output.includes('[tool call] echo({"text":"ci smoke"})')) {
throw new Error('demo smoke did not show the echo tool call.')
}
if (!output.includes('[tool result] ECHO: CI SMOKE')) {
throw new Error('demo smoke did not show the echo tool result.')
}
const buckets = await readdir(sessionsRoot, { withFileTypes: true })
let found = false
for (const bucket of buckets) {
if (!bucket.isDirectory() || !bucket.name.startsWith('cwd-')) continue
const entries = await readdir(join(sessionsRoot, bucket.name))
if (entries.some(entry => /^main-session-.+\.jsonl$/.test(entry))) {
found = true
break
}
}
if (!found) throw new Error('demo smoke did not create a main-session JSONL log in a cwd bucket.')
} finally {
await rm(sessionsRoot, { recursive: true, force: true })
}
if (!output.includes('[tool result] ECHO: CI SMOKE')) {
throw new Error('demo smoke did not show the echo tool result.')
}
const sessionDir = join(root, '.sessions', '_no-cwd')
const entries = await readdir(sessionDir)
if (!entries.some(entry => /^main-session-.+\.jsonl$/.test(entry))) {
throw new Error('demo smoke did not create a main-session JSONL log.')
}
await rm(join(root, '.sessions'), { recursive: true, force: true })
},
}
}
@@ -310,6 +320,10 @@ function builtBinSmokeGate(): Gate {
'vitest.e2e.config.ts',
'packages/ui/stdio-agent/tests/built-bin.e2e.ts',
'packages/ui/acp-agent/tests/built-bin.e2e.ts',
// The worker-entry packages' built bundles: the only automated proof
// that lib/index.js resolves its sibling lib/worker.js under plain node
// (the e2e lane runs unbuilt, so these files self-skip there).
'packages/workflow/workflow-workerthread/tests/built-worker.e2e.ts',
'packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts',
], {
label: 'built-bin smoke',

View File

@@ -48,13 +48,33 @@
{ "doc": "docs/core-data-structures/tools.md", "symbol": "StructuredSchemaNode", "source": "packages/core/tools/src/json-schema.ts" },
{ "doc": "docs/core-data-structures/tools.md", "symbol": "StructuredOutputSchema", "source": "packages/core/tools/src/json-schema.ts" },
{ "doc": "docs/core-data-structures/user-interaction.md", "symbol": "AskUserQuestionOption", "source": "packages/ui/user-interaction/src/index.ts" },
{ "doc": "docs/core-data-structures/user-interaction.md", "symbol": "AskUserQuestionItem", "source": "packages/ui/user-interaction/src/index.ts" },
{ "doc": "docs/core-data-structures/user-interaction.md", "symbol": "AskUserQuestionRequest", "source": "packages/ui/user-interaction/src/index.ts" },
{ "doc": "docs/core-data-structures/user-interaction.md", "symbol": "AskUserQuestionAnswerItem", "source": "packages/ui/user-interaction/src/index.ts" },
{ "doc": "docs/core-data-structures/user-interaction.md", "symbol": "AskUserQuestionAnswer", "source": "packages/ui/user-interaction/src/index.ts" },
{ "doc": "docs/core-data-structures/user-interaction.md", "symbol": "UserInteractionProvider", "source": "packages/ui/user-interaction/src/index.ts" },
{ "doc": "docs/core-data-structures/user-interaction.md", "symbol": "UserInteractionError", "source": "packages/ui/user-interaction/src/index.ts" },
{ "doc": "docs/core-data-structures/approval.md", "symbol": "ApprovalRequestId", "source": "packages/ui/user-approval/src/index.ts" },
{ "doc": "docs/core-data-structures/approval.md", "symbol": "ApprovalOutcome", "source": "packages/ui/user-approval/src/index.ts" },
{ "doc": "docs/core-data-structures/approval.md", "symbol": "ApprovalPolicy", "source": "packages/ui/user-approval/src/index.ts" },
{ "doc": "docs/core-data-structures/approval.md", "symbol": "ApprovalRequest", "source": "packages/ui/user-approval/src/index.ts" },
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashExecRequest", "source": "packages/bash/bash/src/types.ts" },
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashExecSpec", "source": "packages/bash/bash/src/types.ts" },
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashRunResult", "source": "packages/bash/bash/src/types.ts" },
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashSandboxInfo", "source": "packages/bash/bash/src/types.ts" },
{ "doc": "docs/core-data-structures/bash.md", "symbol": "CollectedOutput", "source": "packages/bash/bash/src/types.ts" },
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashTask", "source": "packages/bash/bash/src/types.ts" },
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashTaskRead", "source": "packages/bash/bash/src/types.ts" },
{ "doc": "docs/core-data-structures/sandbox.md", "symbol": "SandboxMode", "source": "packages/sandbox/sandbox/src/index.ts" },
{ "doc": "docs/core-data-structures/sandbox.md", "symbol": "ConfinedSandboxMode", "source": "packages/sandbox/sandbox/src/index.ts" },
{ "doc": "docs/core-data-structures/sandbox.md", "symbol": "SandboxEnforcement", "source": "packages/sandbox/sandbox/src/index.ts" },
{ "doc": "docs/core-data-structures/sandbox.md", "symbol": "SandboxPolicy", "source": "packages/sandbox/sandbox/src/index.ts" },
{ "doc": "docs/core-data-structures/sandbox.md", "symbol": "ConfinedArgv", "source": "packages/sandbox/sandbox/src/index.ts" },
{ "doc": "docs/core-data-structures/code-runtime.md", "symbol": "CodeRunRequest", "source": "packages/code-runtime/code-runtime/src/types.ts" },
{ "doc": "docs/core-data-structures/code-runtime.md", "symbol": "CodeRunResult", "source": "packages/code-runtime/code-runtime/src/types.ts" },
{ "doc": "docs/core-data-structures/code-runtime.md", "symbol": "CodeBindingNamespace", "source": "packages/code-runtime/code-runtime/src/types.ts" },
@@ -75,6 +95,16 @@
{ "doc": "docs/core-data-structures/filesystem.md", "symbol": "FsPolicyExec", "source": "packages/fs/fs-policy/src/types.ts" },
{ "doc": "docs/core-data-structures/filesystem.md", "symbol": "FileReadOutcome", "source": "packages/fs/tool-fs/src/read-render.ts" },
{ "doc": "docs/core-data-structures/skills.md", "symbol": "SkillSource", "source": "packages/skill/skill/src/index.ts" },
{ "doc": "docs/core-data-structures/skills.md", "symbol": "SkillResourceBase", "source": "packages/skill/skill/src/index.ts" },
{ "doc": "docs/core-data-structures/skills.md", "symbol": "SkillSummary", "source": "packages/skill/skill/src/index.ts" },
{ "doc": "docs/core-data-structures/skills.md", "symbol": "SkillCandidate", "source": "packages/skill/skill/src/index.ts" },
{ "doc": "docs/core-data-structures/skills.md", "symbol": "SkillDefinition", "source": "packages/skill/skill/src/index.ts" },
{ "doc": "docs/core-data-structures/skills.md", "symbol": "SkillRegistration", "source": "packages/skill/skill/src/index.ts" },
{ "doc": "docs/core-data-structures/skills.md", "symbol": "SkillLookupOptions", "source": "packages/skill/skill/src/index.ts" },
{ "doc": "docs/core-data-structures/skills.md", "symbol": "SkillProvider", "source": "packages/skill/skill/src/index.ts" },
{ "doc": "docs/core-data-structures/skills.md", "symbol": "Config", "source": "packages/skill/skill/src/index.ts" },
{ "doc": "docs/core-data-structures/compaction.md", "symbol": "CompactionResult", "source": "packages/compact/compact/src/types.ts" },
{ "doc": "docs/core-data-structures/subagent.md", "symbol": "SubagentCapabilities", "source": "packages/subagent/subagent/src/types.ts" },
@@ -96,6 +126,11 @@
{ "doc": "docs/core-data-structures/spill.md", "symbol": "SpillOwner", "source": "packages/spill/spill/src/types.ts" },
{ "doc": "docs/core-data-structures/spill.md", "symbol": "SpillSource", "source": "packages/spill/spill/src/types.ts" },
{ "doc": "docs/core-data-structures/spill.md", "symbol": "SpillRef", "source": "packages/spill/spill/src/types.ts" },
{ "doc": "docs/core-data-structures/spill.md", "symbol": "SpillPath", "source": "packages/spill/spill/src/types.ts" }
{ "doc": "docs/core-data-structures/spill.md", "symbol": "SpillPath", "source": "packages/spill/spill/src/types.ts" },
{ "doc": "docs/core-data-structures/workflow.md", "symbol": "WorkflowStartRequest", "source": "packages/workflow/workflow/src/types.ts" },
{ "doc": "docs/core-data-structures/workflow.md", "symbol": "WorkflowMeta", "source": "packages/workflow/workflow/src/types.ts" },
{ "doc": "docs/core-data-structures/workflow.md", "symbol": "WorkflowResult", "source": "packages/workflow/workflow/src/types.ts" },
{ "doc": "docs/core-data-structures/workflow.md", "symbol": "WorkflowRun", "source": "packages/workflow/workflow/src/types.ts" }
]
}

View File

@@ -18,9 +18,11 @@
* A wrapped paragraph inside a list item or blockquote is still a `paragraph`
* node, so those are caught too. Scope mirrors doc-typecheck plus the two
* AGENTS.md files that doc-sync does NOT otherwise cover (the convention itself
* lives there): README.md, docs/** /*.md, packages/* /*.md, AGENTS.md,
* packages/AGENTS.md. The root and packages/ CLAUDE.md are symlinks to the
* AGENTS.md files, so they are deduped by real path.
* lives there), plus generated system-prompt Markdown goldens: README.md,
* docs/** /*.md, packages/* /*.md, examples/** /system-prompt.golden.md,
* packages/** /system-prompt.golden.md, AGENTS.md, packages/AGENTS.md. The root
* and packages/ CLAUDE.md are symlinks to the AGENTS.md files, so they are
* deduped by real path.
*
* Run: `tsx scripts/verify-md-wrap.ts`.
*/
@@ -34,8 +36,18 @@ import type { Nodes } from 'mdast'
const root = resolve(import.meta.dirname, '..')
/** Files to check: doc-typecheck's scope plus the AGENTS.md pair. */
const PATTERNS = ['README.md', 'README.zh.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md', 'AGENTS.md', 'packages/AGENTS.md']
/** Files to check: doc-typecheck's scope, prompt goldens, and the AGENTS.md pair. */
const PATTERNS = [
'README.md',
'README.zh.md',
'docs/**/*.md',
'packages/*/*.md',
'packages/*/*/*.md',
'examples/**/system-prompt.golden.md',
'packages/**/system-prompt.golden.md',
'AGENTS.md',
'packages/AGENTS.md',
]
/** A located hard-wrap: a prose paragraph spanning more than one source line. */
interface Violation {