Merge remote-tracking branch 'origin/master' into worktree/pr628-merge-20260727
# Conflicts: # .agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md # docs/architecture.i18n.yaml # docs/config-catalog.md # docs/cordis-catalog/events.md # docs/cordis-catalog/services.md # docs/core-data-structures/llm-streaming.i18n.yaml # docs/core-data-structures/llm-streaming.md # docs/core-data-structures/llm-streaming.zh.md # docs/event-producer-consumer.md # docs/module-graph.md # examples/headless-agent/tests/headless.snapshot.ts # packages/compact/compact-basic/tests/compact-loop-repro.spec.ts # packages/cordis/tool-cordis/src/api-catalog.ts # packages/core/agent-loop/README.md # packages/core/agent-loop/src/loop.ts # packages/examples/agent-spine-demo/README.md # packages/llm/README.md # packages/llm/llm-deepseek/src/adapter.ts # packages/llm/llm-pi-ai/src/adapter.ts # packages/llm/llm-retry/README.md # packages/llm/llm/README.md # packages/llm/llm/src/index.ts # packages/llm/llm/tests/service.spec.ts # packages/support/llm-replay/src/index.ts # packages/support/llm-replay/tests/llm-replay.spec.ts
This commit is contained in:
6
packages/examples/cli-demo/README.i18n.yaml
Normal file
6
packages/examples/cli-demo/README.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/examples/cli-demo/README.md
|
||||
README.md: b8f2bde962738a1a23f0e57218ab0f90e8e0b705
|
||||
README.zh.md: 322ba3fb3b253d867832534bd33f65df3a5b8d37
|
||||
@@ -1,5 +1,7 @@
|
||||
# @deepseek-ai/dsh-cli-demo
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Headless one-shot app and bin for running one agent task without an interactive UI or editor client. It composes [`@deepseek-ai/dsh-agent-spine-demo`](../agent-spine-demo/README.md), JSONL persistence, and exactly one fresh top-level agent. The bin submits the task, waits for its durable turn ending, renders the selected output, disposes to quiescence, and exits.
|
||||
|
||||
The package mounts no console logger, interactive UI, user-interaction service, or `ask_user_question` tool. Stdout is reserved for the selected output format; diagnostics use stderr.
|
||||
|
||||
78
packages/examples/cli-demo/README.zh.md
Normal file
78
packages/examples/cli-demo/README.zh.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# @deepseek-ai/dsh-cli-demo
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
无头单次应用及 bin,用于在没有交互式 UI 或编辑器客户端的情况下运行一项 agent 任务。它组合 [`@deepseek-ai/dsh-agent-spine-demo`](../agent-spine-demo/README.md)、JSONL 持久化,以及恰好一个新建顶层 agent。Bin 提交任务,等待其持久轮次结束,渲染所选输出,释放至静默,然后退出。
|
||||
|
||||
该包不挂载 console logger、交互式 UI、用户交互服务或 `ask_user_question` 工具。Stdout 专用于所选输出格式;诊断使用 stderr。
|
||||
|
||||
## 配置
|
||||
|
||||
| 键 | 默认值 | 路由目标 |
|
||||
|---|---|---|
|
||||
| `provider` | 必填 | 已配置 agent 的提供方路由 |
|
||||
| `model` | 必填 | 已配置 agent 的模型 |
|
||||
| `maxParallelToolCalls` | agent-loop 默认值 | 正整数并发工具调用上限;`1` 表示串行 |
|
||||
| `persona` | 无 | `dsh-system-prompt` 中的部署 persona |
|
||||
| `toolOrder` | 字典序 | `dsh-system-prompt` 中显式的面向模型工具顺序 |
|
||||
| `tools` | `{ mode: 'native' }` | 通过 `dsh-agent-spine-demo` 提供的工具注册表呈现配置 |
|
||||
| `dshHome` | `$DSH_HOME` 或 `~/.dsh` | 向模型 bash 公开并用于本地 skill 发现的 Harness 主目录 |
|
||||
| `sessionTitle` | 主干示例限制 | 通过 `dsh-agent-spine-demo` 提供的后备标题词数/字节限制 |
|
||||
| `skills` | 拥有者默认值 | Skill 注册表、本地提供方和面向模型的 skill 工具 |
|
||||
| `toolBash` | 拥有者默认值 | 面向模型的 bash 配置,包括此生产方对后台任务的选用 |
|
||||
| `toolTasks` | 拥有者默认值 | 通用 `task_output` 等待边界 |
|
||||
| `persistenceRoot` | `./.sessions` | JSONL 会话根目录 |
|
||||
| `persistenceCompression` | `'zstd'` | JSONL 工件编码(`'zstd'` 或原始 `'none'`) |
|
||||
| `workspaceContext` | 必填 | Workspace 指令字节预算,或以 `false` 禁用加载 |
|
||||
|
||||
## CLI 契约
|
||||
|
||||
```sh
|
||||
dsh-cli-demo [--config path] [--output-format text|json|stream-json] <task>
|
||||
```
|
||||
|
||||
`--config` 默认为 `./cordis.yml`;`--output-format` 默认为 `text`。必须恰好提供一个非空位置任务,因此含空格的任务需要加引号。`--help` 在不启动的情况下打印用法。不存在 `-p` 或 `--print` 标志。
|
||||
|
||||
根 headless-agent 示例提供其叶节点:
|
||||
|
||||
```sh
|
||||
pnpm run demo:headless "inspect the failing test and fix it"
|
||||
```
|
||||
|
||||
Loader 配置通过仓库安装的可选原生辅助程序解析裸包说明符,因此根命令不需要特殊 Node 标志。
|
||||
|
||||
### 输出格式
|
||||
|
||||
- `text` 写入最后一条含文本的 assistant 消息,后跟一个换行符。
|
||||
- `json` 写入一条 DSH 原生结果记录:`{ type: "result", success, sessionId, turn, result, reason, usage? }`。`usage` 对任务轮次中的每个模型步骤恰好求和一次,包括产生用量但没有提交 assistant 消息的已计费失败重试。
|
||||
- `stream-json` 将顶层会话任务轮次中的每个规范事件写成 `{ type: "session_event", sessionId, event }`,然后写入同一结果记录。子 agent 活动只通过父工具事件与结果出现。
|
||||
|
||||
只有 `reason.kind === "completed"` 会成功退出。其他持久轮次结尾仍会输出部分文本或结果记录,向 stderr 添加诊断,并以非零状态退出。参数和启动失败会让 stdout 保持为空。SIGINT 与 SIGTERM 会取消活动工作,等待释放,并分别以 130 和 143 退出。
|
||||
|
||||
任务轮次会在最终输出前显式刷新。进程退出后,会话日志仍保留在 `persistenceRoot` 下。
|
||||
|
||||
## 操作安全
|
||||
|
||||
Headless-agent 叶节点提供本地 bash、文件系统、skill、subagent、工作流和 todo 能力。因此任务可以修改启动 workspace、运行命令、生成子 agent,并消耗提供方 token。请从目标项目目录运行 CLI,检查叶节点的能力与沙箱配置,不要把非交互式执行当作批准边界。
|
||||
|
||||
## 模型体验
|
||||
|
||||
### 单次任务轮次
|
||||
|
||||
#### 模型所见
|
||||
|
||||
位置任务会成为一条用户消息。通过 `dsh-agent-spine-demo`,顶层 agent 还会收到已配置的 workspace 指令与 persona、skill 目录、可见工具 schema,以及同一轮次后续步骤所需的保留工具结果。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
每个模型步骤中的任务、提示词段、工具 schema、assistant 输出和工具结果都会消耗 token。JSON 事件流和最终渲染不增加模型 token;委派的子工作有自己的模型用量,不计入父结果的 `usage` 总量。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
只要单次 agent 的提示词、schema、模型路由和会话前缀保持不变,工具轮次历史就只追加。改变该组合会建立不同的请求前缀;JSON 输出 mode 不影响 cache。
|
||||
|
||||
## 已知限制与延后工作
|
||||
|
||||
- **每个进程只创建一个新的顶层会话**:其 workspace cwd 是启动目录;此应用不支持恢复、第二条提示词、stdin 上下文或并发顶层会话。
|
||||
- **没有交互式问题或批准提供方**:需要人类回答的工具无法完成,除非其他叶节点按显式策略组合一个非交互式提供方。
|
||||
- **流只包含顶层会话**:子会话不会平铺到流中,聚合用量只涵盖父任务轮次记录的模型步骤。
|
||||
@@ -1,4 +1,3 @@
|
||||
import { spawn } from 'node:child_process'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { mkdtemp, mkdir, readFile, readdir, rm, symlink, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
@@ -6,6 +5,7 @@ import { dirname, join } from 'node:path'
|
||||
import { promisify } from 'node:util'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { zstdDecompress } from 'node:zlib'
|
||||
import { execa } from 'execa'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
/**
|
||||
@@ -23,7 +23,7 @@ const decompress = promisify(zstdDecompress)
|
||||
const dshPackages = [
|
||||
'examples/agent-spine-demo', 'examples/cli-demo', 'core/agent', 'core/session',
|
||||
'core/system-prompt', 'core/tools', 'core/agent-loop', 'llm/llm', 'bash/bash',
|
||||
'bash/bash-local', 'bash/tool-bash', 'support/invariants', 'ui/app-boot',
|
||||
'bash/bash-local', 'bash/tool-bash', 'subprocess/subprocess', 'subprocess/subprocess-local', 'support/invariants', 'ui/app-boot',
|
||||
'session-persistence/session-persistence', 'session-persistence/session-checkpoint-policy',
|
||||
'session-persistence/session-persistence-jsonl',
|
||||
'context/workspace-context',
|
||||
@@ -80,6 +80,8 @@ async function makeConsumer(): Promise<string> {
|
||||
await writeFile(join(dir, 'cordis.yml'), [
|
||||
'- id: mock-llm',
|
||||
" name: './mock-llm.ts'",
|
||||
'- id: subprocess',
|
||||
" name: '@deepseek-ai/dsh-subprocess-local'",
|
||||
'- id: bash',
|
||||
" name: '@deepseek-ai/dsh-bash-local'",
|
||||
'- id: cli-agent',
|
||||
@@ -114,36 +116,34 @@ interface BinResult {
|
||||
readonly stderr: string
|
||||
}
|
||||
|
||||
function runBuiltBin(cwd: string, args: readonly string[], interrupt?: NodeJS.Signals): Promise<BinResult> {
|
||||
return new Promise((resolveResult, reject) => {
|
||||
const child = spawn(process.execPath, [cliBin, ...args], {
|
||||
cwd,
|
||||
env: { ...process.env, DSH_HOME: join(cwd, '.dsh'), DSH_AGENTS_HOME: join(cwd, '.agents') },
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
})
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
async function runBuiltBin(cwd: string, args: readonly string[], interrupt?: NodeJS.Signals): Promise<BinResult> {
|
||||
const subprocess = execa(process.execPath, [cliBin, ...args], {
|
||||
cwd,
|
||||
env: { DSH_HOME: join(cwd, '.dsh'), DSH_AGENTS_HOME: join(cwd, '.agents') },
|
||||
stdin: 'ignore',
|
||||
timeout: 25_000,
|
||||
killSignal: 'SIGKILL',
|
||||
reject: false,
|
||||
stripFinalNewline: false,
|
||||
})
|
||||
// Genuinely custom mid-stream logic: the signal cases deliver `interrupt`
|
||||
// once the first streamed chunk proves the turn is in flight.
|
||||
if (interrupt !== undefined) {
|
||||
let streamed = ''
|
||||
let interrupted = false
|
||||
child.stdout.setEncoding('utf8')
|
||||
child.stdout.on('data', (chunk: string) => {
|
||||
stdout += chunk
|
||||
if (interrupt !== undefined && !interrupted && stdout.includes('assistant/chunk')) {
|
||||
subprocess.stdout.on('data', (chunk: Buffer) => {
|
||||
streamed += chunk.toString('utf8')
|
||||
if (!interrupted && streamed.includes('assistant/chunk')) {
|
||||
interrupted = true
|
||||
child.kill(interrupt)
|
||||
subprocess.kill(interrupt)
|
||||
}
|
||||
})
|
||||
child.stderr.setEncoding('utf8')
|
||||
child.stderr.on('data', (chunk: string) => { stderr += chunk })
|
||||
const timer = setTimeout(() => {
|
||||
child.kill('SIGKILL')
|
||||
reject(new Error(`built CLI did not exit. stdout:\n${stdout}\nstderr:\n${stderr}`))
|
||||
}, 25_000)
|
||||
child.once('error', (error) => { clearTimeout(timer); reject(error) })
|
||||
child.once('exit', (code, signal) => {
|
||||
clearTimeout(timer)
|
||||
resolveResult({ code: code ?? -1, signal, stdout, stderr })
|
||||
})
|
||||
})
|
||||
}
|
||||
const result = await subprocess
|
||||
if (result.timedOut) {
|
||||
throw new Error(`built CLI did not exit. stdout:\n${result.stdout}\nstderr:\n${result.stderr}`)
|
||||
}
|
||||
return { code: result.exitCode ?? -1, signal: result.signal ?? null, stdout: result.stdout, stderr: result.stderr }
|
||||
}
|
||||
|
||||
let consumer: string | undefined
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types",
|
||||
"tsBuildInfoFile": "../../../.typecheck/cli-demo.tsbuildinfo"
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"references": [
|
||||
|
||||
Reference in New Issue
Block a user