Merge remote-tracking branch 'origin/master' into xtr/agent-loop-message-machine

# Conflicts:
#	.agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.md
#	.agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.zh.md
#	.agents/notes/implemented/feature/2026-06-30-hook-bridges.i18n.yaml
#	.agents/notes/implemented/feature/2026-06-30-interception-seams.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-06-sandbox.md
#	.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md
#	.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.i18n.yaml
#	.agents/notes/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.i18n.yaml
#	docs/architecture.i18n.yaml
#	docs/cookbook/adding-a-tool.i18n.yaml
#	docs/cookbook/extension-cookbook.i18n.yaml
#	docs/core-data-structures/llm-streaming.i18n.yaml
#	docs/core-data-structures/session.i18n.yaml
#	docs/core-data-structures/tools.i18n.yaml
#	docs/event-producer-consumer.md
#	docs/persistence-catalog.md
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/escalation-approved/session.jsonl
#	examples/acp-agent/tests/snapshots/escalation-rejected/session.jsonl
#	examples/acp-agent/tests/snapshots/fs-escalation-approved/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl
#	examples/acp-agent/tests/snapshots/permission-switching/session.jsonl
#	examples/acp-agent/tests/snapshots/plan-mode-reject/session.jsonl
#	examples/acp-agent/tests/snapshots/plan-mode/session.jsonl
#	examples/acp-agent/tests/snapshots/session-sandbox-root/session.jsonl
#	packages/context/session-reference/README.md
#	packages/core/agent-loop/tests/agent.spec.ts
#	packages/hooks/hooks-claude/tests/coverage-cases.ts
#	packages/host/runtime/tests/host-runtime.spec.ts
#	packages/llm/llm-retry/tests/retry.spec.ts
#	packages/session-persistence/session-persistence/src/coordinator.ts
#	packages/support/acp-snapshot/README.md
#	packages/support/acp-snapshot/src/normalize.ts
#	packages/ui/acp/acp-feature-support.md
#	packages/ui/acp/src/codec.ts
#	packages/ui/acp/src/index.ts
#	packages/ui/acp/tests/bridge.spec.ts
#	packages/ui/acp/tests/codec.spec.ts
#	packages/ui/acp/tests/config-options.spec.ts
#	packages/ui/acp/tests/dispose.spec.ts
#	packages/ui/acp/tests/edges.spec.ts
#	packages/ui/acp/tests/stream-update.spec.ts
#	packages/ui/acp/tests/turns.spec.ts
This commit is contained in:
_Kerman
2026-07-26 14:05:33 +08:00
1062 changed files with 33621 additions and 30750 deletions

62
scripts/clean.spec.ts Normal file
View File

@@ -0,0 +1,62 @@
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { RepositoryCleaner } from './clean.ts'
const roots: string[] = []
function fixture(): string {
const root = mkdtempSync(join(tmpdir(), 'dsh-clean-'))
roots.push(root)
return root
}
function write(path: string, content = ''): void {
mkdirSync(dirname(path), { recursive: true })
writeFileSync(path, content)
}
function addProject(root: string, path: string): void {
write(join(root, 'tsconfig.json'), JSON.stringify({ files: [], references: [{ path }] }))
write(join(root, path, 'tsconfig.json'), JSON.stringify({
compilerOptions: { composite: true, outDir: 'lib/types' },
include: ['src'],
}))
write(join(root, path, 'src/index.ts'), 'export {}\n')
}
afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
})
describe('RepositoryCleaner', () => {
it('derives live build outputs from project references and removes safe stale package residue', async () => {
const root = fixture()
addProject(root, 'products/shell')
write(join(root, 'products/shell/lib/types/index.js'))
write(join(root, 'products/shell/lib/index.js'))
write(join(root, '.typecheck/legacy.tsbuildinfo'))
write(join(root, 'root.tsbuildinfo'))
write(join(root, 'packages/removed/ghost/node_modules/.bin/tool'))
await new RepositoryCleaner(root).clean()
expect(existsSync(join(root, 'products/shell/lib'))).toBe(false)
expect(existsSync(join(root, 'products/shell/src/index.ts'))).toBe(true)
expect(existsSync(join(root, '.typecheck'))).toBe(false)
expect(existsSync(join(root, 'root.tsbuildinfo'))).toBe(false)
expect(existsSync(join(root, 'packages/removed/ghost'))).toBe(false)
})
it('does not delete any target when a manifest-less package contains an unknown file', async () => {
const root = fixture()
addProject(root, 'products/shell')
write(join(root, 'products/shell/lib/types/index.js'))
write(join(root, 'packages/removed/ghost/notes.txt'))
await expect(new RepositoryCleaner(root).clean()).rejects.toThrow('packages/removed/ghost/notes.txt')
expect(existsSync(join(root, 'products/shell/lib'))).toBe(true)
})
})

