cleanup(cli): remove dsh-cli-demo
This commit is contained in:
+2
-2
@@ -2,5 +2,5 @@
|
||||
# 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 .agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md
|
||||
2026-07-29-dsh-source-launch-tsx-esm.md: 21e912c7c7bbdd70142c202105d9a3035442884a
|
||||
2026-07-29-dsh-source-launch-tsx-esm.zh.md: dc6150b7017777eea99778cc9813e17402354462
|
||||
2026-07-29-dsh-source-launch-tsx-esm.md: aabe6c2d6a92b5cc6568eee1d42df3a6998cf0ca
|
||||
2026-07-29-dsh-source-launch-tsx-esm.zh.md: fdeb8a976e02bb9311a6ec9d097d09151a998a04
|
||||
@@ -35,4 +35,4 @@ The node-compat CI matrix (Node 22.19 and 26) gains `dsh-source-launch-smoke` (`
|
||||
- One launch vector across the whole engines range, including future Node lines that change native TypeScript support; the smoke gate enforces it per matrix line.
|
||||
- TypeScript transformation is delegated to tsx/esbuild again, reversing the prior note's goal of proving Node-native transformation; that goal is unreachable while vendored sources use non-erasable syntax and Node ships no transform mode.
|
||||
- The runtime declared-dependency enforcement in source launches is gone; undeclared workspace imports now surface only through static gates or built-mode resolution failures.
|
||||
- Startup improves ~0.4s over the full tsx default (`demo:headless` and ACP keep `--import tsx`; their graphs were not audited for CJS-hook dependence and their launch latency is not on the interactive path).
|
||||
- Startup improves ~0.4s over the full tsx default (`demo:headless` now aliases the same `dsh run` source launch; ACP keeps `--import tsx` because its graph was not audited for CJS-hook dependence and its launch latency is not on the interactive path).
|
||||
@@ -35,4 +35,4 @@ node-compat CI 矩阵(Node 22.19 与 26)新增 `dsh-source-launch-smoke`(`
|
||||
- 整个 engines 范围(包括未来改变原生 TypeScript 支持的 Node 版本线)只有一个启动向量;冒烟门禁按矩阵行强制执行。
|
||||
- TypeScript 转换重新委托给 tsx/esbuild,逆转了前一篇 Agent Note「证明 Node 原生转换可用」的目标;在 vendor 源码使用不可擦除语法且 Node 不再提供 transform 模式的情况下,该目标不可达。
|
||||
- 源码启动中的运行时依赖声明强制不复存在;未声明的 workspace import 现在只能通过静态门禁或构建模式的解析失败暴露。
|
||||
- 启动相比完整 tsx 默认形态快约 0.4s(`demo:headless` 与 ACP 保持 `--import tsx`:其依赖图未就 CJS 钩子依赖性做审计,且其启动延迟不在交互路径上)。
|
||||
- 启动相比完整 tsx 默认形态快约 0.4s(`demo:headless` 现为同一条 `dsh run` 源码启动命令的别名;ACP 保留 `--import tsx`,因为它的依赖图尚未就 CJS 钩子依赖性做审计,且其启动延迟不在交互路径上)。
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# 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 .agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.md
|
||||
2026-07-20-error-cause-chain-diagnostics.md: 7de1f4f631cec90048ccc8eab7a6654560d84846
|
||||
2026-07-20-error-cause-chain-diagnostics.zh.md: 6ce642eb4a18a1a412fb63c3bb00042a921f4da3
|
||||
2026-07-20-error-cause-chain-diagnostics.md: 89edd4529cc16617fdca736dbbea4b170400d6bd
|
||||
2026-07-20-error-cause-chain-diagnostics.zh.md: 673921c913a9f1fefe6f85303cf5952f8edbcc1d
|
||||
@@ -34,4 +34,4 @@ A TUI run against an unreachable DeepSeek endpoint failed with the single notice
|
||||
- Durable `turn/end` error messages include cause detail. Existing snapshot fixtures replay byte-identically because their scripted errors carry no `cause` (for such errors `errorChain(err)` equals `err.message`); only unit-test expectation strings changed. A fixture recorded from a real transport failure would carry the chain.
|
||||
- `errorChain` renders `message` without the class name (`String(error)` rendered `Error: <message>`), so a bare `TypeError` in a log line loses its type label unless its message is empty (then the name is the fallback). The chain detail was judged worth more than the class name at these seams.
|
||||
- `dsh-stdio` output for failed turns is no longer silent; piped consumers that parsed the transcript see new `[turn …]` lines.
|
||||
- Remaining `renderThrown` copies in `dsh-subagent`, `dsh-workflow`, `dsh-skill`, `dsh-workflow-workerthread`, and `cli-demo` still render without the chain; they wrap package-local errors that carry their own messages, and can adopt `errorChain` when their diagnostics prove insufficient.
|
||||
- Remaining `renderThrown` copies in `dsh-subagent`, `dsh-workflow`, `dsh-skill`, and `dsh-workflow-workerthread` still render without the chain; they wrap package-local errors that carry their own messages, and can adopt `errorChain` when their diagnostics prove insufficient.
|
||||
@@ -34,4 +34,4 @@ TUI 连接不可达的 DeepSeek 端点时,失败只显示一条 `fetch failed`
|
||||
- 持久化的 `turn/end` 错误消息包含 cause 细节。现有快照 fixture(测试前置数据)字节级一致地回放,因为其脚本化错误不带 `cause`(对这类错误 `errorChain(err)` 等于 `err.message`);只有单元测试的期望字符串有变化。从真实传输失败录制的 fixture 会携带完整链。
|
||||
- `errorChain` 渲染 `message` 而不带类名(`String(error)` 会渲染 `Error: <message>`),因此日志行里的裸 `TypeError` 会丢失类型标签,除非消息为空(此时回退到类名)。在这些诊断界面上,链细节被判断为比类名更有价值。
|
||||
- `dsh-stdio` 对失败轮次的输出不再沉默;解析 transcript 的管道消费方会看到新的 `[turn …]` 行。
|
||||
- `dsh-subagent`、`dsh-workflow`、`dsh-skill`、`dsh-workflow-workerthread`、`cli-demo` 里剩余的 `renderThrown` 副本仍不渲染链;它们包装的是自带消息的包内错误,等诊断信息证明不足时再采用 `errorChain`。
|
||||
- `dsh-subagent`、`dsh-workflow`、`dsh-skill`、`dsh-workflow-workerthread` 里剩余的 `renderThrown` 副本仍不渲染链;它们包装的是自带消息的包内错误,等诊断信息证明不足时再采用 `errorChain`。
|
||||
@@ -2,5 +2,5 @@
|
||||
# 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 .agents/notes/implemented/process/2026-07-06-node-engine-floor.md
|
||||
2026-07-06-node-engine-floor.md: ef047d885a442106a35922f4716d2996d8a98ca7
|
||||
2026-07-06-node-engine-floor.zh.md: c409b006baa3451eb5fc5260be4e14ffe88776c8
|
||||
2026-07-06-node-engine-floor.md: e42b809f83fe388526ecbd74192989e0bcdbba67
|
||||
2026-07-06-node-engine-floor.zh.md: 92fa00f7a270f4506cba6c899e9edf6a2fdd3209
|
||||
@@ -15,7 +15,7 @@ Set `engines.node` to `^22.19.0 || >=24.0.0` and test keyless CI on `['22.19', 2
|
||||
Two Node features gate the source runtime:
|
||||
|
||||
- **`node:sqlite`** — `packages/session-persistence/session-persistence-sqlite` does a top-level `import { DatabaseSync } from 'node:sqlite'`. The module dropped its `--experimental-sqlite` flag requirement at **22.13** (LTS) and **23.4** (Current); before those, importing it throws at load.
|
||||
- **Native TypeScript type-stripping** — the built-mode `examples/headless-agent/tests/keyless-smoke.e2e.ts` smoke boots `dsh-cli-demo`'s published `lib/bin.js` under plain `node` (no tsx) and loads the example's `.ts` test adapter (`cli-mock-llm.ts`). Type-stripping is the default from **22.18** (LTS) and **23.6** (Current); before those it needs `--experimental-strip-types`.
|
||||
- **Native TypeScript type-stripping** — the built-mode `examples/headless-agent/tests/keyless-smoke.e2e.ts` smoke boots its unexported `.ts` driver under plain `node` (no tsx) and loads the example's `.ts` test adapter (`cli-mock-llm.ts`). Type-stripping is the default from **22.18** (LTS) and **23.6** (Current); before those it needs `--experimental-strip-types`.
|
||||
|
||||
Those source features clear on the 22.x line at **22.18**, but the installed Pi adapter dependency raises the advertised LTS floor. `@deepseek-ai/dsh-llm-pi-ai` depends on `@earendil-works/pi-ai@0.79.3`, whose package declares `engines.node >=22.19.0`, so the LTS floor is **22.19**. The 24.x branch remains `>=24.0.0`. The disjoint range excludes Node 23 entirely: Node 23.0–23.5 still has at least one flagged source feature, and the 23 line is non-LTS/EOL, so advertising `>=23.6` would add a dead release line and a CI leg no deployment should use.
|
||||
|
||||
@@ -25,7 +25,7 @@ Those source features clear on the 22.x line at **22.18**, but the installed Pi
|
||||
|
||||
- The advertised LTS branch no longer undercuts the Pi adapter dependency floor.
|
||||
- CI proves the Node 22 LTS floor directly with Node 22.19, keeps primary coverage on `node: 24`, and exercises Node 26 as the next even line; focused compatibility smokes run on all three versions.
|
||||
- The built-bin smoke needs no version-conditional flag: at 22.19 type-stripping is already the default, so the test stays the plain `node lib/bin.js` path it documents.
|
||||
- The built-mode smoke needs no version-conditional flag: at 22.19 type-stripping is already the default, so the example-owned TypeScript driver stays a plain `node fixture.ts` path.
|
||||
- A future dependency or source API that raises the runtime floor must move `engines.node`, the compatibility matrix, and this Agent Note in the same change.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -15,7 +15,7 @@ Status: implemented
|
||||
两个 Node 特性决定了源码运行时的门槛:
|
||||
|
||||
- **`node:sqlite`**:`packages/session-persistence/session-persistence-sqlite` 在顶层执行 `import { DatabaseSync } from 'node:sqlite'`。该模块在 **22.13**(LTS)和 **23.4**(Current)取消了 `--experimental-sqlite` 标志要求;在此之前,导入它会在加载时抛出异常。
|
||||
- **原生 TypeScript 类型剥离**——构建模式的 `examples/headless-agent/tests/keyless-smoke.e2e.ts` 冒烟测试使用纯 `node`(无 tsx)启动 `dsh-cli-demo` 已发布的 `lib/bin.js`,并加载示例的 `.ts` 测试适配器(`cli-mock-llm.ts`)。类型剥离从 **22.18**(LTS)和 **23.6**(Current)起成为默认行为;更早版本需要 `--experimental-strip-types`。
|
||||
- **原生 TypeScript 类型剥离**——构建模式的 `examples/headless-agent/tests/keyless-smoke.e2e.ts` 冒烟测试使用纯 `node`(无 tsx)启动该示例未导出的 `.ts` driver,并加载示例的 `.ts` 测试适配器(`cli-mock-llm.ts`)。类型剥离从 **22.18**(LTS)和 **23.6**(Current)起成为默认行为;更早版本需要 `--experimental-strip-types`。
|
||||
|
||||
这些源码特性在 22.x 线上于 **22.18** 全部就绪,但已安装的 Pi 适配器依赖将宣传的 LTS 下限进一步提高。`@deepseek-ai/dsh-llm-pi-ai` 依赖 `@earendil-works/pi-ai@0.79.3`,后者的包声明 `engines.node >=22.19.0`,因此 LTS 下限为 **22.19**。24.x 分支保持 `>=24.0.0`。该不相交范围完全排除了 Node 23:Node 23.0–23.5 至少还有一个源码特性需要标志,而 23 线是非 LTS/已 EOL 的,宣传 `>=23.6` 会增加一条已终止的发布线和一条 CI 分支,而没有任何部署应当使用它。
|
||||
|
||||
@@ -25,7 +25,7 @@ Status: implemented
|
||||
|
||||
- 宣传的 LTS 分支不再低于 Pi 适配器依赖的下限。
|
||||
- CI 通过 Node 22.19 直接验证 Node 22 LTS 下限,将主要覆盖率任务保留在 `node: 24`,并用 Node 26 验证下一个偶数线;三个版本均运行聚焦的兼容性冒烟测试。
|
||||
- built-bin 冒烟测试无需版本条件标志:在 22.19 上类型剥离已是默认行为,因此测试保持其文档所述的纯 `node lib/bin.js` 路径。
|
||||
- 构建模式冒烟测试无需版本条件标志:在 22.19 上类型剥离已是默认行为,因此示例自有的 TypeScript driver 保持使用纯 `node fixture.ts` 路径。
|
||||
- 未来若依赖或源码 API 提高运行时下限,必须在同一变更中同步调整 `engines.node`、兼容性矩阵和本 Agent Note。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# 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 .agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.md
|
||||
2026-07-20-remove-stdio-and-echo-agents.md: 1c7ff4b5337341ea7678c1455dd7bbd3365df6ba
|
||||
2026-07-20-remove-stdio-and-echo-agents.zh.md: 67445d25f6a0534c92c50568da898a5bffc73a52
|
||||
2026-07-20-remove-stdio-and-echo-agents.md: 4ffeacae4afc212b0d7b739def2c0e96780d5e54
|
||||
2026-07-20-remove-stdio-and-echo-agents.zh.md: 462dc275f151dbeb41bf431fc2ee583e74f22a6b
|
||||
@@ -19,18 +19,18 @@ The stdio and Echo agents are removed without compatibility packages, modes, com
|
||||
The remaining application roles are explicit:
|
||||
|
||||
- `@deepseek-ai/dsh-tui` owns terminal-interactive execution. It rejects non-TTY streams before Loader boot; `apps/cli/config/base.cordis.yml` plus the `tui.cordis.yml` overlay own the complete coding composition, with PTY plus terminal-snapshot coverage in `apps/cli/tests/`.
|
||||
- [`@deepseek-ai/dsh-cli-demo`](../../../../packages/examples/cli-demo/README.md) owns non-interactive execution, including pipes. `examples/headless-agent` owns the real-model one-shot composition, replay snapshots, generic real-agent suites, and test-only keyless Loader fixtures.
|
||||
- [`dsh run`](../../../../apps/cli/README.md) owns non-interactive execution. Its `headless` profile is the product composition; `examples/headless-agent` owns replay snapshots, generic real-agent suites, and an unexported keyless Loader driver.
|
||||
- [`@deepseek-ai/dsh-acp-demo`](../../../../packages/examples/acp-demo/README.md) and `@deepseek-ai/dsh-jsonrpc` own their framed protocol integrations.
|
||||
|
||||
The SDK project model and create/config workflows replace the `stdio` run-interface option with `tui`; generated TUI projects compose `@deepseek-ai/dsh-tui` and create or resume one exact session. Repository-facing demo documentation requires a DeepSeek API key and leads with the real Headless or TUI agents.
|
||||
|
||||
Keyless validation is test-owned. The Headless Loader smoke uses a fixture adapter to exercise a real tool round trip, the CLI built-bin suite pins output, persistence, failure, and signal semantics, and package-specific Loader tests keep deterministic adapters beside their scenarios. None is exposed as a runnable mock agent.
|
||||
Keyless validation is test-owned. The Headless Loader smoke uses a fixture adapter to exercise a real tool round trip, the `dsh` built-bin suite pins one-shot output, persistence, failure, and signal semantics, and package-specific Loader tests keep deterministic adapters beside their scenarios. None is exposed as a runnable mock agent.
|
||||
|
||||
## Verification
|
||||
|
||||
TUI and Headless Loader coverage run the real app packages in source and built modes. PTY-driven subprocess coverage is reserved for the TUI lifecycle; other entry-point smokes use the one-shot pipe protocol. Headless proves its task/result and tool-call contracts. Generated graphs and repository searches reject stale package, command, leaf, SDK-interface, `createStdioChat`, and `StdioRuntime` references.
|
||||
|
||||
The built `dsh` bin rejects a piped TUI launch before Loader boot and points at its one-shot `-p` mode; `apps/cli/tests/built-bin.e2e.ts` pins that path, while `cli-demo`'s built-bin suite runs text, JSON, and structurally parsed `stream-json` output under plain Node, persists fresh sessions, and rejects invalid arguments and missing config without contaminating stdout. Code Mode has programmatic TUI snapshots and an ACP overlay demo. Time-context integration uses the real Headless composition for two ordered turns, while its package tests own finer elapsed-time behavior.
|
||||
The built `dsh` bin rejects a piped TUI launch before Loader boot and points at `dsh run`; `apps/cli/tests/built-bin.e2e.ts` pins the product one-shot path under plain Node, including output, persistence, invalid arguments, missing configuration, and signals. The headless example's test-only JSONL driver preserves assembled canonical-event snapshots without creating a second CLI contract. Code Mode has programmatic TUI snapshots and an ACP overlay demo. Time-context integration uses the explicit Headless test composition for two ordered turns, while its package tests own finer elapsed-time behavior.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
+3
-3
@@ -19,18 +19,18 @@ DeepSeek Harness 在 TUI 和 Headless coding agent 之外,还提供了两个
|
||||
保留的应用角色均有明确归属:
|
||||
|
||||
- `@deepseek-ai/dsh-tui` 负责终端交互式执行。它会在 Loader 启动前拒绝非 TTY 流;`apps/cli/config/base.cordis.yml` 与 `tui.cordis.yml` overlay 拥有完整 coding 组装,PTY 与终端快照覆盖则位于 `apps/cli/tests/`。
|
||||
- [`@deepseek-ai/dsh-cli-demo`](../../../../packages/examples/cli-demo/README.md) 负责非交互式执行,包括管道方式。`examples/headless-agent` 拥有真实模型的单次任务组装、回放快照、通用真实 agent 测试套件,以及仅供测试使用的无密钥 Loader fixture。
|
||||
- [`dsh run`](../../../../apps/cli/README.md) 负责非交互式执行。其 `headless` profile 是产品组装;`examples/headless-agent` 负责回放快照、通用真实 agent 测试套件和未导出的无密钥 Loader driver。
|
||||
- [`@deepseek-ai/dsh-acp-demo`](../../../../packages/examples/acp-demo/README.md) 和 `@deepseek-ai/dsh-jsonrpc` 负责各自的分帧协议集成。
|
||||
|
||||
SDK 工程模型与 create/config 工作流将 `stdio` 运行接口选项替换为 `tui`;生成的 TUI 工程组合 `@deepseek-ai/dsh-tui`,并创建或恢复一个确切会话。仓库中的演示文档要求 DeepSeek API key,并优先引导到真实的 Headless 或 TUI agent。
|
||||
|
||||
无密钥验证由测试负责。Headless Loader 冒烟测试使用 fixture 适配器验证真实工具往返;CLI built-bin 测试套件固定输出、持久化、失败和信号语义;各包专属的 Loader 测试则将确定性适配器放在对应场景旁。其中任何一项都不会作为可运行的 mock agent 对外暴露。
|
||||
无密钥验证由测试负责。Headless Loader 冒烟测试使用 fixture 适配器验证真实工具往返;`dsh` built-bin 测试套件固定单次运行的输出、持久化、失败和信号语义;各包专属的 Loader 测试则将确定性适配器放在对应场景旁。其中任何一项都不会作为可运行的 mock agent 对外暴露。
|
||||
|
||||
## 验证
|
||||
|
||||
TUI 与 Headless 的 Loader 覆盖以源码和构建产物两种模式运行真实 app 包。由 PTY 驱动的子进程覆盖仅用于 TUI 生命周期;其他入口冒烟测试使用单次管道协议。Headless 验证任务/结果契约和工具调用契约。生成图谱与仓库搜索会拒绝陈旧的包、命令、叶节点、SDK 接口、`createStdioChat` 和 `StdioRuntime` 引用。
|
||||
|
||||
构建后的 `dsh` 可执行文件会在 Loader 启动前拒绝通过管道启动 TUI,并指向其单次 `-p` 模式;`apps/cli/tests/built-bin.e2e.ts` 固定了该执行路径,而 `cli-demo` 的 built-bin 套件在普通 Node 下运行文本、JSON 和经过结构化解析的 `stream-json` 输出,持久化新建会话,并在不污染 stdout 的情况下拒绝无效参数和缺失配置。Code Mode 由程序化 TUI 快照与 ACP overlay demo 覆盖。时间上下文集成通过真实 Headless 组装执行两个有序轮次,而更细粒度的耗时行为由时间上下文的包级测试负责。
|
||||
构建后的 `dsh` 可执行文件会在 Loader 启动前拒绝通过管道启动 TUI,并指向 `dsh run`;`apps/cli/tests/built-bin.e2e.ts` 在普通 Node 下固定产品的一次性路径,包括输出、持久化、无效参数、缺失配置和信号。headless 示例仅供测试的 JSONL driver 保留组装后的规范事件快照,而不会创建第二套 CLI(命令行界面)契约。Code Mode 由程序化 TUI 快照与 ACP overlay demo 覆盖。时间上下文集成通过显式的 Headless 测试组装执行两个有序轮次,而更细粒度的耗时行为由时间上下文的包级测试负责。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
|
||||
@@ -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 .agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.md
|
||||
2026-08-08-remove-cli-demo.md: 5875f47f2d2463fd6f82f66df5ca08fef8e06aed
|
||||
2026-08-08-remove-cli-demo.zh.md: 1bb9e2c8860f57e1170527cee0cbe694af9990f6
|
||||
@@ -0,0 +1,34 @@
|
||||
# Agent Note: Remove the separate CLI demo
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-08-remove-cli-demo.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
After [`dsh run`](../feature/2026-08-08-dsh-run-headless-command.md) became the product one-shot command, `@deepseek-ai/dsh-cli-demo` remained a second application package for the same job. It carried another executable, argument grammar, app composition, cancellation lifecycle, text/JSON/stream-JSON output contract, built artifact, documentation surface, and test suite. The two front doors also assembled different trees, so a successful demo did not prove the shipped `headless` profile and users had to choose between overlapping commands.
|
||||
|
||||
The replay suites still need canonical session events to pin assembled backend behavior. That testing need does not require a published command or compatibility contract.
|
||||
|
||||
## Decision
|
||||
|
||||
Delete `@deepseek-ai/dsh-cli-demo` completely: its package, bin, parser, app plugin, output formats, tests, workspace references, generated-catalog entries, and active documentation. No alias or compatibility package remains. The root `demo:headless` script is retained only as a direct alias of `dsh run`; the product command owns final-text stdout, the observation URL on stderr, persistence, exit status, and shutdown.
|
||||
|
||||
`examples/headless-agent` becomes an explicit test composition. Its Loader configs mount `@deepseek-ai/dsh-agent-spine-demo`, one root agent, JSONL persistence, and checkpoint policy as separate rows instead of hiding them behind an app bundle. An unexported example-owned TypeScript fixture drives a task and emits canonical events as JSONL for replay snapshots. It is launched only by tests, has no package export or bin, and is not a supported product output format.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Keep `dsh-cli-demo` as an alias or wrapper around `dsh run`.** Rejected because a second bin and package would preserve two discoverable owners without adding capability.
|
||||
- **Move JSON and stream-JSON flags onto `dsh run`.** Rejected because no current product consumer requires them; adopting the old demo protocol would enlarge the canonical CLI contract solely to save test machinery.
|
||||
- **Delete the canonical-event snapshots with the package.** Rejected because they pin model-visible assembled behavior that final-text product acceptance cannot observe.
|
||||
- **Keep the app plugin but delete only its bin.** Rejected because the hidden composition would still duplicate the explicit headless profile and conceal which services the test leaf mounts.
|
||||
|
||||
## Consequences
|
||||
|
||||
This is intentionally breaking. `dsh-cli-demo`, its `--output-format` choices, and imports from `@deepseek-ai/dsh-cli-demo/src/cli.ts` no longer resolve. There is no public event-stream replacement in this change; callers use `dsh run` for one-shot execution and must choose an existing protocol surface when they need structured automation.
|
||||
|
||||
The repository retains backend replay coverage through test-only infrastructure, while product smoke and built-bin acceptance exercise `dsh run`. A separate one-shot package may return only if it owns a genuinely independent, versioned protocol that cannot belong to the product launcher; a second spelling or output shim is not enough.
|
||||
|
||||
## Verification
|
||||
|
||||
Focused Loader smokes cover the explicit composition in source and plain-Node built modes, snapshot tests diff its canonical JSONL and persisted logs, product acceptance covers `dsh run`, and documentation plus generated graph/catalog gates reject live references to the removed package. The frozen Agent Note archive remains historical evidence and is not rewritten.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Agent Note: 移除独立的 CLI demo
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-08-remove-cli-demo.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
在 [`dsh run`](../feature/2026-08-08-dsh-run-headless-command.md) 成为产品的一次性命令后,`@deepseek-ai/dsh-cli-demo` 仍是承担同一工作的第二个应用包。它另行拥有一套可执行文件、参数语法、应用组装、取消生命周期、文本/JSON/stream-JSON 输出契约、构建产物、配套文档和测试套件。两个入口组装的树也不相同,因此 demo 成功不能证明已交付的 `headless` profile 可用,用户还必须在功能重叠的命令之间作出选择。
|
||||
|
||||
回放套件仍需要规范会话事件来固定组装后的后端行为。这一测试需求不需要已发布命令或兼容性契约。
|
||||
|
||||
## 决策
|
||||
|
||||
彻底删除 `@deepseek-ai/dsh-cli-demo`:包括它的包、bin、解析器、应用插件、输出格式、测试、workspace 引用、生成目录条目和现行文档。不保留别名或兼容包。根目录的 `demo:headless` 脚本仅作为 `dsh run` 的直接别名保留;stdout 上的最终文本、stderr 上的观察 URL、持久化、退出状态和关闭行为均由产品命令负责。
|
||||
|
||||
`examples/headless-agent` 成为显式测试组装。其 Loader 配置把 `@deepseek-ai/dsh-agent-spine-demo`、一个根 agent(智能体)、JSONL 持久化和检查点策略挂载为独立配置行,不再将其隐藏在应用组合包之后。一个由示例自有且未导出的 TypeScript fixture(测试前置数据)会驱动任务,并以 JSONL 发出供回放快照使用的规范事件。该 fixture 只由测试启动,没有包导出或 bin,也不是受支持的产品输出格式。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
- **保留 `dsh-cli-demo` 作为 `dsh run` 的别名或包装层。** 不予采纳:第二个 bin 和包会让同一功能继续存在两个可发现的归属方,却没有增加任何能力。
|
||||
- **把 JSON 和 stream-JSON 标志移到 `dsh run`。** 不予采纳:当前没有产品消费方需要这些标志;沿用旧 demo 协议,只会为了保留测试机制而扩大规范 CLI(命令行界面)契约。
|
||||
- **随包一并删除规范事件快照。** 不予采纳:这些快照固定了模型可见的组装行为,而只检查最终文本的产品验收无法观察这些行为。
|
||||
- **保留应用插件,只删除它的 bin。** 不予采纳:隐藏的组装仍会重复显式的 headless profile,并掩盖测试叶节点挂载了哪些服务。
|
||||
|
||||
## 后果
|
||||
|
||||
这是有意为之的破坏性变更。`dsh-cli-demo`、它的 `--output-format` 选项以及对 `@deepseek-ai/dsh-cli-demo/src/cli.ts` 的导入都不再可解析。本变更不提供公开的事件流替代接口;调用方使用 `dsh run` 执行一次性任务,需要结构化自动化时则必须选择现有的协议接口。
|
||||
|
||||
仓库通过仅供测试的基础设施保留后端回放覆盖,产品冒烟测试和 built-bin 验收则运行 `dsh run`。只有当独立的一次性包负责一套真正独立、带版本且不能归产品启动器所有的协议时,它才可以重新引入;第二种命令写法或输出 shim 并不足以构成理由。
|
||||
|
||||
## 验证
|
||||
|
||||
聚焦的 Loader 冒烟测试在源码模式和由普通 Node 启动的构建模式下覆盖显式组装,快照测试对比其规范 JSONL 和持久化日志,产品验收覆盖 `dsh run`,文档检查及生成图谱/目录门禁则拒绝对已移除包的活跃引用。冻结的 Agent Note 归档保留为历史证据,不会被重写。
|
||||
@@ -21,7 +21,6 @@ flowchart LR
|
||||
pkg_session["session"]
|
||||
svc_sessions["ctx.sessions<br/>In-memory session store"]
|
||||
pkg_agent["agent"]
|
||||
pkg_cli_demo["cli-demo"]
|
||||
pkg_session_persistence["session-persistence"]
|
||||
pkg_session_query["session-query"]
|
||||
pkg_session_query_sqlite["session-query-sqlite"]
|
||||
@@ -260,7 +259,6 @@ flowchart LR
|
||||
svc_agentLoop --> pkg_agent_spine_demo
|
||||
svc_agents --> pkg_acp
|
||||
svc_agents --> pkg_agent_loop
|
||||
svc_agents --> pkg_cli_demo
|
||||
svc_agents --> pkg_subagent_inprocess
|
||||
svc_approval --> pkg_tool_bash
|
||||
svc_approval --> pkg_tools
|
||||
@@ -307,7 +305,6 @@ flowchart LR
|
||||
svc_sessionQuery --> pkg_tool_session_query
|
||||
svc_sessions --> pkg_agent
|
||||
svc_sessions --> pkg_agent_loop
|
||||
svc_sessions --> pkg_cli_demo
|
||||
svc_sessions --> pkg_invariants
|
||||
svc_sessions --> pkg_session_persistence
|
||||
svc_sessions --> pkg_session_query
|
||||
@@ -365,7 +362,7 @@ flowchart LR
|
||||
| `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-replay`](../packages/support/llm-replay) | [`agent-loop`](../packages/core/agent-loop), [`compact-basic`](../packages/compact/compact-basic) | - | Adapters register provider implementations; the loop and compaction call the provider-neutral stream service. |
|
||||
| `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Owns isolated per-session replay folds; pressure consumers share immutable revisioned measurements. |
|
||||
| `ctx.toolResultPrune` | `core` | [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction. |
|
||||
| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`cli-demo`](../packages/examples/cli-demo), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`invariants`](../packages/support/invariants) | - | Owns append-only Session instances and emits the durable session event feed. |
|
||||
| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`invariants`](../packages/support/invariants) | - | Owns append-only Session instances and emits the durable session event feed. |
|
||||
| `ctx.invariants` | `core` | [`invariants`](../packages/support/invariants) | - | [`session`](../packages/core/session), [`agent`](../packages/core/agent), [`scope`](../packages/core/scope), [`agent-loop`](../packages/core/agent-loop) | - | Companion subpaths register owner-local checks; the service owns selection, uniqueness, child fibers, and package-attributed failures. |
|
||||
| `ctx.typert` | `core` | [`typert-registry`](../packages/typert/registry) | - | [`typert-loader`](../packages/typert/loader), [`api-gateway`](../packages/api/gateway) | - | Plugins register live zod contributions directly or through dsh-typert-loader; the API gateway consumes invocation descriptors and providers, while other runtime consumers query schemas and reflection metadata at their own edges. |
|
||||
| `ctx.typertGateway` | `core` | [`api-gateway`](../packages/api/gateway) | - | - | - | Associates generated Remote descriptors with live Cordis services, resolves registered identities, and exposes unary calls through the shared Connection RPC carrier. |
|
||||
@@ -387,7 +384,7 @@ flowchart LR
|
||||
| `ctx.sessionProjections` | `core` | [`session-projection`](../packages/session-projection/session-projection) | - | [`tool-todo`](../packages/todo/tool-todo), [`session-title`](../packages/session-title/session-title), [`host-apiproxy`](../packages/host/apiproxy) | - | Domains register state-driven fold units; the eager drive keeps per-session watermark states and api-proxy serves baselines and pushes changed values. |
|
||||
| `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session-projection/session-projection-cache) | - | [`host-apiproxy`](../packages/host/apiproxy) | - | Durably checkpoints projection unit states per session (throttled + turn/end/detach mandatory points) and serves the cold-read ladder: cache row + persistence tail replay, so listings never load full logs. |
|
||||
| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`skill-local`](../packages/skill/skill-local) | [`tool-skill`](../packages/skill/tool-skill) | - | Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies. |
|
||||
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`cli-demo`](../packages/examples/cli-demo), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. |
|
||||
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. |
|
||||
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. |
|
||||
| `ctx.goals` | `core` | [`goal`](../packages/goal/goal) | - | - | - | Folds revisioned objective state from the session log and keeps live continuation activation process-local. |
|
||||
| `ctx.subprocess` | `seam` | [`subprocess`](../packages/subprocess/subprocess) | [`subprocess-local`](../packages/subprocess/subprocess-local) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox), [`lsp-local`](../packages/lsp/lsp-local), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | - | The bash executors, the LSP host, and the out-of-process ACP, Codex, and Claude Code subagent backends spawn their children through ctx.subprocess; the service owns tree lifetime, stdio dispositions (pipes, inherit, bounded spill-backed collection), and kill escalation. |
|
||||
|
||||
@@ -249,46 +249,6 @@ Depends on: [`LocalConfig`](#deepseek-aidsh-bash-local)
|
||||
|
||||
Source: [`packages/bash/bash-sandbox/src/index.ts:35`](../packages/bash/bash-sandbox/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-cli-demo`
|
||||
|
||||
```ts config-catalog
|
||||
/** App config forwarded to the spine, configured agent, and JSONL backend. */
|
||||
export interface Config {
|
||||
/** Provider route for the configured agent. */
|
||||
provider: string
|
||||
/** Model name for the configured agent; a matching adapter must be registered. */
|
||||
model: string
|
||||
/** Bundled agent-loop concurrency cap; `1` is serial and omission uses its default. */
|
||||
maxParallelToolCalls?: number
|
||||
/** Deployment persona forwarded to the system-prompt plugin. */
|
||||
persona?: string
|
||||
/** Explicit model-facing tool order forwarded to the system-prompt plugin. */
|
||||
toolOrder?: string[]
|
||||
/** Tool-registry presentation config forwarded through agent-spine-demo. */
|
||||
tools?: ToolsConfig
|
||||
/** DeepSeek Harness home directory exposed to bash and used for local skill discovery. */
|
||||
dshHome?: string
|
||||
/** Fallback session-title limits forwarded through agent-spine-demo. */
|
||||
sessionTitle?: NonNullable<agentCore.Config['sessionTitle']>
|
||||
/** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
|
||||
persistenceRoot?: string
|
||||
/** JSONL artifact encoding; defaults to checksummed Zstandard frames. */
|
||||
persistenceCompression?: JsonlCompression
|
||||
/** Skill registry, local-provider, and model-facing consumer config. */
|
||||
skills?: agentCore.SkillConfig
|
||||
/** Model-facing bash tool config forwarded through agent-spine-demo. */
|
||||
toolBash?: NonNullable<agentCore.Config['toolBash']>
|
||||
/** Generic background-task control-tool config forwarded through agent-spine-demo. */
|
||||
toolTasks?: NonNullable<agentCore.Config['toolTasks']>
|
||||
/** Controls automatic AGENTS.md/CLAUDE.md loading; configure a byte budget or set `false`. */
|
||||
workspaceContext: agentCore.Config['workspaceContext']
|
||||
}
|
||||
```
|
||||
|
||||
Depends on: [`agentCore`](../packages/examples/agent-spine-demo/src/index.ts) · [`JsonlCompression`](../packages/session-persistence/session-persistence-jsonl/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools)
|
||||
|
||||
Source: [`packages/examples/cli-demo/src/index.ts:26`](../packages/examples/cli-demo/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-client-connection`
|
||||
|
||||
Requires: `httpServer`
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# 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 docs/cookbook/extension-cookbook.md
|
||||
extension-cookbook.md: 26f20b8f6cb57103b8e7340fcd21089ff1b0e5f6
|
||||
extension-cookbook.zh.md: b8bafdb73e91b56dd411cc5e96c21ccf2c5db123
|
||||
extension-cookbook.md: e04f6ffcf9a32ba3ee7344db18cad113b347c667
|
||||
extension-cookbook.zh.md: c2fa2f83c39ca0eb93397f39c88265c028224c7a
|
||||
@@ -90,7 +90,7 @@ export function apply(ctx: Context) {
|
||||
|
||||
## Runnable wirings
|
||||
|
||||
Runnable leaves load their plugin trees from `examples/*/cordis.yml`; the root `demo:*` scripts and those leaf directories are the authoritative inventory. Non-interactive leaves use [`@deepseek-ai/dsh-cli-demo`](../../packages/examples/cli-demo), ACP leaves use [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo), JSON-RPC leaves use [`@deepseek-ai/dsh-jsonrpc-demo`](../../packages/examples/jsonrpc-demo), and the app packages share [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo).
|
||||
Runnable leaves load their plugin trees from `examples/*/cordis.yml`; the root `demo:*` scripts and those leaf directories are the authoritative inventory. The product `dsh` launcher owns Web and one-shot headless execution, ACP leaves use [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo), and JSON-RPC leaves use [`@deepseek-ai/dsh-jsonrpc-demo`](../../packages/examples/jsonrpc-demo). The headless snapshot leaf mounts [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo) and JSONL persistence explicitly, then drives them through an example-owned test fixture rather than a shipped app package.
|
||||
|
||||
## The feature → mechanism map
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ export function apply(ctx: Context) {
|
||||
|
||||
## 可运行的组装示例
|
||||
|
||||
可运行叶子从 `examples/*/cordis.yml` 加载各自的插件树;根目录的 `demo:*` 脚本和这些叶子目录是权威清单。非交互式叶子使用 [`@deepseek-ai/dsh-cli-demo`](../../packages/examples/cli-demo),ACP 叶子使用 [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo),JSON-RPC 叶子使用 [`@deepseek-ai/dsh-jsonrpc-demo`](../../packages/examples/jsonrpc-demo),应用包共享 [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo)。
|
||||
可运行叶子从 `examples/*/cordis.yml` 加载各自的插件树;根目录的 `demo:*` 脚本和这些叶子目录是权威清单。产品 `dsh` 启动器负责 Web 和一次性 headless 执行,ACP 叶子使用 [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo),JSON-RPC 叶子使用 [`@deepseek-ai/dsh-jsonrpc-demo`](../../packages/examples/jsonrpc-demo)。headless 快照叶节点显式挂载 [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo) 和 JSONL 持久化,再通过示例自有的测试 fixture(测试前置数据)驱动这些组件,而不是通过已交付的 app 包。
|
||||
|
||||
## 功能→机制映射
|
||||
|
||||
@@ -122,7 +122,7 @@ export function apply(ctx: Context) {
|
||||
| Skill(技能) | section + 工具注册;调用时通过 `inject()` 注入 skill 内容 |
|
||||
| 记忆 | section provider + 工具 |
|
||||
| 定时任务(cron) | 插件注册面向模型的调度工具;定时器触发 → 空闲时 `followup(…, {source: {kind: 'cron', …}})`/忙碌时 `inject()` 通知 |
|
||||
| UI(GUI;CLI 输出 JSONL) | 监听 `session/event`(助手分片、边界、工具活动);输入 → `followup()` |
|
||||
| UI(GUI;CLI(命令行界面)输出 JSONL) | 监听 `session/event`(助手分片、边界、工具活动);输入 → `followup()` |
|
||||
| 遥测 / 可回放 trace | `session/event` → JSONL;回放 = `sessions.create(id, { seed })` |
|
||||
| 模型适配器 | 通过 `registerAdapter` 注册 `LlmAdapter` 子类(`dsh-llm-deepseek`、`dsh-llm-pi-ai`) |
|
||||
| 插件热重载 | 每个注册都是一个 `ctx.effect` → vendor 的 HMR(热模块替换)直接生效 |
|
||||
@@ -32,7 +32,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:62`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/support/llm-replay), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`session-title`](../packages/session-title/session-title) |
|
||||
| `session/created` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`jsonrpc`](../packages/ui/jsonrpc), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/ui/permission), [`plan-mode`](../packages/plan/plan-mode), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-telemetry`](../packages/telemetry/session-telemetry), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) |
|
||||
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session-persistence/session-persistence), [`session-projection-cache`](../packages/session-projection/session-projection-cache), [`session-telemetry`](../packages/telemetry/session-telemetry), [`session-title`](../packages/session-title/session-title) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:96`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`cli-demo`](../packages/examples/cli-demo), [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`jsonrpc`](../packages/ui/jsonrpc), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-projection`](../packages/session-projection/session-projection), [`session-projection-cache`](../packages/session-projection/session-projection-cache), [`session-telemetry`](../packages/telemetry/session-telemetry), [`session-telemetry-otel`](../packages/telemetry/session-telemetry-otel), [`session-title`](../packages/session-title/session-title), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:96`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`jsonrpc`](../packages/ui/jsonrpc), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-projection`](../packages/session-projection/session-projection), [`session-projection-cache`](../packages/session-projection/session-projection-cache), [`session-telemetry`](../packages/telemetry/session-telemetry), [`session-telemetry-otel`](../packages/telemetry/session-telemetry-otel), [`session-title`](../packages/session-title/session-title), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) |
|
||||
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:105`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session-persistence/session-persistence), [`session-telemetry`](../packages/telemetry/session-telemetry) |
|
||||
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/index.ts:170`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
|
||||
| `settings/updated` | `emit` | [`packages/settings/settings/src/index.ts:157`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
|
||||
|
||||
@@ -205,7 +205,6 @@ flowchart TD
|
||||
subgraph group_examples["packages/examples"]
|
||||
pkg_acp_demo["acp-demo"]
|
||||
pkg_agent_spine_demo["agent-spine-demo"]
|
||||
pkg_cli_demo["cli-demo"]
|
||||
pkg_jsonrpc_demo["jsonrpc-demo"]
|
||||
end
|
||||
subgraph group_feedback["packages/feedback"]
|
||||
@@ -1145,16 +1144,6 @@ flowchart TD
|
||||
pkg_acp_demo --> pkg_session_query_sqlite
|
||||
pkg_acp_demo --> pkg_tools
|
||||
pkg_acp_demo --> pkg_workspace_context
|
||||
pkg_cli_demo --> pkg_agent
|
||||
pkg_cli_demo --> pkg_agent_spine_demo
|
||||
pkg_cli_demo --> pkg_app_boot
|
||||
pkg_cli_demo --> pkg_invariants
|
||||
pkg_cli_demo --> pkg_llm
|
||||
pkg_cli_demo --> pkg_session
|
||||
pkg_cli_demo --> pkg_session_checkpoint_policy
|
||||
pkg_cli_demo --> pkg_session_persistence_jsonl
|
||||
pkg_cli_demo --> pkg_tools
|
||||
pkg_cli_demo --> pkg_workspace_context
|
||||
```
|
||||
|
||||
| Package | Group | Depends on |
|
||||
@@ -1350,4 +1339,3 @@ flowchart TD
|
||||
| [`sdk-client`](../packages/sdk/sdk-client) | `sdk` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/sdk-protocol), [`session`](../packages/core/session) |
|
||||
| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/sdk-client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
|
||||
| [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/acp/acp), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/ui/app-boot), [`invariants`](../packages/support/invariants), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) |
|
||||
| [`cli-demo`](../packages/examples/cli-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/ui/app-boot), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) |
|
||||
@@ -2,5 +2,5 @@
|
||||
# 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 docs/testing.md
|
||||
testing.md: 514ca4e1df7505b02350470d5de4a5ee3647634b
|
||||
testing.zh.md: 6d24d8e74d726d53fa3500f57e34238483c5481c
|
||||
testing.md: fecf02fe887aac5c92c739523f10a0562eba6242
|
||||
testing.zh.md: a34f56fac72baa9916d5f96e83048ad54cf23d4a
|
||||
+2
-2
@@ -9,7 +9,7 @@ How this repo tests, tier by tier, and the rules that keep a green suite meaning
|
||||
- **Unit** (`pnpm run test`): vitest over package and example specs under their `tests/**` directories plus repository script specs under `scripts/**/*.spec.ts`; tests stay with the code area they exercise. Every registry gets an HMR-safety test (dispose the contributing fiber, assert cleanup). Prefer edge cases, error paths, event ordering, concurrency races, and permanent contract regressions (see `packages/core/agent-loop/tests/contract-regressions.spec.ts`).
|
||||
- **Coverage gate** (`pnpm run test:coverage`): the gating run, per-file 100% on `packages/*/*/src`. An uncovered line is often dead code the gate is correctly flagging for deletion, not a missing test to bolt on. Line coverage is necessary, never sufficient — it proves lines ran, not that the feature works as shipped. Per-file 100% on `packages/bash/pwsh-local/src` needs a real `pwsh`: without one its executor suites self-skip and `vitest.config.ts` exempts the file so pwsh-less hosts stay green, while CI runners ship pwsh and enforce the full bar.
|
||||
- **Real-API e2e** (`pnpm run test:e2e`): with-key tests against live provider APIs — the DeepSeek model plus provider-specific smokes that gate on their own keys (`EXA_API_KEY`, `PERPLEXITY_API_KEY`, …); each suite self-skips without its key so keyless CI stays green ([real-API e2e Agent Note](../.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md)).
|
||||
- **Snapshot** (`pnpm run test:snapshot`): keyless expected outputs cover external behavior — transport contracts and presentation, while persisted logs pin assembled backend behavior. ACP boots the real automation-server example, replays a recorded session, and diffs normalized JSON-RPC plus the re-persisted log ([ACP snapshot Agent Note](../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md)); headless pins `stream-json` through its real one-shot process. Use `pnpm run test:snapshot:record` when a model transcript changes and `pnpm run test:snapshot:refresh` when replay input remains valid; review every JSONL and expected-output diff. One ACP scenario (`text-turn`) pins full system-prompt/tool-schema content; other fixtures tokenize it so an edit churns one line ([pinned-header Agent Note](../.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)).
|
||||
- **Snapshot** (`pnpm run test:snapshot`): keyless expected outputs cover external behavior — transport contracts and presentation, while persisted logs pin assembled backend behavior. ACP boots the real automation-server example, replays a recorded session, and diffs normalized JSON-RPC plus the re-persisted log ([ACP snapshot Agent Note](../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md)); headless backend scenarios boot their explicit example composition through an unexported JSONL test driver, while `apps/cli` separately owns product `dsh run` acceptance. Use `pnpm run test:snapshot:record` when a model transcript changes and `pnpm run test:snapshot:refresh` when replay input remains valid; review every JSONL and expected-output diff. One ACP scenario (`text-turn`) pins full system-prompt/tool-schema content; other fixtures tokenize it so an edit churns one line ([pinned-header Agent Note](../.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)).
|
||||
- **Web browser snapshot** (`pnpm run test:web`; required Linux PR gate): Chromium compares replayed browser output with `apps/web/tests/snapshots/`. CI forces read-only `DSH_SNAPSHOT=replay`, never writing expected outputs; record/refresh stay local and every diff is reviewed ([web e2e lane](../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md), [CI gate decision](../.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.md)). `test:web` [builds first](../.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.md) for plugin CSS.
|
||||
|
||||
Committed session-format JSONL uses the canonical packed-row layout, and the keyless snapshot gate discovers every such fixture by its `session` header. In-flight branches carrying older fixture edits merge current `master` and run the [temporary migrator](../scripts/migrate-packed-session-fixtures.ts) through `pnpm run migrate:packed-session-fixtures`; the [removal proposal](../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.md) retires that command and these links after all affected branches converge.
|
||||
@@ -46,4 +46,4 @@ An e2e assertion re-runs the command or re-reads the file externally; a keyword
|
||||
|
||||
## When a snapshot test is required
|
||||
|
||||
Every non-trivial model-, protocol-, or human-visible change adds or updates a keyless scenario in the same PR through a runnable example's owning snapshot suite. Package tests, e2e assertions, mock/test-only compositions, and PR rationale do not replace the assembled transcript; extend the harness when needed. ACP automation scenarios use `examples/<name>/tests/snapshots/`, a scenario table over the [`dsh-acp-snapshot`](../packages/support/acp-snapshot/README.md) suite factory (`examples/acp-agent` is primary); `examples/headless-agent` owns the `stream-json` snapshot and replay fixtures. The `pwsh-tool-turn` ACP scenario boots real `pwsh` and skips where it is absent. Completed interactive-terminal journeys use JSONL-driven scenarios under `apps/cli/tests/snapshots/`; transient presentation uses the package-local semantic matrix, with a PTY case when input, Loader selection, or terminal teardown changes. Browser-rendered web GUI journeys use `apps/web/tests/snapshots/`. New capability seams, lifecycle shapes, or transcript surfaces name every coverage tier at plan time and verify the harness can express it before implementation.
|
||||
Every non-trivial model-, protocol-, or human-visible change adds or updates a keyless scenario in the same PR through a runnable example's owning snapshot suite. Package tests, e2e assertions, mock/test-only compositions, and PR rationale do not replace the assembled transcript; extend the harness when needed. ACP automation scenarios use `examples/<name>/tests/snapshots/`, a scenario table over the [`dsh-acp-snapshot`](../packages/support/acp-snapshot/README.md) suite factory (`examples/acp-agent` is primary); `examples/headless-agent` owns the internal canonical-event JSONL snapshots and replay fixtures. The `pwsh-tool-turn` ACP scenario boots real `pwsh` and skips where it is absent. Completed interactive-terminal journeys use JSONL-driven scenarios under `apps/cli/tests/snapshots/`; transient presentation uses the package-local semantic matrix, with a PTY case when input, Loader selection, or terminal teardown changes. Browser-rendered web GUI journeys use `apps/web/tests/snapshots/`. New capability seams, lifecycle shapes, or transcript surfaces name every coverage tier at plan time and verify the harness can express it before implementation.
|
||||
+2
-2
@@ -9,7 +9,7 @@
|
||||
- **单元测试**(`pnpm run test`):vitest 运行包(package)和示例各自的 `tests/**` 目录下的测试,以及匹配 `scripts/**/*.spec.ts` 的仓库脚本测试;测试文件与其所覆盖的代码区域放在一起。每个注册表都有一个 HMR(热模块替换)安全测试(dispose(资源释放)贡献的 fiber,断言清理完成)。优先覆盖边界情况、错误路径、事件顺序、并发竞态,以及永久性契约回归(见 `packages/core/agent-loop/tests/contract-regressions.spec.ts`)。
|
||||
- **覆盖率门禁**(`pnpm run test:coverage`):门禁级运行,对 `packages/*/*/src` 按文件 100% 覆盖。未覆盖的行往往是门禁正确标记出的死代码(应删除),而非需要补写的测试。行覆盖率是必要条件,但永远不是充分条件:它证明行被执行过,不证明功能按交付预期工作。`packages/bash/pwsh-local/src` 的按文件 100% 覆盖需要真实的 `pwsh`:缺少它时其 executor 套件会自动跳过,`vitest.config.ts` 会豁免该文件以使无 pwsh 的主机保持绿色,而 CI runner 自带 pwsh,仍按完整标准执行门禁。
|
||||
- **真实 API e2e**(`pnpm run test:e2e`):带密钥测试调用真实提供方 API,包括 DeepSeek 模型以及各提供方特有的冒烟测试;这些测试各自由自己的密钥控制(`EXA_API_KEY`、`PERPLEXITY_API_KEY` 等),缺少密钥时套件会自动跳过,使 keyless CI 保持绿色([真实 API e2e Agent Note](../.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md))。
|
||||
- **快照**(`pnpm run test:snapshot`):无密钥预期输出覆盖对外行为(传输契约与呈现),持久化日志则固定组装后的后端行为。ACP 启动真实的自动化服务器示例、回放录制会话,并对归一化 JSON-RPC 与重新持久化的日志执行 diff([ACP 快照 Agent Note](../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md));headless 通过真实单次运行进程固定 `stream-json`。当模型 transcript(文本记录)发生变化时使用 `pnpm run test:snapshot:record`,回放输入仍然有效时使用 `pnpm run test:snapshot:refresh`;请审查每一处 JSONL 与预期输出差异。一个 ACP 场景(`text-turn`)固定完整的系统提示词与工具 schema 内容;其他 fixture(测试前置数据)将其 token 化,因此修改只会扰动一行([pinned-header Agent Note](../.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md))。
|
||||
- **快照**(`pnpm run test:snapshot`):无密钥预期输出覆盖对外行为(传输契约与呈现),持久化日志则固定组装后的后端行为。ACP 启动真实的自动化服务器示例、回放录制会话,并对归一化 JSON-RPC 与重新持久化的日志执行 diff([ACP 快照 Agent Note](../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md));headless 后端场景通过未导出的 JSONL 测试 driver 启动各自显式的示例组装,而 `apps/cli` 则单独负责产品 CLI(命令行界面)`dsh run` 的验收。当模型 transcript(文本记录)发生变化时使用 `pnpm run test:snapshot:record`,回放输入仍然有效时使用 `pnpm run test:snapshot:refresh`;请审查每一处 JSONL 与预期输出差异。一个 ACP 场景(`text-turn`)固定完整的系统提示词与工具 schema 内容;其他 fixture(测试前置数据)将其 token 化,因此修改只会扰动一行([pinned-header Agent Note](../.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md))。
|
||||
- **Web 浏览器快照**(`pnpm run test:web`;必需的 Linux PR(Pull Request)门禁):Chromium 将回放后的浏览器输出与 `apps/web/tests/snapshots/` 比较。CI 强制只读的 `DSH_SNAPSHOT=replay`,绝不写入预期输出;record/refresh 留在本地,每处 diff 都须评审([web e2e 车道](../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md)、[CI 门禁决策](../.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.md))。`test:web` 会[先构建](../.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.md)以交付插件 CSS。
|
||||
|
||||
签入仓库的会话格式 JSONL 使用规范打包行布局,无密钥快照门禁会通过 `session` header 发现每一份此类 fixture。仍携带旧版 fixture 改动的在途分支应合并当前 `master`,并通过 `pnpm run migrate:packed-session-fixtures` 运行[临时迁移器](../scripts/migrate-packed-session-fixtures.ts);待所有受影响分支收敛后,[移除提案](../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.md)会移除该命令及这些链接。
|
||||
@@ -46,4 +46,4 @@ e2e 断言应重新运行命令或从外部重新读取文件;对 agent 自身
|
||||
|
||||
## 何时需要快照测试
|
||||
|
||||
每项非平凡的模型可见、协议可见或人类可见变更,都必须在同一 PR 中,通过可运行示例所属的快照套件添加或更新无密钥场景。包测试、e2e 断言、mock 与仅测试组合、PR 理由都不能取代组装后的 transcript;必要时应扩展 harness。ACP 自动化场景使用 `examples/<name>/tests/snapshots/`,即基于 [`dsh-acp-snapshot`](../packages/support/acp-snapshot/README.md) 套件工厂的场景表(`examples/acp-agent` 为主套件);`examples/headless-agent` 拥有 `stream-json` 快照与回放 fixture。`pwsh-tool-turn` ACP 场景启动真实 `pwsh`,在无 `pwsh` 的主机上跳过。已完成的交互式终端旅程使用 `apps/cli/tests/snapshots/` 下由 JSONL 驱动的场景;瞬态呈现使用包内语义矩阵,输入、Loader 选择或终端清理发生变化时还要添加 PTY 用例。新的能力 seam、生命周期形态或 transcript 呈现接口在计划阶段就要列出每个覆盖层级,并在实现前验证 harness 能够表达它们。
|
||||
每项非平凡的模型可见、协议可见或人类可见变更,都必须在同一 PR 中,通过可运行示例所属的快照套件添加或更新无密钥场景。包测试、e2e 断言、mock 与仅测试组合、PR 理由都不能取代组装后的 transcript;必要时应扩展 harness。ACP 自动化场景使用 `examples/<name>/tests/snapshots/`,即基于 [`dsh-acp-snapshot`](../packages/support/acp-snapshot/README.md) 套件工厂的场景表(`examples/acp-agent` 为主套件);`examples/headless-agent` 拥有内部规范事件 JSONL 快照与回放 fixture。`pwsh-tool-turn` ACP 场景启动真实 `pwsh`,在无 `pwsh` 的主机上跳过。已完成的交互式终端旅程使用 `apps/cli/tests/snapshots/` 下由 JSONL 驱动的场景;瞬态呈现使用包内语义矩阵,输入、Loader 选择或终端清理发生变化时还要添加 PTY 用例。新的能力 seam、生命周期形态或 transcript 呈现接口在计划阶段就要列出每个覆盖层级,并在实现前验证 harness 能够表达它们。
|
||||
@@ -2,5 +2,5 @@
|
||||
# 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 docs/user/guide/index.md
|
||||
index.md: ede09506a996193fe5cf4ae6cd9b64d3529798a6
|
||||
index.zh.md: 5f72a6d3099d2d4721eccebae92eacfe72d33bce
|
||||
index.md: a04698e29755d4a08b012f8b61accb79c470dcb0
|
||||
index.zh.md: 85990f49c7b3cdf7518237c6b558375b7a636243
|
||||
@@ -12,12 +12,14 @@ Harness implements every capability an AI agent needs—including LLM calls, too
|
||||
# Select the LLM backend
|
||||
- name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
|
||||
# Select the one-shot application
|
||||
- id: cli-agent
|
||||
name: '@deepseek-ai/dsh-cli-demo'
|
||||
# Compose one configured agent
|
||||
- id: agent-spine
|
||||
name: '@deepseek-ai/dsh-agent-spine-demo'
|
||||
config:
|
||||
provider: deepseek-official
|
||||
model: deepseek-v4-flash
|
||||
agents:
|
||||
- id: main
|
||||
provider: deepseek-official
|
||||
model: deepseek-v4-flash
|
||||
workspaceContext: false
|
||||
```
|
||||
|
||||
|
||||
@@ -12,12 +12,14 @@ Harness 将一个 AI Agent(智能体) 所需要的所有能力——LLM 调
|
||||
# Select the LLM backend
|
||||
- name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
|
||||
# Select the one-shot application
|
||||
- id: cli-agent
|
||||
name: '@deepseek-ai/dsh-cli-demo'
|
||||
# Compose one configured agent
|
||||
- id: agent-spine
|
||||
name: '@deepseek-ai/dsh-agent-spine-demo'
|
||||
config:
|
||||
provider: deepseek-official
|
||||
model: deepseek-v4-flash
|
||||
agents:
|
||||
- id: main
|
||||
provider: deepseek-official
|
||||
model: deepseek-v4-flash
|
||||
workspaceContext: false
|
||||
```
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# 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 docs/user/guide/quickstart.md
|
||||
quickstart.md: 8b84017ad33bf02579891bc4dcf83eaf7ec39022
|
||||
quickstart.zh.md: dfb24f8fa194866908406c709d059bf1f6595d59
|
||||
quickstart.md: 2f86ce4cb7896ca75b7457186d4633066a899b83
|
||||
quickstart.zh.md: f7ddf88c74b5a32daa1d2b81feeaed192e7a3f9c
|
||||
@@ -35,10 +35,10 @@ DEEPSEEK_API_KEY=sk-your-key-here
|
||||
Run a non-interactive task and print its final answer:
|
||||
|
||||
```sh
|
||||
pnpm run demo:headless "summarize the architecture of this workspace"
|
||||
pnpm run dsh run "summarize the architecture of this workspace"
|
||||
```
|
||||
|
||||
Headless runs one complete model/tool turn, persists the session, prints the result, and exits. Use `--output-format stream-json` when you need the canonical event stream.
|
||||
`dsh run` creates and persists a fresh session, prints the final assistant answer, and exits. While it runs, stderr prints the local browser URL where the session can be observed.
|
||||
|
||||
## Step 3: use the Web UI
|
||||
|
||||
@@ -53,7 +53,7 @@ Open `http://127.0.0.1:3080`. The agent can read and write files, run commands,
|
||||
|
||||
## What happened
|
||||
|
||||
headless-agent uses the `@deepseek-ai/dsh-cli-demo` app. `dsh web` instead boots the `web` profile: the [`dsh-base`](../../../packages/bundle/base/cordis.patch.yml) and [`dsh-web-app`](../../../packages/bundle/web-app/cordis.patch.yml) bundle patch layers composed over an empty root. Both select the DeepSeek model and capability plugins appropriate to their entry mode.
|
||||
`dsh run` boots the `headless` profile: [`dsh-base`](../../../packages/bundle/base/cordis.patch.yml), [`dsh-web-app`](../../../packages/bundle/web-app/cordis.patch.yml), and [`dsh-headless`](../../../packages/bundle/headless/cordis.patch.yml) compose over an empty root. `dsh web` uses the first two layers without the one-shot runner. Both select the DeepSeek model and capability plugins appropriate to their entry mode.
|
||||
|
||||
## Next steps
|
||||
|
||||
|
||||
@@ -35,10 +35,10 @@ DEEPSEEK_API_KEY=sk-your-key-here
|
||||
运行一个非交互式任务并打印最终回答:
|
||||
|
||||
```sh
|
||||
pnpm run demo:headless "summarize the architecture of this workspace"
|
||||
pnpm run dsh run "summarize the architecture of this workspace"
|
||||
```
|
||||
|
||||
Headless 运行一个完整的模型/工具轮次,持久化会话,打印结果后退出。需要规范事件流时可使用 `--output-format stream-json`。
|
||||
`dsh run` 创建并持久化一个新会话,打印最终 assistant 回答,然后退出。运行期间,stderr 会打印可用于观察该会话的本地浏览器 URL。
|
||||
|
||||
## 第三步:使用 Web UI
|
||||
|
||||
@@ -53,7 +53,7 @@ pnpm run dsh web
|
||||
|
||||
## 回头看
|
||||
|
||||
headless-agent 使用 `@deepseek-ai/dsh-cli-demo` app。`dsh web` 则启动 `web` profile:由 [`dsh-base`](../../../packages/bundle/base/cordis.patch.yml) 与 [`dsh-web-app`](../../../packages/bundle/web-app/cordis.patch.yml) 两个组合包的 patch 层在空根之上组合而成。二者都会根据各自入口模式选择 DeepSeek 模型和能力插件。
|
||||
`dsh run` 启动 `headless` profile:[`dsh-base`](../../../packages/bundle/base/cordis.patch.yml)、[`dsh-web-app`](../../../packages/bundle/web-app/cordis.patch.yml) 和 [`dsh-headless`](../../../packages/bundle/headless/cordis.patch.yml) 在空根之上组合。`dsh web` 使用前两层,不包含一次性 runner。二者都会根据各自入口模式选择 DeepSeek 模型和能力插件。
|
||||
|
||||
## 下一步
|
||||
|
||||
|
||||
@@ -34,12 +34,22 @@
|
||||
# budget, so the local numeric default cannot apply here.
|
||||
maxDepth: 'provider-managed'
|
||||
|
||||
- id: cli-agent
|
||||
name: '@deepseek-ai/dsh-cli-demo'
|
||||
- id: agent-spine
|
||||
name: '@deepseek-ai/dsh-agent-spine-demo'
|
||||
config:
|
||||
provider: mock
|
||||
model: mock-delegate
|
||||
agents:
|
||||
- id: main
|
||||
provider: mock
|
||||
model: mock-delegate
|
||||
cwd: !!js process.cwd()
|
||||
persona: 'Test ACP subagent cwd inheritance.'
|
||||
persistenceRoot: './.sessions'
|
||||
persistenceCompression: 'none'
|
||||
workspaceContext: false
|
||||
|
||||
- id: persistence
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: './.sessions'
|
||||
compression: 'none'
|
||||
|
||||
- id: checkpoint-policy
|
||||
name: '@deepseek-ai/dsh-session-checkpoint-policy'
|
||||
@@ -2,14 +2,14 @@
|
||||
/** Test driver: one delegation turn through a headless Loader composition. */
|
||||
|
||||
import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
|
||||
import { runOneShot } from '@deepseek-ai/dsh-cli-demo/src/cli.ts'
|
||||
import { runFixtureTurn } from '../../../../../headless-agent/tests/fixtures/one-shot.ts'
|
||||
|
||||
const configPath = process.argv[2]
|
||||
if (configPath === undefined) throw new Error('acp-subagent cwd driver requires a config path')
|
||||
|
||||
const ctx = await boot('acp-subagent-cwd-e2e', resolveConfigPath(configPath, undefined))
|
||||
try {
|
||||
await runOneShot(ctx, { task: 'delegate' })
|
||||
await runFixtureTurn(ctx, { task: 'delegate' })
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
+15
-4
@@ -31,10 +31,21 @@
|
||||
enableRunInBackground: false
|
||||
maxDepth: 'provider-managed'
|
||||
|
||||
- id: cli-agent
|
||||
name: '@deepseek-ai/dsh-cli-demo'
|
||||
- id: agent-spine
|
||||
name: '@deepseek-ai/dsh-agent-spine-demo'
|
||||
config:
|
||||
provider: mock
|
||||
model: mock-delegate
|
||||
agents:
|
||||
- id: main
|
||||
provider: mock
|
||||
model: mock-delegate
|
||||
cwd: !!js process.cwd()
|
||||
persona: 'This composition test must not start a model turn.'
|
||||
workspaceContext: false
|
||||
|
||||
- id: persistence
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: './.sessions'
|
||||
|
||||
- id: checkpoint-policy
|
||||
name: '@deepseek-ai/dsh-session-checkpoint-policy'
|
||||
@@ -20,10 +20,21 @@
|
||||
enableRunInBackground: false
|
||||
maxDepth: 'provider-managed'
|
||||
|
||||
- id: cli-agent
|
||||
name: '@deepseek-ai/dsh-cli-demo'
|
||||
- id: agent-spine
|
||||
name: '@deepseek-ai/dsh-agent-spine-demo'
|
||||
config:
|
||||
provider: mock
|
||||
model: mock-delegate
|
||||
agents:
|
||||
- id: main
|
||||
provider: mock
|
||||
model: mock-delegate
|
||||
cwd: !!js process.cwd()
|
||||
persona: 'This composition test must not start a model turn.'
|
||||
workspaceContext: false
|
||||
|
||||
- id: persistence
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: './.sessions'
|
||||
|
||||
- id: checkpoint-policy
|
||||
name: '@deepseek-ai/dsh-session-checkpoint-policy'
|
||||
@@ -2,5 +2,5 @@
|
||||
# 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 examples/headless-agent/README.md
|
||||
README.md: 670a91ab402cf98585f2ece70787beb3b4aaf4dc
|
||||
README.zh.md: 6c8b3b5694403c5e09f2904f5c3ca18fe569163e
|
||||
README.md: e00f3d2d4fd21a860239f3d3a3e5eb2d7520f14a
|
||||
README.zh.md: 6cd845783b1c112ba73676474b176ec28a4d0b78
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Headless one-shot agent wiring: DeepSeek V4 + local bash and filesystem tools + subagent delegation + workflows and fresh-agent Ralph iteration + `todo_write` + JSONL persistence, with [`@deepseek-ai/dsh-cli-demo`](../../packages/examples/cli-demo) as the app front door.
|
||||
This directory owns the replay and real-model test composition for a headless coding agent: DeepSeek V4 + local bash and filesystem tools + subagent delegation + workflows and fresh-agent Ralph iteration + `todo_write` + JSONL persistence. It explicitly mounts the shared agent spine, one root agent, persistence, and checkpoint policy; it is not a second product front door.
|
||||
|
||||
## Run it
|
||||
|
||||
@@ -10,17 +10,13 @@ Headless one-shot agent wiring: DeepSeek V4 + local bash and filesystem tools +
|
||||
# repo root .env (gitignored) or exported env:
|
||||
# DEEPSEEK_API_KEY=sk-…
|
||||
# DEEPSEEK_BASE_URL=https://… # optional; defaults to the public API
|
||||
pnpm run demo:headless "fix the failing test in this workspace"
|
||||
pnpm run demo:headless --output-format json -- "summarize the implementation"
|
||||
pnpm run demo:headless --output-format stream-json -- "run the focused tests"
|
||||
pnpm run dsh run "fix the failing test in this workspace"
|
||||
```
|
||||
|
||||
Exactly one nonblank positional task is required; quote tasks containing spaces. There is no `-p` flag. `text` prints the last text-bearing assistant message, `json` prints one DSH-native result record, and `stream-json` emits the top-level session's canonical task-turn events before that record. Child sessions surface only through parent tool events and results.
|
||||
The product command is [`dsh run`](../../apps/cli/README.md): it accepts one nonblank task, creates and persists a fresh session, prints the final assistant text, and exits. The root `demo:headless` script is only an alias of that command.
|
||||
|
||||
Each invocation creates and persists a fresh session, runs all model and tool steps in one turn, flushes, disposes, and exits. This is non-interactive automation: there is no prompt, approval, resume, second turn, or stdin context. The configured tools can mutate the launch workspace, run commands, spawn child agents, and consume provider tokens.
|
||||
Snapshot suites run this directory's configuration through [`tests/fixtures/headless-driver.ts`](tests/fixtures/headless-driver.ts), an unexported test-only process that emits canonical session events as JSONL before its result record. That stream is test infrastructure, not a supported CLI output format. Child sessions surface only through parent tool events and results.
|
||||
|
||||
## Advanced configuration
|
||||
|
||||
[`advanced.cordis.yml`](advanced.cordis.yml) adds Code Mode and the Cordis tools to the shipped leaf.
|
||||
|
||||
The package-level [CLI contract](../../packages/examples/cli-demo/README.md) documents output records, exit status, cancellation, persistence, and model/token effects.
|
||||
[`advanced.cordis.yml`](advanced.cordis.yml) adds Code Mode and the Cordis tools to the test composition.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
无头单次 agent(智能体)接线:DeepSeek V4 + 本地 bash 与文件系统工具 + subagent 委托 + 工作流与全新 agent Ralph 迭代 + `todo_write` + JSONL 持久化,并以 [`@deepseek-ai/dsh-cli-demo`](../../packages/examples/cli-demo) 作为应用入口。
|
||||
本目录负责 headless coding agent(智能体)的回放和真实模型测试组装:DeepSeek V4 + 本地 bash 与文件系统工具 + subagent 委托 + 工作流与全新 agent Ralph 迭代 + `todo_write` + JSONL 持久化。本目录显式挂载共享 agent 主干、一个根 agent、持久化和检查点策略;它不是第二个产品入口。
|
||||
|
||||
## 运行
|
||||
|
||||
@@ -10,17 +10,13 @@
|
||||
# repo root .env (gitignored) or exported env:
|
||||
# DEEPSEEK_API_KEY=sk-…
|
||||
# DEEPSEEK_BASE_URL=https://… # optional; defaults to the public API
|
||||
pnpm run demo:headless "fix the failing test in this workspace"
|
||||
pnpm run demo:headless --output-format json -- "summarize the implementation"
|
||||
pnpm run demo:headless --output-format stream-json -- "run the focused tests"
|
||||
pnpm run dsh run "fix the failing test in this workspace"
|
||||
```
|
||||
|
||||
必须提供一个且仅一个非空的任务位置参数;含空格的任务需要加引号。没有 `-p` 标志。`text` 打印最后一条包含文本的 assistant 消息,`json` 打印一条 DSH 原生结果记录,`stream-json` 则在该记录之前发出顶层会话的规范任务轮次事件。子会话只通过父会话的工具事件和结果对外显示。
|
||||
产品命令是 [`dsh run`](../../apps/cli/README.md):它接受一项非空任务,创建并持久化新会话,打印最终 assistant 文本,然后退出。根目录的 `demo:headless` 脚本只是该命令的别名。
|
||||
|
||||
每次调用都会创建并持久化新会话,在一个轮次中运行所有模型和工具步骤,然后刷写持久化数据、执行 dispose(资源释放),再退出。这是非交互式自动化:没有提示符、批准、恢复、第二轮次或 stdin 上下文。已配置工具可以修改启动时所在的工作区、运行命令、spawn 子 agent,并消耗提供方 token。
|
||||
快照套件通过 [`tests/fixtures/headless-driver.ts`](tests/fixtures/headless-driver.ts) 运行本目录的配置。这个未导出且仅供测试使用的进程会在结果记录之前,以 JSONL 发出规范会话事件。该事件流属于测试基础设施,不是受支持的 CLI(命令行界面)输出格式。子会话只通过父会话的工具事件和结果对外显示。
|
||||
|
||||
## 高级配置
|
||||
|
||||
[`advanced.cordis.yml`](advanced.cordis.yml) 在已交付叶节点上添加 Code Mode 和 Cordis 工具。
|
||||
|
||||
这份包级 [CLI(命令行界面)契约](../../packages/examples/cli-demo/README.md) 说明输出记录、退出状态、取消、持久化以及模型/token 影响。
|
||||
[`advanced.cordis.yml`](advanced.cordis.yml) 在测试组装中添加 Code Mode 和 Cordis 工具。
|
||||
@@ -1,6 +1,6 @@
|
||||
# Replay counterpart to advanced.cordis.yml. It includes the base `cordis.yml`
|
||||
# directly — a config patch cannot target an entry behind a nested include — and
|
||||
# restates advanced.cordis.yml's overlay (the cli-agent config plus the
|
||||
# restates advanced.cordis.yml's overlay (the agent and persistence configs plus the
|
||||
# code-runtime and tool-cordis inserts) so the whole app config lives in one patch.
|
||||
# It re-pins `deepseek-v4-flash`: `cordis.yml` ships `deepseek-v4-pro`, but the
|
||||
# recorded corpus (request headers, provenance) was captured on flash, so replay
|
||||
@@ -15,15 +15,14 @@
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
disabled: true
|
||||
- id: cli-agent
|
||||
name: '@deepseek-ai/dsh-cli-demo'
|
||||
- id: agent-spine
|
||||
name: '@deepseek-ai/dsh-agent-spine-demo'
|
||||
config:
|
||||
provider: deepseek-official
|
||||
model: deepseek-v4-flash
|
||||
persistenceRoot: './.sessions'
|
||||
# Replay fixtures are raw JSONL; the whole-config patch must restate
|
||||
# the compression choice or the default zstd frames hide the logs.
|
||||
persistenceCompression: none
|
||||
agents:
|
||||
- id: main
|
||||
provider: deepseek-official
|
||||
model: deepseek-v4-flash
|
||||
cwd: !!js process.cwd()
|
||||
workspaceContext:
|
||||
maxBytes: 65536
|
||||
tools:
|
||||
@@ -32,6 +31,13 @@
|
||||
You are headless-agent, a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}.
|
||||
|
||||
Verify your work by running the code or tests. Keep answers brief and factual.
|
||||
- id: persistence
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: './.sessions'
|
||||
# Replay fixtures are raw JSONL; the whole-config patch must restate
|
||||
# the compression choice or the default zstd frames hide the logs.
|
||||
compression: none
|
||||
- insert:
|
||||
- id: code-runtime
|
||||
name: '@deepseek-ai/dsh-code-runtime-worker'
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
config:
|
||||
path: ./cordis.yml
|
||||
patches:
|
||||
- id: cli-agent
|
||||
name: '@deepseek-ai/dsh-cli-demo'
|
||||
- id: agent-spine
|
||||
name: '@deepseek-ai/dsh-agent-spine-demo'
|
||||
config:
|
||||
provider: deepseek-official
|
||||
model: deepseek-v4-pro
|
||||
persistenceRoot: './.sessions'
|
||||
persistenceCompression: !!js "process.env.DSH_SNAPSHOT === undefined ? 'zstd' : 'none'"
|
||||
agents:
|
||||
- id: main
|
||||
provider: deepseek-official
|
||||
model: deepseek-v4-pro
|
||||
cwd: !!js process.cwd()
|
||||
workspaceContext:
|
||||
maxBytes: 65536
|
||||
tools:
|
||||
@@ -19,6 +20,11 @@
|
||||
You are headless-agent, a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}.
|
||||
|
||||
Verify your work by running the code or tests. Keep answers brief and factual.
|
||||
- id: persistence
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: './.sessions'
|
||||
compression: !!js "process.env.DSH_SNAPSHOT === undefined ? 'zstd' : 'none'"
|
||||
- insert:
|
||||
- id: code-runtime
|
||||
name: '@deepseek-ai/dsh-code-runtime-worker'
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand.
|
||||
Run `pnpm run gen-doc-graphs` to regenerate. -->
|
||||
|
||||
# Headless Agent App Composition
|
||||
# Headless Agent Snapshot Composition
|
||||
|
||||
The headless demo combines the real DeepSeek adapter and coding capabilities with the one-shot app package, format-pure stdout, and one fresh persisted top-level session.
|
||||
The headless snapshot composition combines the real DeepSeek adapter and coding capabilities with one explicitly configured persisted top-level agent; its JSONL driver is test-only.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
@@ -18,15 +18,12 @@ flowchart LR
|
||||
cfg --> plugin_headless_subprocess
|
||||
plugin_headless_bash["bash<br/>@deepseek-ai/dsh-bash-local"]
|
||||
cfg --> plugin_headless_bash
|
||||
plugin_headless_cli_agent["cli-agent<br/>@deepseek-ai/dsh-cli-demo"]
|
||||
cfg --> plugin_headless_cli_agent
|
||||
plugin_headless_cli_agent --> bundle_agent_core["@deepseek-ai/dsh-agent-spine-demo"]
|
||||
plugin_headless_cli_agent --> bundle_jsonl["@deepseek-ai/dsh-session-persistence-jsonl"]
|
||||
plugin_headless_cli_agent --> frontdoor_cli["one-shot driver<br/>format-pure stdout<br/>fresh top-level agent"]
|
||||
bundle_agent_core --> spine_llm["ctx.llm"]
|
||||
bundle_agent_core --> spine_sessions["ctx.sessions"]
|
||||
bundle_agent_core --> spine_tools["ctx.tools + tool-bash"]
|
||||
bundle_agent_core --> spine_loop["ctx.agents + ctx.agentLoop"]
|
||||
plugin_headless_agent_spine["agent-spine<br/>@deepseek-ai/dsh-agent-spine-demo"]
|
||||
cfg --> plugin_headless_agent_spine
|
||||
plugin_headless_persistence["persistence<br/>@deepseek-ai/dsh-session-persistence-jsonl"]
|
||||
cfg --> plugin_headless_persistence
|
||||
plugin_headless_checkpoint_policy["checkpoint-policy<br/>@deepseek-ai/dsh-session-checkpoint-policy"]
|
||||
cfg --> plugin_headless_checkpoint_policy
|
||||
plugin_headless_token_meter["token-meter<br/>@deepseek-ai/dsh-token-meter"]
|
||||
cfg --> plugin_headless_token_meter
|
||||
plugin_headless_compact_basic["compact-basic<br/>@deepseek-ai/dsh-compact-basic"]
|
||||
@@ -70,7 +67,9 @@ flowchart LR
|
||||
| `llm-deepseek` | `@deepseek-ai/dsh-llm-deepseek` |
|
||||
| `subprocess` | `@deepseek-ai/dsh-subprocess-local` |
|
||||
| `bash` | `@deepseek-ai/dsh-bash-local` |
|
||||
| `cli-agent` | `@deepseek-ai/dsh-cli-demo` |
|
||||
| `agent-spine` | `@deepseek-ai/dsh-agent-spine-demo` |
|
||||
| `persistence` | `@deepseek-ai/dsh-session-persistence-jsonl` |
|
||||
| `checkpoint-policy` | `@deepseek-ai/dsh-session-checkpoint-policy` |
|
||||
| `token-meter` | `@deepseek-ai/dsh-token-meter` |
|
||||
| `compact-basic` | `@deepseek-ai/dsh-compact-basic` |
|
||||
| `session-projection` | `@deepseek-ai/dsh-session-projection` |
|
||||
|
||||
@@ -40,17 +40,18 @@
|
||||
config:
|
||||
timeoutMs: 60000
|
||||
|
||||
# The app bundle pre-creates one fresh `main` agent per invocation.
|
||||
- id: cli-agent
|
||||
name: '@deepseek-ai/dsh-cli-demo'
|
||||
# The example composition pre-creates one fresh `main` agent for its test driver.
|
||||
- id: agent-spine
|
||||
name: '@deepseek-ai/dsh-agent-spine-demo'
|
||||
config:
|
||||
provider: deepseek-official
|
||||
# Stays on flash: the goal/ralph replay corpora were recorded on it, and
|
||||
# their nested-include overlays cannot re-pin the app config (a config
|
||||
# patch cannot target an entry behind a nested include).
|
||||
model: deepseek-v4-flash
|
||||
persistenceRoot: './.sessions'
|
||||
persistenceCompression: !!js "process.env.DSH_SNAPSHOT === undefined ? 'zstd' : 'none'"
|
||||
agents:
|
||||
- id: main
|
||||
provider: deepseek-official
|
||||
# Stays on flash: the goal/ralph replay corpora were recorded on it, and
|
||||
# their nested-include overlays cannot re-pin the app config (a config
|
||||
# patch cannot target an entry behind a nested include).
|
||||
model: deepseek-v4-flash
|
||||
cwd: !!js process.cwd()
|
||||
workspaceContext:
|
||||
maxBytes: 65536
|
||||
persona: |
|
||||
@@ -59,6 +60,15 @@
|
||||
Verify your work by running the code or tests. Keep answers brief and
|
||||
factual.
|
||||
|
||||
- id: persistence
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: './.sessions'
|
||||
compression: !!js "process.env.DSH_SNAPSHOT === undefined ? 'zstd' : 'none'"
|
||||
|
||||
- id: checkpoint-policy
|
||||
name: '@deepseek-ai/dsh-session-checkpoint-policy'
|
||||
|
||||
# Summarize an older range when derived history approaches the context window.
|
||||
- id: token-meter
|
||||
name: '@deepseek-ai/dsh-token-meter'
|
||||
|
||||
+11
-5
@@ -12,15 +12,21 @@
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
disabled: true
|
||||
- id: cli-agent
|
||||
name: '@deepseek-ai/dsh-cli-demo'
|
||||
- id: agent-spine
|
||||
name: '@deepseek-ai/dsh-agent-spine-demo'
|
||||
config:
|
||||
provider: cli-mock
|
||||
model: cli-mock
|
||||
persistenceRoot: './.sessions'
|
||||
agents:
|
||||
- id: main
|
||||
provider: cli-mock
|
||||
model: cli-mock
|
||||
cwd: !!js process.cwd()
|
||||
workspaceContext: false
|
||||
dshHome: './.dsh-home'
|
||||
skills:
|
||||
local:
|
||||
agentsHome: './.agents-home'
|
||||
persona: 'Keyless headless-agent smoke.'
|
||||
- id: persistence
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: './.sessions'
|
||||
@@ -7,10 +7,15 @@
|
||||
config:
|
||||
baseURL: !!js process.env.DSH_SNAPSHOT_BASE_URL
|
||||
thinking: disabled
|
||||
- id: cli-agent
|
||||
- id: agent-spine
|
||||
config:
|
||||
provider: deepseek-official
|
||||
model: deepseek-v4-flash
|
||||
persistenceRoot: './.sessions'
|
||||
agents:
|
||||
- id: main
|
||||
provider: deepseek-official
|
||||
model: deepseek-v4-flash
|
||||
cwd: !!js process.cwd()
|
||||
workspaceContext: false
|
||||
persona: 'Keyless DeepSeek adapter defaults snapshot.'
|
||||
- id: persistence
|
||||
config:
|
||||
root: './.sessions'
|
||||
@@ -17,12 +17,22 @@
|
||||
- id: seed-goal
|
||||
name: './seed-goal.ts'
|
||||
|
||||
- id: cli-agent
|
||||
name: '@deepseek-ai/dsh-cli-demo'
|
||||
- id: agent-spine
|
||||
name: '@deepseek-ai/dsh-agent-spine-demo'
|
||||
config:
|
||||
provider: cli-mock
|
||||
model: cli-mock
|
||||
agents:
|
||||
- id: main
|
||||
provider: cli-mock
|
||||
model: cli-mock
|
||||
cwd: !!js process.cwd()
|
||||
persona: 'Test the persisted goal domain.'
|
||||
persistenceRoot: './.sessions'
|
||||
persistenceCompression: none
|
||||
workspaceContext: false
|
||||
|
||||
- id: persistence
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: './.sessions'
|
||||
compression: none
|
||||
|
||||
- id: checkpoint-policy
|
||||
name: '@deepseek-ai/dsh-session-checkpoint-policy'
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env node
|
||||
/** Snapshot-only Loader driver: stream one fixture turn as canonical JSONL. */
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import { boot, installFailLoud, loadEnv, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import { runFixtureTurn } from './one-shot.ts'
|
||||
|
||||
const NAME = 'headless-test-driver'
|
||||
const [configPath, ...taskParts] = process.argv.slice(2)
|
||||
if (configPath === undefined || taskParts.length === 0 || taskParts.every(part => part.trim() === '')) {
|
||||
throw new Error(`${NAME}: expected <config-path> <task...>`)
|
||||
}
|
||||
|
||||
const uninstallFailLoud = installFailLoud(NAME)
|
||||
let ctx: Context | undefined
|
||||
try {
|
||||
loadEnv(NAME)
|
||||
ctx = await boot(NAME, resolveConfigPath(configPath, process.env.DSH_SNAPSHOT))
|
||||
const result = await runFixtureTurn(ctx, {
|
||||
task: taskParts.join(' '),
|
||||
onEvent: (sessionId: string, event: SessionEvent) => {
|
||||
process.stdout.write(`${JSON.stringify({ type: 'session_event', sessionId, event })}\n`)
|
||||
},
|
||||
})
|
||||
process.stdout.write(`${JSON.stringify(result)}\n`)
|
||||
} catch (error: unknown) {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
||||
process.exitCode = 1
|
||||
} finally {
|
||||
await ctx?.fiber.dispose()
|
||||
uninstallFailLoud()
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/** Test-only direct-agent turn driver shared by assembled Loader fixtures. */
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { createUserMessage, type TokenUsage } from '@deepseek-ai/dsh-llm'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
|
||||
/** Result envelope consumed only by snapshot and composition tests. */
|
||||
export interface FixtureTurnResult {
|
||||
readonly type: 'result'
|
||||
readonly sessionId: string
|
||||
readonly output: string
|
||||
readonly usage?: TokenUsage
|
||||
}
|
||||
|
||||
/** Options for one fixture turn against exactly one configured root agent. */
|
||||
export interface FixtureTurnOptions {
|
||||
readonly task: string
|
||||
readonly onEvent?: (sessionId: string, event: SessionEvent) => void
|
||||
}
|
||||
|
||||
function addUsage(total: TokenUsage | undefined, step: TokenUsage): TokenUsage {
|
||||
const next: TokenUsage = {
|
||||
inputTokens: (total?.inputTokens ?? 0) + step.inputTokens,
|
||||
outputTokens: (total?.outputTokens ?? 0) + step.outputTokens,
|
||||
}
|
||||
for (const key of ['cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens'] as const) {
|
||||
if (total?.[key] !== undefined || step[key] !== undefined) next[key] = (total?.[key] ?? 0) + (step[key] ?? 0)
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
function assistantText(event: Extract<SessionEvent, { type: 'assistant/message' }>): string | undefined {
|
||||
const blocks = event.data.message.content.filter(block => block.type === 'text')
|
||||
return blocks.length === 0 ? undefined : blocks.map(block => block.text).join('')
|
||||
}
|
||||
|
||||
function onlyRootAgent(ctx: Context): Agent {
|
||||
const agents = ctx.get('agents')?.roots() ?? []
|
||||
const [agent] = agents
|
||||
if (agent === undefined || agents.length !== 1) {
|
||||
throw new Error(`headless fixture requires exactly one top-level agent, found ${agents.length}`)
|
||||
}
|
||||
return agent
|
||||
}
|
||||
|
||||
/**
|
||||
* Drive one task from its durable inbox receipt through whole-agent idle.
|
||||
* @param ctx - settled Loader context with exactly one configured root agent.
|
||||
* @param options - task and optional canonical-event observer.
|
||||
* @returns the final assistant text and accumulated model usage.
|
||||
*/
|
||||
export async function runFixtureTurn(ctx: Context, options: FixtureTurnOptions): Promise<FixtureTurnResult> {
|
||||
const agent = onlyRootAgent(ctx)
|
||||
await agent.whenIdle()
|
||||
|
||||
const message = createUserMessage({
|
||||
content: [{ type: 'text', text: options.task }],
|
||||
source: { kind: 'user' },
|
||||
})
|
||||
let received = false
|
||||
let output = ''
|
||||
const usageByStep = new Map<string, TokenUsage>()
|
||||
const disposeListener = ctx.on('session/event', (session, event) => {
|
||||
if (session !== agent.session) return
|
||||
if (!received) {
|
||||
if (event.type !== 'agent/inbox/spliced'
|
||||
|| !event.data.inserted.some(inserted => inserted.id === message.id)) return
|
||||
received = true
|
||||
}
|
||||
options.onEvent?.(session.id, event)
|
||||
if (event.type === 'assistant/chunk' && event.data.chunk.type === 'usage') {
|
||||
usageByStep.set(`${event.data.turn}/${event.data.step}`, event.data.chunk.usage)
|
||||
}
|
||||
if (event.type === 'assistant/message') {
|
||||
output = assistantText(event) ?? output
|
||||
if (event.data.usage !== undefined) {
|
||||
usageByStep.set(`${event.data.turn}/${event.data.step}`, event.data.usage)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
agent.followup(message)
|
||||
await agent.whenIdle()
|
||||
} finally {
|
||||
disposeListener()
|
||||
}
|
||||
await ctx.sessions.flush(agent.session)
|
||||
const usage = [...usageByStep.values()].reduce<TokenUsage | undefined>(addUsage, undefined)
|
||||
return {
|
||||
type: 'result',
|
||||
sessionId: agent.session.id,
|
||||
output,
|
||||
...usage === undefined ? {} : { usage },
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@ import { writeFile } from 'node:fs/promises'
|
||||
import { createServer } from 'node:http'
|
||||
import { once } from 'node:events'
|
||||
import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
|
||||
import { runOneShot } from '@deepseek-ai/dsh-cli-demo/src/cli.ts'
|
||||
import { recordFeedback } from '@deepseek-ai/dsh-command-feedback'
|
||||
import { runFixtureTurn } from './one-shot.ts'
|
||||
|
||||
const configPath = process.argv[2]
|
||||
if (configPath === undefined) throw new Error('telemetry-otel driver requires a config path')
|
||||
@@ -35,14 +35,14 @@ const ctx = await boot('telemetry-otel-e2e', resolveConfigPath(configPath, undef
|
||||
try {
|
||||
// The fixture credential rides the model-visible user message; the exported
|
||||
// copy must scrub it while the canonical log keeps the original bytes.
|
||||
await runOneShot(ctx, { task: 'prove telemetry with key sk-e2efixture1234567890' })
|
||||
await runFixtureTurn(ctx, { task: 'prove telemetry with key sk-e2efixture1234567890' })
|
||||
const mode = process.env.DSH_TELEMETRY_E2E_MODE ?? 'FULL'
|
||||
if (mode !== 'FULL') {
|
||||
const [agent] = ctx.get('agents')?.roots() ?? []
|
||||
if (agent === undefined) throw new Error('telemetry-otel driver requires one root agent')
|
||||
recordFeedback(agent.session, 'fixture feedback')
|
||||
if (mode === 'FEEDBACK_ONLY') {
|
||||
await runOneShot(ctx, { task: 'post-feedback private suffix' })
|
||||
await runFixtureTurn(ctx, { task: 'post-feedback private suffix' })
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -30,12 +30,22 @@
|
||||
exporter:
|
||||
url: !!js process.env.DSH_TELEMETRY_E2E_URL
|
||||
|
||||
- id: cli-agent
|
||||
name: '@deepseek-ai/dsh-cli-demo'
|
||||
- id: agent-spine
|
||||
name: '@deepseek-ai/dsh-agent-spine-demo'
|
||||
config:
|
||||
provider: cli-mock
|
||||
model: cli-mock
|
||||
agents:
|
||||
- id: main
|
||||
provider: cli-mock
|
||||
model: cli-mock
|
||||
cwd: !!js process.cwd()
|
||||
persona: 'Test the session-telemetry-otel plugin.'
|
||||
persistenceRoot: './.sessions'
|
||||
persistenceCompression: 'none'
|
||||
workspaceContext: false
|
||||
|
||||
- id: persistence
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: './.sessions'
|
||||
compression: 'none'
|
||||
|
||||
- id: checkpoint-policy
|
||||
name: '@deepseek-ai/dsh-session-checkpoint-policy'
|
||||
@@ -2,15 +2,15 @@
|
||||
/** Test driver that sends two turns through one Headless Loader composition. */
|
||||
|
||||
import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
|
||||
import { runOneShot } from '@deepseek-ai/dsh-cli-demo/src/cli.ts'
|
||||
import { runFixtureTurn } from './one-shot.ts'
|
||||
|
||||
const configPath = process.argv[2]
|
||||
if (configPath === undefined) throw new Error('time-context driver requires a config path')
|
||||
|
||||
const ctx = await boot('time-context-e2e', resolveConfigPath(configPath, undefined))
|
||||
try {
|
||||
await runOneShot(ctx, { task: 'first' })
|
||||
await runOneShot(ctx, { task: 'second' })
|
||||
await runFixtureTurn(ctx, { task: 'first' })
|
||||
await runFixtureTurn(ctx, { task: 'second' })
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
@@ -12,12 +12,22 @@
|
||||
- id: time-context
|
||||
name: '@deepseek-ai/dsh-time-context'
|
||||
|
||||
- id: cli-agent
|
||||
name: '@deepseek-ai/dsh-cli-demo'
|
||||
- id: agent-spine
|
||||
name: '@deepseek-ai/dsh-agent-spine-demo'
|
||||
config:
|
||||
provider: time-context-mock
|
||||
model: time-context-mock
|
||||
agents:
|
||||
- id: main
|
||||
provider: time-context-mock
|
||||
model: time-context-mock
|
||||
cwd: !!js process.cwd()
|
||||
persona: 'Test the time-context plugin.'
|
||||
persistenceRoot: './.sessions'
|
||||
persistenceCompression: 'none'
|
||||
workspaceContext: false
|
||||
|
||||
- id: persistence
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: './.sessions'
|
||||
compression: 'none'
|
||||
|
||||
- id: checkpoint-policy
|
||||
name: '@deepseek-ai/dsh-session-checkpoint-policy'
|
||||
@@ -47,7 +47,7 @@ const ralphScenarioDir = join(snapshotsDir, 'ralph-loop')
|
||||
const ralphConfigPath = fileURLToPath(new URL('../ralph.cordis.snapshot.yml', import.meta.url))
|
||||
const startupFailureConfigPath = fileURLToPath(new URL('./fixtures/startup-activation-error/cordis.yml', import.meta.url))
|
||||
const startupFailureExpected = join(snapshotsDir, 'startup-activation-error', 'stderr.expected.txt')
|
||||
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url))
|
||||
const binScript = fileURLToPath(new URL('./fixtures/headless-driver.ts', import.meta.url))
|
||||
const dshBinScript = fileURLToPath(new URL('../../../apps/cli/src/bin.ts', import.meta.url))
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
const reasoningConfigPath = fileURLToPath(new URL('./fixtures/cli.cordis.yml', import.meta.url))
|
||||
@@ -242,8 +242,9 @@ describe('headless stream-json snapshots', () => {
|
||||
label: 'headless startup activation error snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-startup-error-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath: startupFailureConfigPath,
|
||||
binArgs: ['--config', startupFailureConfigPath, '--output-format', 'stream-json', 'unreachable task'],
|
||||
binArgs: [startupFailureConfigPath, 'unreachable task'],
|
||||
tsconfigPath,
|
||||
expectedExitCode: 1,
|
||||
})
|
||||
@@ -259,8 +260,9 @@ describe('headless stream-json snapshots', () => {
|
||||
label: 'provider retry headless stream-json snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-provider-retry-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath: retryConfigPath,
|
||||
binArgs: ['--config', retryConfigPath, '--output-format', 'stream-json', prompt],
|
||||
binArgs: [retryConfigPath, prompt],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
DSH_SNAPSHOT: 'replay',
|
||||
@@ -299,8 +301,9 @@ describe('headless stream-json snapshots', () => {
|
||||
label: 'compaction recovery headless stream-json snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-compaction-recovery-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath: compactionConfigPath,
|
||||
binArgs: ['--config', compactionConfigPath, '--output-format', 'stream-json', prompt],
|
||||
binArgs: [compactionConfigPath, prompt],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
DSH_SNAPSHOT: 'replay',
|
||||
@@ -367,8 +370,9 @@ describe('headless stream-json snapshots', () => {
|
||||
label: 'missing-credential headless stream-json snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-missing-credential-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath: credentialsConfigPath,
|
||||
binArgs: ['--config', credentialsConfigPath, '--output-format', 'stream-json', 'say pong'],
|
||||
binArgs: [credentialsConfigPath, 'say pong'],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
// First-run posture: no key in the environment, none under ./.dsh.
|
||||
@@ -404,8 +408,9 @@ describe('headless stream-json snapshots', () => {
|
||||
label: 'invalid-credential headless stream-json snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-invalid-credential-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath: credentialsConfigPath,
|
||||
binArgs: ['--config', credentialsConfigPath, '--output-format', 'stream-json', 'say pong'],
|
||||
binArgs: [credentialsConfigPath, 'say pong'],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
// A key that exists but no HTTP header can carry — the paste this
|
||||
@@ -438,8 +443,9 @@ describe('headless stream-json snapshots', () => {
|
||||
label: 'reasoning effort headless stream-json snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-reasoning-effort-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath: reasoningConfigPath,
|
||||
binArgs: ['--config', reasoningConfigPath, '--output-format', 'stream-json', 'prove dynamic reasoning effort'],
|
||||
binArgs: [reasoningConfigPath, 'prove dynamic reasoning effort'],
|
||||
tsconfigPath,
|
||||
})
|
||||
|
||||
@@ -480,12 +486,10 @@ describe('headless stream-json snapshots', () => {
|
||||
label: 'DeepSeek adapter defaults headless stream-json snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-deepseek-defaults-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath: deepseekDefaultsConfigPath,
|
||||
binArgs: [
|
||||
'--config',
|
||||
deepseekDefaultsConfigPath,
|
||||
'--output-format',
|
||||
'stream-json',
|
||||
'return the deterministic response',
|
||||
],
|
||||
tsconfigPath,
|
||||
@@ -540,8 +544,9 @@ describe('headless stream-json snapshots', () => {
|
||||
label: 'advanced headless stream-json snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-advanced-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath: advancedConfigPath,
|
||||
binArgs: ['--config', advancedConfigPath, '--output-format', 'stream-json', prompt],
|
||||
binArgs: [advancedConfigPath, prompt],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
DSH_SNAPSHOT: 'replay',
|
||||
@@ -611,8 +616,9 @@ describe('headless stream-json snapshots', () => {
|
||||
label: 'goal tools headless stream-json snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-goal-tools-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath: goalConfigPath,
|
||||
binArgs: ['--config', goalConfigPath, '--output-format', 'stream-json', prompt],
|
||||
binArgs: [goalConfigPath, prompt],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
DSH_SNAPSHOT: 'replay',
|
||||
@@ -667,8 +673,9 @@ describe('headless stream-json snapshots', () => {
|
||||
label: 'Ralph loop headless stream-json snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-ralph-loop-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath: ralphConfigPath,
|
||||
binArgs: ['--config', ralphConfigPath, '--output-format', 'stream-json', prompt],
|
||||
binArgs: [ralphConfigPath, prompt],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
DSH_SNAPSHOT: 'replay',
|
||||
@@ -748,8 +755,9 @@ describe('headless stream-json snapshots', () => {
|
||||
label: 'headless persistent PTY snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-pty-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath: ptyConfigPath,
|
||||
binArgs: ['--config', ptyConfigPath, '--output-format', 'stream-json', prompt],
|
||||
binArgs: [ptyConfigPath, prompt],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
DSH_SNAPSHOT: 'replay',
|
||||
|
||||
@@ -9,7 +9,7 @@ import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-l
|
||||
import { PREPARED_ENTRY_FILENAME, prepareDshPlugin } from '@deepseek-ai/dsh-repository-plugin'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
|
||||
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url))
|
||||
const binScript = fileURLToPath(new URL('./fixtures/headless-driver.ts', import.meta.url))
|
||||
const configPath = fileURLToPath(new URL('./fixtures/cli.cordis.yml', import.meta.url))
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
const decompress = promisify(zstdDecompress)
|
||||
@@ -21,8 +21,9 @@ describe('headless-agent keyless smoke', () => {
|
||||
label: 'headless-agent',
|
||||
tempDirPrefix: 'headless-agent-smoke-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath,
|
||||
binArgs: ['--config', configPath, '--output-format', 'stream-json', 'prove the tool path'],
|
||||
binArgs: [configPath, 'prove the tool path'],
|
||||
tsconfigPath,
|
||||
inspect: async (cwd) => {
|
||||
const files = await readdir(cwd, { recursive: true })
|
||||
|
||||
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
|
||||
|
||||
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url))
|
||||
const binScript = fileURLToPath(new URL('./fixtures/headless-driver.ts', import.meta.url))
|
||||
const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url))
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
const hasKey = Boolean(process.env.DEEPSEEK_API_KEY)
|
||||
@@ -16,9 +16,9 @@ describe.skipIf(!hasKey)('headless-agent with real model', () => {
|
||||
label: 'headless-agent real model',
|
||||
tempDirPrefix: 'headless-agent-real-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath,
|
||||
binArgs: [
|
||||
'--config',
|
||||
configPath,
|
||||
'Read task.txt, replace its complete contents with exactly "value=after" followed by a newline, read it again, and report briefly.',
|
||||
],
|
||||
|
||||
@@ -14,7 +14,7 @@ const replayFixture = join(fixtureDir, 'replay.jsonl')
|
||||
const replayOverride = join(fixtureDir, 'replay.override.json')
|
||||
const sessionExpected = join(fixtureDir, 'session.expected.jsonl')
|
||||
const configPath = fileURLToPath(new URL('../semantic-checkpoint.cordis.snapshot.yml', import.meta.url))
|
||||
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url))
|
||||
const binScript = fileURLToPath(new URL('./fixtures/headless-driver.ts', import.meta.url))
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
const sessionId = SessionId('semantic-checkpoint-unknown-outcome')
|
||||
const refreshing = process.env.DSH_SNAPSHOT === 'refresh'
|
||||
@@ -87,8 +87,9 @@ describe('semantic checkpoint recovery snapshot', () => {
|
||||
label: 'semantic checkpoint headless stream-json snapshot',
|
||||
tempDirPrefix: 'dsh-semantic-snapshot-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath,
|
||||
binArgs: ['--config', configPath, '--output-format', 'stream-json', task],
|
||||
binArgs: [configPath, task],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
DSH_SNAPSHOT_FILE: replayFixture,
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
dsh-cli-demo: dsh-cli-demo: plugin tree failed to load: failed to apply loader entry include (cordis:include): failed to apply loader entry activation-error (./activation-error.mjs): startup activation snapshot failure
|
||||
headless-test-driver: plugin tree failed to load: failed to apply loader entry include (cordis:include): failed to apply loader entry activation-error (./activation-error.mjs): startup activation snapshot failure
|
||||
Error: startup activation snapshot failure
|
||||
at activation-error-fixture
|
||||
@@ -19,7 +19,7 @@ const fixtureDir = fileURLToPath(new URL('./subagent-diagnostic-snapshots/descri
|
||||
const replayOverride = join(fixtureDir, 'replay.override.json')
|
||||
const parentExpected = join(fixtureDir, 'parent.expected.jsonl')
|
||||
const configPath = fileURLToPath(new URL('../subagent-diagnostic.cordis.snapshot.yml', import.meta.url))
|
||||
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url))
|
||||
const binScript = fileURLToPath(new URL('./fixtures/headless-driver.ts', import.meta.url))
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
const parentId = SessionId('subagent-diagnostic-parent')
|
||||
const childId = SessionId('subagent-diagnostic-child')
|
||||
@@ -77,8 +77,9 @@ describe('descriptor-less cold child diagnostic snapshot', () => {
|
||||
label: 'subagent diagnostic headless stream-json snapshot',
|
||||
tempDirPrefix: 'dsh-subagent-diag-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath,
|
||||
binArgs: ['--config', configPath, '--output-format', 'stream-json', task],
|
||||
binArgs: [configPath, task],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
DSH_SNAPSHOT_FILE: replayOverride,
|
||||
|
||||
@@ -20,7 +20,7 @@ const childReplay = join(fixtureDir, 'child.replay.jsonl')
|
||||
const parentExpected = join(fixtureDir, 'parent.expected.jsonl')
|
||||
const childExpected = join(fixtureDir, 'child.expected.jsonl')
|
||||
const configPath = fileURLToPath(new URL('../subagent-inheritance.cordis.snapshot.yml', import.meta.url))
|
||||
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url))
|
||||
const binScript = fileURLToPath(new URL('./fixtures/headless-driver.ts', import.meta.url))
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
const sessionId = SessionId('subagent-inheritance-parent')
|
||||
const refreshing = process.env.DSH_SNAPSHOT === 'refresh'
|
||||
@@ -59,8 +59,9 @@ describe('parent-only override inheritance snapshot', () => {
|
||||
label: 'subagent inheritance headless stream-json snapshot',
|
||||
tempDirPrefix: 'dsh-subagent-inherit-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath,
|
||||
binArgs: ['--config', configPath, '--output-format', 'stream-json', task],
|
||||
binArgs: [configPath, task],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
// The primary fixture path must exist for llm-replay's config guard;
|
||||
|
||||
@@ -28,7 +28,7 @@ const replayOverride = join(fixtureDir, 'replay.override.json')
|
||||
const sessionExpected = join(fixtureDir, 'session.expected.jsonl')
|
||||
const precedenceExpected = join(dirname(fixtureDir), 'precedence-change/session.expected.jsonl')
|
||||
const configPath = fileURLToPath(new URL('../workspace-context-resume.cordis.snapshot.yml', import.meta.url))
|
||||
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url))
|
||||
const binScript = fileURLToPath(new URL('./fixtures/headless-driver.ts', import.meta.url))
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
const sessionId = SessionId('workspace-context-resume')
|
||||
const refreshing = process.env.DSH_SNAPSHOT === 'refresh'
|
||||
@@ -119,8 +119,9 @@ describe('workspace-context resume snapshot', () => {
|
||||
label: 'workspace-context resume headless stream-json snapshot',
|
||||
tempDirPrefix: 'dsh-workspace-context-resume-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath,
|
||||
binArgs: ['--config', configPath, '--output-format', 'stream-json', 'Acknowledge the current workspace instruction.'],
|
||||
binArgs: [configPath, 'Acknowledge the current workspace instruction.'],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
DSH_SNAPSHOT_FILE: replayFixture,
|
||||
@@ -175,8 +176,9 @@ describe('workspace-context resume snapshot', () => {
|
||||
label: 'workspace-context precedence-change resume snapshot',
|
||||
tempDirPrefix: 'dsh-workspace-context-precedence-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath,
|
||||
binArgs: ['--config', configPath, '--output-format', 'stream-json', 'Acknowledge the current workspace instruction.'],
|
||||
binArgs: [configPath, 'Acknowledge the current workspace instruction.'],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
DSH_SNAPSHOT_FILE: replayFixture,
|
||||
|
||||
+16
-6
@@ -32,12 +32,22 @@
|
||||
# recursion budget, so the local numeric default cannot apply here.
|
||||
maxDepth: 'provider-managed'
|
||||
|
||||
- id: cli-agent
|
||||
name: '@deepseek-ai/dsh-cli-demo'
|
||||
- id: agent-spine
|
||||
name: '@deepseek-ai/dsh-agent-spine-demo'
|
||||
config:
|
||||
provider: mock
|
||||
model: mock-delegate
|
||||
agents:
|
||||
- id: main
|
||||
provider: mock
|
||||
model: mock-delegate
|
||||
cwd: !!js process.cwd()
|
||||
persona: 'Test SDK subagent cwd inheritance.'
|
||||
persistenceRoot: './.sessions'
|
||||
persistenceCompression: 'none'
|
||||
workspaceContext: false
|
||||
|
||||
- id: persistence
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: './.sessions'
|
||||
compression: 'none'
|
||||
|
||||
- id: checkpoint-policy
|
||||
name: '@deepseek-ai/dsh-session-checkpoint-policy'
|
||||
@@ -2,14 +2,14 @@
|
||||
/** Test driver: one delegation turn through a headless Loader composition. */
|
||||
|
||||
import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
|
||||
import { runOneShot } from '@deepseek-ai/dsh-cli-demo/src/cli.ts'
|
||||
import { runFixtureTurn } from '../../../../../headless-agent/tests/fixtures/one-shot.ts'
|
||||
|
||||
const configPath = process.argv[2]
|
||||
if (configPath === undefined) throw new Error('sdk-subagent cwd driver requires a config path')
|
||||
|
||||
const ctx = await boot('sdk-subagent-cwd-e2e', resolveConfigPath(configPath, undefined))
|
||||
try {
|
||||
await runOneShot(ctx, { task: 'delegate' })
|
||||
await runFixtureTurn(ctx, { task: 'delegate' })
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
@@ -18,7 +18,6 @@
|
||||
"@deepseek-ai/dsh-bash-env": "workspace:*",
|
||||
"@deepseek-ai/dsh-bash-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-bash-sandbox": "workspace:*",
|
||||
"@deepseek-ai/dsh-cli-demo": "workspace:*",
|
||||
"@deepseek-ai/dsh-code-runtime-worker": "workspace:*",
|
||||
"@deepseek-ai/dsh-command-feedback": "workspace:*",
|
||||
"@deepseek-ai/dsh-command-goal": "workspace:*",
|
||||
|
||||
@@ -519,16 +519,6 @@
|
||||
"tests/**/*.ts"
|
||||
]
|
||||
},
|
||||
"packages/examples/cli-demo": {
|
||||
"entry": [
|
||||
"tests/**/*.spec.ts",
|
||||
"tests/**/*.e2e.ts"
|
||||
],
|
||||
"project": [
|
||||
"src/**/*.ts",
|
||||
"tests/**/*.ts"
|
||||
]
|
||||
},
|
||||
"packages/examples/jsonrpc-demo": {
|
||||
"project": [
|
||||
"src/**/*.ts"
|
||||
|
||||
+1
-1
@@ -112,7 +112,7 @@
|
||||
"hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-package-invariants && pnpm run verify-built-package-invariants && pnpm run verify-cordis-config && pnpm run verify-node-next-types && pnpm run verify-runtime-closure && pnpm run verify-vendored-links",
|
||||
"publish:npm-baseline": "tsx scripts/publish-npm-baseline.ts",
|
||||
"dsh": "node --import tsx/esm apps/cli/src/bin.ts",
|
||||
"demo:headless": "node --import tsx packages/examples/cli-demo/src/bin.ts --config examples/headless-agent/cordis.yml",
|
||||
"demo:headless": "node --import tsx/esm apps/cli/src/bin.ts run",
|
||||
"demo:code-mode": "node scripts/demo-code-mode.mjs",
|
||||
"demo:cordis": "node scripts/demo-cordis.mjs",
|
||||
"demo:acp": "node --import tsx packages/examples/acp-demo/src/bin.ts --config examples/acp-agent/cordis.yml",
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# 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/README.md
|
||||
README.md: 64fff8cb3f53386d48a9a831c1cbdd946ad483cc
|
||||
README.zh.md: c346a41d297a545991a2441df625286e1b830998
|
||||
README.md: 36d1c0d0ddb7a3840af10e6a69ea407d3471c661
|
||||
README.zh.md: 9886abe8eedc27a6c62728b93fd63748b2c7dea6
|
||||
@@ -7,11 +7,10 @@ Pre-composed plugin bundles a thin leaf `cordis.yml` loads instead of assembling
|
||||
| Package | npm name | Role |
|
||||
|---|---|---|
|
||||
| [`agent-spine-demo/`](agent-spine-demo/README.md) | `@deepseek-ai/dsh-agent-spine-demo` | Reusable agent-spine bundle |
|
||||
| [`cli-demo/`](cli-demo/README.md) | `@deepseek-ai/dsh-cli-demo` | Headless one-shot application bundle |
|
||||
| [`acp-demo/`](acp-demo/README.md) | `@deepseek-ai/dsh-acp-demo` | ACP automation application bundle |
|
||||
| [`jsonrpc-demo/`](jsonrpc-demo/README.md) | `@deepseek-ai/dsh-jsonrpc-demo` | External-config JSON-RPC runtime |
|
||||
|
||||
`agent-spine-demo` is the shared bundle; `cli-demo` and `acp-demo` add their front doors, while `jsonrpc-demo` boots a deployment-owned plugin tree.
|
||||
`agent-spine-demo` is the shared bundle; `acp-demo` adds its automation front door, while `jsonrpc-demo` boots a deployment-owned plugin tree. Product one-shot execution belongs to `dsh run` rather than a package in this directory.
|
||||
|
||||
These packages are not product API. Product seams and front doors remain in their owning groups; demo bundles select concrete compositions.
|
||||
|
||||
|
||||
@@ -7,11 +7,10 @@
|
||||
| 包 | npm 名称 | 角色 |
|
||||
|---|---|---|
|
||||
| [`agent-spine-demo/`](agent-spine-demo/README.md) | `@deepseek-ai/dsh-agent-spine-demo` | 可复用的 agent 主干组合包 |
|
||||
| [`cli-demo/`](cli-demo/README.md) | `@deepseek-ai/dsh-cli-demo` | 无头单次应用组合包 |
|
||||
| [`acp-demo/`](acp-demo/README.md) | `@deepseek-ai/dsh-acp-demo` | ACP 自动化应用组合包 |
|
||||
| [`jsonrpc-demo/`](jsonrpc-demo/README.md) | `@deepseek-ai/dsh-jsonrpc-demo` | 外部配置 JSON-RPC 运行时 |
|
||||
|
||||
`agent-spine-demo` 是共享组合包;`cli-demo` 与 `acp-demo` 添加各自的前端入口,`jsonrpc-demo` 则启动由部署方拥有的插件树。
|
||||
`agent-spine-demo` 是共享组合包;`acp-demo` 添加自动化入口,`jsonrpc-demo` 则启动由部署方拥有的插件树。产品单次执行归 `dsh run` 所有,而不再由本目录中的 package 提供。
|
||||
|
||||
这些包不是产品 API。产品 seam 与前端入口仍位于各自的归属组;演示组合包只选择具体组合。
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# 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: 6e46ae81421c23806524b0784a976e9f3c8eeab8
|
||||
README.zh.md: b032023fee4bf9d992217cc51731f6356f875daf
|
||||
@@ -1,78 +0,0 @@
|
||||
# @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 owns one idle-to-idle activity interval, renders its 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.
|
||||
|
||||
## Config
|
||||
|
||||
| Key | Default | Routed to |
|
||||
|---|---|---|
|
||||
| `provider` | required | the configured agent's provider route |
|
||||
| `model` | required | the configured agent's model |
|
||||
| `maxParallelToolCalls` | agent-loop default | positive-integer concurrent tool-call cap; `1` is serial |
|
||||
| `persona` | — | the deployment persona in `dsh-system-prompt` |
|
||||
| `toolOrder` | lexicographic | explicit model-facing tool order in `dsh-system-prompt` |
|
||||
| `tools` | `{ mode: 'native' }` | tool-registry presentation config through `dsh-agent-spine-demo` |
|
||||
| `dshHome` | `$DSH_HOME` or `~/.dsh` | Harness home exposed to model bash and used by local skill discovery |
|
||||
| `sessionTitle` | spine example limits | Fallback title word/byte limits through `dsh-agent-spine-demo` |
|
||||
| `skills` | owner defaults | skill registry, local provider, and model-facing skill tool |
|
||||
| `toolBash` | owner defaults | model-facing bash config, including this producer's background opt-in |
|
||||
| `toolTasks` | owner defaults | generic `task_output` wait bounds |
|
||||
| `persistenceRoot` | `./.sessions` | JSONL session root |
|
||||
| `persistenceCompression` | `'zstd'` | JSONL artifact encoding (`'zstd'` or raw `'none'`) |
|
||||
| `workspaceContext` | required | workspace-instruction byte budget, or `false` to disable loading |
|
||||
|
||||
## CLI contract
|
||||
|
||||
```sh
|
||||
dsh-cli-demo [--config path] [--output-format text|json|stream-json] <task>
|
||||
```
|
||||
|
||||
`--config` defaults to `./cordis.yml`; `--output-format` defaults to `text`. Exactly one nonblank positional task is required, so quote tasks containing spaces. `--help` prints usage without booting. There is no `-p` or `--print` flag.
|
||||
|
||||
The root headless-agent example supplies its leaf:
|
||||
|
||||
```sh
|
||||
pnpm run demo:headless "inspect the failing test and fix it"
|
||||
```
|
||||
|
||||
Loader configs resolve bare package specifiers through the optional native helper installed by the repository, so the root command needs no special Node flags.
|
||||
|
||||
### Output formats
|
||||
|
||||
- `text` writes the last assistant message containing text, followed by one newline.
|
||||
- `json` writes one DSH-native result record: `{ type: "result", sessionId, output, usage? }`. `output` is the last committed assistant text in the activity interval. `usage` sums each model step in that interval once, including billed failed attempts that produced usage without a committed assistant message.
|
||||
- `stream-json` writes each canonical event from the top-level session's owned activity interval as `{ type: "session_event", sessionId, event }`, then the same result record. Child-agent activity appears only through the parent tool events and results.
|
||||
|
||||
Normal idle completion exits successfully without assigning a turn reason to the task. Argument, boot, observation, and persistence failures leave stdout empty. SIGINT and SIGTERM cancel active work, await disposal, and exit 130 and 143 respectively.
|
||||
|
||||
The owned activity is explicitly flushed before final output. Session logs remain under `persistenceRoot` after the process exits.
|
||||
|
||||
## Operational safety
|
||||
|
||||
The headless-agent leaf supplies local bash, filesystem, skill, subagent, workflow, and todo capabilities. A task can therefore mutate the launch workspace, run commands, spawn child agents, and consume provider tokens. Run the CLI from the intended project directory, review the leaf's capability and sandbox configuration, and do not treat non-interactive execution as an approval boundary.
|
||||
|
||||
## Model Experience
|
||||
|
||||
### One-shot activity
|
||||
|
||||
#### What the model sees
|
||||
|
||||
The positional task becomes one user message. Through `dsh-agent-spine-demo`, the top-level agent also receives configured workspace instructions and persona, the skill catalog, visible tool schemas, and retained tool results needed for later steps in the owned activity.
|
||||
|
||||
#### Token effect
|
||||
|
||||
The task, prompt sections, tool schemas, assistant output, and tool results consume tokens on each model step. JSON event streaming and final rendering add no model tokens; delegated child work has its own model usage and is not included in the parent result's `usage` total.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
Tool-round history is append-only while the one-shot agent's prompt, schemas, model route, and session prefix remain fixed. Changing that composition establishes a different request prefix; JSON output mode has no cache effect.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **One fresh top-level session per process** — its workspace cwd is the launch directory; there is no resume, second prompt, stdin context, or concurrent top-level session in this app.
|
||||
- **No interactive question or approval provider** — tools that require a human answer cannot complete unless a different leaf composes a non-interactive provider with explicit policy.
|
||||
- **Streaming is top-level-session-only** — child sessions are not flattened into the stream, and aggregate usage covers only model steps recorded on the parent activity interval.
|
||||
@@ -1,78 +0,0 @@
|
||||
# @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 拥有一个从 idle 到 idle 的活动区间,渲染所选输出,执行 dispose(资源释放)直至完全停稳,然后退出。
|
||||
|
||||
该包不挂载 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` | 必填 | 工作区指令字节预算,或以 `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", sessionId, output, usage? }`。`output` 是活动区间内最后提交的 assistant 文本。`usage` 对该区间中的每个模型步骤恰好求和一次,包括产生用量但没有提交 assistant 消息的已计费失败尝试。
|
||||
- `stream-json` 将顶层会话自有活动区间中的每个规范事件写成 `{ type: "session_event", sessionId, event }`,然后写入同一结果记录。子 agent 活动只通过父工具事件与结果出现。
|
||||
|
||||
正常进入 idle 会成功退出,不会为该任务指定轮次原因。参数、启动、观测和持久化失败会让 stdout 保持为空。SIGINT 与 SIGTERM 会取消正在进行的工作,等待 dispose 完成,并分别以 130 和 143 退出。
|
||||
|
||||
自有活动会在最终输出前显式刷新。进程退出后,会话日志仍保留在 `persistenceRoot` 下。
|
||||
|
||||
## 操作安全
|
||||
|
||||
headless-agent 叶节点提供本地 bash、文件系统、skill、subagent、工作流和 todo 能力。因此任务可以修改启动工作区、运行命令、spawn 子 agent,并消耗提供方 token。请从目标项目目录运行 CLI,检查叶节点的能力与沙箱配置,不要把非交互式执行当作批准边界。
|
||||
|
||||
## 模型体验
|
||||
|
||||
### 单次活动
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
任务位置参数会成为一条用户消息。通过 `dsh-agent-spine-demo`,顶层 agent 还会收到已配置的工作区指令与 persona、skill 目录、可见工具 schema,以及自有活动后续步骤所需的保留工具结果。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
每个模型步骤中的任务、提示词段、工具 schema、assistant 输出和工具结果都会消耗 token。JSON 事件流式输出和最终渲染不增加模型 token;委派的子工作有自己的模型用量,不计入父结果的 `usage` 总量。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
只要单次 agent 的提示词、schema、模型路由和会话前缀保持不变,工具轮次历史就仅追加。改变该组合会建立不同的请求前缀;JSON 输出模式不影响缓存。
|
||||
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
- **每个进程只创建一个新的顶层会话**:其工作区 cwd 是启动目录;此应用不支持恢复、第二条提示词、stdin 上下文或并发顶层会话。
|
||||
- **没有交互式问题或批准提供方**:需要人工回答的工具无法完成,除非其他叶节点按显式策略组合一个非交互式提供方。
|
||||
- **流式输出仅限顶层会话**:子会话不会平铺到流中,聚合用量只涵盖父活动区间记录的模型步骤。
|
||||
@@ -1,68 +0,0 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-cli-demo",
|
||||
"description": "Headless one-shot agent app with text and DSH-native JSON output",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
"bin": {
|
||||
"dsh-cli-demo": "lib/bin.js"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./invariant": {
|
||||
"types": "./lib/types/invariant.d.ts",
|
||||
"default": "./lib/invariant.js"
|
||||
},
|
||||
"./bin": {
|
||||
"types": "./lib/types/bin.d.ts",
|
||||
"default": "./lib/bin.js"
|
||||
},
|
||||
"./src/*": "./src/*",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"lib/bin.js",
|
||||
"lib/types/**/*.d.ts"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@cordisjs/plugin-include": "^1.0.4",
|
||||
"@cordisjs/plugin-loader": "^1.0.0-rc.5",
|
||||
"@deepseek-ai/dsh-agent": "^0.0.1",
|
||||
"@deepseek-ai/dsh-agent-spine-demo": "^0.0.1",
|
||||
"@deepseek-ai/dsh-app-boot": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-llm": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session-checkpoint-policy": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tools": "^0.0.1",
|
||||
"@deepseek-ai/dsh-workspace-context": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.7",
|
||||
"schemastery": "^3.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cordisjs/plugin-include": "workspace:^",
|
||||
"@cordisjs/plugin-loader": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-spine-demo": "workspace:^",
|
||||
"@deepseek-ai/dsh-app-boot": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-checkpoint-policy": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/dsh-workspace-context": "workspace:^",
|
||||
"cordis": "^4.0.0-rc.7",
|
||||
"schemastery": "^3.17.0"
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Process wrapper for `dsh-cli-demo`; covered parsing and task execution live in
|
||||
* `cli.ts` while this entry owns Unix signal-to-exit-code mapping.
|
||||
* @module @deepseek-ai/dsh-cli-demo/bin
|
||||
*/
|
||||
|
||||
import { installFailLoud } from '@deepseek-ai/dsh-app-boot'
|
||||
import { executeCli } from './cli.ts'
|
||||
|
||||
const NAME = 'dsh-cli-demo'
|
||||
|
||||
/* v8 ignore start -- thin self-executing process glue; built-bin tests exercise
|
||||
real argv, signals, Loader boot, output, and exit codes */
|
||||
const abort = new AbortController()
|
||||
let signalExitCode: number | undefined
|
||||
const interrupt = (signal: 'SIGINT' | 'SIGTERM', code: number): void => {
|
||||
signalExitCode ??= code
|
||||
if (!abort.signal.aborted) abort.abort(`received ${signal}`)
|
||||
}
|
||||
const onSigint = (): void => { interrupt('SIGINT', 130) }
|
||||
const onSigterm = (): void => { interrupt('SIGTERM', 143) }
|
||||
const uninstallFailLoud = installFailLoud(NAME)
|
||||
process.on('SIGINT', onSigint)
|
||||
process.on('SIGTERM', onSigterm)
|
||||
try {
|
||||
const code = await executeCli(process.argv.slice(2), { signal: abort.signal })
|
||||
process.exitCode = signalExitCode ?? code
|
||||
} finally {
|
||||
process.off('SIGINT', onSigint)
|
||||
process.off('SIGTERM', onSigterm)
|
||||
uninstallFailLoud()
|
||||
}
|
||||
/* v8 ignore stop */
|
||||
@@ -1,406 +0,0 @@
|
||||
/**
|
||||
* Command parser and one-turn driver for `dsh-cli-demo`. The executable wrapper
|
||||
* owns process signals; this module owns output, durability, and cleanup.
|
||||
* @module @deepseek-ai/dsh-cli-demo/cli
|
||||
*/
|
||||
|
||||
import { parseArgs } from 'node:util'
|
||||
import type { Context } from 'cordis'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { createUserMessage, type TokenUsage } from '@deepseek-ai/dsh-llm'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import { boot, loadEnv, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
|
||||
|
||||
const CLI_NAME = 'dsh-cli-demo'
|
||||
const DEFAULT_CONFIG_PATH = './cordis.yml'
|
||||
const OUTPUT_FORMATS = ['text', 'json', 'stream-json'] as const
|
||||
const USAGE = `Usage: ${CLI_NAME} [--config path] [--output-format text|json|stream-json] (-p <task> | <task>)\n`
|
||||
|
||||
/** Supported CLI output encodings. */
|
||||
export type OutputFormat = typeof OUTPUT_FORMATS[number]
|
||||
|
||||
/** Parsed command: help exits before boot; run carries one validated task. */
|
||||
export type CliCommand =
|
||||
| { readonly kind: 'help' }
|
||||
| {
|
||||
readonly kind: 'run'
|
||||
readonly configPath: string
|
||||
readonly outputFormat: OutputFormat
|
||||
readonly task: string
|
||||
}
|
||||
|
||||
/** DSH-native final record emitted by JSON modes. */
|
||||
export interface CliResult {
|
||||
readonly type: 'result'
|
||||
readonly sessionId: string
|
||||
readonly output: string
|
||||
readonly usage?: TokenUsage
|
||||
}
|
||||
|
||||
/** Options for one turn against the configured top-level agent. */
|
||||
export interface OneShotOptions {
|
||||
/** Exactly one nonblank user task. */
|
||||
readonly task: string
|
||||
/** Optional signal that cancels the selected agent. */
|
||||
readonly signal?: AbortSignal
|
||||
/** Synchronous task-turn observer; a throw cancels the agent and fails the run after flush. */
|
||||
readonly onEvent?: (sessionId: string, event: SessionEvent) => void
|
||||
}
|
||||
|
||||
/** Injectable process boundaries used by {@link executeCli}. */
|
||||
export interface CliRuntime {
|
||||
/** Process cwd for config resolution and `.env` loading. */
|
||||
readonly cwd?: string
|
||||
/** Cancellation signal, normally aborted by SIGINT or SIGTERM. */
|
||||
readonly signal?: AbortSignal
|
||||
/** Loader boot boundary. */
|
||||
readonly boot?: (name: string, absoluteConfigPath: string) => Promise<Context>
|
||||
/** Optional `.env` loader boundary. */
|
||||
readonly loadEnv?: (name: string, dir: string, warn: (line: string) => void) => void
|
||||
/** Stdout sink; throws are treated as output failures. */
|
||||
readonly writeStdout?: (chunk: string) => unknown
|
||||
/** Stderr diagnostic sink. */
|
||||
readonly writeStderr?: (chunk: string) => unknown
|
||||
/** Context disposal boundary. */
|
||||
readonly dispose?: (ctx: Context) => Promise<void>
|
||||
}
|
||||
|
||||
interface ParsedArguments {
|
||||
readonly values: {
|
||||
readonly config?: string
|
||||
readonly 'output-format'?: string
|
||||
readonly help?: boolean
|
||||
readonly prompt?: string
|
||||
}
|
||||
readonly positionals: string[]
|
||||
}
|
||||
|
||||
class CliArgumentError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = 'CliArgumentError'
|
||||
}
|
||||
}
|
||||
|
||||
class CliInterruptedError extends Error {
|
||||
constructor(reason: string) {
|
||||
super(reason)
|
||||
this.name = 'CliInterruptedError'
|
||||
}
|
||||
}
|
||||
|
||||
/** Render an arbitrary value without trusting its type traps or string coercion. */
|
||||
function renderUnknown(value: unknown): string {
|
||||
try {
|
||||
return String(value)
|
||||
} catch {
|
||||
return '[unrenderable thrown value]'
|
||||
}
|
||||
}
|
||||
|
||||
/** Normalize an arbitrary thrown value without letting inspection escape containment. */
|
||||
function toError(error: unknown): Error {
|
||||
try {
|
||||
if (error instanceof Error) return error
|
||||
} catch {
|
||||
// A hostile proxy may throw during instanceof; use the total renderer below.
|
||||
}
|
||||
return new Error(renderUnknown(error))
|
||||
}
|
||||
|
||||
function interruptionReason(signal: AbortSignal): string {
|
||||
return signal.reason === undefined ? 'interrupted' : renderUnknown(signal.reason)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the bin arguments and enforce the one-positional-task contract.
|
||||
* @param args - arguments after the executable name.
|
||||
* @returns a help or run command.
|
||||
* @throws {@link CliArgumentError} for unknown flags, invalid formats, or task cardinality.
|
||||
*/
|
||||
export function parseCliArgs(args: readonly string[]): CliCommand {
|
||||
let parsed: ParsedArguments
|
||||
try {
|
||||
parsed = parseArgs({
|
||||
args: [...args],
|
||||
options: {
|
||||
config: { type: 'string' },
|
||||
'output-format': { type: 'string' },
|
||||
help: { type: 'boolean' },
|
||||
prompt: { type: 'string', short: 'p' },
|
||||
},
|
||||
allowPositionals: true,
|
||||
strict: true,
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
throw new CliArgumentError(toError(error).message)
|
||||
}
|
||||
|
||||
if (parsed.values.help === true) return { kind: 'help' }
|
||||
const prompt = parsed.values.prompt
|
||||
if (prompt !== undefined && parsed.positionals.length > 0) {
|
||||
throw new CliArgumentError('-p/--prompt and a positional task are mutually exclusive')
|
||||
}
|
||||
if (prompt === undefined && parsed.positionals.length !== 1) {
|
||||
throw new CliArgumentError(`expected exactly one positional task or -p, received ${parsed.positionals.length} positional(s)`)
|
||||
}
|
||||
// Cardinality was checked above, so the fallback index zero exists.
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
const task = prompt ?? parsed.positionals[0]!
|
||||
if (task.trim().length === 0) throw new CliArgumentError('task must not be blank')
|
||||
|
||||
const requestedFormat = parsed.values['output-format'] ?? 'text'
|
||||
if (!OUTPUT_FORMATS.some(format => format === requestedFormat)) {
|
||||
throw new CliArgumentError(`unsupported output format ${JSON.stringify(requestedFormat)}`)
|
||||
}
|
||||
return {
|
||||
kind: 'run',
|
||||
configPath: parsed.values.config ?? DEFAULT_CONFIG_PATH,
|
||||
outputFormat: requestedFormat as OutputFormat,
|
||||
task,
|
||||
}
|
||||
}
|
||||
|
||||
function addUsage(total: TokenUsage | undefined, step: TokenUsage): TokenUsage {
|
||||
const next: TokenUsage = {
|
||||
inputTokens: (total?.inputTokens ?? 0) + step.inputTokens,
|
||||
outputTokens: (total?.outputTokens ?? 0) + step.outputTokens,
|
||||
}
|
||||
for (const key of ['cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens'] as const) {
|
||||
if (total?.[key] !== undefined || step[key] !== undefined) next[key] = (total?.[key] ?? 0) + (step[key] ?? 0)
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
function assistantText(event: Extract<SessionEvent, { type: 'assistant/message' }>): string | undefined {
|
||||
const blocks = event.data.message.content.filter(block => block.type === 'text')
|
||||
return blocks.length === 0 ? undefined : blocks.map(block => block.text).join('')
|
||||
}
|
||||
|
||||
/** Wait for startup quiescence while making pre-run cancellation terminal. */
|
||||
async function waitForStartupIdle(agent: Agent, signal?: AbortSignal): Promise<void> {
|
||||
if (signal === undefined) {
|
||||
await agent.whenIdle()
|
||||
return
|
||||
}
|
||||
if (signal.aborted) {
|
||||
agent.cancel({ kind: 'user' })
|
||||
throw new CliInterruptedError(interruptionReason(signal))
|
||||
}
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const onAbort = (): void => {
|
||||
agent.cancel({ kind: 'user' })
|
||||
reject(new CliInterruptedError(interruptionReason(signal)))
|
||||
}
|
||||
signal.addEventListener('abort', onAbort, { once: true })
|
||||
void agent.whenIdle().then(resolve, reject).finally(() => {
|
||||
signal.removeEventListener('abort', onAbort)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Run one owned activity interval on the configured top-level agent, from the
|
||||
* task's durable enqueue receipt through whole-agent idle.
|
||||
* @param ctx - settled Loader root containing one agent plus `ctx.sessions`.
|
||||
* @param options - task, optional cancellation, and optional stream observer.
|
||||
* @returns the DSH-native result envelope after durable quiescence.
|
||||
*/
|
||||
export async function runOneShot(ctx: Context, options: OneShotOptions): Promise<CliResult> {
|
||||
const agents = ctx.get('agents')?.roots() ?? []
|
||||
const [agent] = agents
|
||||
if (agent === undefined || agents.length !== 1) {
|
||||
throw new Error(`config must create exactly one top-level agent, found ${agents.length}`)
|
||||
}
|
||||
await waitForStartupIdle(agent, options.signal)
|
||||
|
||||
const message = createUserMessage({ content: [{ type: 'text', text: options.task }], source: { kind: 'user' } })
|
||||
let received = false
|
||||
let output = ''
|
||||
const usageByStep = new Map<string, TokenUsage>()
|
||||
let outputError: Error | undefined
|
||||
let interrupted: CliInterruptedError | undefined
|
||||
const observe = (sessionId: string, event: SessionEvent): void => {
|
||||
if (outputError !== undefined || options.onEvent === undefined) return
|
||||
try {
|
||||
options.onEvent(sessionId, event)
|
||||
} catch (error: unknown) {
|
||||
outputError = toError(error)
|
||||
queueMicrotask(() => {
|
||||
agent.cancel({ kind: 'user' })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const disposeListener = ctx.on('session/event', (session, event) => {
|
||||
if (session !== agent.session) return
|
||||
if (!received) {
|
||||
if (event.type !== 'agent/inbox/spliced'
|
||||
|| !event.data.inserted.some(inserted => inserted.id === message.id)) return
|
||||
received = true
|
||||
}
|
||||
observe(session.id, event)
|
||||
if (event.type === 'assistant/chunk' && event.data.chunk.type === 'usage') {
|
||||
usageByStep.set(`${event.data.turn}/${event.data.step}`, event.data.chunk.usage)
|
||||
}
|
||||
if (event.type === 'assistant/message') {
|
||||
output = assistantText(event) ?? output
|
||||
if (event.data.usage !== undefined) {
|
||||
usageByStep.set(`${event.data.turn}/${event.data.step}`, event.data.usage)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const signal = options.signal
|
||||
let onAbort: (() => void) | undefined
|
||||
if (signal !== undefined) {
|
||||
onAbort = (): void => {
|
||||
interrupted ??= new CliInterruptedError(interruptionReason(signal))
|
||||
agent.cancel({ kind: 'user' })
|
||||
}
|
||||
signal.addEventListener('abort', onAbort, { once: true })
|
||||
/* v8 ignore next -- closes the race between startup-idle completion and listener registration */
|
||||
if (signal.aborted) onAbort()
|
||||
}
|
||||
|
||||
try {
|
||||
if (interrupted === undefined) agent.followup(message)
|
||||
await agent.whenIdle()
|
||||
} finally {
|
||||
if (onAbort !== undefined) signal?.removeEventListener('abort', onAbort)
|
||||
disposeListener()
|
||||
}
|
||||
|
||||
await ctx.sessions.flush(agent.session)
|
||||
if (outputError !== undefined) throw outputError
|
||||
if (interrupted !== undefined) throw interrupted
|
||||
const usage = [...usageByStep.values()].reduce<TokenUsage | undefined>(addUsage, undefined)
|
||||
return {
|
||||
type: 'result',
|
||||
sessionId: agent.session.id,
|
||||
output,
|
||||
...usage === undefined ? {} : { usage },
|
||||
}
|
||||
}
|
||||
|
||||
function renderResult(outputFormat: OutputFormat, result: CliResult): string {
|
||||
return outputFormat === 'text' ? `${result.output}\n` : `${JSON.stringify(result)}\n`
|
||||
}
|
||||
|
||||
/**
|
||||
* Race Loader boot with cancellation without abandoning a context that becomes
|
||||
* available after the caller has been released. Waiting for that late context
|
||||
* would recreate the signal hang, so its disposal and diagnostics run detached.
|
||||
*/
|
||||
async function bootInterruptibly(
|
||||
start: () => Promise<Context>,
|
||||
signal: AbortSignal | undefined,
|
||||
disposeLateContext: (ctx: Context) => Promise<void>,
|
||||
reportLateDisposalFailure: (error: unknown) => void,
|
||||
): Promise<Context> {
|
||||
if (signal === undefined) return await start()
|
||||
if (signal.aborted) throw new CliInterruptedError(interruptionReason(signal))
|
||||
|
||||
let onAbort!: () => void
|
||||
const interruptedBoot = new Promise<never>((_resolve, reject) => {
|
||||
onAbort = (): void => {
|
||||
reject(new CliInterruptedError(interruptionReason(signal)))
|
||||
}
|
||||
signal.addEventListener('abort', onAbort, { once: true })
|
||||
/* v8 ignore next -- closes registration against a non-standard synchronously mutating signal */
|
||||
if (signal.aborted) onAbort()
|
||||
})
|
||||
const booting = Promise.resolve().then(start)
|
||||
try {
|
||||
return await Promise.race([booting, interruptedBoot])
|
||||
} catch (error: unknown) {
|
||||
// The awaited race permits the signal to change after the preflight check.
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-condition
|
||||
if (signal.aborted) {
|
||||
void booting.then(
|
||||
async (lateContext) => {
|
||||
try {
|
||||
await disposeLateContext(lateContext)
|
||||
} catch (error: unknown) {
|
||||
reportLateDisposalFailure(error)
|
||||
}
|
||||
},
|
||||
() => {},
|
||||
)
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
signal.removeEventListener('abort', onAbort)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute one CLI invocation. Argument and boot failures never write stdout;
|
||||
* context disposal is awaited before return, and its failure does not replace
|
||||
* an earlier diagnostic.
|
||||
* @param args - arguments after the executable name.
|
||||
* @param runtime - optional injected process boundaries for tests and embedding.
|
||||
* @returns the ordinary process exit code; the thin bin overrides it for Unix signals.
|
||||
*/
|
||||
export async function executeCli(args: readonly string[], runtime: CliRuntime = {}): Promise<number> {
|
||||
/* v8 ignore next -- default process sinks are exercised by the built-bin smoke */
|
||||
const writeStdout = runtime.writeStdout ?? (chunk => process.stdout.write(chunk))
|
||||
/* v8 ignore next -- default process sinks are exercised by the built-bin smoke */
|
||||
const writeStderr = runtime.writeStderr ?? (chunk => process.stderr.write(chunk))
|
||||
let command: CliCommand
|
||||
try {
|
||||
command = parseCliArgs(args)
|
||||
} catch (error: unknown) {
|
||||
writeStderr(`${CLI_NAME}: ${toError(error).message}\n${USAGE}`)
|
||||
return 1
|
||||
}
|
||||
if (command.kind === 'help') {
|
||||
writeStdout(USAGE)
|
||||
return 0
|
||||
}
|
||||
|
||||
/* v8 ignore next -- default process cwd is exercised by the built-bin smoke */
|
||||
const cwd = runtime.cwd ?? process.cwd()
|
||||
/* v8 ignore next -- default env/boot boundaries are exercised by the Loader and built-bin smokes */
|
||||
const loadEnvironment = runtime.loadEnv ?? loadEnv
|
||||
/* v8 ignore next -- default env/boot boundaries are exercised by the Loader and built-bin smokes */
|
||||
const bootContext = runtime.boot ?? boot
|
||||
/* v8 ignore next -- default disposal is exercised by the built-bin smoke */
|
||||
const disposeContext = runtime.dispose ?? (target => target.fiber.dispose())
|
||||
let ctx: Context | undefined
|
||||
let exitCode = 1
|
||||
let diagnostic: string | undefined
|
||||
try {
|
||||
loadEnvironment(CLI_NAME, cwd, line => writeStderr(line))
|
||||
ctx = await bootInterruptibly(
|
||||
() => bootContext(CLI_NAME, resolveConfigPath(command.configPath, undefined, cwd)),
|
||||
runtime.signal,
|
||||
disposeContext,
|
||||
error => writeStderr(`${CLI_NAME}: dispose after interrupted boot failed: ${toError(error).message}\n`),
|
||||
)
|
||||
const result = await runOneShot(ctx, {
|
||||
task: command.task,
|
||||
...runtime.signal === undefined ? {} : { signal: runtime.signal },
|
||||
...command.outputFormat === 'stream-json'
|
||||
? { onEvent: (sessionId: string, event: SessionEvent) => {
|
||||
writeStdout(`${JSON.stringify({ type: 'session_event', sessionId, event })}\n`)
|
||||
} }
|
||||
: {},
|
||||
})
|
||||
writeStdout(renderResult(command.outputFormat, result))
|
||||
exitCode = 0
|
||||
} catch (error: unknown) {
|
||||
diagnostic = `${CLI_NAME}: ${toError(error).message}\n`
|
||||
} finally {
|
||||
if (ctx !== undefined) {
|
||||
try {
|
||||
await disposeContext(ctx)
|
||||
} catch (error: unknown) {
|
||||
diagnostic = `${diagnostic ?? ''}${CLI_NAME}: dispose failed: ${toError(error).message}\n`
|
||||
exitCode = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if (diagnostic !== undefined) writeStderr(diagnostic)
|
||||
return exitCode
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
/**
|
||||
* Headless one-shot app composition: the default agent spine, JSONL session
|
||||
* persistence, and one fresh top-level agent. The CLI driver owns task
|
||||
* submission and output; the app deliberately mounts no interactive or logging
|
||||
* front door so stdout remains protocol-pure.
|
||||
* @module @deepseek-ai/dsh-cli-demo
|
||||
*/
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import z from 'schemastery'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import ToolRegistry, { type Config as ToolsConfig } from '@deepseek-ai/dsh-tools'
|
||||
import * as agentCore from '@deepseek-ai/dsh-agent-spine-demo'
|
||||
import SessionPersistenceJsonl, {
|
||||
JsonlCompressionSchema,
|
||||
type JsonlCompression,
|
||||
} from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
import * as sessionCheckpointPolicy from '@deepseek-ai/dsh-session-checkpoint-policy'
|
||||
import * as workspaceContext from '@deepseek-ai/dsh-workspace-context'
|
||||
|
||||
const DEFAULT_PERSISTENCE_ROOT = './.sessions'
|
||||
|
||||
export const name = 'cli-demo'
|
||||
|
||||
/** App config forwarded to the spine, configured agent, and JSONL backend. */
|
||||
export interface Config {
|
||||
/** Provider route for the configured agent. */
|
||||
provider: string
|
||||
/** Model name for the configured agent; a matching adapter must be registered. */
|
||||
model: string
|
||||
/** Bundled agent-loop concurrency cap; `1` is serial and omission uses its default. */
|
||||
maxParallelToolCalls?: number
|
||||
/** Deployment persona forwarded to the system-prompt plugin. */
|
||||
persona?: string
|
||||
/** Explicit model-facing tool order forwarded to the system-prompt plugin. */
|
||||
toolOrder?: string[]
|
||||
/** Tool-registry presentation config forwarded through agent-spine-demo. */
|
||||
tools?: ToolsConfig
|
||||
/** DeepSeek Harness home directory exposed to bash and used for local skill discovery. */
|
||||
dshHome?: string
|
||||
/** Fallback session-title limits forwarded through agent-spine-demo. */
|
||||
sessionTitle?: NonNullable<agentCore.Config['sessionTitle']>
|
||||
/** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
|
||||
persistenceRoot?: string
|
||||
/** JSONL artifact encoding; defaults to checksummed Zstandard frames. */
|
||||
persistenceCompression?: JsonlCompression
|
||||
/** Skill registry, local-provider, and model-facing consumer config. */
|
||||
skills?: agentCore.SkillConfig
|
||||
/** Model-facing bash tool config forwarded through agent-spine-demo. */
|
||||
toolBash?: NonNullable<agentCore.Config['toolBash']>
|
||||
/** Generic background-task control-tool config forwarded through agent-spine-demo. */
|
||||
toolTasks?: NonNullable<agentCore.Config['toolTasks']>
|
||||
/** Controls automatic AGENTS.md/CLAUDE.md loading; configure a byte budget or set `false`. */
|
||||
workspaceContext: agentCore.Config['workspaceContext']
|
||||
}
|
||||
|
||||
// Each front door keeps a complete Loader schema so its deployment contract is
|
||||
// readable without a cross-package config facade.
|
||||
/* jscpd:ignore-start */
|
||||
export const Config: z<Config> = z.object({
|
||||
provider: z.string().required(),
|
||||
model: z.string().required(),
|
||||
maxParallelToolCalls: z.number().step(1).min(1),
|
||||
persistenceRoot: z.string().default(DEFAULT_PERSISTENCE_ROOT),
|
||||
persistenceCompression: JsonlCompressionSchema,
|
||||
persona: z.string(),
|
||||
dshHome: z.string(),
|
||||
sessionTitle: agentCore.SessionTitleConfigSchema,
|
||||
skills: agentCore.SkillConfigSchema,
|
||||
// Absent means lexicographic order; schemastery's native array default is [].
|
||||
toolOrder: z.array(z.string()).default(undefined as unknown as string[]),
|
||||
tools: ToolRegistry.Config,
|
||||
toolBash: agentCore.ToolBashConfigSchema,
|
||||
toolTasks: z.union([z.const(false), agentCore.ToolTasksConfigSchema]),
|
||||
workspaceContext: z.union([z.const(false), workspaceContext.Config]).required(),
|
||||
})
|
||||
/* jscpd:ignore-end */
|
||||
|
||||
/**
|
||||
* Compose the UI-less spine, a fresh top-level agent rooted at the process cwd,
|
||||
* and JSONL persistence. Swappable adapters, executors, and product tools stay
|
||||
* in the leaf `cordis.yml`.
|
||||
* @param ctx - app context that owns the composed child plugins.
|
||||
* @param config - validated app configuration.
|
||||
*/
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
ctx.plugin(agentCore, {
|
||||
...agentCore.pickSpineConfig(config),
|
||||
agents: [{ id: SessionId('main'), provider: config.provider, model: config.model, cwd: process.cwd() }],
|
||||
})
|
||||
ctx.plugin(SessionPersistenceJsonl, {
|
||||
root: config.persistenceRoot ?? DEFAULT_PERSISTENCE_ROOT,
|
||||
...(config.persistenceCompression === undefined ? {} : { compression: config.persistenceCompression }),
|
||||
})
|
||||
ctx.plugin(sessionCheckpointPolicy)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
/**
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-cli-demo`.
|
||||
* @module @deepseek-ai/dsh-cli-demo/invariant
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from 'cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-cli-demo'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'cli-demo-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: this composition package owns no independent event stream or mutable data;
|
||||
* Loader and built-entry tests cover its wiring.
|
||||
*/
|
||||
const install: InvariantInstaller = () => {}
|
||||
|
||||
/**
|
||||
* Register this package's invariant companion.
|
||||
* @param ctx - Cordis context carrying the invariant service.
|
||||
* @returns the installed registration's disposer after setup succeeds.
|
||||
*/
|
||||
export const apply = (ctx: Context): Promise<() => void> =>
|
||||
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
||||
/* jscpd:ignore-end */
|
||||
@@ -1,223 +0,0 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { mkdtemp, mkdir, readFile, readdir, rm, symlink, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
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'
|
||||
|
||||
/**
|
||||
* Published-entry smoke: run `lib/bin.js` under plain Node in a symlinked external consumer.
|
||||
* The consumer's mock model is an example-local TypeScript plugin (Node 22.19+ — the engines
|
||||
* floor — strips types natively, so plain `node` loads it), its config carries a `disabled:
|
||||
* true` unresolvable entry (the fail-loud entry-load guard must not mistake an intentionally
|
||||
* fiber-less entry for a failed import), and the optional spill pair loads from the consumer
|
||||
* install — so every passing boot proves all three alongside the CLI's own output contract.
|
||||
*/
|
||||
|
||||
const repoRoot = fileURLToPath(new URL('../../../../', import.meta.url))
|
||||
const cliBin = join(repoRoot, 'packages/examples/cli-demo/lib/bin.js')
|
||||
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', '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',
|
||||
'spill/spill', 'spill/spill-local', 'spill/spill-policy', 'util/retention',
|
||||
]
|
||||
const vendorPackages = ['cordis', 'loader', 'include', 'timer', 'schemastery', 'cosmokit']
|
||||
|
||||
async function packageName(dir: string): Promise<string> {
|
||||
return (JSON.parse(await readFile(join(dir, 'package.json'), 'utf8')) as { name: string }).name
|
||||
}
|
||||
|
||||
async function linkPackage(dir: string, nodeModules: string): Promise<void> {
|
||||
const target = join(nodeModules, await packageName(dir))
|
||||
await mkdir(dirname(target), { recursive: true })
|
||||
await symlink(dir, target)
|
||||
}
|
||||
|
||||
async function makeConsumer(): Promise<string> {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'cli-built-bin-'))
|
||||
const nodeModules = join(dir, 'node_modules')
|
||||
for (const rel of dshPackages) await linkPackage(join(repoRoot, 'packages', rel), nodeModules)
|
||||
for (const rel of vendorPackages) await linkPackage(join(repoRoot, 'vendor', rel), nodeModules)
|
||||
await writeFile(join(dir, 'mock-llm.ts'), [
|
||||
// Real type annotations: this file exists to prove plain Node's type
|
||||
// stripping loads an example-local TS plugin from a built consumer.
|
||||
"import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'",
|
||||
"import type { Context } from 'cordis'",
|
||||
'class Mock extends LlmAdapter {',
|
||||
' async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {',
|
||||
" const text: string = options.messages.flatMap(message => message.content).filter(block => block.type === 'text').at(-1)?.text ?? ''",
|
||||
" yield { type: 'block-start', index: 0, blockType: 'text' }",
|
||||
" if (text === 'hang') {",
|
||||
" yield { type: 'text-delta', index: 0, text: 'partial' }",
|
||||
' await new Promise<never>((resolve, reject) => {',
|
||||
" const timer = setTimeout(() => reject(new Error('hang timeout')), 30000)",
|
||||
" const onAbort = () => { clearTimeout(timer); reject(new Error('aborted')) }",
|
||||
' if (options.signal.aborted) onAbort()',
|
||||
" else options.signal.addEventListener('abort', onAbort, { once: true })",
|
||||
' })',
|
||||
' return',
|
||||
' }',
|
||||
' const reply = `BUILT: ${text}`',
|
||||
" yield { type: 'text-delta', index: 0, text: reply }",
|
||||
" yield { type: 'block-end', index: 0, block: { type: 'text', text: reply } }",
|
||||
" yield { type: 'usage', usage: { inputTokens: 4, outputTokens: 2 } }",
|
||||
" yield { type: 'finish', reason: { kind: 'stop' } }",
|
||||
' }',
|
||||
'}',
|
||||
"export const name = 'built-cli-mock'",
|
||||
"export const inject = ['llm']",
|
||||
"export function apply(ctx: Context) { ctx.llm.registerAdapter(['built-cli-mock'], new Mock()) }",
|
||||
'',
|
||||
].join('\n'))
|
||||
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',
|
||||
" name: '@deepseek-ai/dsh-cli-demo'",
|
||||
' config:',
|
||||
' provider: built-cli-mock',
|
||||
' model: built-cli-mock',
|
||||
" persona: 'built CLI test'",
|
||||
" persistenceRoot: './.sessions'",
|
||||
' workspaceContext: false',
|
||||
'- id: spill-local',
|
||||
" name: '@deepseek-ai/dsh-spill-local'",
|
||||
'- id: spill-policy',
|
||||
" name: '@deepseek-ai/dsh-spill-policy'",
|
||||
' config:',
|
||||
' maxInlineBytes: 50000',
|
||||
// A `disabled: true` entry settles without a fiber by design; the fail-loud
|
||||
// entry-load guard must not mistake it for a failed import. The nonexistent
|
||||
// path makes that distinction observable while a clean run proves boot continued.
|
||||
'- id: off',
|
||||
" name: './does-not-exist.ts'",
|
||||
' disabled: true',
|
||||
'',
|
||||
].join('\n'))
|
||||
return dir
|
||||
}
|
||||
|
||||
interface BinResult {
|
||||
readonly code: number
|
||||
readonly signal: NodeJS.Signals | null
|
||||
readonly stdout: string
|
||||
readonly stderr: string
|
||||
}
|
||||
|
||||
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
|
||||
subprocess.stdout.on('data', (chunk: Buffer) => {
|
||||
streamed += chunk.toString('utf8')
|
||||
if (!interrupted && streamed.includes('assistant/chunk')) {
|
||||
interrupted = true
|
||||
subprocess.kill(interrupt)
|
||||
}
|
||||
})
|
||||
}
|
||||
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
|
||||
|
||||
afterEach(async () => {
|
||||
if (consumer !== undefined) await rm(consumer, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 })
|
||||
consumer = undefined
|
||||
})
|
||||
|
||||
describe.skipIf(!existsSync(cliBin))('dsh-cli-demo BUILT bin', () => {
|
||||
it('runs text, json, and stream-json under plain Node and persists fresh sessions', async () => {
|
||||
consumer = await makeConsumer()
|
||||
const text = await runBuiltBin(consumer, ['--config', './cordis.yml', 'hello'])
|
||||
expect(text).toMatchObject({ code: 0, signal: null, stdout: 'BUILT: hello\n', stderr: '' })
|
||||
|
||||
const json = await runBuiltBin(consumer, ['--config', './cordis.yml', '--output-format', 'json', 'json task'])
|
||||
expect(JSON.parse(json.stdout)).toMatchObject({
|
||||
type: 'result', output: 'BUILT: json task',
|
||||
usage: { inputTokens: 4, outputTokens: 2 },
|
||||
})
|
||||
|
||||
const stream = await runBuiltBin(consumer, ['--config', './cordis.yml', '--output-format', 'stream-json', 'stream task'])
|
||||
const lines = stream.stdout.trimEnd().split('\n').map(line => JSON.parse(line) as Record<string, unknown>)
|
||||
expect(lines[0]).toMatchObject({
|
||||
type: 'session_event',
|
||||
event: {
|
||||
type: 'agent/inbox/spliced',
|
||||
data: {
|
||||
target: 'next-turn',
|
||||
start: 0,
|
||||
inserted: [{ content: [{ type: 'text', text: 'stream task' }], source: { kind: 'user' } }],
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(lines.findIndex(line =>
|
||||
(line['event'] as { type?: string } | undefined)?.type === 'turn/start')).toBeGreaterThan(0)
|
||||
expect(lines.at(-1)).toMatchObject({ type: 'result', output: 'BUILT: stream task' })
|
||||
const sessionsRoot = join(consumer, '.sessions')
|
||||
const files = await readdir(sessionsRoot, { recursive: true })
|
||||
const logs = files.filter(file => file.endsWith('.jsonl.zstd'))
|
||||
expect(logs).toHaveLength(3)
|
||||
const compressed = await readFile(join(sessionsRoot, logs[0]!))
|
||||
expect(compressed.subarray(0, 4).toString('hex')).toBe('28b52ffd')
|
||||
expect(JSON.parse((await decompress(compressed)).toString())).toMatchObject({ type: 'session' })
|
||||
}, 30_000)
|
||||
|
||||
it('keeps stdout empty for invalid argv and missing config', async () => {
|
||||
consumer = await makeConsumer()
|
||||
for (const args of [
|
||||
['--config', './cordis.yml'],
|
||||
['--config', './cordis.yml', 'one', 'two'],
|
||||
['--config', './missing.yml', 'task'],
|
||||
]) {
|
||||
const result = await runBuiltBin(consumer, args)
|
||||
expect(result.code).not.toBe(0)
|
||||
expect(result.stdout).toBe('')
|
||||
expect(result.stderr.length).toBeGreaterThan(0)
|
||||
}
|
||||
}, 30_000)
|
||||
|
||||
describe.skipIf(process.platform === 'win32')('POSIX signal delivery', () => {
|
||||
it.each([
|
||||
['SIGINT', 130],
|
||||
['SIGTERM', 143],
|
||||
] as const)('cancels and disposes on %s with exit %i', async (signal, code) => {
|
||||
consumer = await makeConsumer()
|
||||
const result = await runBuiltBin(
|
||||
consumer,
|
||||
['--config', './cordis.yml', '--output-format', 'stream-json', 'hang'],
|
||||
signal,
|
||||
)
|
||||
expect(result, JSON.stringify(result)).toMatchObject({ code, signal: null })
|
||||
expect(result.stdout).toContain('"kind":"aborted"')
|
||||
expect(result.stderr).toBe(`dsh-cli-demo: received ${signal}\n`)
|
||||
}, 30_000)
|
||||
})
|
||||
})
|
||||
@@ -1,202 +0,0 @@
|
||||
import { mkdtemp } from 'node:fs/promises'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import { agentEvents } from '@deepseek-ai/dsh-agent'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { CallId, type Message } from '@deepseek-ai/dsh-llm'
|
||||
import { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type { ToolExecution } from '@deepseek-ai/dsh-tools'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import * as cliDemo from '../src/index.ts'
|
||||
|
||||
const testToolSignal = new AbortController().signal
|
||||
|
||||
const contexts: Context[] = []
|
||||
|
||||
async function skillConfig(catalogDescriptionMaxLength?: number): Promise<NonNullable<cliDemo.Config['skills']>> {
|
||||
const home = await mkdtemp(join(tmpdir(), 'dsh-cli-demo-skills-'))
|
||||
return {
|
||||
local: { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents') },
|
||||
...catalogDescriptionMaxLength === undefined ? {} : { tool: { catalogDescriptionMaxLength } },
|
||||
}
|
||||
}
|
||||
|
||||
async function mount(config: cliDemo.Config, withBash = false): Promise<Context> {
|
||||
const ctx = new Context()
|
||||
if (withBash) {
|
||||
ctx.provide('bash', {
|
||||
sandboxMode: undefined,
|
||||
resolve() { throw new Error('composition test does not execute bash') },
|
||||
run() { throw new Error('composition test does not execute bash') },
|
||||
start() { throw new Error('composition test does not execute bash') },
|
||||
})
|
||||
}
|
||||
contexts.push(ctx)
|
||||
config.persistenceRoot ??= await mkdtemp(join(tmpdir(), 'dsh-cli-demo-persistence-'))
|
||||
await ctx.plugin(cliDemo, config)
|
||||
await new Promise(resolve => setTimeout(resolve, 80))
|
||||
return ctx
|
||||
}
|
||||
|
||||
async function composePrefix(ctx: Context): Promise<Message[]> {
|
||||
const agent = ctx.agentLoop.create(SessionId(`cli-demo-prefix-${randomUUID()}`), {}, { cwd: '/tmp' })
|
||||
const signal = new AbortController().signal
|
||||
const decision = await agentEvents(ctx, agent).waterfall(
|
||||
'agent/pre-step', { messages: [], turn: 1, step: 1, signal },
|
||||
() => Promise.resolve({ kind: 'enter', messages: [] }),
|
||||
)
|
||||
if (decision.kind === 'enter') {
|
||||
for (const message of decision.messages) {
|
||||
agent.session.append('user/message', message, { surfaceOp: 'append' })
|
||||
}
|
||||
}
|
||||
return agent.session.deriveMessages()
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose()))
|
||||
})
|
||||
|
||||
describe('dsh-cli-demo app composition', () => {
|
||||
it('composes the UI-less spine, JSONL persistence, and a main agent', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-cli-demo-compose-'))
|
||||
const ctx = await mount({
|
||||
provider: 'mock',
|
||||
model: 'mock',
|
||||
persona: 'Headless.',
|
||||
tools: { mode: 'native' },
|
||||
persistenceRoot: root,
|
||||
persistenceCompression: 'none',
|
||||
skills: await skillConfig(),
|
||||
workspaceContext: false,
|
||||
})
|
||||
const [agent] = ctx.get('agents')?.roots() ?? []
|
||||
expect(ctx.get('agentLoop')).toBeDefined()
|
||||
expect(ctx.get('sessionPersistence')).toBeDefined()
|
||||
expect((ctx.get('sessionPersistence') as unknown as { config: { compression?: string } }).config.compression).toBe('none')
|
||||
expect(agent?.session.header.cwd).toBe(process.cwd())
|
||||
expect(ctx.get('userInteraction')).toBeUndefined()
|
||||
expect(ctx.get('tools')?.get('ask_user_question')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('covers direct-apply defaults and forwards skill and tool-order config', async () => {
|
||||
const oldDshHome = process.env.DSH_HOME
|
||||
const oldAgentsHome = process.env.DSH_AGENTS_HOME
|
||||
const home = await mkdtemp(join(tmpdir(), 'dsh-cli-demo-defaults-'))
|
||||
process.env.DSH_HOME = join(home, '.dsh')
|
||||
process.env.DSH_AGENTS_HOME = join(home, '.agents')
|
||||
try {
|
||||
const ctx = new Context()
|
||||
contexts.push(ctx)
|
||||
cliDemo.apply(ctx, { provider: 'mock', model: 'mock', workspaceContext: false })
|
||||
await new Promise(resolve => setTimeout(resolve, 80))
|
||||
expect(ctx.get('sessionPersistence')).toBeDefined()
|
||||
const [agent] = ctx.get('agents')?.roots() ?? []
|
||||
expect(agent?.session.id).toMatch(/^main-session-/)
|
||||
expect(await ctx.skills.list()).toEqual([])
|
||||
} finally {
|
||||
if (oldDshHome === undefined) delete process.env.DSH_HOME
|
||||
else process.env.DSH_HOME = oldDshHome
|
||||
if (oldAgentsHome === undefined) delete process.env.DSH_AGENTS_HOME
|
||||
else process.env.DSH_AGENTS_HOME = oldAgentsHome
|
||||
}
|
||||
|
||||
const ctx = await mount({
|
||||
provider: 'mock',
|
||||
model: 'mock',
|
||||
toolOrder: ['zulu', TOOL_ORDER_REST],
|
||||
skills: await skillConfig(6),
|
||||
workspaceContext: false,
|
||||
})
|
||||
ctx.skills.register({ name: 'cli-skill', description: 'CLI skill', source: 'runtime', content: 'body' })
|
||||
for (const name of ['alpha', 'zulu']) {
|
||||
ctx.tools.register({
|
||||
name,
|
||||
description: name,
|
||||
parameters: {},
|
||||
output: { schema: { type: 'null' }, render: () => [] },
|
||||
execute: async () => null,
|
||||
})
|
||||
}
|
||||
expect(JSON.stringify(await composePrefix(ctx))).toContain('- `cli-skill`: CLI...')
|
||||
expect((await ctx.systemPrompt.assemble()).tools.map(tool => tool.name)).toEqual([
|
||||
'zulu',
|
||||
'alpha',
|
||||
'skill',
|
||||
'task_kill',
|
||||
'task_list',
|
||||
'task_output',
|
||||
])
|
||||
})
|
||||
|
||||
it('forwards the complete shared spine configuration', async () => {
|
||||
const dshHome = await mkdtemp(join(tmpdir(), 'dsh-cli-demo-home-'))
|
||||
const agentsHome = await mkdtemp(join(tmpdir(), 'dsh-cli-demo-agents-'))
|
||||
const ctx = await mount({
|
||||
provider: 'mock',
|
||||
model: 'mock',
|
||||
maxParallelToolCalls: 3,
|
||||
dshHome,
|
||||
skills: { local: { agentsHome } },
|
||||
toolBash: { enableRunInBackground: false },
|
||||
toolTasks: { waitTimeoutMs: 7, maxWaitTimeoutMs: 11 },
|
||||
workspaceContext: false,
|
||||
}, true)
|
||||
|
||||
expect(ctx.get('agentLoop')?.config.maxParallelToolCalls).toBe(3)
|
||||
const execution: ToolExecution = {
|
||||
signal: testToolSignal,
|
||||
token: Symbol('cli-demo-dsh-home-test') as ToolExecution['token'],
|
||||
callId: CallId('cli-demo-dsh-home'),
|
||||
name: 'bash',
|
||||
arguments: { command: 'true' },
|
||||
}
|
||||
expect(ctx.bashEnv.collect(execution)).toMatchObject({ DSH_HOME: dshHome })
|
||||
const bash = ctx.tools.schemas().find(tool => tool.name === 'bash')
|
||||
expect(Object.keys((bash!.parameters as { properties: Record<string, unknown> }).properties))
|
||||
.not.toContain('run_in_background')
|
||||
|
||||
const id = ctx.tasks.start({
|
||||
kind: 'bash',
|
||||
label: 'config forwarding probe',
|
||||
run: () => ({ cancel: () => {}, done: Promise.resolve({ status: 'completed' }) }),
|
||||
})
|
||||
const wait = vi.spyOn(ctx.tasks, 'wait')
|
||||
await ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('cli-demo-task-config'),
|
||||
name: 'task_output',
|
||||
arguments: { task_id: id, wait: true },
|
||||
})
|
||||
expect(wait).toHaveBeenCalledWith(id, 7, undefined, testToolSignal)
|
||||
})
|
||||
|
||||
it('accepts false to keep task services without model-facing task controls', async () => {
|
||||
const ctx = await mount({
|
||||
provider: 'mock',
|
||||
model: 'mock',
|
||||
skills: { enabled: false },
|
||||
toolTasks: false,
|
||||
workspaceContext: false,
|
||||
})
|
||||
|
||||
expect(ctx.get('tasks')).toBeDefined()
|
||||
expect(ctx.get('tools')?.get('task_output')).toBeUndefined()
|
||||
expect(ctx.get('tools')?.get('task_list')).toBeUndefined()
|
||||
expect(ctx.get('tools')?.get('task_kill')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('exposes the Loader-safe namespace plugin shape and schema', () => {
|
||||
expect(cliDemo.name).toBe('cli-demo')
|
||||
expect(cliDemo.Config).toBeDefined()
|
||||
expect('default' in cliDemo).toBe(false)
|
||||
const loader = Object.create(Loader.prototype) as Loader
|
||||
const unwrapped = loader.unwrapExports(cliDemo) as Record<string, unknown>
|
||||
expect(unwrapped).toBe(cliDemo)
|
||||
expect(unwrapped.name).toBe('cli-demo')
|
||||
expect(typeof unwrapped.apply).toBe('function')
|
||||
})
|
||||
})
|
||||
@@ -1,614 +0,0 @@
|
||||
import { readdir, mkdtemp } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { Context } from 'cordis'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { createUserMessage,
|
||||
CallId,
|
||||
LlmAdapter,
|
||||
resolveRetryPolicy,
|
||||
type GenerateOptions,
|
||||
type ResolvedRetryPolicy,
|
||||
type StreamChunk,
|
||||
type TokenUsage,
|
||||
} from '@deepseek-ai/dsh-llm'
|
||||
import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import * as cliDemo from '../src/index.ts'
|
||||
import {
|
||||
executeCli,
|
||||
parseCliArgs,
|
||||
runOneShot,
|
||||
type CliResult,
|
||||
} from '../src/cli.ts'
|
||||
|
||||
type ScriptEntry = readonly StreamChunk[] | 'hang'
|
||||
|
||||
class ScriptedAdapter extends LlmAdapter {
|
||||
readonly requests: GenerateOptions[] = []
|
||||
private cursor = 0
|
||||
private readonly retryPolicy = resolveRetryPolicy({
|
||||
mode: 'normal',
|
||||
backoff: { initialDelayMs: 1, maxDelayMs: 1, jitterRatio: 0 },
|
||||
}, 'cli test provider retryPolicy')
|
||||
|
||||
constructor(private readonly script: readonly ScriptEntry[]) {
|
||||
super()
|
||||
}
|
||||
|
||||
override providerRetryPolicy(_provider: string): ResolvedRetryPolicy {
|
||||
return this.retryPolicy
|
||||
}
|
||||
|
||||
async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
||||
this.requests.push(options)
|
||||
const entry = this.script[this.cursor++]
|
||||
if (entry === undefined) throw new Error('script exhausted')
|
||||
if (entry === 'hang') {
|
||||
yield { type: 'block-start', index: 0, blockType: 'text' }
|
||||
yield { type: 'text-delta', index: 0, text: 'partial' }
|
||||
await new Promise<void>((_resolve, reject) => {
|
||||
if (options.signal?.aborted === true) {
|
||||
reject(new Error('aborted'))
|
||||
return
|
||||
}
|
||||
options.signal?.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true })
|
||||
})
|
||||
return
|
||||
}
|
||||
for (const chunk of entry) yield chunk
|
||||
}
|
||||
}
|
||||
|
||||
function textResponse(text: string, usage?: TokenUsage, finish: 'stop' | 'max-tokens' = 'stop'): StreamChunk[] {
|
||||
return [
|
||||
{ type: 'block-start', index: 0, blockType: 'text' },
|
||||
{ type: 'text-delta', index: 0, text },
|
||||
{ type: 'block-end', index: 0, block: { type: 'text', text } },
|
||||
...usage === undefined ? [] : [{ type: 'usage', usage } as const],
|
||||
{ type: 'finish', reason: { kind: finish } },
|
||||
]
|
||||
}
|
||||
|
||||
function toolResponse(usage: TokenUsage): StreamChunk[] {
|
||||
const id = CallId('cli-call')
|
||||
const args = JSON.stringify({ text: 'round trip' })
|
||||
return [
|
||||
{ type: 'block-start', index: 0, blockType: 'text' },
|
||||
{ type: 'text-delta', index: 0, text: 'working' },
|
||||
{ type: 'block-end', index: 0, block: { type: 'text', text: 'working' } },
|
||||
{ type: 'block-start', index: 1, blockType: 'tool-call' },
|
||||
{ type: 'tool-call-delta', index: 1, id, name: 'echo', argumentsDelta: args },
|
||||
{ type: 'block-end', index: 1, block: { type: 'tool-call', id, name: 'echo', arguments: args } },
|
||||
{ type: 'usage', usage },
|
||||
{ type: 'finish', reason: { kind: 'tool-calls' } },
|
||||
]
|
||||
}
|
||||
|
||||
function failedResponse(usage: TokenUsage): StreamChunk[] {
|
||||
return [
|
||||
{ type: 'block-start', index: 0, blockType: 'text' },
|
||||
{ type: 'text-delta', index: 0, text: 'discarded' },
|
||||
{ type: 'usage', usage },
|
||||
{ type: 'finish', reason: { kind: 'error', failure: { message: 'temporary', code: 'SERVER' } } },
|
||||
]
|
||||
}
|
||||
|
||||
function reasoningResponse(text: string): StreamChunk[] {
|
||||
return [
|
||||
{ type: 'block-start', index: 0, blockType: 'reasoning' },
|
||||
{ type: 'reasoning-delta', index: 0, text },
|
||||
{ type: 'block-end', index: 0, block: { type: 'reasoning', text } },
|
||||
{ type: 'finish', reason: { kind: 'stop' } },
|
||||
]
|
||||
}
|
||||
|
||||
interface Harness {
|
||||
readonly ctx: Context
|
||||
readonly agent: Agent
|
||||
readonly persistenceRoot: string
|
||||
}
|
||||
|
||||
const liveContexts: Context[] = []
|
||||
|
||||
async function harness(script: readonly ScriptEntry[]): Promise<Harness> {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-cli-runner-'))
|
||||
const ctx = new Context()
|
||||
liveContexts.push(ctx)
|
||||
await ctx.plugin(cliDemo, {
|
||||
provider: 'mock',
|
||||
model: 'mock',
|
||||
persistenceRoot: root,
|
||||
skills: { enabled: false },
|
||||
workspaceContext: false,
|
||||
})
|
||||
await new Promise(resolve => setTimeout(resolve, 80))
|
||||
ctx.llm.registerAdapter(['mock'], new ScriptedAdapter(script))
|
||||
ctx.tools.register({
|
||||
name: 'echo',
|
||||
description: 'Echo text.',
|
||||
parameters: { text: { type: 'string', required: true } },
|
||||
output: {
|
||||
schema: { type: 'string' },
|
||||
render: (_args, value) => [{ type: 'text', text: value as string }],
|
||||
},
|
||||
execute: async args => `ECHO: ${(args as { text: string }).text}`,
|
||||
})
|
||||
const [agent] = ctx.agents.roots()
|
||||
if (agent === undefined) throw new Error('test main agent missing')
|
||||
return { ctx, agent, persistenceRoot: root }
|
||||
}
|
||||
|
||||
async function invoke(
|
||||
ctx: Context,
|
||||
args: readonly string[],
|
||||
options: { signal?: AbortSignal; failStdout?: boolean; failDispose?: boolean } = {},
|
||||
): Promise<{ code: number; stdout: string; stderr: string }> {
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
const code = await executeCli(args, {
|
||||
cwd: '/tmp/cli-cwd',
|
||||
...options.signal === undefined ? {} : { signal: options.signal },
|
||||
boot: async () => ctx,
|
||||
loadEnv: () => {},
|
||||
writeStdout: (chunk) => {
|
||||
if (options.failStdout === true) throw new Error('stdout closed')
|
||||
stdout += chunk
|
||||
},
|
||||
writeStderr: (chunk) => { stderr += chunk },
|
||||
...options.failDispose === true
|
||||
? { dispose: async (target: Context) => {
|
||||
await target.fiber.dispose()
|
||||
throw new Error('dispose exploded')
|
||||
} }
|
||||
: {},
|
||||
})
|
||||
return { code, stdout, stderr }
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(liveContexts.splice(0).map(ctx => ctx.fiber.dispose()))
|
||||
})
|
||||
|
||||
describe('parseCliArgs', () => {
|
||||
it('parses defaults, explicit options, spaces, and an option-like task after --', () => {
|
||||
expect(parseCliArgs(['task with spaces'])).toEqual({
|
||||
kind: 'run', configPath: './cordis.yml', outputFormat: 'text', task: 'task with spaces',
|
||||
})
|
||||
expect(parseCliArgs(['--config', 'custom.yml', '--output-format', 'stream-json', 'do it'])).toEqual({
|
||||
kind: 'run', configPath: 'custom.yml', outputFormat: 'stream-json', task: 'do it',
|
||||
})
|
||||
expect(parseCliArgs(['--', '-task'])).toMatchObject({ task: '-task' })
|
||||
expect(parseCliArgs(['-p', 'flag task'])).toMatchObject({ task: 'flag task' })
|
||||
expect(parseCliArgs(['--prompt', 'long-flag task'])).toMatchObject({ task: 'long-flag task' })
|
||||
expect(parseCliArgs(['--help', 'ignored'])).toEqual({ kind: 'help' })
|
||||
})
|
||||
|
||||
it('rejects missing, blank, extra, invalid-format, and unsupported flags', () => {
|
||||
expect(() => parseCliArgs([])).toThrow('received 0')
|
||||
expect(() => parseCliArgs([' '])).toThrow('must not be blank')
|
||||
expect(() => parseCliArgs(['-p', ' '])).toThrow('must not be blank')
|
||||
expect(() => parseCliArgs(['one', 'two'])).toThrow('received 2')
|
||||
expect(() => parseCliArgs(['-p', 'task', 'positional'])).toThrow('mutually exclusive')
|
||||
expect(() => parseCliArgs(['--output-format', 'xml', 'task'])).toThrow('unsupported output format')
|
||||
expect(() => parseCliArgs(['-x', 'task'])).toThrow('Unknown option')
|
||||
})
|
||||
})
|
||||
|
||||
describe('runOneShot and executeCli', () => {
|
||||
it('prints help and argument diagnostics without booting or contaminating stdout', async () => {
|
||||
let booted = false
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
const runtime = {
|
||||
boot: async (): Promise<Context> => { booted = true; throw new Error('unexpected') },
|
||||
writeStdout: (chunk: string): void => { stdout += chunk },
|
||||
writeStderr: (chunk: string): void => { stderr += chunk },
|
||||
}
|
||||
expect(await executeCli(['--help'], runtime)).toBe(0)
|
||||
expect(stdout).toContain('Usage: dsh-cli-demo')
|
||||
stdout = ''
|
||||
expect(await executeCli([], runtime)).toBe(1)
|
||||
expect(stdout).toBe('')
|
||||
expect(stderr).toContain('received 0')
|
||||
expect(booted).toBe(false)
|
||||
})
|
||||
|
||||
it('leaves stdout empty for environment and boot failures and resolves the default config', async () => {
|
||||
let bootPath = ''
|
||||
let stderr = ''
|
||||
const code = await executeCli(['task'], {
|
||||
cwd: '/tmp/cli-work',
|
||||
loadEnv: (_name, _dir, warn) => { warn('env warning\n') },
|
||||
boot: async (_name, path) => { bootPath = path; throw 'boot exploded' },
|
||||
writeStdout: () => { throw new Error('stdout must stay empty') },
|
||||
writeStderr: (chunk) => { stderr += chunk },
|
||||
})
|
||||
expect(code).toBe(1)
|
||||
expect(bootPath).toBe(resolve('/tmp/cli-work/cordis.yml'))
|
||||
expect(stderr).toContain('env warning')
|
||||
expect(stderr).toContain('boot exploded')
|
||||
})
|
||||
|
||||
it('contains a thrown value whose inspection and coercion both fail', async () => {
|
||||
const hostile = new Proxy({}, {
|
||||
getPrototypeOf: () => { throw new Error('prototype trap escaped') },
|
||||
get: (target, key, receiver) => {
|
||||
if (key === Symbol.toPrimitive) throw new Error('coercion escaped')
|
||||
return Reflect.get(target, key, receiver) as unknown
|
||||
},
|
||||
})
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
const code = await executeCli(['task'], {
|
||||
boot: async () => { throw hostile },
|
||||
loadEnv: () => {},
|
||||
writeStdout: (chunk) => { stdout += chunk },
|
||||
writeStderr: (chunk) => { stderr += chunk },
|
||||
})
|
||||
expect(code).toBe(1)
|
||||
expect(stdout).toBe('')
|
||||
expect(stderr).toBe('dsh-cli-demo: [unrenderable thrown value]\n')
|
||||
})
|
||||
|
||||
it('interrupts Loader boot and contains every late boot outcome', async () => {
|
||||
const abort = new AbortController()
|
||||
const lateContext = new Context()
|
||||
liveContexts.push(lateContext)
|
||||
const boot = Promise.withResolvers<Context>()
|
||||
const disposed = Promise.withResolvers<undefined>()
|
||||
let disposeCalls = 0
|
||||
let stderr = ''
|
||||
const running = executeCli(['task'], {
|
||||
signal: abort.signal,
|
||||
boot: () => boot.promise,
|
||||
loadEnv: () => {},
|
||||
writeStdout: () => {},
|
||||
writeStderr: (chunk) => { stderr += chunk },
|
||||
dispose: async (ctx) => {
|
||||
disposeCalls += 1
|
||||
await ctx.fiber.dispose()
|
||||
disposed.resolve(undefined)
|
||||
},
|
||||
})
|
||||
abort.abort('received SIGTERM')
|
||||
await expect(running).resolves.toBe(1)
|
||||
expect(stderr).toContain('received SIGTERM')
|
||||
expect(disposeCalls).toBe(0)
|
||||
boot.resolve(lateContext)
|
||||
await disposed.promise
|
||||
expect(disposeCalls).toBe(1)
|
||||
|
||||
const rejectedBoot = Promise.withResolvers<Context>()
|
||||
const rejectedAbort = new AbortController()
|
||||
const rejected = executeCli(['task'], {
|
||||
signal: rejectedAbort.signal,
|
||||
boot: () => rejectedBoot.promise,
|
||||
loadEnv: () => {},
|
||||
writeStdout: () => {},
|
||||
writeStderr: () => {},
|
||||
})
|
||||
rejectedAbort.abort('stop rejected boot')
|
||||
await expect(rejected).resolves.toBe(1)
|
||||
rejectedBoot.reject(new Error('late boot rejection'))
|
||||
await Promise.resolve()
|
||||
|
||||
let ordinaryBootStderr = ''
|
||||
const ordinaryBootFailure = await executeCli(['task'], {
|
||||
signal: new AbortController().signal,
|
||||
boot: async () => { throw new Error('ordinary boot failure') },
|
||||
loadEnv: () => {},
|
||||
writeStdout: () => {},
|
||||
writeStderr: (chunk) => { ordinaryBootStderr += chunk },
|
||||
})
|
||||
expect(ordinaryBootFailure).toBe(1)
|
||||
expect(ordinaryBootStderr).toContain('ordinary boot failure')
|
||||
|
||||
const failedCleanupBoot = Promise.withResolvers<Context>()
|
||||
const failedCleanupAbort = new AbortController()
|
||||
const cleanupFailure = Promise.withResolvers<undefined>()
|
||||
const failedCleanupContext = new Context()
|
||||
liveContexts.push(failedCleanupContext)
|
||||
const failedCleanup = executeCli(['task'], {
|
||||
signal: failedCleanupAbort.signal,
|
||||
boot: () => failedCleanupBoot.promise,
|
||||
loadEnv: () => {},
|
||||
writeStdout: () => {},
|
||||
writeStderr: (chunk) => {
|
||||
if (chunk.includes('dispose after interrupted boot failed: late cleanup')) cleanupFailure.resolve(undefined)
|
||||
},
|
||||
dispose: async (ctx) => {
|
||||
await ctx.fiber.dispose()
|
||||
throw new Error('late cleanup')
|
||||
},
|
||||
})
|
||||
failedCleanupAbort.abort('stop failed cleanup boot')
|
||||
await expect(failedCleanup).resolves.toBe(1)
|
||||
failedCleanupBoot.resolve(failedCleanupContext)
|
||||
await cleanupFailure.promise
|
||||
})
|
||||
|
||||
it('renders text, flushes a persisted fresh session, and disposes the context', async () => {
|
||||
const { ctx, agent, persistenceRoot } = await harness([textResponse('final answer')])
|
||||
const output = await invoke(ctx, ['task'])
|
||||
expect(output).toEqual({ code: 0, stdout: 'final answer\n', stderr: '' })
|
||||
expect(agent.status).toBe('idle')
|
||||
const files = await readdir(persistenceRoot, { recursive: true })
|
||||
expect(files.some(file => file.endsWith('.jsonl.zstd'))).toBe(true)
|
||||
})
|
||||
|
||||
it('writes correlated session events in stream-json mode', async () => {
|
||||
const { ctx } = await harness([textResponse('streamed answer')])
|
||||
const output = await invoke(ctx, ['--output-format', 'stream-json', 'task'])
|
||||
const records = output.stdout.trim().split('\n').map(line => JSON.parse(line) as { type: string })
|
||||
|
||||
expect(output.code).toBe(0)
|
||||
expect(records.some(record => record.type === 'session_event')).toBe(true)
|
||||
expect(records.at(-1)).toMatchObject({ type: 'result', output: 'streamed answer' })
|
||||
})
|
||||
|
||||
it('sums usage across tool steps and selects the last text-bearing assistant message', async () => {
|
||||
const first = { inputTokens: 10, outputTokens: 3, cacheReadTokens: 2, cacheWriteTokens: 1 }
|
||||
const second = { inputTokens: 7, outputTokens: 5, cacheReadTokens: 4, reasoningTokens: 6 }
|
||||
const { ctx } = await harness([toolResponse(first), textResponse('done', second)])
|
||||
const output = await invoke(ctx, ['--output-format', 'json', 'task'])
|
||||
const result = JSON.parse(output.stdout) as CliResult
|
||||
expect(output.code).toBe(0)
|
||||
expect(result).toMatchObject({ type: 'result', output: 'done' })
|
||||
expect(result.usage).toEqual({
|
||||
inputTokens: 17,
|
||||
outputTokens: 8,
|
||||
cacheReadTokens: 6,
|
||||
cacheWriteTokens: 1,
|
||||
reasoningTokens: 6,
|
||||
})
|
||||
})
|
||||
|
||||
it('reports usage committed by the recovered assistant message', async () => {
|
||||
const failed = { inputTokens: 11, outputTokens: 2, cacheReadTokens: 3 }
|
||||
const recovered = { inputTokens: 7, outputTokens: 5, reasoningTokens: 4 }
|
||||
const { ctx } = await harness([failedResponse(failed), textResponse('done', recovered)])
|
||||
|
||||
const result = await runOneShot(ctx, { task: 'task' })
|
||||
|
||||
expect(result.usage).toEqual({
|
||||
inputTokens: 7,
|
||||
outputTokens: 5,
|
||||
reasoningTokens: 4,
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps the prior text when a later assistant message has no text blocks', async () => {
|
||||
const { ctx } = await harness([
|
||||
toolResponse({ inputTokens: 1, outputTokens: 1 }),
|
||||
reasoningResponse('reasoning only'),
|
||||
])
|
||||
const result = await runOneShot(ctx, { task: 'task' })
|
||||
expect(result.output).toBe('working')
|
||||
})
|
||||
|
||||
it('observes only the correlated main message turn', async () => {
|
||||
const { ctx, agent } = await harness([
|
||||
textResponse('startup'),
|
||||
textResponse('autonomous'),
|
||||
textResponse('streamed'),
|
||||
])
|
||||
const other = ctx.sessions.create(SessionId('unrelated'))
|
||||
let startupStarted!: () => void
|
||||
const started = new Promise<void>((resolve) => { startupStarted = resolve })
|
||||
const releaseStartup = Promise.withResolvers<undefined>()
|
||||
ctx.on('session/event', (session, event) => {
|
||||
if (session === agent.session && event.type === 'assistant/message'
|
||||
&& event.data.turn === 1) startupStarted()
|
||||
})
|
||||
ctx.on('agent/turn-stopping', async ({ agent: subject, turn }) => {
|
||||
if (subject === agent && turn === 1) await releaseStartup.promise
|
||||
})
|
||||
agent.followup(createUserMessage({
|
||||
content: [{ type: 'text', text: 'startup' }],
|
||||
source: { kind: 'plugin', plugin: 'startup' },
|
||||
}))
|
||||
await started
|
||||
|
||||
const followup = agent.followup.bind(agent)
|
||||
let injectedBeforeReceipt = false
|
||||
agent.followup = (input) => {
|
||||
if (!injectedBeforeReceipt && input.source.kind === 'user') {
|
||||
injectedBeforeReceipt = true
|
||||
agent.inbox.append('next-step', createUserMessage({
|
||||
content: [{ type: 'text', text: 'wrong receipt' }],
|
||||
source: { kind: 'plugin', plugin: 'test-wrong-receipt' },
|
||||
}))
|
||||
other.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'unrelated session event' }],
|
||||
source: { kind: 'plugin', plugin: 'test' },
|
||||
}), { surfaceOp: 'append' })
|
||||
agent.session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'uncorrelated main-session event' }],
|
||||
source: { kind: 'plugin', plugin: 'test-before-receipt' },
|
||||
}), { surfaceOp: 'append' })
|
||||
}
|
||||
followup(input)
|
||||
}
|
||||
|
||||
let replacementQueued = false
|
||||
ctx.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject !== agent || status !== 'idle' || replacementQueued) return
|
||||
replacementQueued = true
|
||||
agent.followup(createUserMessage({
|
||||
content: [{ type: 'text', text: 'autonomous' }],
|
||||
source: { kind: 'plugin', plugin: 'test' },
|
||||
}))
|
||||
other.append('turn/start', { turn: 1 })
|
||||
other.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
})
|
||||
const streamed: { sessionId: string; event: SessionEvent }[] = []
|
||||
const result = runOneShot(ctx, {
|
||||
task: 'task',
|
||||
onEvent: (sessionId, event) => { streamed.push({ sessionId, event }) },
|
||||
})
|
||||
releaseStartup.resolve(undefined)
|
||||
|
||||
const outcome = await result
|
||||
expect(outcome).toMatchObject({ type: 'result', output: 'streamed' })
|
||||
const events = streamed.map(item => item.event)
|
||||
expect(events.find(event => event.type === 'turn/start'))
|
||||
.toMatchObject({ type: 'turn/start', data: { turn: 3 } })
|
||||
expect(events.at(-1)).toMatchObject({ type: 'turn/end', data: { turn: 3 } })
|
||||
expect(streamed.every(item => item.sessionId === agent.session.id)).toBe(true)
|
||||
expect(events.some(event => event.type === 'user/message'
|
||||
&& event.data.source.kind === 'plugin'
|
||||
&& event.data.source.plugin === 'test')).toBe(false)
|
||||
expect(events.some(event => event.type === 'user/message'
|
||||
&& event.data.source.kind === 'plugin'
|
||||
&& event.data.source.plugin === 'test-before-receipt')).toBe(false)
|
||||
})
|
||||
|
||||
it('correlates a task whose step history is replaced', async () => {
|
||||
const { ctx } = await harness([textResponse('rewritten answer')])
|
||||
ctx.on('agent/pre-step', async () => ({
|
||||
kind: 'enter',
|
||||
messages: [createUserMessage({
|
||||
content: [{ type: 'text', text: 'rewritten task' }],
|
||||
source: { kind: 'plugin', plugin: 'test' },
|
||||
})],
|
||||
}))
|
||||
|
||||
await expect(runOneShot(ctx, { task: 'original task' })).resolves.toMatchObject({
|
||||
type: 'result',
|
||||
output: 'rewritten answer',
|
||||
})
|
||||
})
|
||||
|
||||
it('settles rejected tasks at whole-agent idle without attributing a result', async () => {
|
||||
const blocked = await harness([])
|
||||
blocked.ctx.on('agent/pre-step', async () => ({
|
||||
kind: 'reject' as const,
|
||||
}))
|
||||
await expect(runOneShot(blocked.ctx, { task: 'task' })).resolves.toMatchObject({ output: '' })
|
||||
|
||||
const failed = await harness([])
|
||||
failed.ctx.on('agent/pre-step', async () => { throw new Error('pre-step exploded') })
|
||||
await expect(runOneShot(failed.ctx, { task: 'task' })).resolves.toMatchObject({ output: '' })
|
||||
})
|
||||
|
||||
it('emits partial data without attributing a turn outcome', async () => {
|
||||
const { ctx } = await harness([textResponse('partial', { inputTokens: 2, outputTokens: 3 }, 'max-tokens')])
|
||||
const output = await invoke(ctx, ['--output-format', 'json', 'task'])
|
||||
expect(JSON.parse(output.stdout)).toMatchObject({ type: 'result', output: 'partial' })
|
||||
expect(output.code).toBe(0)
|
||||
expect(output.stderr).toBe('')
|
||||
})
|
||||
|
||||
it('cancels an active turn, emits its durable aborted result, and disposes', async () => {
|
||||
const { ctx, agent } = await harness(['hang'])
|
||||
const abort = new AbortController()
|
||||
let started!: () => void
|
||||
const running = new Promise<void>((resolveStarted) => { started = resolveStarted })
|
||||
ctx.on('session/event', (session, event) => {
|
||||
if (session === agent.session && event.type === 'assistant/chunk') started()
|
||||
})
|
||||
const outcome = invoke(ctx, ['--output-format', 'json', 'task'], { signal: abort.signal })
|
||||
await running
|
||||
abort.abort('received SIGINT')
|
||||
const output = await outcome
|
||||
expect(output.stdout).toBe('')
|
||||
expect(output.code).toBe(1)
|
||||
expect(output.stderr).toContain('received SIGINT')
|
||||
expect(agent.status).toBe('idle')
|
||||
})
|
||||
|
||||
it('contains stream-writer failures, cancels, flushes, and returns the output error', async () => {
|
||||
const { ctx, agent } = await harness(['hang'])
|
||||
await expect(runOneShot(ctx, {
|
||||
task: 'task',
|
||||
onEvent: () => { throw new Error('stream sink failed') },
|
||||
})).rejects.toThrow('stream sink failed')
|
||||
expect(agent.status).toBe('idle')
|
||||
})
|
||||
|
||||
it('handles cancellation before submission, a missing main agent, and final-output failure', async () => {
|
||||
const early = await harness([textResponse('unused')])
|
||||
const fakeSignal = {
|
||||
aborted: true,
|
||||
reason: undefined,
|
||||
} as unknown as AbortSignal
|
||||
await expect(runOneShot(early.ctx, { task: 'task', signal: fakeSignal })).rejects.toThrow('interrupted')
|
||||
|
||||
const raced = await harness([textResponse('unused')])
|
||||
let registrations = 0
|
||||
const racedSignal = {
|
||||
aborted: false,
|
||||
reason: 'cancel before followup',
|
||||
addEventListener: (_type: string, listener: () => void) => {
|
||||
registrations += 1
|
||||
if (registrations === 2) listener()
|
||||
},
|
||||
removeEventListener: () => {},
|
||||
} as unknown as AbortSignal
|
||||
await expect(runOneShot(raced.ctx, { task: 'task', signal: racedSignal }))
|
||||
.rejects.toThrow('cancel before followup')
|
||||
expect(raced.agent.session.events.some(event => event.type === 'turn/start')).toBe(false)
|
||||
|
||||
const preBootAbort = new AbortController()
|
||||
preBootAbort.abort('before boot completed')
|
||||
const preBoot = await invoke(early.ctx, ['task'], { signal: preBootAbort.signal })
|
||||
expect(preBoot).toMatchObject({ code: 1, stdout: '' })
|
||||
expect(preBoot.stderr).toContain('before boot completed')
|
||||
|
||||
const empty = new Context()
|
||||
liveContexts.push(empty)
|
||||
await expect(runOneShot(empty, { task: 'task' })).rejects.toThrow('exactly one top-level agent')
|
||||
|
||||
const final = await harness([textResponse('answer')])
|
||||
const output = await invoke(final.ctx, ['task'], { failStdout: true })
|
||||
expect(output.code).toBe(1)
|
||||
expect(output.stdout).toBe('')
|
||||
expect(output.stderr).toContain('stdout closed')
|
||||
expect(final.agent.status).toBe('idle')
|
||||
|
||||
const disposal = await harness([textResponse('answer')])
|
||||
const disposalOutput = await invoke(disposal.ctx, ['task'], { failDispose: true })
|
||||
expect(disposalOutput).toMatchObject({ code: 1, stdout: 'answer\n' })
|
||||
expect(disposalOutput.stderr).toContain('dispose exploded')
|
||||
})
|
||||
|
||||
it('reports disposal failure alongside an earlier run failure', async () => {
|
||||
const ctx = new Context()
|
||||
liveContexts.push(ctx)
|
||||
const output = await invoke(ctx, ['task'], { failDispose: true })
|
||||
expect(output).toEqual({
|
||||
code: 1,
|
||||
stdout: '',
|
||||
stderr: 'dsh-cli-demo: config must create exactly one top-level agent, found 0\n'
|
||||
+ 'dsh-cli-demo: dispose failed: dispose exploded\n',
|
||||
})
|
||||
})
|
||||
|
||||
it('cancels startup work and queued work before the correlated turn begins', async () => {
|
||||
const startup = await harness(['hang'])
|
||||
let started!: () => void
|
||||
const running = new Promise<void>((resolveStarted) => { started = resolveStarted })
|
||||
startup.ctx.on('session/event', (session, event) => {
|
||||
if (session === startup.agent.session && event.type === 'assistant/chunk') started()
|
||||
})
|
||||
startup.agent.followup(createUserMessage({ content: [{ type: 'text', text: 'first' }], source: { kind: 'user' } }))
|
||||
await running
|
||||
const startupAbort = new AbortController()
|
||||
const waiting = runOneShot(startup.ctx, { task: 'second', signal: startupAbort.signal })
|
||||
startupAbort.abort('cancel startup')
|
||||
await expect(waiting).rejects.toThrow('cancel startup')
|
||||
await startup.agent.whenIdle()
|
||||
|
||||
const queued = await harness([textResponse('unused')])
|
||||
const queuedAbort = new AbortController()
|
||||
queued.ctx.on('session/event', (session, event) => {
|
||||
if (session === queued.agent.session && event.type === 'agent/inbox/spliced'
|
||||
&& event.data.inserted.some(message => message.source.kind === 'user')) {
|
||||
queueMicrotask(() => { queuedAbort.abort('cancel queued') })
|
||||
}
|
||||
})
|
||||
await expect(runOneShot(queued.ctx, { task: 'task', signal: queuedAbort.signal })).rejects.toThrow('cancel queued')
|
||||
await queued.agent.whenIdle()
|
||||
})
|
||||
})
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../vendor/schemastery"
|
||||
},
|
||||
{
|
||||
"path": "../../../vendor/cordis"
|
||||
},
|
||||
{
|
||||
"path": "../../llm/llm"
|
||||
},
|
||||
{
|
||||
"path": "../../core/session"
|
||||
},
|
||||
{
|
||||
"path": "../../core/agent"
|
||||
},
|
||||
{
|
||||
"path": "../../core/system-prompt"
|
||||
},
|
||||
{
|
||||
"path": "../../core/tools"
|
||||
},
|
||||
{
|
||||
"path": "../agent-spine-demo"
|
||||
},
|
||||
{
|
||||
"path": "../../session-persistence/session-checkpoint-policy"
|
||||
},
|
||||
{
|
||||
"path": "../../session-persistence/session-persistence-jsonl"
|
||||
},
|
||||
{
|
||||
"path": "../../ui/app-boot"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { defineConfig } from 'tsdown'
|
||||
|
||||
/** Builds the plugin and executable entries from declarations emitted by `tsc -b`. */
|
||||
export default defineConfig({
|
||||
entry: ['lib/types/index.js', 'lib/types/invariant.js', 'lib/types/bin.js'],
|
||||
outDir: 'lib',
|
||||
format: ['esm'],
|
||||
platform: 'node',
|
||||
target: 'es2024',
|
||||
fixedExtension: false,
|
||||
dts: false,
|
||||
clean: false,
|
||||
})
|
||||
@@ -6,7 +6,7 @@ import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import { decodeGoalChange } from '@deepseek-ai/dsh-goal'
|
||||
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
|
||||
|
||||
const binScript = fileURLToPath(new URL('../../../examples/cli-demo/src/bin.ts', import.meta.url))
|
||||
const binScript = fileURLToPath(new URL('../../../../examples/headless-agent/tests/fixtures/headless-driver.ts', import.meta.url))
|
||||
const configPath = fileURLToPath(new URL(
|
||||
'../../../../examples/headless-agent/tests/fixtures/goal-domain/cordis.yml',
|
||||
import.meta.url,
|
||||
@@ -30,8 +30,9 @@ describe('goal domain through a real cordis.yml and headless process', () => {
|
||||
label: 'goal-domain',
|
||||
tempDirPrefix: 'goal-domain-e2e-',
|
||||
binScript,
|
||||
libBinScript: binScript,
|
||||
configPath,
|
||||
binArgs: ['--config', configPath, '--output-format', 'json', 'prove the persisted goal domain'],
|
||||
binArgs: [configPath, 'prove the persisted goal domain'],
|
||||
tsconfigPath: repoTsconfig,
|
||||
inspect: async (cwd) => {
|
||||
const logs = await jsonlFiles(join(cwd, '.sessions'))
|
||||
@@ -41,7 +42,7 @@ describe('goal domain through a real cordis.yml and headless process', () => {
|
||||
},
|
||||
})
|
||||
expect(stderr).toBe('')
|
||||
const result = JSON.parse(stdout) as Record<string, unknown>
|
||||
const result = JSON.parse(stdout.trimEnd().split('\n').at(-1) ?? '') as Record<string, unknown>
|
||||
expect(result).toMatchObject({
|
||||
type: 'result',
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
resolveExampleMode,
|
||||
} from '@deepseek-ai/dsh-loader-smoke'
|
||||
|
||||
const SRC_BIN = '/repo/packages/examples/cli-demo/src/bin.ts'
|
||||
const SRC_BIN = '/repo/packages/examples/acp-demo/src/bin.ts'
|
||||
const TSCONFIG = '/repo/tsconfig.json'
|
||||
|
||||
const originalMode = process.env[EXAMPLE_MODE_ENV]
|
||||
@@ -65,7 +65,7 @@ describe('resolveExampleLaunch', () => {
|
||||
env: { DSH_HOME: '/tmp/home' },
|
||||
})
|
||||
expect(args).not.toContain('--import')
|
||||
expect(args).toContain('/repo/packages/examples/cli-demo/lib/bin.js')
|
||||
expect(args).toContain('/repo/packages/examples/acp-demo/lib/bin.js')
|
||||
expect(args.slice(-2)).toEqual(['--config', './cordis.yml'])
|
||||
expect(env.TSX_TSCONFIG_PATH).toBeUndefined()
|
||||
expect(env.DSH_HOME).toBe('/tmp/home')
|
||||
@@ -100,6 +100,6 @@ describe('resolveExampleLaunch', () => {
|
||||
it('defaults the mode from the environment', () => {
|
||||
process.env[EXAMPLE_MODE_ENV] = 'lib'
|
||||
const { args } = resolveExampleLaunch({ srcBin: SRC_BIN })
|
||||
expect(args).toContain('/repo/packages/examples/cli-demo/lib/bin.js')
|
||||
expect(args).toContain('/repo/packages/examples/acp-demo/lib/bin.js')
|
||||
})
|
||||
})
|
||||
@@ -2,5 +2,5 @@
|
||||
# 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/ui/app-boot/README.md
|
||||
README.md: 359f05a83b41db6db5ede40db7317a0fb15de43b
|
||||
README.zh.md: a916236e30b50cc884d9d5876f27fcb1aa6f0777
|
||||
README.md: c256b89288e3e384c1dd3e64629a06d7cfef31f6
|
||||
README.zh.md: 88d1c4ad0ced2f5a6440a1b64e34e738a842f938
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Shared boot glue for the app bins ([`dsh`](../../../apps/cli/README.md), [`dsh-cli-demo`](../../examples/cli-demo/README.md), [`dsh-acp-demo`](../../examples/acp-demo/README.md)): each bin is a thin self-executing composition over these helpers, parameterized by its diagnostic prefix, so loader-failure behavior has one owner instead of drifting between published artifacts.
|
||||
Shared boot glue for the app bins ([`dsh`](../../../apps/cli/README.md) and [`dsh-acp-demo`](../../examples/acp-demo/README.md)): each bin is a thin self-executing composition over these helpers, parameterized by its diagnostic prefix, so loader-failure behavior has one owner instead of drifting between published artifacts.
|
||||
|
||||
| Export | Role |
|
||||
|---|---|
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
供 app bin([`dsh`](../../../apps/cli/README.md)、[`dsh-cli-demo`](../../examples/cli-demo/README.md)、[`dsh-acp-demo`](../../examples/acp-demo/README.md))共用的启动粘合层:每个 bin 都是在这些 helper 上构建的精简自执行组合,并以自身诊断前缀参数化。这样,Loader 故障行为只由一处负责,不会在已发布产物之间逐渐分化。
|
||||
供 app bin([`dsh`](../../../apps/cli/README.md) 与 [`dsh-acp-demo`](../../examples/acp-demo/README.md))共用的启动粘合层:每个 bin 都是在这些 helper 上构建的精简自执行组合,并以自身诊断前缀参数化。这样,Loader 故障行为只由一处负责,不会在已发布产物之间逐渐分化。
|
||||
|
||||
| 导出 | 职责 |
|
||||
|---|---|
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Shared boot glue for the app bins (`dsh`, `dsh-cli-demo`, `dsh-acp-demo`): load the gitignored
|
||||
* Shared boot glue for the app bins (`dsh`, `dsh-acp-demo`): load the gitignored
|
||||
* `.env`, install the fail-loud Loader guards, resolve the config path (snapshot-aware), load the
|
||||
* optional user patch layers from the Harness home (`~/.dsh`), expose its path resolver to
|
||||
* config expressions, and drive the Cordis Loader against a leaf `cordis.yml` until the tree settles.
|
||||
|
||||
Generated
-51
@@ -313,9 +313,6 @@ importers:
|
||||
'@deepseek-ai/dsh-bash-sandbox':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/bash/bash-sandbox
|
||||
'@deepseek-ai/dsh-cli-demo':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/examples/cli-demo
|
||||
'@deepseek-ai/dsh-code-runtime-worker':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/code-runtime/code-runtime-worker
|
||||
@@ -3203,54 +3200,6 @@ importers:
|
||||
specifier: 0.0.0-test.0
|
||||
version: 0.0.0-test.0
|
||||
|
||||
packages/examples/cli-demo:
|
||||
devDependencies:
|
||||
'@cordisjs/plugin-include':
|
||||
specifier: workspace:^
|
||||
version: link:../../../vendor/include
|
||||
'@cordisjs/plugin-loader':
|
||||
specifier: workspace:^
|
||||
version: link:../../../vendor/loader
|
||||
'@deepseek-ai/dsh-agent':
|
||||
specifier: workspace:^
|
||||
version: link:../../core/agent
|
||||
'@deepseek-ai/dsh-agent-spine-demo':
|
||||
specifier: workspace:^
|
||||
version: link:../agent-spine-demo
|
||||
'@deepseek-ai/dsh-app-boot':
|
||||
specifier: workspace:^
|
||||
version: link:../../ui/app-boot
|
||||
'@deepseek-ai/dsh-invariants':
|
||||
specifier: workspace:^
|
||||
version: link:../../support/invariants
|
||||
'@deepseek-ai/dsh-llm':
|
||||
specifier: workspace:^
|
||||
version: link:../../llm/llm
|
||||
'@deepseek-ai/dsh-session':
|
||||
specifier: workspace:^
|
||||
version: link:../../core/session
|
||||
'@deepseek-ai/dsh-session-checkpoint-policy':
|
||||
specifier: workspace:^
|
||||
version: link:../../session-persistence/session-checkpoint-policy
|
||||
'@deepseek-ai/dsh-session-persistence-jsonl':
|
||||
specifier: workspace:^
|
||||
version: link:../../session-persistence/session-persistence-jsonl
|
||||
'@deepseek-ai/dsh-system-prompt':
|
||||
specifier: workspace:^
|
||||
version: link:../../core/system-prompt
|
||||
'@deepseek-ai/dsh-tools':
|
||||
specifier: workspace:^
|
||||
version: link:../../core/tools
|
||||
'@deepseek-ai/dsh-workspace-context':
|
||||
specifier: workspace:^
|
||||
version: link:../../context/workspace-context
|
||||
cordis:
|
||||
specifier: ^4.0.0-rc.7
|
||||
version: link:../../../vendor/cordis
|
||||
schemastery:
|
||||
specifier: ^3.17.0
|
||||
version: link:../../../vendor/schemastery
|
||||
|
||||
packages/examples/jsonrpc-demo:
|
||||
dependencies:
|
||||
'@deepseek-ai/dsh-app-boot':
|
||||
|
||||
@@ -124,7 +124,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
pkg: 'session',
|
||||
title: 'In-memory session store',
|
||||
mode: 'core',
|
||||
consumers: ['agent-loop', 'agent', 'cli-demo', 'session-persistence', 'session-query', 'session-query-sqlite', 'subagent-inprocess', 'invariants'],
|
||||
consumers: ['agent-loop', 'agent', 'session-persistence', 'session-query', 'session-query-sqlite', 'subagent-inprocess', 'invariants'],
|
||||
note: 'Owns append-only Session instances and emits the durable session event feed.',
|
||||
},
|
||||
{
|
||||
@@ -303,7 +303,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
pkg: 'agent',
|
||||
title: 'Agent service',
|
||||
mode: 'core',
|
||||
consumers: ['agent-loop', 'acp', 'cli-demo', 'subagent-inprocess'],
|
||||
consumers: ['agent-loop', 'acp', 'subagent-inprocess'],
|
||||
note: 'Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation.',
|
||||
},
|
||||
{
|
||||
@@ -635,10 +635,10 @@ const APP_EXAMPLES = [
|
||||
{
|
||||
id: 'headless',
|
||||
rel: 'examples/headless-agent/composition.md',
|
||||
title: 'Headless Agent App Composition',
|
||||
title: 'Headless Agent Snapshot Composition',
|
||||
label: 'examples/headless-agent',
|
||||
config: 'examples/headless-agent/cordis.yml',
|
||||
summary: 'The headless demo combines the real DeepSeek adapter and coding capabilities with the one-shot app package, format-pure stdout, and one fresh persisted top-level session.',
|
||||
summary: 'The headless snapshot composition combines the real DeepSeek adapter and coding capabilities with one explicitly configured persisted top-level agent; its JSONL driver is test-only.',
|
||||
},
|
||||
{
|
||||
id: 'acp',
|
||||
@@ -657,9 +657,7 @@ function renderAppExpansion(lines: string[], appNode: string, pluginName: string
|
||||
const jsonl = nodeId('bundle', 'jsonl')
|
||||
lines.push(` ${appNode} --> ${agentCore}["@deepseek-ai/dsh-agent-spine-demo"]`)
|
||||
lines.push(` ${appNode} --> ${jsonl}["@deepseek-ai/dsh-session-persistence-jsonl"]`)
|
||||
if (pluginName === '@deepseek-ai/dsh-cli-demo') {
|
||||
lines.push(` ${appNode} --> ${nodeId('frontdoor', 'cli')}["one-shot driver<br/>format-pure stdout<br/>fresh top-level agent"]`)
|
||||
} else if (pluginName === '@deepseek-ai/dsh-acp-demo') {
|
||||
if (pluginName === '@deepseek-ai/dsh-acp-demo') {
|
||||
lines.push(` ${appNode} --> ${nodeId('frontdoor', 'acp')}["@deepseek-ai/dsh-acp<br/>automation-only JSON-RPC stdio<br/>fresh sessions created by client"]`)
|
||||
}
|
||||
lines.push(
|
||||
@@ -685,7 +683,7 @@ function renderAppComposition(example: AppExample): string {
|
||||
const pluginNode = nodeId(`plugin_${example.id}`, plugin.id)
|
||||
lines.push(` ${pluginNode}["${escLabel(plugin.id)}<br/>${escLabel(plugin.name)}"]`)
|
||||
lines.push(` cfg --> ${pluginNode}`)
|
||||
if (plugin.name === '@deepseek-ai/dsh-cli-demo' || plugin.name === '@deepseek-ai/dsh-acp-demo') {
|
||||
if (plugin.name === '@deepseek-ai/dsh-acp-demo') {
|
||||
renderAppExpansion(lines, pluginNode, plugin.name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -602,7 +602,6 @@ function builtBinSmokeGate(needs: string[] = ['build']): Gate {
|
||||
'vitest.e2e.config.ts',
|
||||
'examples/headless-agent/tests/keyless-smoke.e2e.ts',
|
||||
'apps/cli/tests/built-bin.e2e.ts',
|
||||
'packages/examples/cli-demo/tests/built-bin.e2e.ts',
|
||||
'packages/examples/acp-demo/tests/built-bin.e2e.ts',
|
||||
'packages/host/directory-picker-native/tests/built-worker.e2e.ts',
|
||||
'packages/ui/jsonrpc/tests/built-scope-carrier.e2e.ts',
|
||||
|
||||
@@ -154,7 +154,6 @@
|
||||
{ "path": "./packages/core/agent-loop" },
|
||||
{ "path": "./packages/llm/llm-retry" },
|
||||
{ "path": "./packages/examples/agent-spine-demo" },
|
||||
{ "path": "./packages/examples/cli-demo" },
|
||||
{ "path": "./packages/subprocess/subprocess" },
|
||||
{ "path": "./packages/subprocess/subprocess-local" },
|
||||
{ "path": "./packages/bash/bash" },
|
||||
|
||||
Reference in New Issue
Block a user