mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(cli): close shared config review gaps
This commit is contained in:
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.md
|
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.md
|
||||||
2026-07-17-dedicated-full-screen-tui-front-door.md: 47f5b7f94ce56d592c6a7897d8bcd470e12a0a60
|
2026-07-17-dedicated-full-screen-tui-front-door.md: 10564b110cc2e56615bde86830c0d39d5a7cee38
|
||||||
2026-07-17-dedicated-full-screen-tui-front-door.zh.md: bbbfaafd8c5fc382dad070daf1eba26a0c6dcedd
|
2026-07-17-dedicated-full-screen-tui-front-door.zh.md: 4624a4db3db598793eee257f829a080f4d7ad711
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ The interactive channel must remain a Cordis plugin over the same agent, session
|
|||||||
|
|
||||||
DeepSeek Harness ships [`@deepseek-ai/dsh-tui`](../../../../packages/ui/tui/README.md) as a dedicated Cordis plugin. It owns terminal input and presentation only; agent lifecycle, session persistence, tool execution, and the model-facing question tool remain separate composition entries. The plugin requires both stdin and stdout to be TTYs and fails instead of silently changing to line-oriented behavior.
|
DeepSeek Harness ships [`@deepseek-ai/dsh-tui`](../../../../packages/ui/tui/README.md) as a dedicated Cordis plugin. It owns terminal input and presentation only; agent lifecycle, session persistence, tool execution, and the model-facing question tool remain separate composition entries. The plugin requires both stdin and stdout to be TTYs and fails instead of silently changing to line-oriented behavior.
|
||||||
|
|
||||||
There is one terminal front door. `@deepseek-ai/dsh-tui` mounts before the configured agent, and `apps/cli/config/tui.cordis.yml` — an overlay over the shared `base.cordis.yml` — owns the interactive coding composition, with the Code Mode overlay in `examples/code-mode`. Non-interactive tasks use `@deepseek-ai/dsh-cli-demo`; ACP remains a separate automation protocol.
|
There is one terminal front door. `@deepseek-ai/dsh-tui` mounts before the configured agent, and `apps/cli/config/tui.cordis.yml` — an overlay over the shared `base.cordis.yml` — owns the interactive coding composition. Non-interactive tasks use the official headless surface; ACP remains a separate automation protocol and owns the supported Code Mode demo.
|
||||||
|
|
||||||
The selected front door receives the exact generated or resumed `SessionId` used by the pre-created agent. It mounts before the agent composition, waits for the matching root agent, and enters full-screen mode only after that agent exists. A matching `agent-loop/config-start-failed` event is therefore reported before screen takeover and exits with status 1.
|
The selected front door receives the exact generated or resumed `SessionId` used by the pre-created agent. It mounts before the agent composition, waits for the matching root agent, and enters full-screen mode only after that agent exists. A matching `agent-loop/config-start-failed` event is therefore reported before screen takeover and exits with status 1.
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Status: implemented
|
|||||||
|
|
||||||
DeepSeek Harness 将 [`@deepseek-ai/dsh-tui`](../../../../packages/ui/tui/README.md) 作为独立的 Cordis 插件交付。该插件只负责终端输入与呈现;agent 生命周期、会话持久化、工具执行以及模型可见的提问工具仍由不同组合项负责。插件要求 stdin 和 stdout 均为 TTY;条件不满足时会失败,不会静默切换为逐行输出。
|
DeepSeek Harness 将 [`@deepseek-ai/dsh-tui`](../../../../packages/ui/tui/README.md) 作为独立的 Cordis 插件交付。该插件只负责终端输入与呈现;agent 生命周期、会话持久化、工具执行以及模型可见的提问工具仍由不同组合项负责。插件要求 stdin 和 stdout 均为 TTY;条件不满足时会失败,不会静默切换为逐行输出。
|
||||||
|
|
||||||
只有一个终端入口。`@deepseek-ai/dsh-tui` 在已配置 agent 之前挂载,而 `apps/cli/config/tui.cordis.yml`——叠加在共享 `base.cordis.yml` 之上的 overlay——拥有交互式 coding 组装,Code Mode overlay 则位于 `examples/code-mode`。非交互任务使用 `@deepseek-ai/dsh-cli-demo`;ACP 仍是独立的自动化协议。
|
只有一个终端入口。`@deepseek-ai/dsh-tui` 在已配置 agent 之前挂载,而 `apps/cli/config/tui.cordis.yml`——叠加在共享 `base.cordis.yml` 之上的 overlay——拥有交互式 coding 组装。非交互任务使用官方 headless 界面;ACP 仍是独立的自动化协议,并拥有受支持的 Code Mode demo。
|
||||||
|
|
||||||
所选入口接收预创建 agent 使用的同一个新建或恢复 `SessionId`。入口先于 agent 组合挂载,等待相符的根 agent 出现,然后才进入全屏模式。因此,相符的 `agent-loop/config-start-failed` 事件会在接管屏幕前报告,并以状态码 1 退出。
|
所选入口接收预创建 agent 使用的同一个新建或恢复 `SessionId`。入口先于 agent 组合挂载,等待相符的根 agent 出现,然后才进入全屏模式。因此,相符的 `agent-loop/config-start-failed` 事件会在接管屏幕前报告,并以状态码 1 退出。
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md
|
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md
|
||||||
2026-07-20-dsh-cli-personal-config.md: cc965438214b68078647596af5a28fd666e7bd95
|
2026-07-20-dsh-cli-personal-config.md: 3331e36c86002d91fb272868268707fed014d01a
|
||||||
2026-07-20-dsh-cli-personal-config.zh.md: 02f9c578061e1c25044c377c3ec8f80594275ae7
|
2026-07-20-dsh-cli-personal-config.zh.md: 172d84b075ed7ecc127c317b47e30581db67f89a
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ Two coupled pieces, aligned with the `apps/` assembly tier proposed by the `dsh
|
|||||||
|
|
||||||
**The `dsh` CLI (`apps/cli`, npm name `@deepseek-ai/dsh`).** `apps/*` joins the workspaces as the product-assembly tier over `packages/*` libraries. The bin's dispatch reserves `web` and `-p`/`--prompt` for PR #443 (they exit with a pointer) so the two branches merge as a near-union; everything else runs the default surface: the interactive TUI, booting the shipped `examples/tui-agent/cordis.yml` (or an explicit config argument) with the invoking directory as the workspace. The committed `bin/dsh` launcher resolves the checkout through its own real path and runs the bin **from source** through Node's native TypeScript transform plus the app-owned tsconfig-paths loader, so `ln -sf "$(pwd)/bin/dsh" ~/.local/bin/dsh` installs a command that always executes the current working tree. `pnpm run demo:tui` runs the same entry.
|
**The `dsh` CLI (`apps/cli`, npm name `@deepseek-ai/dsh`).** `apps/*` joins the workspaces as the product-assembly tier over `packages/*` libraries. The bin's dispatch reserves `web` and `-p`/`--prompt` for PR #443 (they exit with a pointer) so the two branches merge as a near-union; everything else runs the default surface: the interactive TUI, booting the shipped `examples/tui-agent/cordis.yml` (or an explicit config argument) with the invoking directory as the workspace. The committed `bin/dsh` launcher resolves the checkout through its own real path and runs the bin **from source** through Node's native TypeScript transform plus the app-owned tsconfig-paths loader, so `ln -sf "$(pwd)/bin/dsh" ~/.local/bin/dsh` installs a command that always executes the current working tree. `pnpm run demo:tui` runs the same entry.
|
||||||
|
|
||||||
**Personal config (`dsh-app-boot`).** The personal overlay lives in the Harness home — `$DSH_HOME`, else `~/.dsh` — resolved by the shared [`resolveDshHome`](../architecture/2026-07-24-single-harness-home-resolver.md) (`@deepseek-ai/dsh-paths`), the same single root skills and AGENTS.md resolve against. The dsh TUI surface consumes its two optional files; the demo bins boot their committed trees verbatim:
|
**Personal config (`dsh-app-boot`).** The personal overlay lives in the Harness home — `$DSH_HOME`, else `~/.dsh` — resolved by the shared [`resolveDshHome`](../architecture/2026-07-24-single-harness-home-resolver.md) (`@deepseek-ai/dsh-paths`), the same single root skills and AGENTS.md resolve against. The official dsh surfaces consume its two optional files; the demo bins boot their committed trees verbatim:
|
||||||
|
|
||||||
- `.env` — loaded after the invoking directory's `.env`; `process.loadEnvFile` never overrides, so precedence is ambient > project `.env` > personal `.env`.
|
- `.env` — loaded after the invoking directory's `.env`; `process.loadEnvFile` never overrides, so precedence is ambient > project `.env` > personal `.env`.
|
||||||
- `config.yaml` — a top-level YAML array of `@cordisjs/plugin-include` `PatchOptions`, parsed with the include's own `!!js` dialect (`loadPersonalPatches`) and passed to `boot()`, which forwards it as the root include's `patches`. Patch semantics are exactly the committed overlay semantics (the Code Mode overlay is the template): an id-targeted patch replaces the named entry's whole `config`, `insert` appends entries, an unmatched id warns and is skipped.
|
- `config.yaml` — a top-level YAML array of `@cordisjs/plugin-include` `PatchOptions`, parsed with the include's own `!!js` dialect (`loadPersonalPatches`) and passed to `boot()`, which forwards it as the root include's `patches`. Patch semantics match the shipped surface overlays: an id-targeted patch replaces the named entry's whole `config`, `insert` appends entries, and an unmatched id is a silent no-op.
|
||||||
- A missing file means no overlay; a present-but-unreadable, unparsable, or non-array file throws at boot (misconfiguration fails loud, never a silent skip).
|
- A missing file means no overlay; a present-but-unreadable, unparsable, or non-array file throws at boot (misconfiguration fails loud, never a silent skip).
|
||||||
|
|
||||||
The PTY smoke's launcher isolates `$DSH_HOME` to a per-test directory, exactly as it already isolates `DSH_AGENTS_HOME`, so a developer's real personal overlay cannot leak into fixtures; only the dsh CLI reads personal config, so no other test launcher needed changes.
|
The PTY smoke's launcher isolates `$DSH_HOME` to a per-test directory, exactly as it already isolates `DSH_AGENTS_HOME`, so a developer's real personal overlay cannot leak into fixtures; only the dsh CLI reads personal config, so no other test launcher needed changes.
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ Status: implemented
|
|||||||
|
|
||||||
**`dsh` CLI(`apps/cli`,npm 名 `@deepseek-ai/dsh`)。** `apps/*` 作为 `packages/*` 库之上的产品装配层加入 workspaces。bin 的分发把 `web` 和 `-p`/`--prompt` 保留给 PR #443(它们以指引退出),使两个分支能以接近并集的方式合并;其余一切都运行默认表面:交互式 TUI,加载随仓库提供的 `examples/tui-agent/cordis.yml`(或显式的配置参数),并以调用目录为工作区。已提交的 `bin/dsh` 启动器通过自身真实路径解析 checkout,通过 Node 的原生 TypeScript 转换和应用自身持有的 tsconfig-paths loader **从源码**运行该 bin,因此 `ln -sf "$(pwd)/bin/dsh" ~/.local/bin/dsh` 安装的命令永远执行当前工作树。`pnpm run demo:tui` 运行同一入口。
|
**`dsh` CLI(`apps/cli`,npm 名 `@deepseek-ai/dsh`)。** `apps/*` 作为 `packages/*` 库之上的产品装配层加入 workspaces。bin 的分发把 `web` 和 `-p`/`--prompt` 保留给 PR #443(它们以指引退出),使两个分支能以接近并集的方式合并;其余一切都运行默认表面:交互式 TUI,加载随仓库提供的 `examples/tui-agent/cordis.yml`(或显式的配置参数),并以调用目录为工作区。已提交的 `bin/dsh` 启动器通过自身真实路径解析 checkout,通过 Node 的原生 TypeScript 转换和应用自身持有的 tsconfig-paths loader **从源码**运行该 bin,因此 `ln -sf "$(pwd)/bin/dsh" ~/.local/bin/dsh` 安装的命令永远执行当前工作树。`pnpm run demo:tui` 运行同一入口。
|
||||||
|
|
||||||
**个人配置(`dsh-app-boot`)。** 个人 overlay 存放在 Harness home——`$DSH_HOME`,否则 `~/.dsh`——由共享的 [`resolveDshHome`](../architecture/2026-07-24-single-harness-home-resolver.md)(`@deepseek-ai/dsh-paths`)解析,与 skills、AGENTS.md 解析所依据的单一根目录相同。dsh 的 TUI 表面消费其中两个可选文件;各示例 bin 仍然逐字节按已提交的配置树启动:
|
**个人配置(`dsh-app-boot`)。** 个人 overlay 存放在 Harness home——`$DSH_HOME`,否则 `~/.dsh`——由共享的 [`resolveDshHome`](../architecture/2026-07-24-single-harness-home-resolver.md)(`@deepseek-ai/dsh-paths`)解析,与 skills、AGENTS.md 解析所依据的单一根目录相同。dsh 的官方界面消费其中两个可选文件;各示例 bin 仍然逐字节按已提交的配置树启动:
|
||||||
|
|
||||||
- `.env`——在调用目录的 `.env` 之后加载;`process.loadEnvFile` 从不覆盖已有值,因此优先级为环境变量 > 项目 `.env` > 个人 `.env`。
|
- `.env`——在调用目录的 `.env` 之后加载;`process.loadEnvFile` 从不覆盖已有值,因此优先级为环境变量 > 项目 `.env` > 个人 `.env`。
|
||||||
- `config.yaml`——顶层 YAML 数组,元素为 `@cordisjs/plugin-include` 的 `PatchOptions`,用 include 自己的 `!!js` 方言解析(`loadPersonalPatches`)并传给 `boot()`,由它作为根 include 的 `patches` 转发。补丁语义与已提交 overlay 完全一致(Code Mode overlay 是模板):按 id 定位的补丁替换该配置项的整个 `config`,`insert` 追加配置项,未匹配的 id 记录警告并跳过。
|
- `config.yaml`——顶层 YAML 数组,元素为 `@cordisjs/plugin-include` 的 `PatchOptions`,用 include 自己的 `!!js` 方言解析(`loadPersonalPatches`)并传给 `boot()`,由它作为根 include 的 `patches` 转发。补丁语义与交付的 surface overlay 一致:按 id 定位的补丁替换该配置项的整个 `config`,`insert` 追加配置项,未匹配的 id 静默不执行任何操作。
|
||||||
- 文件缺失即无 overlay;文件存在但不可读、不可解析或非数组则在启动时抛出(配置错误响亮失败,绝不静默跳过)。
|
- 文件缺失即无 overlay;文件存在但不可读、不可解析或非数组则在启动时抛出(配置错误响亮失败,绝不静默跳过)。
|
||||||
|
|
||||||
PTY 冒烟测试的启动器把 `$DSH_HOME` 隔离到每个测试自己的目录,与它已有的 `DSH_AGENTS_HOME` 隔离方式完全一致,开发者真实的个人 overlay 不可能泄漏进 fixture;只有 dsh CLI 读取个人配置,因此其他测试启动器无需改动。
|
PTY 冒烟测试的启动器把 `$DSH_HOME` 隔离到每个测试自己的目录,与它已有的 `DSH_AGENTS_HOME` 隔离方式完全一致,开发者真实的个人 overlay 不可能泄漏进 fixture;只有 dsh CLI 读取个人配置,因此其他测试启动器无需改动。
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# 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
|
# 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: 72b8f5e0eb161acb7152d1b1dd4e455addf36dbf
|
2026-07-20-remove-stdio-and-echo-agents.md: b578e49f004cabf0e37385ea06a769ced5388217
|
||||||
2026-07-20-remove-stdio-and-echo-agents.zh.md: 95af2e74ccce4738fed713c3c58904c48517a479
|
2026-07-20-remove-stdio-and-echo-agents.zh.md: 3d8eecf73f17f3b62dc4b35b0f3c1af03f6f3a86
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ The stdio and Echo agents are removed without compatibility packages, modes, com
|
|||||||
|
|
||||||
The remaining application roles are explicit:
|
The remaining application roles are explicit:
|
||||||
|
|
||||||
- [`@deepseek-ai/dsh-tui`](../../../../packages/ui/tui/README.md) 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 the Code Mode overlay in `examples/code-mode` and PTY plus terminal-snapshot coverage in `apps/cli/tests/`.
|
- [`@deepseek-ai/dsh-tui`](../../../../packages/ui/tui/README.md) 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.
|
- [`@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.
|
||||||
- [`@deepseek-ai/dsh-acp-demo`](../../../../packages/examples/acp-demo/README.md) and `@deepseek-ai/dsh-jsonrpc` own their framed protocol integrations.
|
- [`@deepseek-ai/dsh-acp-demo`](../../../../packages/examples/acp-demo/README.md) and `@deepseek-ai/dsh-jsonrpc` own their framed protocol integrations.
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ Keyless validation is test-owned. The Headless Loader smoke uses a fixture adapt
|
|||||||
|
|
||||||
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.
|
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 TUI PTY smoke includes the Code Mode overlay composition, while `examples/cordis-agent/tests/keyless-smoke.e2e.ts` provides a minimal PTY boot over the real Cordis-agent Loader tree. 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. 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 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.
|
||||||
|
|
||||||
## Alternatives considered
|
## Alternatives considered
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ DeepSeek Harness 在 TUI 和 Headless coding agent 之外,还提供了两个
|
|||||||
|
|
||||||
保留的应用角色均有明确归属:
|
保留的应用角色均有明确归属:
|
||||||
|
|
||||||
- [`@deepseek-ai/dsh-tui`](../../../../packages/ui/tui/README.md) 负责终端交互式执行。它会在 Loader 启动前拒绝非 TTY 流;`examples/tui-agent` 拥有完整 coding 组装、Code Mode 覆盖层、PTY 覆盖和终端快照。
|
- [`@deepseek-ai/dsh-tui`](../../../../packages/ui/tui/README.md) 负责终端交互式执行。它会在 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。
|
- [`@deepseek-ai/dsh-cli-demo`](../../../../packages/examples/cli-demo/README.md) 负责非交互式执行,包括管道方式。`examples/headless-agent` 拥有真实模型的单次任务组装、回放快照、通用真实 agent 测试套件,以及仅供测试使用的无密钥 Loader fixture。
|
||||||
- [`@deepseek-ai/dsh-acp-demo`](../../../../packages/examples/acp-demo/README.md) 和 `@deepseek-ai/dsh-jsonrpc` 负责各自的分帧协议集成。
|
- [`@deepseek-ai/dsh-acp-demo`](../../../../packages/examples/acp-demo/README.md) 和 `@deepseek-ai/dsh-jsonrpc` 负责各自的分帧协议集成。
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ SDK 工程模型与 create/config 工作流将 `stdio` 运行接口选项替换
|
|||||||
|
|
||||||
TUI 与 Headless 的 Loader 覆盖以源码和构建产物两种模式运行真实 app 包。由 PTY 驱动的子进程覆盖仅用于 TUI 生命周期;其他入口冒烟测试使用单次管道协议。Headless 验证任务/结果契约和工具调用契约。生成图谱与仓库搜索会拒绝陈旧的包、命令、叶节点、SDK 接口、`createStdioChat` 和 `StdioRuntime` 引用。
|
TUI 与 Headless 的 Loader 覆盖以源码和构建产物两种模式运行真实 app 包。由 PTY 驱动的子进程覆盖仅用于 TUI 生命周期;其他入口冒烟测试使用单次管道协议。Headless 验证任务/结果契约和工具调用契约。生成图谱与仓库搜索会拒绝陈旧的包、命令、叶节点、SDK 接口、`createStdioChat` 和 `StdioRuntime` 引用。
|
||||||
|
|
||||||
TUI PTY 冒烟测试包含 Code Mode 覆盖层组装,而 `examples/cordis-agent/tests/keyless-smoke.e2e.ts` 会基于真实 Cordis-agent Loader 目录树执行最小 PTY 启动。构建后的 `dsh` 可执行文件会在 Loader 启动前拒绝通过管道启动 TUI,并指向其单次 `-p` 模式;`apps/cli/tests/built-bin.e2e.ts` 固定了该执行路径,而 `cli-demo` 的 built-bin 套件在普通 Node 下运行文本、JSON 和经过结构化解析的 `stream-json` 输出,持久化新建会话,并在不污染 stdout 的情况下拒绝无效参数和缺失配置。时间上下文集成通过真实 Headless 组装执行两个有序轮次,而更细粒度的耗时行为由时间上下文的包级测试负责。
|
构建后的 `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 组装执行两个有序轮次,而更细粒度的耗时行为由时间上下文的包级测试负责。
|
||||||
|
|
||||||
## 曾考虑的替代方案
|
## 曾考虑的替代方案
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.md
|
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-07-29-shared-base-config-overlays.md
|
||||||
2026-07-29-shared-base-config-overlays.md: 4b9b8de2f07bf1747ed7aa50d67740df768c7491
|
2026-07-29-shared-base-config-overlays.md: ee642cbc786bef708791fb58e655c5a3f0e9c4e7
|
||||||
2026-07-29-shared-base-config-overlays.zh.md: c0ccc5880909afd1dec0cbb387067a3d72c1ae16
|
2026-07-29-shared-base-config-overlays.zh.md: b7fc9c6b121b8d0eb94d734af6bda6df45e25b1d
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Precedence is list order, last write winning per row: base, then the surface ove
|
|||||||
|
|
||||||
A patch replaces its target row's whole `config` rather than merging, which shapes the split: a row whose value differs per surface lives in the overlays, never in the base, so no row is patched by three layers at once. Session identity therefore cannot ride a config key at all — it moved to `dsh-agent-loop`'s `CONFIGURED_AGENT_IDENTITIES_KEY`, as [the launcher-owned identity note](../architecture/2026-07-28-launcher-owned-resume-identity.md) now records.
|
A patch replaces its target row's whole `config` rather than merging, which shapes the split: a row whose value differs per surface lives in the overlays, never in the base, so no row is patched by three layers at once. Session identity therefore cannot ride a config key at all — it moved to `dsh-agent-loop`'s `CONFIGURED_AGENT_IDENTITIES_KEY`, as [the launcher-owned identity note](../architecture/2026-07-28-launcher-owned-resume-identity.md) now records.
|
||||||
|
|
||||||
`examples/tui-agent`, `examples/cordis-agent`, and `packages/examples/tui-demo` are deleted. The TUI tests move to `apps/cli/tests/`, the cordis-toolset e2e to `packages/cordis/tool-cordis/tests/`, and `examples/code-mode` survives as a genuine example: an overlay that patches `tools.mode` and inserts the code runtime.
|
`examples/tui-agent`, `examples/cordis-agent`, `examples/code-mode`, and `packages/examples/tui-demo` are deleted. The TUI tests move to `apps/cli/tests/`, the cordis-toolset e2e to `packages/cordis/tool-cordis/tests/`, and the supported Code Mode demo remains the ACP overlay at `examples/acp-agent/code-mode.cordis.yml`.
|
||||||
|
|
||||||
## Alternatives considered
|
## Alternatives considered
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ A patch whose `id` matches no row stays a no-op rather than an error. That is de
|
|||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
Composition is checked by booting each tree through the real Loader and inspecting settled entries, not by reading YAML; both surfaces settle with zero unloaded rows, and Web starts its `httpServer` with sandboxed Bash and filesystem providers. The three-layer case (`base` + `tui` + `code-mode`) confirms `tools.mode` reaching `code` over the TUI overlay's `native`.
|
Composition is checked by booting each tree through the real Loader and inspecting settled entries, not by reading YAML; both surfaces settle with zero unloaded rows, and Web starts its `httpServer` with sandboxed Bash and filesystem providers. Code Mode remains covered by the ACP overlay and programmatic TUI snapshots rather than a separate shipped TUI application.
|
||||||
|
|
||||||
All eight terminal snapshot scenarios replay byte-identically after moving, and the 14-case PTY smoke passes, including two cases that assert a personal overlay reaches an **inserted** row — the behavior the vendored `plugin-include` fix enables ([`vendor/README.md`](../../../../vendor/README.md) local modification 8, covered by `packages/ui/app-boot/tests/config-reload.spec.ts`).
|
All eight terminal snapshot scenarios replay byte-identically after moving, and the 14-case PTY smoke passes, including two cases that assert a personal overlay reaches an **inserted** row — the behavior the vendored `plugin-include` fix enables ([`vendor/README.md`](../../../../vendor/README.md) local modification 8, covered by `packages/ui/app-boot/tests/config-reload.spec.ts`).
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Status: implemented
|
|||||||
|
|
||||||
patch 会整体替换目标配置项的 `config` 而不合并,这决定了拆分方式:取值因 surface 而异的配置项住在 overlay 中,绝不住在 base 里,从而没有任何配置项会被三层同时 patch。因此会话身份根本不能经由配置键传递——它迁移到了 `dsh-agent-loop` 的 `CONFIGURED_AGENT_IDENTITIES_KEY`,如[启动器持有身份的 note](../architecture/2026-07-28-launcher-owned-resume-identity.md) 现在所记录。
|
patch 会整体替换目标配置项的 `config` 而不合并,这决定了拆分方式:取值因 surface 而异的配置项住在 overlay 中,绝不住在 base 里,从而没有任何配置项会被三层同时 patch。因此会话身份根本不能经由配置键传递——它迁移到了 `dsh-agent-loop` 的 `CONFIGURED_AGENT_IDENTITIES_KEY`,如[启动器持有身份的 note](../architecture/2026-07-28-launcher-owned-resume-identity.md) 现在所记录。
|
||||||
|
|
||||||
`examples/tui-agent`、`examples/cordis-agent` 与 `packages/examples/tui-demo` 均被删除。TUI 测试迁往 `apps/cli/tests/`,cordis 工具集的 e2e 迁入 `packages/cordis/tool-cordis/tests/`,而 `examples/code-mode` 作为一个名副其实的示例保留下来:一个 patch `tools.mode` 并 insert code runtime 的 overlay。
|
`examples/tui-agent`、`examples/cordis-agent`、`examples/code-mode` 与 `packages/examples/tui-demo` 均被删除。TUI 测试迁往 `apps/cli/tests/`,cordis 工具集的 e2e 迁入 `packages/cordis/tool-cordis/tests/`,受支持的 Code Mode demo 则保留为 `examples/acp-agent/code-mode.cordis.yml` 中的 ACP overlay。
|
||||||
|
|
||||||
## 备选方案
|
## 备选方案
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ patch 会整体替换目标配置项的 `config` 而不合并,这决定了拆
|
|||||||
|
|
||||||
## 验证
|
## 验证
|
||||||
|
|
||||||
组合的正确性通过用真实 Loader 启动每棵树并检查已就绪的条目来核对,而不是靠阅读 YAML:两个界面都能稳定完成且没有未加载项;Web 会以沙箱化 Bash 与文件系统提供方启动 `httpServer`。三层叠加的情形(`base` + `tui` + `code-mode`)确认 `tools.mode` 越过 TUI overlay 的 `native` 达到了 `code`。
|
组合的正确性通过用真实 Loader 启动每棵树并检查已就绪的条目来核对,而不是靠阅读 YAML:两个界面都能稳定完成且没有未加载项;Web 会以沙箱化 Bash 与文件系统提供方启动 `httpServer`。Code Mode 继续由 ACP overlay 与程序化 TUI 快照覆盖,而不再维护独立交付的 TUI 应用。
|
||||||
|
|
||||||
全部八个终端快照场景在迁移后逐字节重放一致,14 个用例的 PTY 冒烟测试全部通过,其中两个用例断言个人 overlay 能触达一个 **insert 进来的**配置项——这正是 vendored `plugin-include` 修复所启用的行为([`vendor/README.md`](../../../../vendor/README.md) 本地修改第 8 条,由 `packages/ui/app-boot/tests/config-reload.spec.ts` 覆盖)。
|
全部八个终端快照场景在迁移后逐字节重放一致,14 个用例的 PTY 冒烟测试全部通过,其中两个用例断言个人 overlay 能触达一个 **insert 进来的**配置项——这正是 vendored `plugin-include` 修复所启用的行为([`vendor/README.md`](../../../../vendor/README.md) 本地修改第 8 条,由 `packages/ui/app-boot/tests/config-reload.spec.ts` 覆盖)。
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.md
|
# pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.md
|
||||||
2026-07-28-storage-root-and-derived-medium-recovery.md: 06fa98b10dc5ac3164d8905e7005a42d9e99ae92
|
2026-07-28-storage-root-and-derived-medium-recovery.md: 3937b17502d0bf640625e73822b758b1862b5391
|
||||||
2026-07-28-storage-root-and-derived-medium-recovery.zh.md: b7bd18ffdbfaf412d9a91940cf1770e273f5b847
|
2026-07-28-storage-root-and-derived-medium-recovery.zh.md: ff2f196a911986d23b7a102e6336f353c6aef278
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ English | [中文](2026-07-28-storage-root-and-derived-medium-recovery.zh.md)
|
|||||||
|
|
||||||
The persisted projection cache ([RFC](2026-07-27-session-projection-and-command-log.md), shipped as `dsh-session-projection-cache`) surfaced two gaps in the storage substrate it landed on. Both are properties of the domain-KV stack ([design](2026-07-24-domain-kv-storage-and-workspace.md)), not of the cache itself, and both bite the cache first because it is the first *derived* medium on that stack.
|
The persisted projection cache ([RFC](2026-07-27-session-projection-and-command-log.md), shipped as `dsh-session-projection-cache`) surfaced two gaps in the storage substrate it landed on. Both are properties of the domain-KV stack ([design](2026-07-24-domain-kv-storage-and-workspace.md)), not of the cache itself, and both bite the cache first because it is the first *derived* medium on that stack.
|
||||||
|
|
||||||
**Where the files actually live.** The shipped composition gives the json backend a relative root — `root: './.storages'` (apps/cli/cordis.yml) — and `AppCLIEntry.composePatches` patches only the session store's root to the global harness home (`$DSH_HOME/sessions`, default `~/.dsh/sessions`, profile-overridable via `persistenceRoot`); no equivalent patch or profile key exists for `storage-json`. `JsonStorageBackend` never resolves its root either — each unit open joins the still-relative path against whatever `process.cwd()` is at that moment (packages/storage/storage-json/src/index.ts) — the exact hazard the JSONL session backend resolves-once to prevent ("later process.cwd() changes cannot split one backend across roots", packages/session-persistence/session-persistence-jsonl/src/index.ts). Net effect: session logs are global across launch directories, but `workspace.json` and `session_projcache.json` land under `<launch dir>/.storages/`. Two launches from different directories share their sessions yet see different workspace registries and different projection caches — and the cache exists precisely to serve the cross-session cold listing, which now misses for every session last cached under another launch directory.
|
**Where the files actually live.** The shipped Web overlay gives the json backend a relative root — `root: './.storages'` (`apps/cli/config/web.cordis.yml`) — while the shared base defaults the session store to the global harness home (`$DSH_HOME/sessions`, default `~/.dsh/sessions`); no equivalent global root exists for `storage-json`. `JsonStorageBackend` never resolves its root either — each unit open joins the still-relative path against whatever `process.cwd()` is at that moment (packages/storage/storage-json/src/index.ts) — the exact hazard the JSONL session backend resolves-once to prevent ("later process.cwd() changes cannot split one backend across roots", packages/session-persistence/session-persistence-jsonl/src/index.ts). Net effect: session logs are global across launch directories, but `workspace.json` and `session_projcache.json` land under `<launch dir>/.storages/`. Two launches from different directories share their sessions yet see different workspace registries and different projection caches — and the cache exists precisely to serve the cross-session cold listing, which now misses for every session last cached under another launch directory.
|
||||||
|
|
||||||
**How recovery works today.** Inside a healthy medium the cache is fully self-healing by design: a `stateVersion`-mismatched row is discarded and refolded, a log shrunk below a row's watermark is detected by the anchored restore floor and answered with one full re-read, and every background write is fail-soft. But at the *medium* level there is no recovery at all: a truncated, hand-edited, or version-bumped `session_projcache.json` fails `openJsonUnit` with `malformed-medium`/`version-mismatch` (packages/storage/storage-json/src/format.ts), a schema-drifted record fails domain open with `invalid-record` (packages/storage/storage-domain/src/index.ts), the rejection propagates through `SessionProjectionCache[Service.init]`, and under the CLI's fail-loud boot the assembly refuses to start. A file whose entire content is rebuildable from session logs can brick boot. This contradicts the cache package's own stated stance ("a stale or unreadable cache costs a longer tail replay, never a wrong value") and the cache domain spec's JSDoc ("version bumps discard the whole medium"), which today describes an aspiration, not the implementation. The same fail-loud path is *correct* for `workspace.json` — workspace records are authoritative, not derivable — so the missing concept is a per-domain declaration of authority, not a global behavior change.
|
**How recovery works today.** Inside a healthy medium the cache is fully self-healing by design: a `stateVersion`-mismatched row is discarded and refolded, a log shrunk below a row's watermark is detected by the anchored restore floor and answered with one full re-read, and every background write is fail-soft. But at the *medium* level there is no recovery at all: a truncated, hand-edited, or version-bumped `session_projcache.json` fails `openJsonUnit` with `malformed-medium`/`version-mismatch` (packages/storage/storage-json/src/format.ts), a schema-drifted record fails domain open with `invalid-record` (packages/storage/storage-domain/src/index.ts), the rejection propagates through `SessionProjectionCache[Service.init]`, and under the CLI's fail-loud boot the assembly refuses to start. A file whose entire content is rebuildable from session logs can brick boot. This contradicts the cache package's own stated stance ("a stale or unreadable cache costs a longer tail replay, never a wrong value") and the cache domain spec's JSDoc ("version bumps discard the whole medium"), which today describes an aspiration, not the implementation. The same fail-loud path is *correct* for `workspace.json` — workspace records are authoritative, not derivable — so the missing concept is a per-domain declaration of authority, not a global behavior change.
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Status: proposed
|
|||||||
|
|
||||||
持久投影缓存([RFC](2026-07-27-session-projection-and-command-log.md),已作为 `dsh-session-projection-cache` 落地)暴露了它所依托的存储基座的两个缺口。二者都是 domain-KV 栈([设计](2026-07-24-domain-kv-storage-and-workspace.md))的属性而非缓存自身的问题,且都首先咬到缓存——因为它是这条栈上第一个*派生*介质。
|
持久投影缓存([RFC](2026-07-27-session-projection-and-command-log.md),已作为 `dsh-session-projection-cache` 落地)暴露了它所依托的存储基座的两个缺口。二者都是 domain-KV 栈([设计](2026-07-24-domain-kv-storage-and-workspace.md))的属性而非缓存自身的问题,且都首先咬到缓存——因为它是这条栈上第一个*派生*介质。
|
||||||
|
|
||||||
**文件到底存在哪。** 出厂组合给 json 后端的是相对根目录——`root: './.storages'`(apps/cli/cordis.yml)——而 `AppCLIEntry.composePatches` 只把会话存储的根 patch 到全局 harness home(`$DSH_HOME/sessions`,默认 `~/.dsh/sessions`,可经 profile 键 `persistenceRoot` 覆盖);`storage-json` 没有对应的 patch 也没有 profile 键。`JsonStorageBackend` 自己也从不 resolve 根——每次打开 unit 都把仍然相对的路径 join 到当时的 `process.cwd()` 上(packages/storage/storage-json/src/index.ts)——这正是 JSONL 会话后端用「构造时 resolve 一次」防住的那个隐患("later process.cwd() changes cannot split one backend across roots",packages/session-persistence/session-persistence-jsonl/src/index.ts)。净效果:会话日志跨启动目录全局共享,但 `workspace.json` 和 `session_projcache.json` 落在 `<启动目录>/.storages/` 下。从两个不同目录启动,会话相同,工作区注册表和投影缓存却各是一份——而缓存存在的意义恰恰是跨会话冷列表,如今凡是上次在别的启动目录下缓存过的会话全部 miss。
|
**文件到底存在哪。** 出厂 Web overlay 给 json 后端的是相对根目录——`root: './.storages'`(`apps/cli/config/web.cordis.yml`)——而共享 base 将会话存储默认为全局 harness home(`$DSH_HOME/sessions`,默认 `~/.dsh/sessions`);`storage-json` 没有对应的全局根目录。`JsonStorageBackend` 自己也从不 resolve 根——每次打开 unit 都把仍然相对的路径 join 到当时的 `process.cwd()` 上(packages/storage/storage-json/src/index.ts)——这正是 JSONL 会话后端用「构造时 resolve 一次」防住的那个隐患("later process.cwd() changes cannot split one backend across roots",packages/session-persistence/session-persistence-jsonl/src/index.ts)。净效果:会话日志跨启动目录全局共享,但 `workspace.json` 和 `session_projcache.json` 落在 `<启动目录>/.storages/` 下。从两个不同目录启动,会话相同,工作区注册表和投影缓存却各是一份——而缓存存在的意义恰恰是跨会话冷列表,如今凡是上次在别的启动目录下缓存过的会话全部 miss。
|
||||||
|
|
||||||
**现在是怎么恢复的。** 在健康介质内部,缓存按设计完全自愈:`stateVersion` 不匹配的行被丢弃重折,日志缩短到行水位以下由带锚的 restore floor 检出并以一次全量重读回答,每次后台写都是 fail-soft。但在*介质*层面完全没有恢复:被截断、被手改或版本被 bump 的 `session_projcache.json` 会让 `openJsonUnit` 以 `malformed-medium`/`version-mismatch` 失败(packages/storage/storage-json/src/format.ts),schema 漂移的记录让域 open 以 `invalid-record` 失败(packages/storage/storage-domain/src/index.ts),拒绝一路穿过 `SessionProjectionCache[Service.init]`,在 CLI 的 fail-loud 启动下整个组装拒绝启动。一个内容完全可从会话日志重建的文件能把启动搞死。这与缓存包自己声明的立场("a stale or unreadable cache costs a longer tail replay, never a wrong value")和缓存域 spec 的 JSDoc("version bumps discard the whole medium")相矛盾——后者今天描述的是愿望而非实现。同一条 fail-loud 路径对 `workspace.json` 却是*正确*的——工作区记录是权威数据,不可派生——所以缺的概念是按域声明权威性,而不是全局改行为。
|
**现在是怎么恢复的。** 在健康介质内部,缓存按设计完全自愈:`stateVersion` 不匹配的行被丢弃重折,日志缩短到行水位以下由带锚的 restore floor 检出并以一次全量重读回答,每次后台写都是 fail-soft。但在*介质*层面完全没有恢复:被截断、被手改或版本被 bump 的 `session_projcache.json` 会让 `openJsonUnit` 以 `malformed-medium`/`version-mismatch` 失败(packages/storage/storage-json/src/format.ts),schema 漂移的记录让域 open 以 `invalid-record` 失败(packages/storage/storage-domain/src/index.ts),拒绝一路穿过 `SessionProjectionCache[Service.init]`,在 CLI 的 fail-loud 启动下整个组装拒绝启动。一个内容完全可从会话日志重建的文件能把启动搞死。这与缓存包自己声明的立场("a stale or unreadable cache costs a longer tail replay, never a wrong value")和缓存域 spec 的 JSDoc("version bumps discard the whole medium")相矛盾——后者今天描述的是愿望而非实现。同一条 fail-loud 路径对 `workspace.json` 却是*正确*的——工作区记录是权威数据,不可派生——所以缺的概念是按域声明权威性,而不是全局改行为。
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ flowchart LR
|
|||||||
cfg --> plugin_tui_llm_pi_ai
|
cfg --> plugin_tui_llm_pi_ai
|
||||||
plugin_tui_session_persistence_jsonl["session-persistence-jsonl<br/>@deepseek-ai/dsh-session-persistence-jsonl"]
|
plugin_tui_session_persistence_jsonl["session-persistence-jsonl<br/>@deepseek-ai/dsh-session-persistence-jsonl"]
|
||||||
cfg --> plugin_tui_session_persistence_jsonl
|
cfg --> plugin_tui_session_persistence_jsonl
|
||||||
|
plugin_tui_session_query_sqlite["session-query-sqlite<br/>@deepseek-ai/dsh-session-query-sqlite"]
|
||||||
|
cfg --> plugin_tui_session_query_sqlite
|
||||||
plugin_tui_subprocess["subprocess<br/>@deepseek-ai/dsh-subprocess-local"]
|
plugin_tui_subprocess["subprocess<br/>@deepseek-ai/dsh-subprocess-local"]
|
||||||
cfg --> plugin_tui_subprocess
|
cfg --> plugin_tui_subprocess
|
||||||
plugin_tui_bash_local["bash-local<br/>@deepseek-ai/dsh-bash-local"]
|
plugin_tui_bash_local["bash-local<br/>@deepseek-ai/dsh-bash-local"]
|
||||||
@@ -114,6 +116,7 @@ flowchart LR
|
|||||||
| `llm-retry` | `@deepseek-ai/dsh-llm-retry` |
|
| `llm-retry` | `@deepseek-ai/dsh-llm-retry` |
|
||||||
| `llm-pi-ai` | `@deepseek-ai/dsh-llm-pi-ai` |
|
| `llm-pi-ai` | `@deepseek-ai/dsh-llm-pi-ai` |
|
||||||
| `session-persistence-jsonl` | `@deepseek-ai/dsh-session-persistence-jsonl` |
|
| `session-persistence-jsonl` | `@deepseek-ai/dsh-session-persistence-jsonl` |
|
||||||
|
| `session-query-sqlite` | `@deepseek-ai/dsh-session-query-sqlite` |
|
||||||
| `subprocess` | `@deepseek-ai/dsh-subprocess-local` |
|
| `subprocess` | `@deepseek-ai/dsh-subprocess-local` |
|
||||||
| `bash-local` | `@deepseek-ai/dsh-bash-local` |
|
| `bash-local` | `@deepseek-ai/dsh-bash-local` |
|
||||||
| `tool-bash` | `@deepseek-ai/dsh-tool-bash` |
|
| `tool-bash` | `@deepseek-ai/dsh-tool-bash` |
|
||||||
|
|||||||
@@ -70,7 +70,15 @@
|
|||||||
- id: session-persistence-jsonl
|
- id: session-persistence-jsonl
|
||||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||||
config:
|
config:
|
||||||
root: !!js process.getBuiltinModule('node:path').join(process.env.DSH_HOME || process.getBuiltinModule('node:path').join(process.getBuiltinModule('node:os').homedir(), '.dsh'), 'sessions')
|
root: !!js >-
|
||||||
|
(() => { const path = process.getBuiltinModule('node:path'); const home = process.getBuiltinModule('node:os').homedir(); const configured = process.env.DSH_HOME; const selected = configured !== undefined && configured.trim().length > 0 ? configured : path.join(home, '.dsh'); const expanded = selected === '~' ? home : selected.startsWith('~/') || selected.startsWith('~\\') ? path.join(home, selected.slice(2)) : selected; return path.join(path.resolve(expanded), 'sessions') })()
|
||||||
|
|
||||||
|
# TUI consumes this shared session capability. Its launcher supplies a unique
|
||||||
|
# process-local path; non-TUI surfaces disable the row in their overlay.
|
||||||
|
- id: session-query-sqlite
|
||||||
|
name: '@deepseek-ai/dsh-session-query-sqlite'
|
||||||
|
config:
|
||||||
|
path: !!js launcherSessionQueryPath ?? './.sessions/session-query.db'
|
||||||
|
|
||||||
- id: subprocess
|
- id: subprocess
|
||||||
name: '@deepseek-ai/dsh-subprocess-local'
|
name: '@deepseek-ai/dsh-subprocess-local'
|
||||||
|
|||||||
@@ -50,8 +50,7 @@
|
|||||||
config:
|
config:
|
||||||
cwd: !!js process.cwd()
|
cwd: !!js process.cwd()
|
||||||
|
|
||||||
# The shipped TUI presents the native tool registry. `examples/code-mode` is the
|
# The shipped TUI presents the native tool registry.
|
||||||
# overlay that switches this row to the `run_code` transport.
|
|
||||||
- id: tools
|
- id: tools
|
||||||
config:
|
config:
|
||||||
mode: native
|
mode: native
|
||||||
@@ -78,11 +77,6 @@
|
|||||||
# The derived query index behind `/resume`. The launcher provides a unique
|
# The derived query index behind `/resume`. The launcher provides a unique
|
||||||
# process-local path because this SQLite backend has one writer owner; the
|
# process-local path because this SQLite backend has one writer owner; the
|
||||||
# project-local fallback applies when no launcher sets the typed slot.
|
# project-local fallback applies when no launcher sets the typed slot.
|
||||||
- id: session-query-sqlite
|
|
||||||
name: '@deepseek-ai/dsh-session-query-sqlite'
|
|
||||||
config:
|
|
||||||
path: !!js launcherSessionQueryPath ?? './.sessions/session-query.db'
|
|
||||||
|
|
||||||
- id: session-reference
|
- id: session-reference
|
||||||
name: '@deepseek-ai/dsh-session-reference'
|
name: '@deepseek-ai/dsh-session-reference'
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,9 @@
|
|||||||
- id: hmr
|
- id: hmr
|
||||||
disabled: true
|
disabled: true
|
||||||
|
|
||||||
- id: system-prompt
|
# Session query is a TUI capability; Web owns its own session presentation.
|
||||||
config:
|
- id: session-query-sqlite
|
||||||
persona: ''
|
disabled: true
|
||||||
|
|
||||||
- id: tools
|
- id: tools
|
||||||
config:
|
config:
|
||||||
@@ -26,10 +26,6 @@
|
|||||||
# once the web UI owns the choice per session.
|
# once the web UI owns the choice per session.
|
||||||
mode: !!js process.env.DSH_TOOLS_MODE
|
mode: !!js process.env.DSH_TOOLS_MODE
|
||||||
|
|
||||||
- id: agent-loop
|
|
||||||
config:
|
|
||||||
agents: []
|
|
||||||
|
|
||||||
- id: llm-deepseek
|
- id: llm-deepseek
|
||||||
config:
|
config:
|
||||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||||
|
|||||||
@@ -71,6 +71,7 @@
|
|||||||
"@deepseek-ai/dsh-sandbox-local": "workspace:^",
|
"@deepseek-ai/dsh-sandbox-local": "workspace:^",
|
||||||
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
|
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
|
||||||
"@deepseek-ai/dsh-scope": "workspace:^",
|
"@deepseek-ai/dsh-scope": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-tool-cordis": "workspace:^",
|
||||||
"@deepseek-ai/dsh-session": "workspace:^",
|
"@deepseek-ai/dsh-session": "workspace:^",
|
||||||
"@deepseek-ai/dsh-session-checkpoint-policy": "workspace:^",
|
"@deepseek-ai/dsh-session-checkpoint-policy": "workspace:^",
|
||||||
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
|
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { createRequire } from 'node:module'
|
|||||||
import { networkInterfaces } from 'node:os'
|
import { networkInterfaces } from 'node:os'
|
||||||
import { join, resolve } from 'node:path'
|
import { join, resolve } from 'node:path'
|
||||||
import { Context } from 'cordis'
|
import { Context } from 'cordis'
|
||||||
import type { FiberState } from 'cordis'
|
|
||||||
import type { PatchOptions } from '@cordisjs/plugin-include'
|
import type { PatchOptions } from '@cordisjs/plugin-include'
|
||||||
import yaml from 'js-yaml'
|
import yaml from 'js-yaml'
|
||||||
import { boot, installFailLoud, loadEnv, loadOverlayPatches, loadPersonalPatches } from '@deepseek-ai/dsh-app-boot'
|
import { boot, installFailLoud, loadEnv, loadOverlayPatches, loadPersonalPatches } from '@deepseek-ai/dsh-app-boot'
|
||||||
@@ -88,14 +87,6 @@ const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
|
|||||||
})
|
})
|
||||||
const includeYamlSchema = yaml.JSON_SCHEMA.extend(jsExprType)
|
const includeYamlSchema = yaml.JSON_SCHEMA.extend(jsExprType)
|
||||||
|
|
||||||
/**
|
|
||||||
* Value mirror of cordis's `FiberState` const enum members the sweep needs
|
|
||||||
* (a const enum has no runtime object to import; same rationale as the
|
|
||||||
* client-side mirror in dsh-client-web).
|
|
||||||
*/
|
|
||||||
const FIBER_ACTIVE = 2 as FiberState.ACTIVE
|
|
||||||
const FIBER_PENDING = 0 as FiberState.PENDING
|
|
||||||
|
|
||||||
/** Constructor facts for one dsh invocation over the shared composition (argv already parsed by the surface bin). */
|
/** Constructor facts for one dsh invocation over the shared composition (argv already parsed by the surface bin). */
|
||||||
export interface AppCLIEntryOptions {
|
export interface AppCLIEntryOptions {
|
||||||
/** Absolute path of the shared base config the Loader includes. */
|
/** Absolute path of the shared base config the Loader includes. */
|
||||||
@@ -174,10 +165,8 @@ export class AppCLIEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compose the patch set from the non-yml config sources: computed
|
* Compose the patch set from profile json, CLI flags, and the resolved
|
||||||
* engineering defaults (the global session root), profile json (user
|
* frontend dist. Patches replace a row's config wholesale, so each patched row's yml
|
||||||
* config, overriding those defaults), CLI flags, and the resolved frontend
|
|
||||||
* dist. Patches replace a row's config wholesale, so each patched row's yml
|
|
||||||
* static values are re-read here (bypass parse) and merged under the overrides.
|
* static values are re-read here (bypass parse) and merged under the overrides.
|
||||||
*/
|
*/
|
||||||
private composePatches(): void {
|
private composePatches(): void {
|
||||||
@@ -189,7 +178,6 @@ export class AppCLIEntry {
|
|||||||
overrides.set(entryId, bag)
|
overrides.set(entryId, bag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Source 1: profile json (missing file = empty; unmapped key = loud).
|
// Source 1: profile json (missing file = empty; unmapped key = loud).
|
||||||
for (const [key, value] of Object.entries(this.readProfile())) {
|
for (const [key, value] of Object.entries(this.readProfile())) {
|
||||||
const mapping = PROFILE_MAPPINGS.find(m => m.jsonPath === key)
|
const mapping = PROFILE_MAPPINGS.find(m => m.jsonPath === key)
|
||||||
@@ -215,7 +203,6 @@ export class AppCLIEntry {
|
|||||||
// user config. Workspace knowledge stays here.
|
// user config. Workspace knowledge stays here.
|
||||||
put('webserver', 'distIndex', this.resolveDistIndex())
|
put('webserver', 'distIndex', this.resolveDistIndex())
|
||||||
|
|
||||||
|
|
||||||
this.patches = [...overrides.entries()].map(([id, bag]) => {
|
this.patches = [...overrides.entries()].map(([id, bag]) => {
|
||||||
const yml = rows.get(id)
|
const yml = rows.get(id)
|
||||||
if (yml === undefined) throw new Error(`dsh: patch target row "${id}" not found in ${this.options.configPath}`)
|
if (yml === undefined) throw new Error(`dsh: patch target row "${id}" not found in ${this.options.configPath}`)
|
||||||
@@ -241,28 +228,9 @@ export class AppCLIEntry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Install the diagnostic for plugin rejections that happen after settled boot. */
|
||||||
* Shared boot catches import failures, installFailLoud catches late apply
|
|
||||||
* rejections, and the all-ACTIVE sweep
|
|
||||||
* below catches PENDING fibers (cordis inject waiting has no timeout).
|
|
||||||
*/
|
|
||||||
private assertBoot(): void {
|
private assertBoot(): void {
|
||||||
installFailLoud('dsh')
|
installFailLoud('dsh')
|
||||||
const failures: string[] = []
|
|
||||||
for (const entry of this.ctx.loader.entries()) {
|
|
||||||
if (entry.fiber === undefined || entry.disabled) continue
|
|
||||||
const state = entry.fiber.state
|
|
||||||
if (state === FIBER_ACTIVE) continue
|
|
||||||
if (state === FIBER_PENDING) {
|
|
||||||
const missing = Object.keys(entry.fiber.inject).filter(service => this.ctx.get(service) === undefined)
|
|
||||||
failures.push(`${entry.options.name}: pending (waiting for service${missing.length === 1 ? '' : 's'}: ${missing.join(', ') || 'unknown'})`)
|
|
||||||
} else {
|
|
||||||
failures.push(`${entry.options.name}: fiber state ${String(state)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (failures.length > 0) {
|
|
||||||
throw new Error(`dsh: ${String(failures.length)} entr${failures.length === 1 ? 'y' : 'ies'} did not activate\n${failures.join('\n')}`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -126,6 +126,9 @@ Examples:
|
|||||||
dsh --resume <id> continue a past session
|
dsh --resume <id> continue a past session
|
||||||
`)
|
`)
|
||||||
.exitOverride()
|
.exitOverride()
|
||||||
|
// Stop parent options at a subcommand boundary so `web --config` belongs to
|
||||||
|
// Web while `--config ... web` remains a leaked default-surface option.
|
||||||
|
.enablePositionalOptions()
|
||||||
// Default surface: option-only (no positional), so `web` can be a real
|
// Default surface: option-only (no positional), so `web` can be a real
|
||||||
// subcommand without a positional collision.
|
// subcommand without a positional collision.
|
||||||
.option('-p, --prompt <task>', 'answer this task without the interactive UI, then exit')
|
.option('-p, --prompt <task>', 'answer this task without the interactive UI, then exit')
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
* workspace, and an in-place resume enters the selected session's own directory.
|
* workspace, and an in-place resume enters the selected session's own directory.
|
||||||
* `dsh meta`
|
* `dsh meta`
|
||||||
* ({@link runMeta}) is the one exception — it makes this harness checkout the
|
* ({@link runMeta}) is the one exception — it makes this harness checkout the
|
||||||
* workspace. `dsh upgrade` ({@link runSkillSession}) are fresh
|
* workspace. `dsh upgrade` ({@link runSkillSession}) is a fresh
|
||||||
* sessions whose first turn auto-invokes a bundled skill. After boot, the
|
* session whose first turn auto-invokes a bundled skill. After boot, the
|
||||||
* agent's system prompt is told the path to this harness checkout so it can
|
* agent's system prompt is told the path to this harness checkout so it can
|
||||||
* find its own source.
|
* find its own source.
|
||||||
* @module @deepseek-ai/dsh/tui
|
* @module @deepseek-ai/dsh/tui
|
||||||
@@ -146,7 +146,7 @@ export async function runTui(
|
|||||||
const app: { current?: Context } = {}
|
const app: { current?: Context } = {}
|
||||||
// Resume always enters the default surface because meta rejects parent
|
// Resume always enters the default surface because meta rejects parent
|
||||||
// options, including `--resume`. The resumed session already persists its cwd.
|
// options, including `--resume`. The resumed session already persists its cwd.
|
||||||
const resumeArgs = (sessionId: string, _targetCwd?: string): string[] => [
|
const resumeArgs = (sessionId: string): string[] => [
|
||||||
`--resume=${sessionId}`,
|
`--resume=${sessionId}`,
|
||||||
// Both config flags must survive the handoff: resuming into a different
|
// Both config flags must survive the handoff: resuming into a different
|
||||||
// tree than the session was created in would silently change the agent.
|
// tree than the session was created in would silently change the agent.
|
||||||
@@ -167,7 +167,7 @@ export async function runTui(
|
|||||||
process.execPath,
|
process.execPath,
|
||||||
...process.execArgv,
|
...process.execArgv,
|
||||||
entry,
|
entry,
|
||||||
...resumeArgs(sessionId, cwd),
|
...resumeArgs(sessionId),
|
||||||
]
|
]
|
||||||
// `execve` inherits the cwd, and the target session may belong to another
|
// `execve` inherits the cwd, and the target session may belong to another
|
||||||
// workspace. Enter it BEFORE teardown commits: an unreachable directory
|
// workspace. Enter it BEFORE teardown commits: an unreachable directory
|
||||||
@@ -199,14 +199,14 @@ export async function runTui(
|
|||||||
const replaceTree = configReplace !== undefined
|
const replaceTree = configReplace !== undefined
|
||||||
const patches = replaceTree ? [] : [
|
const patches = replaceTree ? [] : [
|
||||||
...loadOverlayPatches(NAME, TUI_OVERLAY),
|
...loadOverlayPatches(NAME, TUI_OVERLAY),
|
||||||
...config === undefined
|
...resolvedConfig === undefined
|
||||||
? loadPersonalPatches(NAME) ?? []
|
? loadPersonalPatches(NAME) ?? []
|
||||||
: loadOverlayPatches(NAME, resolveConfigPath(resolve(config), undefined)),
|
: loadOverlayPatches(NAME, resolveConfigPath(resolvedConfig, undefined)),
|
||||||
]
|
]
|
||||||
const queryIndexPath = join(tmpdir(), SESSION_QUERY_DB)
|
const queryIndexPath = join(tmpdir(), SESSION_QUERY_DB)
|
||||||
const ctx = await boot(
|
const ctx = await boot(
|
||||||
NAME,
|
NAME,
|
||||||
replaceTree ? resolveConfigPath(resolve(configReplace), undefined) : BASE_CONFIG,
|
resolvedConfigReplace === undefined ? BASE_CONFIG : resolveConfigPath(resolvedConfigReplace, undefined),
|
||||||
patches,
|
patches,
|
||||||
(hostCtx) => {
|
(hostCtx) => {
|
||||||
// The launcher owns session identity and the exit line: a config-mounted
|
// The launcher owns session identity and the exit line: a config-mounted
|
||||||
@@ -230,7 +230,7 @@ export async function runTui(
|
|||||||
rm(`${queryIndexPath}-wal`, { force: true }),
|
rm(`${queryIndexPath}-wal`, { force: true }),
|
||||||
rm(`${queryIndexPath}-shm`, { force: true }),
|
rm(`${queryIndexPath}-shm`, { force: true }),
|
||||||
])
|
])
|
||||||
}, 'launcherSessionQueryPath.cleanup')
|
}, `${SESSION_QUERY_SQLITE_PATH_KEY}.cleanup`)
|
||||||
if (resumeHost !== undefined) hostCtx.provide('tuiResumeHost', resumeHost)
|
if (resumeHost !== undefined) hostCtx.provide('tuiResumeHost', resumeHost)
|
||||||
// Seed the first turn only for a fresh session, so resuming never
|
// Seed the first turn only for a fresh session, so resuming never
|
||||||
// re-invokes the skill.
|
// re-invokes the skill.
|
||||||
|
|||||||
@@ -31,10 +31,9 @@ describe('parseDshArgs', () => {
|
|||||||
expect(parse(['--resume', 'sess', '--config', 'app.yml'])).toEqual({ mode: 'tui', config: 'app.yml', resume: 'sess' })
|
expect(parse(['--resume', 'sess', '--config', 'app.yml'])).toEqual({ mode: 'tui', config: 'app.yml', resume: 'sess' })
|
||||||
expect(parse(['-p', 'do the thing'])).toEqual({ mode: 'headless', prompt: 'do the thing' })
|
expect(parse(['-p', 'do the thing'])).toEqual({ mode: 'headless', prompt: 'do the thing' })
|
||||||
expect(parse(['meta'])).toEqual({ mode: 'meta' })
|
expect(parse(['meta'])).toEqual({ mode: 'meta' })
|
||||||
// Credential setup is option-free: it writes the Harness-home .env, so
|
|
||||||
// there is nothing for a flag to select.
|
|
||||||
// Bare `web` carries no host/port: the shipped Web overlay owns the default.
|
// Bare `web` carries no host/port: the shipped Web overlay owns the default.
|
||||||
expect(parse(['web'])).toEqual({ mode: 'web', dev: false })
|
expect(parse(['web'])).toEqual({ mode: 'web', dev: false })
|
||||||
|
expect(parse(['web', '--config', 'web.yml'])).toEqual({ mode: 'web', dev: false, config: 'web.yml' })
|
||||||
// Host/port are unvalidated pass-throughs (the webserver schema gates them
|
// Host/port are unvalidated pass-throughs (the webserver schema gates them
|
||||||
// at boot); the adapter only coerces the port string to a number.
|
// at boot); the adapter only coerces the port string to a number.
|
||||||
expect(parse(['web', '--host', '0.0.0.0', '--port', '8080', '--dev', '--workspace-root', '/w']))
|
expect(parse(['web', '--host', '0.0.0.0', '--port', '8080', '--dev', '--workspace-root', '/w']))
|
||||||
@@ -72,7 +71,7 @@ describe('parseDshArgs', () => {
|
|||||||
expect(exitCode(['meta', '--config', 'c.yml'])).toBe(1)
|
expect(exitCode(['meta', '--config', 'c.yml'])).toBe(1)
|
||||||
expect(exitCode(['meta', '--config-replace', 'tree.yml'])).toBe(1)
|
expect(exitCode(['meta', '--config-replace', 'tree.yml'])).toBe(1)
|
||||||
expect(exitCode(['meta', '-p', 'task'])).toBe(1)
|
expect(exitCode(['meta', '-p', 'task'])).toBe(1)
|
||||||
// `upgrade` take no options: any leaked default-surface flag is a
|
// `upgrade` takes no options: any leaked default-surface flag is a
|
||||||
// mistyped invocation, not a silently-dropped input.
|
// mistyped invocation, not a silently-dropped input.
|
||||||
expect(exitCode(['upgrade', '--resume', 's'])).toBe(1)
|
expect(exitCode(['upgrade', '--resume', 's'])).toBe(1)
|
||||||
expect(exitCode(['upgrade', '--config', 'c.yml'])).toBe(1)
|
expect(exitCode(['upgrade', '--config', 'c.yml'])).toBe(1)
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write docs/cordis-tutorial/01-first-plugin.md
|
# pnpm run verify-translation-pairing --write docs/cordis-tutorial/01-first-plugin.md
|
||||||
01-first-plugin.md: 0af2b8a292c2968c37e633d620192414479caafc
|
01-first-plugin.md: 39f140b91d4c9f8cc197df4d08bd2c4c168cc858
|
||||||
01-first-plugin.zh.md: cbea3ca2f7d875f481ef6a17749a5fac9774de1b
|
01-first-plugin.zh.md: 0715cd525f980ab237e65baf6982192d2e0c286c
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ The process exits on its own once nothing is left running. What happened:
|
|||||||
2. The Loader read `cordis.yml`, resolved `./hello.ts`, and mounted it as a child plugin.
|
2. The Loader read `cordis.yml`, resolved `./hello.ts`, and mounted it as a child plugin.
|
||||||
3. Cordis called your `apply(ctx)`.
|
3. Cordis called your `apply(ctx)`.
|
||||||
|
|
||||||
There is no framework bootstrap code in your file: a plugin describes what it contributes, and `cordis.yml` composes the application. The [TUI agent](../../apps/cli/config/base.cordis.yml), for example, is a longer plugin composition.
|
There is no framework bootstrap code in your file: a plugin describes what it contributes, and `cordis.yml` composes the application. The [TUI agent](../../apps/cli/config/tui.cordis.yml), for example, is a longer plugin composition.
|
||||||
|
|
||||||
## The two other plugin shapes
|
## The two other plugin shapes
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ hello from my first plugin
|
|||||||
2. Loader 读取 `cordis.yml`,解析 `./hello.ts`,然后将其作为子插件挂载。
|
2. Loader 读取 `cordis.yml`,解析 `./hello.ts`,然后将其作为子插件挂载。
|
||||||
3. Cordis 调用你的 `apply(ctx)`。
|
3. Cordis 调用你的 `apply(ctx)`。
|
||||||
|
|
||||||
你的文件中没有框架启动代码:插件描述自己的贡献,`cordis.yml` 则组合应用。例如,[TUI agent(智能体)](../../apps/cli/config/base.cordis.yml) 就是一个更长的插件组合。
|
你的文件中没有框架启动代码:插件描述自己的贡献,`cordis.yml` 则组合应用。例如,[TUI agent(智能体)](../../apps/cli/config/tui.cordis.yml) 就是一个更长的插件组合。
|
||||||
|
|
||||||
## 其他两种插件形态
|
## 其他两种插件形态
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write examples/README.md
|
# pnpm run verify-translation-pairing --write examples/README.md
|
||||||
README.md: 61aff168c656082d20c12416b62512ac17f445cb
|
README.md: a502f34128da497586d593f64d0ce1c05f68a067
|
||||||
README.zh.md: 50fcaae6ee2997055d3c97480f2e5bed0983fc5e
|
README.zh.md: e3c111bb6a8f67899b6f434345baa3b47640b7ee
|
||||||
|
|||||||
@@ -10,12 +10,6 @@ A non-interactive agent demo that accepts one positional task, runs one complete
|
|||||||
|
|
||||||
Run with: `pnpm run demo:headless "task"` (needs `DEEPSEEK_API_KEY`). See [headless-agent/README.md](headless-agent/README.md) for the output contract, safety boundaries, and snapshot suite.
|
Run with: `pnpm run demo:headless "task"` (needs `DEEPSEEK_API_KEY`). See [headless-agent/README.md](headless-agent/README.md) for the output contract, safety boundaries, and snapshot suite.
|
||||||
|
|
||||||
## code-mode
|
|
||||||
|
|
||||||
An **overlay** over the shipped TUI that reduces the model-facing registry to the `run_code` transport, so the model batches tool work into TypeScript programs instead of one call per turn.
|
|
||||||
|
|
||||||
Run with: `pnpm run demo:code-mode` (needs `DEEPSEEK_API_KEY`). See [../apps/cli/README.md](../apps/cli/README.md). The interactive agent itself is not an example: `pnpm run demo:tui` boots [`apps/cli/config/tui.cordis.yml`](../apps/cli/config/tui.cordis.yml) over the shared base, and its PTY and snapshot scenarios live in `apps/cli/tests/`.
|
|
||||||
|
|
||||||
## jsonrpc-agent
|
## jsonrpc-agent
|
||||||
|
|
||||||
An unattended coding agent driven through the Python SDK: JSON-RPC stdio, foreground-only `bash`, `read` / `write` / `edit`, one foreground `subagent`, `todo_write`, JSONL persistence, and compaction. It excludes terminal UI, stdout logging, approvals, skills, and background task controls. See [jsonrpc-agent/README.md](jsonrpc-agent/README.md).
|
An unattended coding agent driven through the Python SDK: JSON-RPC stdio, foreground-only `bash`, `read` / `write` / `edit`, one foreground `subagent`, `todo_write`, JSONL persistence, and compaction. It excludes terminal UI, stdout logging, approvals, skills, and background task controls. See [jsonrpc-agent/README.md](jsonrpc-agent/README.md).
|
||||||
@@ -30,6 +24,6 @@ Run the browser UI at `http://127.0.0.1:3081` with `pnpm run demo:cordis`, or th
|
|||||||
|
|
||||||
An agent exposed as an **Agent Client Protocol (ACP)** automation server over JSON-RPC stdio, via [`@deepseek-ai/dsh-acp-demo`](../packages/examples/acp-demo). Programmatic clients create fresh sessions, send text prompts, consume committed assistant text, answer one-shot permission requests, and cancel work. It owns the ACP keyless snapshot suite.
|
An agent exposed as an **Agent Client Protocol (ACP)** automation server over JSON-RPC stdio, via [`@deepseek-ai/dsh-acp-demo`](../packages/examples/acp-demo). Programmatic clients create fresh sessions, send text prompts, consume committed assistant text, answer one-shot permission requests, and cancel work. It owns the ACP keyless snapshot suite.
|
||||||
|
|
||||||
Run with: `pnpm run demo:acp` (needs `DEEPSEEK_API_KEY`); `pnpm run demo:code-mode acp` boots the same server in Code Mode via the `code-mode.cordis.yml` overlay. See [acp-agent/README.md](acp-agent/README.md) for the protocol and snapshot-test contracts.
|
Run with: `pnpm run demo:acp` (needs `DEEPSEEK_API_KEY`); `pnpm run demo:code-mode` boots the same server in Code Mode via the `code-mode.cordis.yml` overlay. See [acp-agent/README.md](acp-agent/README.md) for the protocol and snapshot-test contracts.
|
||||||
|
|
||||||
The default `cordis.yml` composes [`@deepseek-ai/dsh-sandbox-local`](../packages/sandbox/sandbox-local), [`@deepseek-ai/dsh-bash-sandbox`](../packages/bash/bash-sandbox), and [`@deepseek-ai/dsh-user-approval`](../packages/ui/user-approval). `workspace-write` confines bash and filesystem mutations to each session workspace; a wider retry becomes a one-shot machine permission request over ACP.
|
The default `cordis.yml` composes [`@deepseek-ai/dsh-sandbox-local`](../packages/sandbox/sandbox-local), [`@deepseek-ai/dsh-bash-sandbox`](../packages/bash/bash-sandbox), and [`@deepseek-ai/dsh-user-approval`](../packages/ui/user-approval). `workspace-write` confines bash and filesystem mutations to each session workspace; a wider retry becomes a one-shot machine permission request over ACP.
|
||||||
|
|||||||
@@ -10,12 +10,6 @@
|
|||||||
|
|
||||||
运行:`pnpm run demo:headless "task"`(需要 `DEEPSEEK_API_KEY`)。输出契约、安全边界和快照套件详见 [headless-agent/README.md](headless-agent/README.md)。
|
运行:`pnpm run demo:headless "task"`(需要 `DEEPSEEK_API_KEY`)。输出契约、安全边界和快照套件详见 [headless-agent/README.md](headless-agent/README.md)。
|
||||||
|
|
||||||
## code-mode
|
|
||||||
|
|
||||||
叠加在交付 TUI 之上的 **overlay**:把面向模型的注册表收敛为 `run_code` 这一个传输,使模型把工具工作批量写进 TypeScript 程序,而不是每轮一次调用。
|
|
||||||
|
|
||||||
运行:`pnpm run demo:code-mode`(需要 `DEEPSEEK_API_KEY`)。详见 [../apps/cli/README.md](../apps/cli/README.md)。交互式 agent 本身不再是示例:`pnpm run demo:tui` 在共享 base 之上启动 [`apps/cli/config/tui.cordis.yml`](../apps/cli/config/tui.cordis.yml),其 PTY 与快照场景位于 `apps/cli/tests/`。
|
|
||||||
|
|
||||||
## jsonrpc-agent
|
## jsonrpc-agent
|
||||||
|
|
||||||
通过 Python SDK 驱动的无人值守编码 agent:JSON-RPC stdio、仅前台 `bash`、`read`/`write`/`edit`、一个前台 `subagent`、`todo_write`、JSONL 持久化和压缩。它不包含终端 UI、stdout 日志、批准、skill 和后台任务控制。详见 [jsonrpc-agent/README.md](jsonrpc-agent/README.md)。
|
通过 Python SDK 驱动的无人值守编码 agent:JSON-RPC stdio、仅前台 `bash`、`read`/`write`/`edit`、一个前台 `subagent`、`todo_write`、JSONL 持久化和压缩。它不包含终端 UI、stdout 日志、批准、skill 和后台任务控制。详见 [jsonrpc-agent/README.md](jsonrpc-agent/README.md)。
|
||||||
@@ -30,6 +24,6 @@
|
|||||||
|
|
||||||
作为 **Agent Client Protocol (ACP)** 自动化服务器通过 JSON-RPC stdio 公开的 agent,由 [`@deepseek-ai/dsh-acp-demo`](../packages/examples/acp-demo) 提供。程序化客户端可以创建新会话、发送文本提示词、消费已提交的 assistant 文本、回答一次性权限请求并取消工作。它拥有 ACP 无密钥快照套件。
|
作为 **Agent Client Protocol (ACP)** 自动化服务器通过 JSON-RPC stdio 公开的 agent,由 [`@deepseek-ai/dsh-acp-demo`](../packages/examples/acp-demo) 提供。程序化客户端可以创建新会话、发送文本提示词、消费已提交的 assistant 文本、回答一次性权限请求并取消工作。它拥有 ACP 无密钥快照套件。
|
||||||
|
|
||||||
运行:`pnpm run demo:acp`(需要 `DEEPSEEK_API_KEY`);`pnpm run demo:code-mode acp` 通过 `code-mode.cordis.yml` 覆盖以 Code Mode 启动同一服务器。协议与快照测试契约详见 [acp-agent/README.md](acp-agent/README.md)。
|
运行:`pnpm run demo:acp`(需要 `DEEPSEEK_API_KEY`);`pnpm run demo:code-mode` 通过 `code-mode.cordis.yml` 覆盖以 Code Mode 启动同一服务器。协议与快照测试契约详见 [acp-agent/README.md](acp-agent/README.md)。
|
||||||
|
|
||||||
默认 `cordis.yml` 组合 [`@deepseek-ai/dsh-sandbox-local`](../packages/sandbox/sandbox-local)、[`@deepseek-ai/dsh-bash-sandbox`](../packages/bash/bash-sandbox) 和 [`@deepseek-ai/dsh-user-approval`](../packages/ui/user-approval)。`workspace-write` 将 bash 和文件系统变更限制在每个会话 workspace 中;范围更广的重试会通过 ACP 成为一次性机器权限请求。
|
默认 `cordis.yml` 组合 [`@deepseek-ai/dsh-sandbox-local`](../packages/sandbox/sandbox-local)、[`@deepseek-ai/dsh-bash-sandbox`](../packages/bash/bash-sandbox) 和 [`@deepseek-ai/dsh-user-approval`](../packages/ui/user-approval)。`workspace-write` 将 bash 和文件系统变更限制在每个会话 workspace 中;范围更广的重试会通过 ACP 成为一次性机器权限请求。
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write examples/acp-agent/README.md
|
# pnpm run verify-translation-pairing --write examples/acp-agent/README.md
|
||||||
README.md: 0d63ec1f2d9165b9faf0817bd94fbe15b97fa961
|
README.md: a37954d4d52900413a506f227759a0a7dd2a25d0
|
||||||
README.zh.md: 84482aad8352ab38527dcf4d9e1bfefc8d496c91
|
README.zh.md: a38431ea2f01fe16ff39b5dd00ec23e881713f5d
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Automation-oriented [Agent Client Protocol](https://agentclientprotocol.com) ser
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
pnpm run demo:acp # needs DEEPSEEK_API_KEY (repo-root .env or env)
|
pnpm run demo:acp # needs DEEPSEEK_API_KEY (repo-root .env or env)
|
||||||
pnpm run demo:code-mode acp # same protocol with the Code Mode tool transport
|
pnpm run demo:code-mode # same protocol with the Code Mode tool transport
|
||||||
```
|
```
|
||||||
|
|
||||||
The leaf loads the ACP app, DeepSeek adapter, sandboxed bash and filesystem stacks, one-shot approval policy, compaction, subagents, workflows, hooks, a derived session-query index, and repeat guard. The app creates one fresh agent per `session/new`, persists sessions to JSONL, and keeps stdout protocol-pure. [`session-query.cordis.yml`](session-query.cordis.yml) explicitly opts into the workspace-authorized query tools and generic timeout/spill policies for their dedicated snapshot; [`fs.cordis.yml`](fs.cordis.yml) adds spill storage for filesystem scenarios, [`code-mode.cordis.yml`](code-mode.cordis.yml) adds `run_code` and its generated TypeScript SDK, and [`web.cordis.yml`](web.cordis.yml) adds the web seam, the local fetch provider, `web_fetch`, and a loopback HTML fixture server for the web-fetch snapshot.
|
The leaf loads the ACP app, DeepSeek adapter, sandboxed bash and filesystem stacks, one-shot approval policy, compaction, subagents, workflows, hooks, a derived session-query index, and repeat guard. The app creates one fresh agent per `session/new`, persists sessions to JSONL, and keeps stdout protocol-pure. [`session-query.cordis.yml`](session-query.cordis.yml) explicitly opts into the workspace-authorized query tools and generic timeout/spill policies for their dedicated snapshot; [`fs.cordis.yml`](fs.cordis.yml) adds spill storage for filesystem scenarios, [`code-mode.cordis.yml`](code-mode.cordis.yml) adds `run_code` and its generated TypeScript SDK, and [`web.cordis.yml`](web.cordis.yml) adds the web seam, the local fetch provider, `web_fetch`, and a loopback HTML fixture server for the web-fetch snapshot.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
pnpm run demo:acp # needs DEEPSEEK_API_KEY (repo-root .env or env)
|
pnpm run demo:acp # needs DEEPSEEK_API_KEY (repo-root .env or env)
|
||||||
pnpm run demo:code-mode acp # same protocol with the Code Mode tool transport
|
pnpm run demo:code-mode # same protocol with the Code Mode tool transport
|
||||||
```
|
```
|
||||||
|
|
||||||
该叶节点加载 ACP 应用、DeepSeek 适配器、受沙箱限制的 bash 与文件系统栈、一次性批准策略、压缩(compaction)、subagent、工作流、钩子、派生会话查询索引和重复守卫。应用为每次 `session/new` 创建一个新 agent,将会话持久化到 JSONL,并保持 stdout 只含协议内容。[`session-query.cordis.yml`](session-query.cordis.yml) 为其专用快照显式选用 workspace 授权的查询工具和通用超时/溢出策略;[`fs.cordis.yml`](fs.cordis.yml) 为文件系统场景添加溢出存储,[`code-mode.cordis.yml`](code-mode.cordis.yml) 添加 `run_code` 及其生成的 TypeScript SDK,[`web.cordis.yml`](web.cordis.yml) 则为 web-fetch 快照添加 web seam、本地抓取提供方、`web_fetch` 与一个回环 HTML fixture(测试前置数据)服务器。
|
该叶节点加载 ACP 应用、DeepSeek 适配器、受沙箱限制的 bash 与文件系统栈、一次性批准策略、压缩(compaction)、subagent、工作流、钩子、派生会话查询索引和重复守卫。应用为每次 `session/new` 创建一个新 agent,将会话持久化到 JSONL,并保持 stdout 只含协议内容。[`session-query.cordis.yml`](session-query.cordis.yml) 为其专用快照显式选用 workspace 授权的查询工具和通用超时/溢出策略;[`fs.cordis.yml`](fs.cordis.yml) 为文件系统场景添加溢出存储,[`code-mode.cordis.yml`](code-mode.cordis.yml) 添加 `run_code` 及其生成的 TypeScript SDK,[`web.cordis.yml`](web.cordis.yml) 则为 web-fetch 快照添加 web seam、本地抓取提供方、`web_fetch` 与一个回环 HTML fixture(测试前置数据)服务器。
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Code Mode adds `ctx.codeRuntime` and changes the registry to one wire tool,
|
# Code Mode adds `ctx.codeRuntime` and changes the registry to one wire tool,
|
||||||
# `run_code`, plus its generated TypeScript SDK prompt. The app bin selects this
|
# `run_code`, plus its generated TypeScript SDK prompt. The app bin selects this
|
||||||
# overlay for `demo:code-mode acp` and snapshot recording, and selects the sibling
|
# overlay for `demo:code-mode` and snapshot recording, and selects the sibling
|
||||||
# replay overlay for `DSH_SNAPSHOT=replay`. A config patch replaces the whole app
|
# replay overlay for `DSH_SNAPSHOT=replay`. A config patch replaces the whole app
|
||||||
# config, so unchanged base fields are restated below.
|
# config, so unchanged base fields are restated below.
|
||||||
- id: base
|
- id: base
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Workspace umbrella for runnable demos and example-owned test compositions: declares their cordis.yml packages so plain Node resolves real exports\u2192lib. Not a build target.",
|
"description": "Workspace umbrella for runnable demos and example-owned test compositions: declares their cordis.yml packages so plain Node resolves real exports→lib. Not a build target.",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cordisjs/plugin-hmr": "workspace:*",
|
"@cordisjs/plugin-hmr": "workspace:*",
|
||||||
"@cordisjs/plugin-include": "workspace:*",
|
"@cordisjs/plugin-include": "workspace:*",
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write packages/host/apiproxy/README.md
|
# pnpm run verify-translation-pairing --write packages/host/apiproxy/README.md
|
||||||
README.md: 1a62323a6b47a0d59dc697ada97144b3c523f64a
|
README.md: 6182bba7b64e2692d08ccfd95cd3bc91e733cebd
|
||||||
README.zh.md: b3be5537738c3071dd0fb0fbcf2953eba1804749
|
README.zh.md: edc890ce9b2977a5ab2ec4763225bc08e29ab7ee
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
English | [中文](README.zh.md)
|
English | [中文](README.zh.md)
|
||||||
|
|
||||||
The API gateway every client shape shares: the TS contract (`src/api/`, zero Node dependencies, importable from the browser), the fetch carrier pair (`src/fetch/`: `toFetchHandler` on the host side, `AbstractApiClient` plus platform subclasses on the client side), and the host-side implementation (`src/api-proxy.ts`: `createApiProxy` plus the default-exported `ApiProxyService` gateway plugin — config `{provider, model, workspaceRoot?}`, provides `ctx.apiProxy`). Transport-agnostic by design: this package registers no routes; carriers (HTTP today, IPC later) wrap `ctx.apiProxy` themselves. The shipped core composition lives in [`apps/cli/cordis.yml`](../../../apps/cli/config/base.cordis.yml).
|
The API gateway every client shape shares: the TS contract (`src/api/`, zero Node dependencies, importable from the browser), the fetch carrier pair (`src/fetch/`: `toFetchHandler` on the host side, `AbstractApiClient` plus platform subclasses on the client side), and the host-side implementation (`src/api-proxy.ts`: `createApiProxy` plus the default-exported `ApiProxyService` gateway plugin — config `{provider, model, workspaceRoot?}`, provides `ctx.apiProxy`). Transport-agnostic by design: this package registers no routes; carriers (HTTP today, IPC later) wrap `ctx.apiProxy` themselves. The shipped core composition lives in [`apps/cli/config/base.cordis.yml`](../../../apps/cli/config/base.cordis.yml).
|
||||||
|
|
||||||
## Contract layer (`/api`)
|
## Contract layer (`/api`)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[English](README.md) | 中文
|
[English](README.md) | 中文
|
||||||
|
|
||||||
所有客户端形态共用的 API 网关:TS 契约(`src/api/`,不依赖 Node,可从浏览器导入)、fetch 载体对(`src/fetch/`:宿主侧的 `toFetchHandler`,以及客户端侧的 `AbstractApiClient` 与平台子类)和宿主侧实现(`src/api-proxy.ts`:`createApiProxy` 加上默认导出的 `ApiProxyService` 网关插件,其配置为 `{provider, model, workspaceRoot?}`,提供 `ctx.apiProxy`)。该包(package)在设计上与传输方式无关,不注册任何路由;载体(目前为 HTTP,未来可以是 IPC)自行包装 `ctx.apiProxy`。已发布的核心组合位于 [`apps/cli/cordis.yml`](../../../apps/cli/config/base.cordis.yml)。
|
所有客户端形态共用的 API 网关:TS 契约(`src/api/`,不依赖 Node,可从浏览器导入)、fetch 载体对(`src/fetch/`:宿主侧的 `toFetchHandler`,以及客户端侧的 `AbstractApiClient` 与平台子类)和宿主侧实现(`src/api-proxy.ts`:`createApiProxy` 加上默认导出的 `ApiProxyService` 网关插件,其配置为 `{provider, model, workspaceRoot?}`,提供 `ctx.apiProxy`)。该包(package)在设计上与传输方式无关,不注册任何路由;载体(目前为 HTTP,未来可以是 IPC)自行包装 `ctx.apiProxy`。已发布的核心组合位于 [`apps/cli/config/base.cordis.yml`](../../../apps/cli/config/base.cordis.yml)。
|
||||||
|
|
||||||
## 契约层(`/api`)
|
## 契约层(`/api`)
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write packages/ui/app-boot/README.md
|
# pnpm run verify-translation-pairing --write packages/ui/app-boot/README.md
|
||||||
README.md: 2ccf03a2b30a416334e3b8dbe806875019213a31
|
README.md: 4d8c7de65515a251f227075c7baf041fc1b210c8
|
||||||
README.zh.md: 15c13adad5064df5e882f55dbef367c146376c6d
|
README.zh.md: d9b69a2b102a60685524288f75edeaabb21802db
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ Shared boot glue for the app bins ([`dsh`](../../../apps/cli/README.md), [`dsh-c
|
|||||||
| `loadEnv(binName, dir?, warn?)` | Load the gitignored `.env` (Node `process.loadEnvFile`); absent file is fine, an unloadable one warns a single labelled line (default: stderr) |
|
| `loadEnv(binName, dir?, warn?)` | Load the gitignored `.env` (Node `process.loadEnvFile`); absent file is fine, an unloadable one warns a single labelled line (default: stderr) |
|
||||||
| `installFailLoud(binName, proc?)` | Turn a post-`boot()` unhandled Loader rejection into one labelled stderr line + `exit(1)`; returns the uninstaller (for tests) |
|
| `installFailLoud(binName, proc?)` | Turn a post-`boot()` unhandled Loader rejection into one labelled stderr line + `exit(1)`; returns the uninstaller (for tests) |
|
||||||
| `assertEntriesLoaded(ctx, binName)` | Throw when a settled tree holds an enabled entry with no fiber, reporting every unresolved plugin name as a Cordis startup failure |
|
| `assertEntriesLoaded(ctx, binName)` | Throw when a settled tree holds an enabled entry with no fiber, reporting every unresolved plugin name as a Cordis startup failure |
|
||||||
|
| `assertEntriesActive(ctx, binName)` | Throw when a settled enabled fiber is not ACTIVE, including missing injected services for PENDING entries |
|
||||||
| `loadPersonalPatches(binName, dir?)` | Parse the optional `config.yaml` in the Harness home (default [`resolveDshHome()`](../../util/paths/README.md): `$DSH_HOME`, else `~/.dsh`) — a top-level YAML array of include `PatchOptions` (id-targeted config overrides, `insert` lists, `!!js` allowed); absent file → `undefined`, an unreadable/unparsable/non-array file throws |
|
| `loadPersonalPatches(binName, dir?)` | Parse the optional `config.yaml` in the Harness home (default [`resolveDshHome()`](../../util/paths/README.md): `$DSH_HOME`, else `~/.dsh`) — a top-level YAML array of include `PatchOptions` (id-targeted config overrides, `insert` lists, `!!js` allowed); absent file → `undefined`, an unreadable/unparsable/non-array file throws |
|
||||||
| `boot(binName, absoluteConfigPath, patches?, prepare?)` | Create the root context, run optional host preparation before plugins mount (`prepare` is where a bin provides launcher-owned context slots a mounted app reads, such as [`MAIN_SESSION_ID_KEY`](../tui/README.md)), then mount the Loader/include tree, await it, assert entries loaded, and return the root context |
|
| `loadOverlayPatches(binName, file)` | Parse a required patch-list file with the same shape as personal config; read or parse failures throw a labelled error |
|
||||||
|
| `boot(binName, absoluteConfigPath, patches?, prepare?)` | Create the root context, install Loader, run optional host preparation before config-tree entries mount (`prepare` may use Loader and provide launcher-owned context slots such as [`MAIN_SESSION_ID_KEY`](../tui/README.md)), then mount and await the include tree, assert entries loaded and ACTIVE, and return the root context |
|
||||||
| `addHarnessSourceSection(ctx, sourceRoot)` | Add a global `harness:source` prompt section (ordered just after the harness identity, before the persona) telling the agent the on-disk path to its own source checkout; a no-op returning `undefined` when the booted tree has no `systemPrompt` service. The section is registered against that service's fiber, so a dev HMR reload of the system prompt drops it until the next boot |
|
| `addHarnessSourceSection(ctx, sourceRoot)` | Add a global `harness:source` prompt section (ordered just after the harness identity, before the persona) telling the agent the on-disk path to its own source checkout; a no-op returning `undefined` when the booted tree has no `systemPrompt` service. The section is registered against that service's fiber, so a dev HMR reload of the system prompt drops it until the next boot |
|
||||||
| `HARNESS_SOURCE_SECTION` | The `'harness:source'` section name `addHarnessSourceSection` registers under |
|
| `HARNESS_SOURCE_SECTION` | The `'harness:source'` section name `addHarnessSourceSection` registers under |
|
||||||
|
|
||||||
@@ -23,10 +25,10 @@ This package carries no loader hooks and no dev-mode surface. The [`dsh` app](..
|
|||||||
|
|
||||||
## Personal config
|
## Personal config
|
||||||
|
|
||||||
A developer's machine-local preferences live outside every repository in the Harness home (default `~/.dsh`, overridable via `$DSH_HOME`; the single root [`resolveDshHome`](../../util/paths/README.md) resolves), consumed by the `dsh` CLI's TUI surface ([`apps/cli`](../../../apps/cli/README.md)); the demo bins boot their committed trees verbatim. Two optional files:
|
A developer's machine-local preferences live outside every repository in the Harness home (default `~/.dsh`, overridable via `$DSH_HOME`; the single root [`resolveDshHome`](../../util/paths/README.md) resolves), consumed by the official `dsh` surfaces ([`apps/cli`](../../../apps/cli/README.md)); the demo bins boot their committed trees verbatim. Two optional files:
|
||||||
|
|
||||||
- **`.env`** — loaded after the invoking directory's `.env`; `process.loadEnvFile` never overrides, so precedence is ambient environment > project `.env` > personal `.env`.
|
- **`.env`** — loaded after the invoking directory's `.env`; `process.loadEnvFile` never overrides, so precedence is ambient environment > project `.env` > personal `.env`.
|
||||||
- **`config.yaml`** — loader overlay patches applied over the shipped default config, with the same semantics as an include entry's `patches` (the committed Code Mode overlay is the template): an id-targeted patch replaces the named entry's whole `config` (restate unchanged fields), `insert` adds entries, and `!!js` expressions interpolate at mount — so a personal `apiKey` can reference the personal `.env`. A patch naming an entry id absent from the booted tree is skipped with a loader warning. An empty or comments-only file throws (it parses to nothing, not to a list); disable the overlay with `[]` or by deleting the file.
|
- **`config.yaml`** — loader overlay patches applied over the shipped default config, with the same semantics as the shipped surface overlays: an id-targeted patch replaces the named entry's whole `config` (restate unchanged fields), `insert` adds entries, and `!!js` expressions interpolate at mount — so a personal `apiKey` can reference the personal `.env`. A patch naming an entry id absent from the booted tree is a silent no-op. An empty or comments-only file throws (it parses to nothing, not to a list); disable the overlay with `[]` or by deleting the file.
|
||||||
|
|
||||||
Subprocess test launchers point `DSH_HOME` at an isolated per-test directory so a developer's personal overlay can never leak into fixtures.
|
Subprocess test launchers point `DSH_HOME` at an isolated per-test directory so a developer's personal overlay can never leak into fixtures.
|
||||||
|
|
||||||
@@ -44,4 +46,3 @@ No direct invalidation from `boot()`; a consumer that calls `addHarnessSourceSec
|
|||||||
- **Snapshot replay swapping is basename-specific** — only a config ending in `cordis.yml` or `cordis.yaml` maps to the sibling `cordis.snapshot.yml`; custom config names require caller-managed selection.
|
- **Snapshot replay swapping is basename-specific** — only a config ending in `cordis.yml` or `cordis.yaml` maps to the sibling `cordis.snapshot.yml`; custom config names require caller-managed selection.
|
||||||
- **Environment loading is cwd-scoped and optional** — the helper loads one `.env` file and warns on failure; it does not search parents, merge profiles, or validate required variables.
|
- **Environment loading is cwd-scoped and optional** — the helper loads one `.env` file and warns on failure; it does not search parents, merge profiles, or validate required variables.
|
||||||
- **Personal config is patch-shaped** — an id-targeted patch replaces the entry's whole `config` rather than deep-merging, so a personal override restates the base fields it keeps.
|
- **Personal config is patch-shaped** — an id-targeted patch replaces the entry's whole `config` rather than deep-merging, so a personal override restates the base fields it keeps.
|
||||||
- **Personal patches see only the booted file's own entries** — an overlay leaf that reaches its base through a nested include entry (the Code Mode configs) resolves personal patch ids against the overlay's top-level entries, not the included subtree.
|
|
||||||
|
|||||||
@@ -10,8 +10,10 @@
|
|||||||
| `loadEnv(binName, dir?, warn?)` | 加载已被 git 忽略的 `.env`(Node `process.loadEnvFile`);文件不存在不影响启动,文件无法加载时输出一行带标签的警告(默认写入 stderr) |
|
| `loadEnv(binName, dir?, warn?)` | 加载已被 git 忽略的 `.env`(Node `process.loadEnvFile`);文件不存在不影响启动,文件无法加载时输出一行带标签的警告(默认写入 stderr) |
|
||||||
| `installFailLoud(binName, proc?)` | 将 `boot()` 之后未处理的 Loader rejection 转换为一行带标签的 stderr 消息并执行 `exit(1)`;返回卸载函数(供测试使用) |
|
| `installFailLoud(binName, proc?)` | 将 `boot()` 之后未处理的 Loader rejection 转换为一行带标签的 stderr 消息并执行 `exit(1)`;返回卸载函数(供测试使用) |
|
||||||
| `assertEntriesLoaded(ctx, binName)` | 树结算后,如果其中存在已启用但没有 fiber 的条目,则抛出异常,并以 Cordis 启动故障的形式报告每个未解析插件的名称 |
|
| `assertEntriesLoaded(ctx, binName)` | 树结算后,如果其中存在已启用但没有 fiber 的条目,则抛出异常,并以 Cordis 启动故障的形式报告每个未解析插件的名称 |
|
||||||
|
| `assertEntriesActive(ctx, binName)` | 树结算后,如果已启用的 fiber 未处于 ACTIVE 状态,则抛出异常;对于 PENDING 条目还会列出缺失的注入服务 |
|
||||||
| `loadPersonalPatches(binName, dir?)` | 解析 Harness home 中可选的 `config.yaml`(默认使用 [`resolveDshHome()`](../../util/paths/README.md):先取 `$DSH_HOME`,否则取 `~/.dsh`):其顶层是一个 YAML 数组,内容为 include 的 `PatchOptions`(按 id 定位的配置覆盖、`insert` 列表,允许 `!!js`);文件不存在时返回 `undefined`,文件不可读、不可解析或内容不是数组时抛出异常 |
|
| `loadPersonalPatches(binName, dir?)` | 解析 Harness home 中可选的 `config.yaml`(默认使用 [`resolveDshHome()`](../../util/paths/README.md):先取 `$DSH_HOME`,否则取 `~/.dsh`):其顶层是一个 YAML 数组,内容为 include 的 `PatchOptions`(按 id 定位的配置覆盖、`insert` 列表,允许 `!!js`);文件不存在时返回 `undefined`,文件不可读、不可解析或内容不是数组时抛出异常 |
|
||||||
| `boot(binName, absoluteConfigPath, patches?, prepare?)` | 创建根上下文,在插件挂载前执行可选的宿主准备操作(`prepare` 正是 bin 提供由启动器拥有、供已挂载应用读取的上下文插槽之处,例如 [`MAIN_SESSION_ID_KEY`](../tui/README.md)),再挂载 Loader/include 树并等待其结算,断言所有条目均已加载,最后返回根上下文 |
|
| `loadOverlayPatches(binName, file)` | 解析一份必需的 patch 列表文件,其形状与个人配置相同;读取或解析失败时抛出带标签的错误 |
|
||||||
|
| `boot(binName, absoluteConfigPath, patches?, prepare?)` | 创建根上下文并安装 Loader,在配置树条目挂载前执行可选的宿主准备操作(`prepare` 可以使用 Loader,也可以提供由启动器拥有的上下文插槽,例如 [`MAIN_SESSION_ID_KEY`](../tui/README.md)),再挂载并等待 include 树结算,断言所有条目均已加载且处于 ACTIVE 状态,最后返回根上下文 |
|
||||||
| `addHarnessSourceSection(ctx, sourceRoot)` | 添加全局 `harness:source` 提示词段落(顺序紧随 harness 身份、位于 persona 之前),告知 agent(智能体)自身源代码 checkout 的磁盘路径;如果已启动树没有此项服务,则不执行操作并返回 `undefined`。这里的服务是 `systemPrompt`;该段落注册到它的 fiber,因此开发环境 HMR(热模块替换)重新加载系统提示词后,它会消失直至下次启动 |
|
| `addHarnessSourceSection(ctx, sourceRoot)` | 添加全局 `harness:source` 提示词段落(顺序紧随 harness 身份、位于 persona 之前),告知 agent(智能体)自身源代码 checkout 的磁盘路径;如果已启动树没有此项服务,则不执行操作并返回 `undefined`。这里的服务是 `systemPrompt`;该段落注册到它的 fiber,因此开发环境 HMR(热模块替换)重新加载系统提示词后,它会消失直至下次启动 |
|
||||||
| `HARNESS_SOURCE_SECTION` | `'harness:source'` 段落名称,供 `addHarnessSourceSection` 注册使用 |
|
| `HARNESS_SOURCE_SECTION` | `'harness:source'` 段落名称,供 `addHarnessSourceSection` 注册使用 |
|
||||||
|
|
||||||
@@ -23,10 +25,10 @@
|
|||||||
|
|
||||||
## 个人配置
|
## 个人配置
|
||||||
|
|
||||||
开发者的机器本地偏好位于所有仓库之外的 Harness home 中(默认 `~/.dsh`,可由 `$DSH_HOME` 覆盖;统一由根级 [`resolveDshHome`](../../util/paths/README.md) 解析),并由 `dsh` CLI(命令行界面)的 TUI 界面([`apps/cli`](../../../apps/cli/README.md))使用;demo bin 会原样启动仓库中提交的树。这里有两个可选文件:
|
开发者的机器本地偏好位于所有仓库之外的 Harness home 中(默认 `~/.dsh`,可由 `$DSH_HOME` 覆盖;统一由根级 [`resolveDshHome`](../../util/paths/README.md) 解析),并由官方 `dsh` 界面([`apps/cli`](../../../apps/cli/README.md))使用;demo bin 会原样启动仓库中提交的树。这里有两个可选文件:
|
||||||
|
|
||||||
- **`.env`**:在调用目录的 `.env` 之后加载;`process.loadEnvFile` 从不覆盖已有值,因此优先级为环境中的值 > 项目 `.env` > 个人 `.env`。
|
- **`.env`**:在调用目录的 `.env` 之后加载;`process.loadEnvFile` 从不覆盖已有值,因此优先级为环境中的值 > 项目 `.env` > 个人 `.env`。
|
||||||
- **`config.yaml`**:在发布的默认配置上应用 Loader overlay patch,语义与 include 条目的 `patches` 相同(以仓库提交的 Code Mode overlay 为模板):按 id 定位的 patch 会替换对应条目的整个 `config`(未改字段也要重述),`insert` 会添加条目,`!!js` 表达式则在挂载时插值,因此个人 `apiKey` 可以引用个人 `.env`。如果 patch 指定的条目 id 不在已启动树中,Loader 会发出警告并跳过。空文件或仅含注释的文件会抛出异常(其解析结果为空,而不是列表);如需禁用 overlay,请使用 `[]` 或删除该文件。
|
- **`config.yaml`**:在发布的默认配置上应用 Loader overlay patch,语义与交付的 surface overlay 相同:按 id 定位的 patch 会替换对应条目的整个 `config`(未改字段也要重述),`insert` 会添加条目,`!!js` 表达式则在挂载时插值,因此个人 `apiKey` 可以引用个人 `.env`。如果 patch 指定的条目 id 不在已启动树中,则静默不执行任何操作。空文件或仅含注释的文件会抛出异常(其解析结果为空,而不是列表);如需禁用 overlay,请使用 `[]` 或删除该文件。
|
||||||
|
|
||||||
子进程测试 launcher 会把 `DSH_HOME` 指向逐测试隔离的目录,确保开发者的个人 overlay 不会泄漏到 fixture(测试前置数据)中。
|
子进程测试 launcher 会把 `DSH_HOME` 指向逐测试隔离的目录,确保开发者的个人 overlay 不会泄漏到 fixture(测试前置数据)中。
|
||||||
|
|
||||||
@@ -44,4 +46,3 @@
|
|||||||
- **快照回放替换仅识别特定 basename**:只有以 `cordis.yml` 或 `cordis.yaml` 结尾的配置会映射到同级 `cordis.snapshot.yml`;自定义配置名称需要调用方自行选择。
|
- **快照回放替换仅识别特定 basename**:只有以 `cordis.yml` 或 `cordis.yaml` 结尾的配置会映射到同级 `cordis.snapshot.yml`;自定义配置名称需要调用方自行选择。
|
||||||
- **环境加载局限于 cwd 且为可选操作**:helper 只加载一个 `.env` 文件,并在失败时发出警告;它不会搜索父目录、合并 profile 或验证必需变量。
|
- **环境加载局限于 cwd 且为可选操作**:helper 只加载一个 `.env` 文件,并在失败时发出警告;它不会搜索父目录、合并 profile 或验证必需变量。
|
||||||
- **个人配置采用 patch 形式**:按 id 定位的 patch 会替换条目的整个 `config`,而不是深度合并,因此个人覆盖必须重述需要保留的基础字段。
|
- **个人配置采用 patch 形式**:按 id 定位的 patch 会替换条目的整个 `config`,而不是深度合并,因此个人覆盖必须重述需要保留的基础字段。
|
||||||
- **个人 patch 只能看到已启动文件自身的条目**:如果 overlay 叶子通过嵌套 include 条目访问其基础配置(例如 Code Mode 配置),个人 patch id 只会在 overlay 的顶层条目中解析,不会进入被 include 的子树。
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { pathToFileURL } from 'node:url'
|
|||||||
import { readFileSync } from 'node:fs'
|
import { readFileSync } from 'node:fs'
|
||||||
import { basename, dirname, join, resolve } from 'node:path'
|
import { basename, dirname, join, resolve } from 'node:path'
|
||||||
import * as yaml from 'js-yaml'
|
import * as yaml from 'js-yaml'
|
||||||
import { Context } from 'cordis'
|
import { Context, type FiberState } from 'cordis'
|
||||||
import Loader from '@cordisjs/plugin-loader'
|
import Loader from '@cordisjs/plugin-loader'
|
||||||
import Include, { type PatchOptions } from '@cordisjs/plugin-include'
|
import Include, { type PatchOptions } from '@cordisjs/plugin-include'
|
||||||
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
|
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
|
||||||
@@ -192,6 +192,31 @@ export function assertEntriesLoaded(ctx: Context, binName: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Runtime mirrors for Cordis's erased const-enum fiber states. */
|
||||||
|
const FIBER_ACTIVE = 2 as FiberState.ACTIVE
|
||||||
|
const FIBER_PENDING = 0 as FiberState.PENDING
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reject enabled Loader entries whose fibers did not reach ACTIVE after settle.
|
||||||
|
* @param ctx - The settled application root.
|
||||||
|
* @param binName - Diagnostic prefix.
|
||||||
|
*/
|
||||||
|
export function assertEntriesActive(ctx: Context, binName: string): void {
|
||||||
|
const failures: string[] = []
|
||||||
|
for (const entry of ctx.loader.entries()) {
|
||||||
|
if (entry.fiber === undefined || entry.disabled || entry.fiber.state === FIBER_ACTIVE) continue
|
||||||
|
if (entry.fiber.state === FIBER_PENDING) {
|
||||||
|
const missing = Object.keys(entry.fiber.inject).filter(service => ctx.get(service) === undefined)
|
||||||
|
failures.push(`${entry.options.name}: pending (waiting for service${missing.length === 1 ? '' : 's'}: ${missing.join(', ') || 'unknown'})`)
|
||||||
|
} else {
|
||||||
|
failures.push(`${entry.options.name}: fiber state ${String(entry.fiber.state)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (failures.length > 0) {
|
||||||
|
throw new Error(`${binName}: ${String(failures.length)} entr${failures.length === 1 ? 'y' : 'ies'} did not activate\n${failures.join('\n')}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Boot the Loader against `absoluteConfigPath` and return only after the whole
|
* Boot the Loader against `absoluteConfigPath` and return only after the whole
|
||||||
* tree settles. Entry names load through the Loader's internal module loader
|
* tree settles. Entry names load through the Loader's internal module loader
|
||||||
@@ -231,6 +256,7 @@ export async function boot(
|
|||||||
})
|
})
|
||||||
await ctx.loader.await()
|
await ctx.loader.await()
|
||||||
assertEntriesLoaded(ctx, binName)
|
assertEntriesLoaded(ctx, binName)
|
||||||
|
assertEntriesActive(ctx, binName)
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { describe, expect, it, vi } from 'vitest'
|
|||||||
import { Context } from 'cordis'
|
import { Context } from 'cordis'
|
||||||
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
|
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
|
||||||
import {
|
import {
|
||||||
addHarnessSourceSection, assertEntriesLoaded, boot, HARNESS_SOURCE_SECTION,
|
addHarnessSourceSection, assertEntriesActive, assertEntriesLoaded, boot, HARNESS_SOURCE_SECTION,
|
||||||
installFailLoud, loadEnv, loadOverlayPatches, resolveConfigPath, type FailLoudProcess,
|
installFailLoud, loadEnv, loadOverlayPatches, resolveConfigPath, type FailLoudProcess,
|
||||||
} from '../src/index.ts'
|
} from '../src/index.ts'
|
||||||
|
|
||||||
@@ -212,6 +212,33 @@ describe('boot', () => {
|
|||||||
writeFileSync(join(dir, 'cordis.yml'), '- id: ghost\n name: ./missing.mjs\n')
|
writeFileSync(join(dir, 'cordis.yml'), '- id: ghost\n name: ./missing.mjs\n')
|
||||||
await expect(boot(NAME, join(dir, 'cordis.yml'))).rejects.toThrow(`${NAME}: plugin(s) failed to load: ./missing.mjs`)
|
await expect(boot(NAME, join(dir, 'cordis.yml'))).rejects.toThrow(`${NAME}: plugin(s) failed to load: ./missing.mjs`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('rejects a settled tree with a pending inject and names every missing service', async () => {
|
||||||
|
const dir = tmp()
|
||||||
|
writeFileSync(join(dir, 'waiting.mjs'), "export const inject = ['alpha', 'beta']\nexport function apply() {}\n")
|
||||||
|
writeFileSync(join(dir, 'cordis.yml'), '- id: waiting\n name: ./waiting.mjs\n')
|
||||||
|
await expect(boot(NAME, join(dir, 'cordis.yml'))).rejects.toThrow('./waiting.mjs: pending (waiting for services: alpha, beta)')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses singular diagnostics for one missing pending dependency', () => {
|
||||||
|
const ctx = {
|
||||||
|
loader: { entries: () => [{ disabled: false, options: { name: 'waiting' }, fiber: { state: 0, inject: { alpha: {} } } }] },
|
||||||
|
get: () => undefined,
|
||||||
|
} as unknown as Context
|
||||||
|
expect(() =>{ assertEntriesActive(ctx, NAME) }).toThrow('waiting: pending (waiting for service: alpha)')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reports unknown pending dependencies and unexpected fiber states', () => {
|
||||||
|
const entries = [
|
||||||
|
{ disabled: false, options: { name: 'unknown' }, fiber: { state: 0, inject: {} } },
|
||||||
|
{ disabled: false, options: { name: 'failed' }, fiber: { state: 3, inject: {} } },
|
||||||
|
]
|
||||||
|
const ctx = {
|
||||||
|
loader: { entries: () => entries },
|
||||||
|
get: () => undefined,
|
||||||
|
} as unknown as Context
|
||||||
|
expect(() =>{ assertEntriesActive(ctx, NAME) }).toThrow(`${NAME}: 2 entries did not activate\nunknown: pending (waiting for services: unknown)\nfailed: fiber state 3`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('addHarnessSourceSection', () => {
|
describe('addHarnessSourceSection', () => {
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write packages/ui/tui/README.md
|
# pnpm run verify-translation-pairing --write packages/ui/tui/README.md
|
||||||
README.md: 88c4501d87b7f24de1f5cc0d67f4c0e03ec49aa4
|
README.md: d3ea5b41e8398c92853c6f96160129d7b1c475ed
|
||||||
README.zh.md: f03120e5a7820e2bcb572ab31b535211cf859c82
|
README.zh.md: 9ee509abe2cf5bf6360b7c91d318b2b69745d224
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ A launcher can seed a fresh session's first turn by providing `INITIAL_SKILL_KEY
|
|||||||
fileSearchExcludedDirectories: ['.git', 'node_modules', 'dist']
|
fileSearchExcludedDirectories: ['.git', 'node_modules', 'dist']
|
||||||
```
|
```
|
||||||
|
|
||||||
Startup fails before mounting when either process stream is not a TTY. The composing app must mount the TUI before its config-created agent so the front door can observe `agent-loop/config-start-failed`; a matching exact-session failure is written before fullscreen mode starts and exits with status 1 instead of leaving a blank terminal. Disposal stops extension admission, unloads the `ctx.tui` provider and its dependent plugins, aborts running commands, removes the TUI definitions, stops loaders, rejects pending questions, drains terminal input, restores terminal state, unregisters event listeners and the user-interaction provider, and never exits a replacement process during HMR.
|
Startup fails before mounting when either process stream is not a TTY. The composing app must mount the TUI before its config-created agent so the front door can observe `agent-loop/config-start-failed`; a matching exact-session failure is written before fullscreen mode starts and exits with status 1 instead of leaving a blank terminal. Disposal stops extension admission, unloads the `ctx.tui` provider and its dependent plugins, aborts running commands, removes the TUI definitions, stops loaders, rejects pending questions, drains terminal input, restores terminal state, unregisters event listeners and the user-interaction provider, and never exits a replacement process during HMR. A user exit disposes the application root so sibling resources close, then exits; a five-second fallback prevents one stuck disposer from trapping the process.
|
||||||
|
|
||||||
## Color
|
## Color
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ Footer 将会话报告的用量汇总为 `↑<uncached input> ↓<output>`;任
|
|||||||
fileSearchExcludedDirectories: ['.git', 'node_modules', 'dist']
|
fileSearchExcludedDirectories: ['.git', 'node_modules', 'dist']
|
||||||
```
|
```
|
||||||
|
|
||||||
任一进程流不是 TTY 时,启动会在挂载前失败。组合 app 必须先挂载 TUI,再挂载由配置创建的 agent,使入口能够观察 `agent-loop/config-start-failed`;完全匹配会话的失败会在全屏模式启动前写出并以状态 1 退出,而不是留下空白终端。dispose(资源释放)会停止接收扩展请求,卸载 `ctx.tui` 提供方及其依赖插件,中止运行中的命令,移除 TUI 定义,停止 loader,拒绝待处理问题,排空终端输入,恢复终端状态,注销事件 listener 和用户交互提供方,并且绝不会在 HMR 期间退出替换进程。
|
任一进程流不是 TTY 时,启动会在挂载前失败。组合 app 必须先挂载 TUI,再挂载由配置创建的 agent,使入口能够观察 `agent-loop/config-start-failed`;完全匹配会话的失败会在全屏模式启动前写出并以状态 1 退出,而不是留下空白终端。dispose(资源释放)会停止接收扩展请求,卸载 `ctx.tui` 提供方及其依赖插件,中止运行中的命令,移除 TUI 定义,停止 loader,拒绝待处理问题,排空终端输入,恢复终端状态,注销事件 listener 和用户交互提供方,并且绝不会在 HMR 期间退出替换进程。用户退出会先 dispose 应用根上下文以关闭同级资源,再退出进程;五秒兜底可避免某个卡住的 disposer 困住进程。
|
||||||
|
|
||||||
## 颜色
|
## 颜色
|
||||||
|
|
||||||
|
|||||||
@@ -841,11 +841,9 @@ export function createTuiChat(
|
|||||||
palette,
|
palette,
|
||||||
overlayManager,
|
overlayManager,
|
||||||
// Optional and independently mounted: read at each use so config row order
|
// Optional and independently mounted: read at each use so config row order
|
||||||
// cannot decide whether /resume works.
|
// cannot decide whether /resume works. Strict lookup excludes a closing or
|
||||||
// The TUI and query provider are sibling Loader fibers. During a command
|
// closed provider rather than dispatching into a stale SQLite handle.
|
||||||
// callback Cordis may transiently mark the provider non-ACTIVE even though
|
sessionQuery: () => ctx.get('sessionQuery'),
|
||||||
// its init completed and its disposal is ordered after this consumer.
|
|
||||||
sessionQuery: () => ctx.get('sessionQuery', false),
|
|
||||||
ui,
|
ui,
|
||||||
editor,
|
editor,
|
||||||
appendNotice,
|
appendNotice,
|
||||||
@@ -1654,9 +1652,35 @@ export function mountTui(ctx: Context, config: Config, runtime: TuiRuntime): voi
|
|||||||
if (existing !== undefined) start(existing)
|
if (existing !== undefined) start(existing)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ROOT_DISPOSE_TIMEOUT_MS = 5_000
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispose the whole application before process exit, with a bounded fallback.
|
||||||
|
* @param ctx - The TUI plugin context whose root owns sibling resources.
|
||||||
|
* @param code - Process status to report.
|
||||||
|
* @param exit - Exit boundary, replaceable by tests.
|
||||||
|
*/
|
||||||
|
export function disposeRootAndExit(
|
||||||
|
ctx: Context,
|
||||||
|
code: number,
|
||||||
|
exit: (status: number) => void = (status) => { process.exit(status) },
|
||||||
|
): void {
|
||||||
|
let exited = false
|
||||||
|
const exitOnce = (): void => {
|
||||||
|
if (exited) return
|
||||||
|
exited = true
|
||||||
|
exit(code)
|
||||||
|
}
|
||||||
|
const timeout = setTimeout(exitOnce, ROOT_DISPOSE_TIMEOUT_MS)
|
||||||
|
void ctx.root.fiber.dispose().then(
|
||||||
|
() => { clearTimeout(timeout); exitOnce() },
|
||||||
|
() => { clearTimeout(timeout); exitOnce() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** Cordis entry point using the process terminal; explicit TUI composition requires a TTY pair. */
|
/** Cordis entry point using the process terminal; explicit TUI composition requires a TTY pair. */
|
||||||
/* v8 ignore start -- production process wiring; fake-terminal tests cover mountTui/createTuiChat,
|
/* v8 ignore start -- production process wiring; fake-terminal tests cover mountTui/createTuiChat,
|
||||||
and the tui-agent PTY smoke covers the real entry */
|
and apps/cli PTY smokes cover the real entry */
|
||||||
export function apply(ctx: Context, config: Config): void {
|
export function apply(ctx: Context, config: Config): void {
|
||||||
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
||||||
throw new Error('ui-tui: both stdin and stdout must be TTYs; use the one-shot @deepseek-ai/dsh-cli-demo app for pipes')
|
throw new Error('ui-tui: both stdin and stdout must be TTYs; use the one-shot @deepseek-ai/dsh-cli-demo app for pipes')
|
||||||
@@ -1676,9 +1700,7 @@ export function apply(ctx: Context, config: Config): void {
|
|||||||
initialSkill === undefined ? {} : { initialSkill },
|
initialSkill === undefined ? {} : { initialSkill },
|
||||||
), {
|
), {
|
||||||
terminal: new ProcessTerminal(),
|
terminal: new ProcessTerminal(),
|
||||||
exit: (code) => {
|
exit: (code) => { disposeRootAndExit(ctx, code) },
|
||||||
void ctx.fiber.dispose().finally(() => { process.exit(code) })
|
|
||||||
},
|
|
||||||
...resumeHost === undefined ? {} : { handoffResume: (sessionId, cwd) => resumeHost.handoff(sessionId, cwd) },
|
...resumeHost === undefined ? {} : { handoffResume: (sessionId, cwd) => resumeHost.handoff(sessionId, cwd) },
|
||||||
...goodbyeMessage === undefined ? {} : { goodbyeMessage },
|
...goodbyeMessage === undefined ? {} : { goodbyeMessage },
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import SessionReferenceService, { formatSessionReferenceMention } from '@deepsee
|
|||||||
import type {} from '@deepseek-ai/dsh-llm-retry'
|
import type {} from '@deepseek-ai/dsh-llm-retry'
|
||||||
import {
|
import {
|
||||||
createTuiChat,
|
createTuiChat,
|
||||||
|
disposeRootAndExit,
|
||||||
FILE_REFERENCE_PROMPT,
|
FILE_REFERENCE_PROMPT,
|
||||||
mountTui,
|
mountTui,
|
||||||
renderSkillInvocation,
|
renderSkillInvocation,
|
||||||
@@ -502,6 +503,30 @@ describe('goodbye message and /resume', () => {
|
|||||||
await dispose(result)
|
await dispose(result)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('treats a closed session-query provider as unavailable', async () => {
|
||||||
|
let queryCtx: Context | undefined
|
||||||
|
const result = await setup({
|
||||||
|
cwd: '/workspace',
|
||||||
|
async configureContext(ctx) {
|
||||||
|
await ctx.plugin({
|
||||||
|
apply(child: Context) {
|
||||||
|
queryCtx = child
|
||||||
|
child.provide('sessionQuery', {
|
||||||
|
listSessions: () => Promise.reject(new Error('closed database must not be called')),
|
||||||
|
} as never)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await queryCtx!.fiber.dispose()
|
||||||
|
result.terminal.send('/resume')
|
||||||
|
result.terminal.send('\r')
|
||||||
|
await tick()
|
||||||
|
expect(result.terminal.output).toContain('session query is not mounted')
|
||||||
|
expect(result.terminal.output).not.toContain('closed database')
|
||||||
|
await dispose(result)
|
||||||
|
})
|
||||||
|
|
||||||
it('keeps persisted query records readable without a persistence service', async () => {
|
it('keeps persisted query records readable without a persistence service', async () => {
|
||||||
const target = header('query-only-persisted', 10, '/workspace')
|
const target = header('query-only-persisted', 10, '/workspace')
|
||||||
const result = await setup({
|
const result = await setup({
|
||||||
@@ -4962,6 +4987,59 @@ describe('TUI extension service', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('application exit', () => {
|
||||||
|
it('disposes the root fiber rather than only the TUI child before exiting', async () => {
|
||||||
|
const rootDispose = vi.fn(() => Promise.resolve())
|
||||||
|
const childDispose = vi.fn(() => Promise.resolve())
|
||||||
|
const ctx = {
|
||||||
|
root: { fiber: { dispose: rootDispose } },
|
||||||
|
fiber: { dispose: childDispose },
|
||||||
|
} as unknown as Context
|
||||||
|
const exit = vi.fn()
|
||||||
|
disposeRootAndExit(ctx, 7, exit)
|
||||||
|
await Promise.resolve()
|
||||||
|
expect(rootDispose).toHaveBeenCalledOnce()
|
||||||
|
expect(childDispose).not.toHaveBeenCalled()
|
||||||
|
expect(exit).toHaveBeenCalledOnce()
|
||||||
|
expect(exit).toHaveBeenCalledWith(7)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('forces exit when root disposal does not settle', async () => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
try {
|
||||||
|
let settle!: () => void
|
||||||
|
const disposal = new Promise<void>((resolve) => { settle = resolve })
|
||||||
|
const ctx = {
|
||||||
|
root: { fiber: { dispose: () => disposal } },
|
||||||
|
} as unknown as Context
|
||||||
|
const exit = vi.fn()
|
||||||
|
disposeRootAndExit(ctx, 9, exit)
|
||||||
|
await vi.advanceTimersByTimeAsync(4_999)
|
||||||
|
expect(exit).not.toHaveBeenCalled()
|
||||||
|
await vi.advanceTimersByTimeAsync(1)
|
||||||
|
expect(exit).toHaveBeenCalledOnce()
|
||||||
|
expect(exit).toHaveBeenCalledWith(9)
|
||||||
|
settle()
|
||||||
|
await disposal
|
||||||
|
await Promise.resolve()
|
||||||
|
expect(exit).toHaveBeenCalledOnce()
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('exits after a rejected root disposal without an unhandled rejection', async () => {
|
||||||
|
const ctx = {
|
||||||
|
root: { fiber: { dispose: () => Promise.reject(new Error('cleanup failed')) } },
|
||||||
|
} as unknown as Context
|
||||||
|
const exit = vi.fn()
|
||||||
|
disposeRootAndExit(ctx, 5, exit)
|
||||||
|
await Promise.resolve()
|
||||||
|
await Promise.resolve()
|
||||||
|
expect(exit).toHaveBeenCalledWith(5)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('terminal mounting', () => {
|
describe('terminal mounting', () => {
|
||||||
it('starts immediately when the configured agent already exists', async () => {
|
it('starts immediately when the configured agent already exists', async () => {
|
||||||
const ctx = new Context()
|
const ctx = new Context()
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -365,6 +365,9 @@ importers:
|
|||||||
'@deepseek-ai/dsh-tool-bash':
|
'@deepseek-ai/dsh-tool-bash':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/bash/tool-bash
|
version: link:../../packages/bash/tool-bash
|
||||||
|
'@deepseek-ai/dsh-tool-cordis':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../packages/cordis/tool-cordis
|
||||||
'@deepseek-ai/dsh-tool-fs':
|
'@deepseek-ai/dsh-tool-fs':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/fs/tool-fs
|
version: link:../../packages/fs/tool-fs
|
||||||
|
|||||||
@@ -1,28 +1,16 @@
|
|||||||
/**
|
/** Boot the ACP Code Mode overlay. Requires a DeepSeek API key. */
|
||||||
* Boot the TUI or ACP Code Mode overlay, defaulting to TUI. Each overlay
|
|
||||||
* includes its base example, selects Code Mode, and adds the worker runtime.
|
|
||||||
* All require a DeepSeek API key; unsupported arguments fail with usage.
|
|
||||||
*/
|
|
||||||
import { spawn } from 'node:child_process'
|
import { spawn } from 'node:child_process'
|
||||||
|
|
||||||
// Each UI's node invocation matches its base demo script plus the overlay config.
|
if (process.argv.length > 2) {
|
||||||
const UIS = new Map([
|
console.error('usage: pnpm run demo:code-mode')
|
||||||
['tui', [
|
|
||||||
'--import',
|
|
||||||
'tsx/esm',
|
|
||||||
'apps/cli/src/bin.ts',
|
|
||||||
'--config',
|
|
||||||
'examples/code-mode/cordis.yml',
|
|
||||||
]],
|
|
||||||
['acp', ['--import', 'tsx', 'packages/examples/acp-demo/src/bin.ts', '--config', 'examples/acp-agent/code-mode.cordis.yml']],
|
|
||||||
])
|
|
||||||
|
|
||||||
const ui = process.argv[2] ?? 'tui'
|
|
||||||
const args = UIS.get(ui)
|
|
||||||
if (!args || process.argv.length > 3) {
|
|
||||||
console.error('usage: pnpm run demo:code-mode [tui|acp]')
|
|
||||||
process.exit(2)
|
process.exit(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
const child = spawn(process.execPath, args, { stdio: 'inherit' })
|
const child = spawn(process.execPath, [
|
||||||
|
'--import',
|
||||||
|
'tsx',
|
||||||
|
'packages/examples/acp-demo/src/bin.ts',
|
||||||
|
'--config',
|
||||||
|
'examples/acp-agent/code-mode.cordis.yml',
|
||||||
|
], { stdio: 'inherit' })
|
||||||
child.on('exit', (code, signal) => { process.exit(signal !== null ? 1 : code ?? 1) })
|
child.on('exit', (code, signal) => { process.exit(signal !== null ? 1 : code ?? 1) })
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ interface PluginReference {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const root = resolve(import.meta.dirname, '..')
|
const root = resolve(import.meta.dirname, '..')
|
||||||
|
// These example files are overlays consumed by the built dsh app, so their bare
|
||||||
|
// specifiers resolve from apps/cli rather than the examples workspace.
|
||||||
|
const appOverlayFiles = new Set(['examples/web-cordis/cordis.yml'])
|
||||||
const metadataFields = ['id', 'name', 'group', 'disabled', 'inject', 'intercept', 'isolate'] as const
|
const metadataFields = ['id', 'name', 'group', 'disabled', 'inject', 'intercept', 'isolate'] as const
|
||||||
const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
|
const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
|
||||||
kind: 'scalar',
|
kind: 'scalar',
|
||||||
@@ -109,7 +112,7 @@ function validateExampleResolution(): string[] {
|
|||||||
const dependencies = exampleManifest.dependencies ?? {}
|
const dependencies = exampleManifest.dependencies ?? {}
|
||||||
const localPackages = localPackageDirectories()
|
const localPackages = localPackageDirectories()
|
||||||
const rootReferences = rootProjectReferences()
|
const rootReferences = rootProjectReferences()
|
||||||
const exampleReferences = pluginReferences.filter(reference => reference.file.startsWith('examples/'))
|
const exampleReferences = pluginReferences.filter(reference => reference.file.startsWith('examples/') && !appOverlayFiles.has(reference.file))
|
||||||
violations.push(...missingPluginDependencies(exampleReferences, dependencies, 'examples/package.json'))
|
violations.push(...missingPluginDependencies(exampleReferences, dependencies, 'examples/package.json'))
|
||||||
const requiredPackages = new Set(exampleReferences.map(reference => packageNameFromSpecifier(reference.name)))
|
const requiredPackages = new Set(exampleReferences.map(reference => packageNameFromSpecifier(reference.name)))
|
||||||
|
|
||||||
@@ -129,12 +132,9 @@ function validateExampleResolution(): string[] {
|
|||||||
|
|
||||||
function validateAppResolution(): string[] {
|
function validateAppResolution(): string[] {
|
||||||
const dependencies = readManifest('apps/cli/package.json').dependencies ?? {}
|
const dependencies = readManifest('apps/cli/package.json').dependencies ?? {}
|
||||||
const shipped = new Set([
|
const shipped = new Set(globSync('*.cordis.yml', { cwd: resolve(root, 'apps/cli/config') })
|
||||||
'apps/cli/config/base.cordis.yml',
|
.map(file => `apps/cli/config/${file}`))
|
||||||
'apps/cli/config/tui.cordis.yml',
|
const references = pluginReferences.filter(reference => shipped.has(reference.file) || appOverlayFiles.has(reference.file))
|
||||||
'apps/cli/config/web.cordis.yml',
|
|
||||||
])
|
|
||||||
const references = pluginReferences.filter(reference => shipped.has(reference.file))
|
|
||||||
return missingPluginDependencies(references, dependencies, 'apps/cli/package.json')
|
return missingPluginDependencies(references, dependencies, 'apps/cli/package.json')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user