165
scripts/clean.ts Normal file
View File

@@ -0,0 +1,165 @@
import { lstat, readdir, rm } from 'node:fs/promises'
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
import { fileURLToPath } from 'node:url'
import ts from 'typescript'
import { repositoryConfigHost } from './ts-project.ts'
const knownOrphanEntries = new Set(['node_modules', 'lib', '.typecheck'])
function isMissing(error: unknown): boolean {
return error instanceof Error && 'code' in error && error.code === 'ENOENT'
}
async function exists(path: string): Promise<boolean> {
try {
await lstat(path)
return true
} catch (error) {
if (isMissing(error)) return false
throw error
}
}
async function childDirectories(path: string): Promise<string[]> {
try {
const entries = await readdir(path, { withFileTypes: true })
return entries.filter(entry => entry.isDirectory()).map(entry => join(path, entry.name))
} catch (error) {
if (isMissing(error)) return []
throw error
}
}
function repositoryPath(root: string, path: string): string {
return relative(root, path).split(sep).join('/')
}
function parseConfig(configPath: string): ts.ParsedCommandLine {
const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, repositoryConfigHost)
if (!parsed) throw new Error(`clean: cannot parse TypeScript config ${configPath}`)
if (parsed.errors.length > 0) {
throw new Error(parsed.errors.map(error => ts.flattenDiagnosticMessageText(error.messageText, '\n')).join('\n'))
}
return parsed
}
/** Plans and removes repository-owned build output without crossing the repository boundary. */
export class RepositoryCleaner {
constructor(private readonly root: string) {}
/**
* Remove generated build state and package directories containing only known residue.
* @returns Repository-relative paths that were removed.
*/
async clean(): Promise<string[]> {
const targets = await this.plan()
// Planning validates every target first, so an unsafe orphan prevents all deletion.
for (const target of targets) await rm(target, { recursive: true, force: true })
return targets.map(target => repositoryPath(this.root, target))
}
private async plan(): Promise<string[]> {
const targets = new Set<string>()
const unsafeOrphans: string[] = []
// These checks cover legacy root-level incremental state emitted by older configs.
await this.addIfPresent(targets, join(this.root, '.typecheck'))
for (const entry of await readdir(this.root, { withFileTypes: true })) {
if (entry.isFile() && entry.name.endsWith('.tsbuildinfo')) targets.add(join(this.root, entry.name))
}
// The root project-reference graph is the source of truth for live build targets.
// Each emitting project declares lib/types as outDir; its parent lib also owns
// the sibling runtime bundles, so the complete build output root is removed.
for (const outputDirectory of this.buildOutputDirectories()) {
await this.addIfPresent(targets, outputDirectory)
}
for (const groupDirectory of await childDirectories(join(this.root, 'packages'))) {
for (const packageDirectory of await childDirectories(groupDirectory)) {
// A package.json marks a live package; its output was discovered from the
// project graph above, and its package-local node_modules must be preserved.
if (await exists(join(packageDirectory, 'package.json'))) {
continue
}
// A manifest-less package directory is stale only when every remaining
// entry is known generated residue; unknown files make the whole clean fail.
const entries = await readdir(packageDirectory)
const unknown = entries.filter(entry => !knownOrphanEntries.has(entry) && !entry.endsWith('.tsbuildinfo'))
if (unknown.length > 0) {
unsafeOrphans.push(...unknown.map(entry => repositoryPath(this.root, join(packageDirectory, entry))))
} else {
targets.add(packageDirectory)
}
}
}
if (unsafeOrphans.length > 0) {
throw new Error([
'clean: refusing to remove package directories without package.json; unknown entries remain:',
...unsafeOrphans.sort().map(path => ` ${path}`),
].join('\n'))
}
return [...targets].sort()
}
private buildOutputDirectories(): string[] {
const outputs = new Set<string>()
const pending = [join(this.root, 'tsconfig.json')]
const visited = new Set<string>()
while (pending.length > 0) {
const nextConfigPath = pending.pop()
if (nextConfigPath === undefined) break
const configPath = resolve(nextConfigPath)
if (visited.has(configPath)) continue
visited.add(configPath)
const parsed = parseConfig(configPath)
if (parsed.options.outDir !== undefined) {
const typesDirectory = resolve(parsed.options.outDir)
if (basename(typesDirectory) !== 'types') {
throw new Error(`clean: expected TypeScript outDir to end in /types: ${repositoryPath(this.root, typesDirectory)}`)
}
const outputDirectory = dirname(typesDirectory)
this.assertRepositoryTarget(outputDirectory)
outputs.add(outputDirectory)
}
for (const reference of parsed.projectReferences ?? []) {
pending.push(ts.resolveProjectReferencePath(reference))
}
}
return [...outputs]
}
private assertRepositoryTarget(path: string): void {
const repositoryRelative = relative(this.root, path)
if (repositoryRelative === '' || repositoryRelative === '..' || repositoryRelative.startsWith(`..${sep}`) || isAbsolute(repositoryRelative)) {
throw new Error(`clean: refusing build output outside repository: ${path}`)
}
}
private async addIfPresent(targets: Set<string>, path: string): Promise<void> {
// Missing outputs are normal on a clean checkout; only existing paths become deletion targets.
if (await exists(path)) targets.add(path)
}
}
const scriptPath = fileURLToPath(import.meta.url)
if (process.argv[1] !== undefined && resolve(process.argv[1]) === scriptPath) {
try {
const removed = await new RepositoryCleaner(resolve(dirname(scriptPath), '..')).clean()
if (removed.length === 0) {
console.log('clean: already clean')
} else {
console.log(`clean: removed ${removed.length} paths`)
}
} catch (error) {
console.error(error instanceof Error ? error.message : error)
process.exitCode = 1
}
}

