Merge latest origin/master into worktree/agent-execution-context-rfc
# Conflicts: # docs/event-producer-consumer.md # docs/module-graph.md # packages/examples/README.md
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Boot the REPL or ACP Code Mode overlay, defaulting to REPL. Each overlay
|
||||
* Boot the REPL, TUI, or ACP Code Mode overlay, defaulting to REPL. Each overlay
|
||||
* includes its base example, selects Code Mode, and adds the worker runtime.
|
||||
* Both require a DeepSeek API key; unsupported arguments fail with usage.
|
||||
* All require a DeepSeek API key; unsupported arguments fail with usage.
|
||||
*/
|
||||
import { spawn } from 'node:child_process'
|
||||
|
||||
@@ -10,13 +10,14 @@ import { spawn } from 'node:child_process'
|
||||
// Loader's HMR path).
|
||||
const UIS = new Map([
|
||||
['repl', ['--expose-internals', '--import', 'tsx', 'packages/examples/stdio-demo/src/bin.ts', 'examples/coding-agent/code-mode.cordis.yml']],
|
||||
['tui', ['--expose-internals', '--import', 'tsx', 'packages/examples/stdio-demo/src/bin.ts', 'examples/tui-agent/code-mode.cordis.yml']],
|
||||
['acp', ['--import', 'tsx', 'packages/examples/acp-demo/src/bin.ts', '--config', '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]')
|
||||
console.error('usage: pnpm run demo:code-mode [repl|tui|acp]')
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
|
||||
@@ -253,7 +253,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
pkg: 'subagent',
|
||||
title: 'Subagent provider registry',
|
||||
mode: 'seam',
|
||||
implementations: ['subagent-spawn', 'subagent-fork', 'subagent-acp', 'subagent-mock'],
|
||||
implementations: ['subagent-spawn', 'subagent-fork', 'subagent-acp'],
|
||||
consumers: ['tool-subagent'],
|
||||
note: 'Providers implement transports; tool-subagent exposes one configured provider as a model-facing tool name.',
|
||||
},
|
||||
@@ -440,7 +440,15 @@ const APP_EXAMPLES = [
|
||||
title: 'Coding Agent App Composition',
|
||||
label: 'examples/coding-agent',
|
||||
config: 'examples/coding-agent/cordis.yml',
|
||||
summary: 'The coding REPL demo adds the real DeepSeek adapter, filesystem tools, todo_write, compaction, and both subagent transports on top of the stdio app package.',
|
||||
summary: 'The coding-agent demo adds the real DeepSeek adapter, filesystem tools, todo_write, compaction, and both subagent transports on top of the stdio app package.',
|
||||
},
|
||||
{
|
||||
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 reuses the coding-agent backend and tool composition while fixing the shared terminal app to the full-screen dsh-tui front door.',
|
||||
},
|
||||
{
|
||||
id: 'cordis',
|
||||
@@ -462,13 +470,18 @@ const APP_EXAMPLES = [
|
||||
|
||||
type AppExample = typeof APP_EXAMPLES[number]
|
||||
|
||||
function renderAppExpansion(lines: string[], appNode: string, pluginName: string): void {
|
||||
function renderAppExpansion(lines: string[], appNode: string, pluginName: string, exampleId: string): void {
|
||||
const agentCore = nodeId('bundle', 'agent_core')
|
||||
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-stdio-demo') {
|
||||
lines.push(` ${appNode} --> ${nodeId('frontdoor', 'stdio')}["readline UI<br/>console logger<br/>pre-created main agent"]`)
|
||||
const frontDoor = exampleId === 'tui'
|
||||
? '@deepseek-ai/dsh-tui<br/>pre-created main agent'
|
||||
: exampleId === 'coding'
|
||||
? '@deepseek-ai/dsh-stdio<br/>pre-created main agent'
|
||||
: 'dsh-tui (TTY) / dsh-stdio (pipes)<br/>pre-created main agent'
|
||||
lines.push(` ${appNode} --> ${nodeId('frontdoor', 'stdio')}["${frontDoor}"]`)
|
||||
} else if (pluginName === '@deepseek-ai/dsh-acp-demo') {
|
||||
lines.push(` ${appNode} --> ${nodeId('frontdoor', 'acp')}["@deepseek-ai/dsh-acp<br/>JSON-RPC stdio bridge<br/>sessions created by client"]`)
|
||||
}
|
||||
@@ -496,7 +509,7 @@ function renderAppComposition(example: AppExample): string {
|
||||
lines.push(` ${pluginNode}["${escLabel(plugin.id)}<br/>${escLabel(plugin.name)}"]`)
|
||||
lines.push(` cfg --> ${pluginNode}`)
|
||||
if (plugin.name === '@deepseek-ai/dsh-stdio-demo' || plugin.name === '@deepseek-ai/dsh-acp-demo') {
|
||||
renderAppExpansion(lines, pluginNode, plugin.name)
|
||||
renderAppExpansion(lines, pluginNode, plugin.name, example.id)
|
||||
}
|
||||
}
|
||||
lines.push(
|
||||
@@ -986,6 +999,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/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',
|
||||
@@ -997,6 +1011,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/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',
|
||||
|
||||
@@ -19,7 +19,7 @@ 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 type { SubagentProvider } from '@deepseek-ai/dsh-subagent'
|
||||
import SkillService from '@deepseek-ai/dsh-skill'
|
||||
import * as SkillLocal from '@deepseek-ai/dsh-skill-local'
|
||||
import TaskService from '@deepseek-ai/dsh-tasks'
|
||||
@@ -39,6 +39,17 @@ import * as ToolWorkflow from '@deepseek-ai/dsh-tool-workflow'
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const OUT = 'docs/tool-catalog.md'
|
||||
|
||||
/** Register the descriptor needed to mount schema-producing consumers. */
|
||||
function registerCatalogSubagentProvider(ctx: Context, name: string): void {
|
||||
const provider: SubagentProvider = {
|
||||
name,
|
||||
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
|
||||
inheritsParentContext: false,
|
||||
start: () => Promise.reject(new Error('tool-catalog provider cannot start a child')),
|
||||
}
|
||||
ctx.subagents.registerProvider(provider)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool package plus its hand-maintained boot recipe. The caller mounts the
|
||||
* prompt and registry; each recipe supplies only package-specific seams and
|
||||
@@ -191,8 +202,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
shippedNames: ['subagent', 'subagent_fork'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(SubagentService)
|
||||
// Register a scripted provider under the name the tool delegates to.
|
||||
await ctx.plugin(SubagentMock, { name: 'mock' })
|
||||
registerCatalogSubagentProvider(ctx, 'mock')
|
||||
await ctx.plugin(ToolSubagent, { provider: 'mock' })
|
||||
},
|
||||
note:
|
||||
@@ -234,7 +244,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
// 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' })
|
||||
registerCatalogSubagentProvider(ctx, 'mock')
|
||||
await ctx.plugin(VmWorkflowEngine, { provider: 'mock' })
|
||||
await ctx.plugin(ToolWorkflow)
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"comment": "Maps each ` ```ts type-equiv ` block (by doc + declared symbol) to the source symbol it must match verbatim. verify-type-equiv.ts enforces a 1:1 correspondence: every type-equiv block has exactly one entry here, and every entry resolves to exactly one block. Add an entry when you add a type-equiv block; remove it when you remove the block.",
|
||||
"comment": "Maps each ` ```ts type-equiv ` block (by doc + declared symbol) to the source declaration and original JSDoc it must match. verify-type-equiv.ts enforces a 1:1 correspondence: every type-equiv block has exactly one entry here, and every entry resolves to exactly one block. Add an entry when you add a type-equiv block; remove it when you remove the block.",
|
||||
"entries": [
|
||||
{ "doc": "docs/core-data-structures/core.md", "symbol": "Branded", "source": "packages/util/brand/src/index.ts" },
|
||||
{ "doc": "docs/core-data-structures/core.md", "symbol": "ContentBlockMap", "source": "packages/llm/llm/src/types.ts" },
|
||||
@@ -49,6 +49,7 @@
|
||||
{ "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceEventType", "source": "packages/core/session/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceOp", "source": "packages/core/session/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceIntent", "source": "packages/core/session/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/session.md", "symbol": "SessionSurface", "source": "packages/core/session/src/surface.ts" },
|
||||
{ "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceFoldReplacement", "source": "packages/core/session/src/surface.ts" },
|
||||
{ "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceFoldResult", "source": "packages/core/session/src/surface.ts" },
|
||||
|
||||
|
||||
@@ -66,7 +66,6 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/support/invariants': { kind: 'none', reason: 'The observer validates requests but never rewrites their context.' },
|
||||
'packages/support/loader-smoke': { kind: 'none', reason: 'The test harness observes child-process streams without changing live requests.' },
|
||||
'packages/support/llm-replay': { kind: 'none', reason: 'The keyless adapter invokes no provider model.' },
|
||||
'packages/support/subagent-mock': { kind: 'indirect', reason: 'Only dsh-tool-subagent renders its configured test outcome.' },
|
||||
'packages/tasks/tasks': { kind: 'indirect', reason: 'Producer and control-surface plugins own all model rendering over the task registry.' },
|
||||
'packages/examples/acp-demo': { kind: 'indirect', reason: 'The app bundle delegates request composition to dsh-agent-spine-demo and dsh-acp.' },
|
||||
'packages/ui/app-boot': { kind: 'indirect', reason: 'Only the loaded plugin tree contributes model context.' },
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Verify every `ts type-equiv` block against the source symbol named by the
|
||||
* manifest. Blocks and entries have a one-to-one relationship; comparison
|
||||
* ignores comments and whitespace but preserves declaration structure.
|
||||
* ignores whitespace and non-JSDoc comments but preserves declaration
|
||||
* structure and every original JSDoc comment.
|
||||
*/
|
||||
|
||||
import { globSync, readFileSync, existsSync } from 'node:fs'
|
||||
@@ -34,12 +35,8 @@ interface EquivBlock {
|
||||
code: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove comments and normalize whitespace so prose-only edits do not drift
|
||||
* structural copies. This is intentionally not a general tokenizer: repo type
|
||||
* declarations do not contain comment delimiters inside string literals.
|
||||
*/
|
||||
function normalize(code: string): string {
|
||||
/** Normalize declaration structure independently of comments and whitespace. */
|
||||
function normalizeStructure(code: string): string {
|
||||
return code
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.replace(/(^|[^:])\/\/.*$/gm, '$1')
|
||||
@@ -47,6 +44,15 @@ function normalize(code: string): string {
|
||||
.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract normalized JSDoc comments in source order. Type declarations in this
|
||||
* repository do not contain comment delimiters inside string literals.
|
||||
*/
|
||||
function normalizeJSDoc(code: string): string[] {
|
||||
return [...code.matchAll(/\/\*\*[\s\S]*?\*\//g)]
|
||||
.map(match => match[0].replace(/\s+/g, ' ').trim())
|
||||
}
|
||||
|
||||
/** Strip source-only export modifiers. */
|
||||
function stripExport(code: string): string {
|
||||
return code.replace(/^export\s+(default\s+)?/, '')
|
||||
@@ -54,8 +60,14 @@ function stripExport(code: string): string {
|
||||
|
||||
/** Parse the declared symbol name from a type-equiv block body. */
|
||||
function blockSymbol(code: string): string | null {
|
||||
const m = /(?:export\s+(?:default\s+)?)?(?:abstract\s+)?(?:interface|type|class|enum)\s+([A-Za-z0-9_]+)/.exec(code)
|
||||
return m?.[1] ?? null
|
||||
const sf = ts.createSourceFile('type-equiv.ts', code, ts.ScriptTarget.Latest, /* setParentNodes */ false, ts.ScriptKind.TS)
|
||||
for (const stmt of sf.statements) {
|
||||
const named =
|
||||
ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt)
|
||||
|| ts.isClassDeclaration(stmt) || ts.isEnumDeclaration(stmt)
|
||||
if (named && stmt.name) return stmt.name.text
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** Extract every ` ```ts type-equiv ` block from one Markdown file. */
|
||||
@@ -88,11 +100,12 @@ function extractEquivBlocks(docRel: string): EquivBlock[] {
|
||||
return blocks
|
||||
}
|
||||
|
||||
/** The declaration text of `symbol` in `sourceRel`, with `export` stripped, or
|
||||
/**
|
||||
* The declaration text of `symbol` in `sourceRel`, with `export` stripped, or
|
||||
* null when the symbol is not declared there. Uses the TS parser so it spans
|
||||
* interfaces, type aliases (including mapped/generic ones), classes, and enums
|
||||
* uniformly, and excludes the leading JSDoc (getStart skips leading trivia)
|
||||
* while keeping inline member comments. */
|
||||
* uniformly while including declaration and member JSDoc.
|
||||
*/
|
||||
function sourceDeclaration(sourceRel: string, symbol: string): string | null {
|
||||
const abs = resolve(root, sourceRel)
|
||||
const text = readFileSync(abs, 'utf8')
|
||||
@@ -102,7 +115,13 @@ function sourceDeclaration(sourceRel: string, symbol: string): string | null {
|
||||
ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt)
|
||||
|| ts.isClassDeclaration(stmt) || ts.isEnumDeclaration(stmt)
|
||||
if (named && stmt.name?.text === symbol) {
|
||||
return stripExport(stmt.getText(sf))
|
||||
const declarationStart = stmt.getStart(sf)
|
||||
const jsDoc = ts.getJSDocCommentsAndTags(stmt)
|
||||
.filter(ts.isJSDoc)
|
||||
.map(doc => text.slice(doc.pos, doc.end))
|
||||
.join('\n')
|
||||
const declaration = stripExport(text.slice(declarationStart, stmt.getEnd()))
|
||||
return jsDoc === '' ? declaration : `${jsDoc}\n${declaration}`
|
||||
}
|
||||
}
|
||||
return null
|
||||
@@ -178,11 +197,18 @@ for (const e of entries) {
|
||||
errors.push(`symbol ${e.symbol} not found in ${e.source} (manifest entry for ${e.doc})`)
|
||||
continue
|
||||
}
|
||||
if (normalize(decl) !== normalize(stripExport(b.code))) {
|
||||
const doc = stripExport(b.code)
|
||||
const sourceStructure = normalizeStructure(decl)
|
||||
const docStructure = normalizeStructure(doc)
|
||||
const sourceJSDoc = normalizeJSDoc(decl)
|
||||
const docJSDoc = normalizeJSDoc(doc)
|
||||
if (sourceStructure !== docStructure || JSON.stringify(sourceJSDoc) !== JSON.stringify(docJSDoc)) {
|
||||
errors.push(
|
||||
`DRIFT: ${e.doc}:${b.line} — type-equiv block for ${e.symbol} does not match ${e.source}.\n`
|
||||
+ ` source: ${normalize(decl)}\n`
|
||||
+ ` doc: ${normalize(stripExport(b.code))}`,
|
||||
+ ` source structure: ${sourceStructure}\n`
|
||||
+ ` doc structure: ${docStructure}\n`
|
||||
+ ` source JSDoc: ${JSON.stringify(sourceJSDoc)}\n`
|
||||
+ ` doc JSDoc: ${JSON.stringify(docJSDoc)}`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
@@ -190,7 +216,7 @@ for (const e of entries) {
|
||||
}
|
||||
|
||||
if (errors.length === 0) {
|
||||
console.log(`verify-type-equiv: ${verified} type-equiv block(s) match source (1:1 with manifest).`)
|
||||
console.log(`verify-type-equiv: ${verified} type-equiv block(s) match source structure and JSDoc (1:1 with manifest).`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user