View File

@@ -29,8 +29,8 @@ describe('cordisConfigFiles', () => {
}
expect(cordisConfigFiles(root)).toEqual([
'examples/agent.cordis.yaml',
'examples/headless.cordis.yml',
join('examples', 'agent.cordis.yaml'),
join('examples', 'headless.cordis.yml'),
])
})
})

View File

@@ -7,7 +7,7 @@ import { spawn } from 'node:child_process'
// Each UI's node invocation matches its base demo script plus the overlay config.
const UIS = new Map([
['tui', ['--import', 'tsx', 'packages/examples/tui-demo/src/bin.ts', 'examples/tui-agent/code-mode.cordis.yml']],
['tui', ['--import', 'tsx', 'apps/cli/src/bin.ts', '--config', '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']],
])

View File

@@ -4,8 +4,8 @@
"docs/architecture.md": 1800,
"docs/cordis-primer.md": 600,
"docs/defensive-patterns.md": 550,
"docs/testing.md": 1020,
"docs/testing.md": 1100,
"examples/AGENTS.md": 310,
"packages/AGENTS.md": 660,
"packages/README.md": 760
"packages/README.md": 790
}

View File

@@ -127,8 +127,10 @@ export const LINK_MAP: Record<string, string> = {
SessionEventResultFilter: 'session-query.md',
SessionEventSearchDocument: 'session-query.md',
SessionEventSearchHit: 'session-query.md',
SessionEventSearchPage: 'session-query.md',
SessionEventSearchRequest: 'session-query.md',
SessionEventTrace: 'session-query.md',
SessionEventTraceObservation: 'session-query.md',
SessionEventTraceRequest: 'session-query.md',
SessionEventWindow: 'session-query.md',
SessionLineageTrace: 'session-query.md',
@@ -138,6 +140,8 @@ export const LINK_MAP: Record<string, string> = {
SessionSearchHit: 'session-query.md',
SessionSearchPage: 'session-query.md',
SessionSearchRequest: 'session-query.md',
SessionTitleObservation: 'session-query.md',
SessionTitleObservationResult: 'session-query.md',
SessionTitleProvider: 'session-title.md',
SessionTitleSnapshot: 'session-title.md',
SkillDefinition: 'skills.md',
@@ -207,8 +211,17 @@ const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
BashEnvVariableInfo: 'service-local metadata type is owned by packages/bash/tool-bash/src/index.ts',
CompactAgentContext: 'compaction service input is owned by packages/compact/compact/src/index.ts',
CreateAgentOptions: 'agent creation contract is owned by packages/core/agent/README.md',
Domain: 'domain interface is owned by packages/storage/storage-domain/README.md',
DomainChanged: 'event-local snapshot is owned by packages/storage/storage-domain/src/events.ts',
DomainFacility: 'domain form facility is owned by packages/storage/storage-domain/README.md',
DomainImpl: 'domain implementation contract is owned by packages/storage/storage-domain/README.md',
DomainSpec: 'domain declaration contract is owned by packages/storage/storage-domain/README.md',
StorageBackend: 'backend contract is owned by packages/storage/storage/src/backend.ts',
StorageForms: 'merge-extensible form map is owned by packages/storage/storage/src/index.ts',
InvariantInstaller: 'service-local contribution contract is owned by packages/support/invariants/README.md',
LocaleDict: 'service-local dictionary shape is owned by packages/client/i18n/src/index.ts',
WebBootGraph: 'web boot graph wire shape is owned by packages/client/modules/src/client/index.ts',
WebRoute: 'route registration contract is owned by packages/host/webserver/src/index.ts',
ThemeTokens: 'service-local token dictionary is owned by packages/client/ui-theme/src/index.ts',
Translate: 'service-local bound translator is owned by packages/client/i18n/src/index.ts',
TuiOverlayRequest: 'service-local extension contract is owned by packages/ui/tui/README.md',
@@ -224,6 +237,8 @@ const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
WorkflowAgentEndInfo: 'event-local snapshot is owned by packages/workflow/workflow/src/index.ts',
WorkflowAgentInfo: 'event-local snapshot is owned by packages/workflow/workflow/src/index.ts',
WorkflowResultInfo: 'event-local snapshot is owned by packages/workflow/workflow/src/index.ts',
Workspace: 'workspace entity contract is owned by packages/workspace/workspace/README.md',
WorkspaceId: 'branded id is owned by packages/workspace/workspace/README.md',
}
/** Collect named references from parameter, generic-constraint/default, and return types. */

View File

@@ -77,7 +77,10 @@ const GROUP_ORDER = [
'session-persistence',
'session-query',
'session-title',
'storage',
'workspace',
'support',
'acp',
'ui',
]
@@ -129,24 +132,49 @@ const SERVICE_ROLES: ServiceRole[] = [
title: 'Durable session persistence seam',
mode: 'seam',
implementations: ['session-persistence-jsonl', 'session-persistence-sqlite'],
consumers: ['agent-loop', 'tool-bash', 'hooks-claude', 'hooks-codex', 'acp', 'session-query', 'session-query-sqlite'],
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: 'storage',
pkg: 'storage',
title: 'Non-session storage hub',
mode: 'seam',
implementations: ['storage-json', 'storage-sqlite'],
consumers: ['storage-domain'],
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.',
},
{
key: 'storageDomain',
pkg: 'storage-domain',
title: 'Domain data facility',
mode: 'core',
consumers: ['workspace'],
note: 'Waits for every configured backend, then publishes the domain form as one lifecycle-bound service for typed durable state.',
},
{
key: 'workspace',
pkg: 'workspace',
title: 'Workspace entity registry',
mode: 'core',
consumers: ['apiproxy'],
note: 'Owns WorkspaceId-branded records over the domain facility; stable sessionIds accounts drive Host RPC and GUI projections.',
},
{
key: 'sessionQuery',
pkg: 'session-query',
title: 'Session reads, traces, filters, and search',
mode: 'seam',
implementations: ['session-query-sqlite'],
consumers: ['session-reference'],
note: 'The interface supplies exact reads, filters, and traces; its concrete backend adds full-text reconciliation, ranking, snippets, and cursor generations on the same service.',
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.',
},
{
key: 'sessionReferences',
pkg: 'session-reference',
title: 'Cross-session snapshot preparation',
mode: 'core',
consumers: ['tui', 'acp'],
consumers: ['tui'],
note: 'Projects bounded current-surface conversation snapshots into durable untrusted message context; host adapters own mention syntax.',
},
{
@@ -170,7 +198,7 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'tools',
title: 'Tool registry and guarded execution pipeline',
mode: 'core',
consumers: ['agent-loop', 'tool-ask-user', 'tool-bash', 'tool-cordis', 'tool-fs', 'tool-pty', 'tool-skill', 'tool-subagent', 'tool-todo', 'tool-web', 'acp'],
consumers: ['agent-loop', 'tool-ask-user', 'tool-bash', 'tool-cordis', 'tool-fs', 'tool-pty', 'tool-skill', 'tool-subagent', 'tool-todo', 'tool-web'],
note: 'Registers capabilities, owns Code Mode transport, and routes calls through pre-policy, monotonic guards, around dispatch, post-policy, and final-result observation.',
},
{
@@ -178,8 +206,8 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'user-interaction',
title: 'Human question/answer seam',
mode: 'seam',
implementations: ['tui', 'acp'],
consumers: ['tool-ask-user', 'tui', 'acp'],
implementations: ['tui'],
consumers: ['tool-ask-user', 'tui'],
note: 'UI front doors provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise.',
},
{
@@ -187,7 +215,6 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'plan-mode',
title: 'Plan collaboration state',
mode: 'core',
consumers: ['acp'],
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.',
},
{
@@ -195,8 +222,8 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'commands',
title: 'Human command registry',
mode: 'core',
consumers: ['tui', 'acp'],
note: 'Plugins register direct human commands; TUI and ACP consume the same effective per-agent catalog without sending invocations to the model.',
consumers: ['tui'],
note: 'Plugins register direct human commands; TUI consumes the effective per-agent catalog without sending invocations to the model.',
},
{
key: 'tui',
@@ -295,7 +322,6 @@ const SERVICE_ROLES: ServiceRole[] = [
title: 'Permission presets',
mode: 'core',
implementations: [],
consumers: ['acp'],
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.',
},
{
@@ -361,6 +387,22 @@ const SERVICE_ROLES: ServiceRole[] = [
consumers: ['spill-policy'],
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.',
},
{
key: 'httpServer',
pkg: 'webserver',
title: 'HTTP route registration',
mode: 'core',
consumers: ['connection', 'modules', 'hmr'],
note: 'Plain node:http carrier: named-route registry, index transform taps, and the static dist fallback; web-transport plugins register their own routes.',
},
{
key: 'clientModuleHost',
pkg: 'modules',
title: 'Client plugin graph host',
mode: 'core',
consumers: ['hmr'],
note: 'Composes the __DSH_BOOT__ entry graph from an incremental dshClient scan, serves plugin bundles, and notifies rebuilt/graph-changed subscribers.',
},
{
key: 'workflows',
pkg: 'workflow',
@@ -531,10 +573,10 @@ const APP_EXAMPLES = [
{
id: 'acp',
rel: 'examples/acp-agent/composition.md',
title: 'ACP Agent App Composition',
title: 'ACP Automation App Composition',
label: 'examples/acp-agent',
config: 'examples/acp-agent/cordis.yml',
summary: 'The ACP demo exposes the same agent spine over JSON-RPC stdio, with no stdout logger and no pre-created agent; clients create sessions through the ACP bridge.',
summary: 'The ACP demo exposes fresh baseline-prompt agent sessions to programmatic clients over JSON-RPC stdio, with no stdout logger, human UI, or pre-created agent.',
},
]
@@ -550,7 +592,7 @@ function renderAppExpansion(lines: string[], appNode: string, pluginName: string
} 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') {
lines.push(` ${appNode} --> ${nodeId('frontdoor', 'acp')}["@deepseek-ai/dsh-acp<br/>JSON-RPC stdio bridge<br/>sessions created by client"]`)
lines.push(` ${appNode} --> ${nodeId('frontdoor', 'acp')}["@deepseek-ai/dsh-acp<br/>automation-only JSON-RPC stdio<br/>fresh sessions created by client"]`)
}
lines.push(
` ${agentCore} --> ${nodeId('spine', 'llm')}["ctx.llm"]`,
@@ -1059,35 +1101,6 @@ function renderToolPipeline(): string {
].join('\n')
}
function renderSnapshotReplay(): string {
const maintenance = 'curated Mermaid sequence based on the snapshot test harness'
return [
...generatedHeader('ACP Snapshot Replay'),
'This graph explains what a snapshot scenario proves: recorded real-model session logs are replayed keylessly, ACP stdout is normalized and diffed, and scenario workspaces preserve tool side effects that the UI stream alone cannot prove.',
'',
'```mermaid',
'sequenceDiagram',
' participant Recorder as Real API recording',
' participant Fixture as snapshot fixture',
' participant Workspace',
' participant Replay as llm-replay adapter',
' participant ACP as acp-agent subprocess',
' participant Expected as stdout expected output',
' Recorder->>Fixture: session.jsonl + workspace inputs',
' Fixture->>Workspace: seed files and hook configs',
' Fixture->>Replay: recorded StreamChunk script',
` Replay->>ACP: deterministic ${mermaidCode('llm/stream')} chunks`,
' ACP->>Workspace: bash, fs, and hook side effects',
' ACP->>Expected: normalized sessionUpdate stream',
' Expected-->>ACP: diff must be empty',
'```',
'',
'The fs and hook snapshot matrix is valuable because it proves world state, hook decisions, and failed tool-card rendering, not just that replay returns text.',
'',
...maintenanceFooter(maintenance),
].join('\n')
}
function renderDocs(): GraphDoc[] {
const pkgs = collectPackageGraph(root, GROUP_ORDER, 'gen-doc-graphs')
const docs: GraphDoc[] = [
@@ -1096,7 +1109,6 @@ function renderDocs(): GraphDoc[] {
{ rel: 'docs/event-producer-consumer.md', content: renderEventRelations(pkgs) },
{ rel: 'docs/agent-lifecycle.md', content: renderLifecycle() },
{ rel: 'docs/tool-execution-pipeline.md', content: renderToolPipeline() },
{ rel: 'packages/ui/acp/snapshot-replay.md', content: renderSnapshotReplay() },
]
docs.unshift({ rel: 'docs/graph-atlas.md', content: renderIndex(docs) })
return docs
@@ -1112,7 +1124,6 @@ function renderIndex(docs: GraphDoc[]): string {
'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',
'packages/ui/acp/snapshot-replay.md': 'ACP snapshot replay',
}
const modes: Record<string, string> = {
'docs/capability-seams.md': 'hybrid generated',
@@ -1123,7 +1134,6 @@ function renderIndex(docs: GraphDoc[]): string {
'docs/event-producer-consumer.md': 'hybrid generated',
'docs/agent-lifecycle.md': 'curated',
'docs/tool-execution-pipeline.md': 'curated',
'packages/ui/acp/snapshot-replay.md': 'curated',
}
const rows = [
'| [module dependency graph](module-graph.md) | `generated` |',

View File

@@ -38,6 +38,7 @@ const GROUP_ORDER = [
'session-query',
'session-title',
'support',
'acp',
'ui',
]

View File

@@ -11,6 +11,8 @@ import { basename, resolve } from 'node:path'
import { Context } from 'cordis'
import type { ToolSchema } from '@deepseek-ai/dsh-llm'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import SessionStore from '@deepseek-ai/dsh-session'
import SessionQuerySqlite from '@deepseek-ai/dsh-session-query-sqlite'
import GoalService from '@deepseek-ai/dsh-goal'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { type Config as ToolsConfig } from '@deepseek-ai/dsh-tools'
@@ -39,6 +41,7 @@ import * as ToolGoal from '@deepseek-ai/dsh-tool-goal'
import Lsp from '@deepseek-ai/dsh-lsp'
import * as ToolLsp from '@deepseek-ai/dsh-tool-lsp'
import * as ToolSkill from '@deepseek-ai/dsh-tool-skill'
import * as ToolSessionQuery from '@deepseek-ai/dsh-tool-session-query'
import * as ToolTasks from '@deepseek-ai/dsh-tool-tasks'
import * as ToolTodo from '@deepseek-ai/dsh-tool-todo'
import * as ToolSubagent from '@deepseek-ai/dsh-tool-subagent'
@@ -316,6 +319,20 @@ const TOOL_PACKAGES: ToolPackage[] = [
await ctx.plugin(ToolSkill)
},
},
{
pkg: '@deepseek-ai/dsh-tool-session-query',
dir: 'tool-session-query',
source: 'packages/session-query/tool-session-query/src/index.ts',
requires: ['ctx.tools', 'ctx.systemPrompt', 'ctx.sessionQuery', 'a calling Agent for workspace authority'],
writes: ['tool/call', 'tool/result'],
async mount(ctx) {
await ctx.plugin(SessionStore)
await ctx.plugin(SessionQuerySqlite, { path: ':memory:' })
await ctx.plugin(ToolSessionQuery)
},
note:
'The five read-only tools hide provider cursors and authorize every result from the immutable calling agent session. The package is opt-in; compositions that need enforced deadlines or bounded inline output also mount the generic timeout or spill policies.',
},
{
pkg: '@deepseek-ai/dsh-tool-subagent',
dir: 'tool-subagent',
@@ -354,7 +371,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
await ctx.plugin(ToolTodo)
},
note:
'todo_write is session-owned state; UIs render the latest todo/write event as a checklist or ACP plan.',
'todo_write is session-owned state; UIs render the latest todo/write event as a checklist.',
},
{
pkg: '@deepseek-ai/dsh-tool-workflow',

File diff suppressed because one or more lines are too long

View File

@@ -3,7 +3,6 @@
"required": [
".agents/notes/README.md",
".agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md",
".agents/notes/implemented/architecture/2026-06-11-custom-schema-dsl.md",
".agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md",
".agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.md",
".agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md",
@@ -44,11 +43,9 @@
".agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md",
".agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md",
".agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.md",
".agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.md",
".agents/notes/implemented/feature/2026-06-14-acp-multi-session.md",
".agents/notes/implemented/feature/2026-06-15-code-mode.md",
".agents/notes/implemented/feature/2026-06-17-filesystem-tool-schemas.md",
".agents/notes/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md",
".agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md",
".agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md",
".agents/notes/implemented/feature/2026-06-22-acp-subagent-backend.md",
@@ -90,7 +87,6 @@
".agents/notes/implemented/process/2026-07-06-export-surface-jsdoc-gate.md",
".agents/notes/implemented/process/2026-07-06-generated-config-catalog.md",
".agents/notes/implemented/process/2026-07-06-node-engine-floor.md",
".agents/notes/implemented/process/2026-07-06-parallel-github-ci-gates.md",
".agents/notes/implemented/process/2026-07-06-parallel-pre-push-gates.md",
".agents/notes/implemented/process/2026-07-10-readme-known-limitations-gate.md",
".agents/notes/implemented/process/2026-07-12-package-model-experience-contract.md",
@@ -108,7 +104,6 @@
".agents/notes/implemented/simplification/2026-07-04-drop-image-content-block.md",
".agents/notes/implemented/simplification/2026-07-04-drop-inert-request-knobs.md",
".agents/notes/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md",
".agents/notes/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md",
".agents/notes/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md",
".agents/notes/implemented/simplification/2026-07-04-prune-write-only-fs-surface.md",
".agents/notes/implemented/simplification/2026-07-04-remove-agent-steering-mirror.md",
@@ -134,7 +129,7 @@
".agents/notes/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.md",
".agents/notes/proposed/feature/2026-07-08-interactive-side-sessions.md",
".agents/notes/implemented/feature/2026-07-10-sqlite-session-query-provider.md",
".agents/notes/proposed/feature/2026-07-13-stream-workflow-progress-through-tool-calls.md",
".agents/notes/rejected/feature/2026-07-13-stream-workflow-progress-through-tool-calls.md",
".agents/notes/proposed/process/2026-06-11-api-extractor-reports.md",
".agents/notes/proposed/process/2026-06-11-architectural-conformance.md",
".agents/notes/proposed/process/2026-06-11-supply-chain-and-vendor-drift.md",

View File

@@ -11,7 +11,8 @@ interface ProjectGraph {
options: ts.CompilerOptions
}
const configHost: ts.ParseConfigFileHost = {
/** TypeScript config host shared by repository scripts. */
export const repositoryConfigHost: ts.ParseConfigFileHost = {
useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
readDirectory: (...args) => ts.sys.readDirectory(...args),
fileExists: fileName => ts.sys.fileExists(fileName),
@@ -52,7 +53,7 @@ function loadProjectGraph(projectRoot: string): ProjectGraph {
/** Parse one config file and fail loud on any config diagnostic. */
function parseConfig(configPath: string): ts.ParsedCommandLine {
const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, configHost)
const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, repositoryConfigHost)
if (!parsed) throw new Error(`cannot parse TypeScript config ${configPath}`)
if (parsed.errors.length > 0) {
throw new Error(parsed.errors.map(error => ts.flattenDiagnosticMessageText(error.messageText, '\n')).join('\n'))

View File

@@ -585,6 +585,16 @@
"symbol": "SessionSurfaceSnapshot",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.md",
"symbol": "SessionTitleObservation",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.md",
"symbol": "SessionTitleObservationResult",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.md",
"symbol": "SessionEventRecord",
@@ -625,6 +635,11 @@
"symbol": "SessionEventTrace",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.md",
"symbol": "SessionEventTraceObservation",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-title.md",
"symbol": "SessionTitleProviderId",
@@ -1385,6 +1400,11 @@
"symbol": "SessionSearchPage",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.md",
"symbol": "SessionEventSearchPage",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.md",
"symbol": "SessionEventSearchHit",
@@ -1512,6 +1532,16 @@
"symbol": "SessionSurfaceSnapshot",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionTitleObservation",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionTitleObservationResult",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionEventRecord",
@@ -1552,6 +1582,11 @@
"symbol": "SessionSearchPage",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionEventSearchPage",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionEventSearchHit",
@@ -1597,6 +1632,11 @@
"symbol": "SessionEventTrace",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.zh.md",
"symbol": "SessionEventTraceObservation",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/tools.zh.md",
"symbol": "ToolOutputDefinition",

View File

@@ -57,6 +57,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/client/ui-conversation': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-question': { kind: 'indirect', reason: 'The package mounts dsh-tool-ask-user; that tool owns the model-visible schema and answer rendering.' },
'packages/client/ui-trajectory': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-workspace': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-theme': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/i18n': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/web': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
@@ -66,7 +67,6 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/fs/fs-sandbox': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-fs.' },
'packages/hooks/hook-protocol': { kind: 'indirect', reason: 'Only the hook bridge plugins render decoded hook output to a model.' },
'packages/host/apiproxy': { kind: 'none', reason: 'The wire contract and fetch carriers move already-composed messages and register no model surface.' },
'packages/host/runtime': { kind: 'indirect', reason: 'The assembly mounts model-facing plugins and injects provider/model defaults into agents.' },
'packages/host/webserver': { kind: 'none', reason: 'The HTTP carrier bridges browser and API handler and registers no model surface.' },
'packages/llm/llm': { kind: 'none', reason: 'The adapter registry forwards already-assembled requests unchanged.' },
'packages/llm/token-meter': { kind: 'indirect', reason: 'The measurement service leaves model-visible changes to its consumers.' },
@@ -90,6 +90,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/support/agent-loop-testkit': { kind: 'none', reason: 'The test helper mounts services but neither drives nor modifies model requests.' },
'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-mock-server': { kind: 'none', reason: 'The test server substitutes provider wire behavior without invoking a real model.' },
'packages/support/llm-replay': { kind: 'none', reason: 'The keyless adapter invokes no provider model.' },
'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.' },