Merge branch 'stack/agent-profiles-1-seam' into stack/agent-profiles-2-configs
This commit is contained in:
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-27-dispose-ladder-to-consumer.md
|
||||
2026-07-27-dispose-ladder-to-consumer.md: 97b551ff509e3b424f6bf5725939cf54acc961a7
|
||||
2026-07-27-dispose-ladder-to-consumer.zh.md: 7fff744e64109549a65d4f5bb17ff2d6ddfc6888
|
||||
2026-07-27-dispose-ladder-to-consumer.md: e9af88e8e7ef962213a74e96a249241cbe8d5994
|
||||
2026-07-27-dispose-ladder-to-consumer.zh.md: 89f8e107c56d42787c59bc6f8fa8fc7b3ef73208
|
||||
@@ -10,7 +10,7 @@ English | [中文](2026-07-27-dispose-ladder-to-consumer.zh.md)
|
||||
|
||||
## Decision
|
||||
|
||||
The ladder moves to its one consumer. `dsh-subagent-acp` owns `disposeAcpChild(child, eofGraceMs, graceMs)`, built entirely on the seam's public verbs: close `stdin`, bound a `waitForExit` on `eofGraceMs`, then `terminate()` (whose SIGTERM→spec-grace→SIGKILL escalation already encodes the signal tiers), then a final bounded whole-tree wait that throws if survivors remain. The seam keeps `kill`/`terminate`/`waitForExit` — mechanisms, not policy — and `waitForExit(signal?)` is exactly the quiescence probe a consumer ladder needs to hold each tier on real tree exit. `dsh-subprocess-local` drops its `dsh-timeout` dependency; the seam's handle loses one method and one exported interface.
|
||||
The ladder moves to its one consumer. `dsh-subagent-acp` owns `disposeAcpChild(child, eofGraceMs)`, built entirely on the seam's public verbs: close `stdin`, bound a `waitForExit` on `eofGraceMs`, then call `terminate()`, whose SIGTERM→spec-grace→SIGKILL escalation already owns the signal timer, and await an unbounded `waitForExit()` for the subprocess owner's whole-tree exit proof. The seam keeps `kill`/`terminate`/`waitForExit` — mechanisms, not policy — and `waitForExit(signal?)` is exactly the quiescence probe a consumer ladder needs to hold the cooperative tier on real tree exit without deriving another timer from the termination grace. The seam's handle loses one method and one exported interface.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -20,4 +20,4 @@ The ladder moves to its one consumer. `dsh-subagent-acp` owns `disposeAcpChild(c
|
||||
|
||||
## Consequences
|
||||
|
||||
Bought: the seam is one method and one type smaller; implementations owe four verbs and no teardown policy; `dsh-subprocess-local` loses a dependency; the ladder's tier windows live beside the config fields that tune them. Cost: a future backend wanting EOF-first teardown writes ~20 lines against the verbs (or lifts the ACP helper); the ladder's tier-tier tests moved from the seam suite to the ACP suite, and the seam suite pins the verbs the ladder composes (bounded `waitForExit` false-then-true across an escalation) instead of the composed policy.
|
||||
Bought: the seam is one method and one type smaller; implementations owe four verbs and no teardown policy; the cooperative EOF window lives beside the ACP config field that tunes it, while the subprocess owner alone owns the termination window and final join. Cost: a future backend wanting EOF-first teardown writes ~20 lines against the verbs (or lifts the ACP helper); the ladder's tier tests live in the ACP suite, and the seam suite pins the verbs the ladder composes (bounded `waitForExit` false before escalation and an unbounded whole-tree join after it) instead of the composed policy.
|
||||
@@ -10,7 +10,7 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
阶梯移入其唯一消费方。`dsh-subagent-acp` 拥有 `disposeAcpChild(child, eofGraceMs, graceMs)`,完全构建在 seam 的公开动词之上:关闭 `stdin`,以 `eofGraceMs` 约束一次 `waitForExit`,随后 `terminate()`(其 SIGTERM→spec 宽限期→SIGKILL 升级已编码了信号层级),最后进行有界的整树等待,若仍有存活进程则抛出。seam 保留 `kill`/`terminate`/`waitForExit`——机制而非策略——而 `waitForExit(signal?)` 恰是消费方阶梯在每一层确认进程树真正退出所需的完全停稳探针。`dsh-subprocess-local` 卸下 `dsh-timeout` 依赖;seam 的句柄少了一个方法和一个导出接口。
|
||||
阶梯移入其唯一消费方。`dsh-subagent-acp` 拥有 `disposeAcpChild(child, eofGraceMs)`,完全构建在 seam 的公开动词之上:关闭 `stdin`,以 `eofGraceMs` 约束一次 `waitForExit`,随后调用 `terminate()`(其 SIGTERM→spec 宽限期→SIGKILL 升级已拥有信号定时器),再无界等待 `waitForExit()`,由子进程责任方证明整棵进程树已经退出。seam 保留 `kill`/`terminate`/`waitForExit`——机制而非策略——而 `waitForExit(signal?)` 恰是消费方阶梯在协作层确认进程树真正退出所需的停稳探针,无需从终止宽限期再派生一个定时器。seam 的句柄少了一个方法和一个导出接口。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
@@ -20,4 +20,4 @@ Status: implemented
|
||||
|
||||
## 后果
|
||||
|
||||
换来的是:seam 少了一个方法和一个类型;实现只需提供四个动词,无需提供拆卸策略;`dsh-subprocess-local` 少了一个依赖;阶梯的层级时间窗与调节它们的配置字段住在一起。代价:未来想要 EOF 打头拆卸的后端需针对这些动词写约 20 行(或直接搬 ACP 的辅助函数);阶梯的层级测试从 seam 套件移入 ACP 套件,seam 套件转而钉住阶梯所组合的动词(升级前后有界 `waitForExit` 先假后真),而非组合后的策略。
|
||||
买到的:seam 少了一个方法和一个类型;实现只欠四个动词,不欠拆卸策略;协作式 EOF 时间窗与调节它的 ACP 配置字段住在一起,而终止时间窗与最终的整树退出等待仅由子进程责任方拥有。代价:未来想要 EOF 打头拆卸的后端需针对这些动词写约 20 行(或直接搬 ACP 的辅助函数);阶梯的层级测试位于 ACP 套件,seam 套件转而钉住阶梯所组合的动词(升级前有界 `waitForExit` 返回假,升级后无界等待整棵进程树退出),而非组合后的策略。
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.md
|
||||
2026-07-28-web-agent-runtime-context.md: 449c9d4ba2b144d02dee4b98ae80c86815aec5c1
|
||||
2026-07-28-web-agent-runtime-context.zh.md: def1674be5f193739bfb214a24f34590ee075d5f
|
||||
2026-07-28-web-agent-runtime-context.md: 6c2bb8faf0db56d2a29b47726ab295c3501cce2d
|
||||
2026-07-28-web-agent-runtime-context.zh.md: 09cbbdeabc244c265381b1ad67c3629b4c0763f5
|
||||
@@ -10,13 +10,13 @@ The shared CLI base configured an empty deployment persona, the Web overlay did
|
||||
|
||||
## Decision
|
||||
|
||||
The shared Web/headless overlay (`apps/cli/config/web.cordis.yml`) supplies a concise coding-agent persona containing the resolved `{{model}}` and session `{{cwd}}`. `dsh web` additionally resolves the harness checkout from the launcher's module URL, installs the existing `harness:source` section, and adds an `app:web-surface` section before serving requests. The launcher registers that setup before mounting the config tree; its `systemPrompt` injection therefore installs both sections before later prompt consumers such as the agent loop can activate and emit a request header. The [source-checkout/workdir decision](2026-07-30-source-checkout-workdir-distinction.md) owns the source section's wording and its warning not to infer one path from the other.
|
||||
The Web profile composes the `dsh-base` and `dsh-web-app` bundles. The Web bundle supplies a concise coding-agent persona containing the resolved `{{model}}` and session `{{cwd}}`; its `web-runtime` plugin adds the `app:web-surface` section when `surfaceContext` is true. Before mounting the profile tree, the `dsh web` alias reads that same composed setting and installs the existing `harness:source` section only when the surface context is enabled. The headless bundle and a profile that owns its complete prompt set `surfaceContext: false`, suppressing the Web prompt and managed shell facts; the Web alias also suppresses the source section without checking an overlay path. Every mounted prompt contribution still activates before consumers such as the agent loop can emit a request header. The [source-checkout/workdir decision](2026-07-30-source-checkout-workdir-distinction.md) owns the source section's wording and its warning not to infer one path from the other.
|
||||
|
||||
The Web section treats unqualified references to “this page,” “this GUI,” or “this app” as references to the DeepSeek Harness Web GUI. It also states that the browser provides no implicit DOM, route, or screenshot context, so the model can identify the product without claiming visual state it did not receive. The assembled text is logged in `request/header`, preserving the model-visible/logged invariant.
|
||||
|
||||
## Verification
|
||||
|
||||
The focused startup-order test registers a later `systemPrompt` consumer and proves that it observes both launcher sections on its first activation. The keyless fresh-round-trip Web scenario boots the shipped base plus Web overlay, registers the same launcher context as `dsh web`, runs a real session through the HTTP/SSE application, and snapshots the first four system-prompt sections with source and working-directory paths normalized. The snapshot pins the harness identity, source checkout, Web orientation, and resolved coding-agent persona in request order.
|
||||
The Web runtime unit tests pin both enabled and disabled `surfaceContext` behavior, while the Web alias unit test pins default-on and explicit-off source-section gating from the composed row. The keyless fresh-round-trip Web scenario boots the shipped base plus Web bundle, runs a real session through the HTTP/SSE application, and snapshots the system-prompt prefix with source and working-directory paths normalized. The snapshot pins the harness identity, source checkout, Web orientation, and resolved coding-agent persona in request order. The Core Web snapshot applies the RL overlay and pins its complete system prompt with no source or Web section.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -30,4 +30,4 @@ The focused startup-order test registers a later `systemPrompt` consumer and pro
|
||||
|
||||
## Consequences
|
||||
|
||||
Web requests gain a short stable prompt prefix and may invalidate provider prefix caches once when this change is deployed. Agents can distinguish the GUI source checkout from the selected Workspace and resolve ordinary references to the current app without a clarification round trip. References to a specific visual state remain bounded by the explicit no-DOM/no-route/no-screenshot statement and may still require a path, description, or attachment.
|
||||
Ordinary Web requests gain a short stable prompt prefix and may invalidate provider prefix caches once when this change is deployed. Agents can distinguish the GUI source checkout from the selected Workspace and resolve ordinary references to the current app without a clarification round trip. References to a specific visual state remain bounded by the explicit no-DOM/no-route/no-screenshot statement and may still require a path, description, or attachment. Complete-prompt profiles can opt out through the Web runtime's composition setting without a launcher path check.
|
||||
@@ -10,13 +10,13 @@ CLI 共享 base 配置了空的部署 persona,Web overlay 没有替换它,
|
||||
|
||||
## 决策
|
||||
|
||||
`apps/cli/config/web.cordis.yml` 这份 Web/无头共享 overlay 提供一段简洁的编码 agent persona,其中包含解析后的 `{{model}}` 与会话 `{{cwd}}`。`dsh web` 还会根据启动器模块的 URL 解析 harness checkout,安装现有的 `harness:source` 提示词段,并在对外提供请求服务前添加 `app:web-surface` 提示词段。启动器会在挂载配置树前注册这项设置;因此,它的 `systemPrompt` 注入会在 agent loop(智能体循环)等后续提示词消费方激活并发出 request header 之前安装这两个提示词段。源码提示词段的措辞,以及其中不得从一条路径推断另一条路径的警告,均由另行记录的[源码 checkout 与工作目录区分决策](2026-07-30-source-checkout-workdir-distinction.md)负责。
|
||||
Web profile 会组合 `dsh-base` 与 `dsh-web-app` 两个组合包。Web 组合包提供一段简洁的编码 agent persona,其中包含解析后的 `{{model}}` 与会话 `{{cwd}}`;当 `surfaceContext` 为 true 时,其 `web-runtime` 插件会添加 `app:web-surface` 提示词段。挂载 profile 配置树前,`dsh web` 别名会读取组合后的同一项设置,并且仅在启用表层上下文时安装现有的 `harness:source` 提示词段。无头组合包和拥有完整提示词的 profile 都会设置 `surfaceContext: false`,从而抑制 Web 提示词与受管 shell 事实;Web 别名也会抑制源码提示词段,而无需检查 overlay 路径。每项已挂载的提示词贡献仍会在 agent loop(智能体循环)等消费方发出 request header 前激活。源码提示词段的措辞,以及其中不得从一条路径推断另一条路径的警告,均由另行记录的[源码 checkout 与工作目录区分决策](2026-07-30-source-checkout-workdir-distinction.md)负责。
|
||||
|
||||
Web 提示词段把未限定的「这个页面」「这个 GUI」或「这个应用」解释为 DeepSeek Harness Web GUI。同时,它会明确说明浏览器不会隐式提供 DOM、路由或截图上下文,使模型能够识别产品,但不会声称掌握未收到的视觉状态。组装后的文本会记录在 `request/header` 中,从而保持「模型可见内容必须有日志记录」这一不变量。
|
||||
|
||||
## 验证
|
||||
|
||||
聚焦启动顺序的测试会注册一个后续的 `systemPrompt` 消费方,并证明该消费方首次激活时就能观察到启动器的两个提示词段。无密钥的 Web fresh-round-trip 场景会启动已交付的 base 与 Web overlay,注册与 `dsh web` 相同的启动器上下文,并通过 HTTP/SSE 应用运行一个真实会话。测试会把源码路径和工作目录规范化,然后对系统提示词的前四个段落生成快照。该快照按请求顺序固定 harness 身份、源码 checkout、Web 界面定位,以及解析后的编码 agent persona。
|
||||
Web 运行时单元测试会固定启用和禁用 `surfaceContext` 时的行为,Web 别名单元测试则会固定组合后配置行对源码提示词段的默认启用与显式禁用门控。无密钥的 Web fresh-round-trip 场景会启动已交付的 base 与 Web 组合包,通过 HTTP/SSE 应用运行一个真实会话,并在规范化源码路径与工作目录后对系统提示词前缀生成快照。该快照按请求顺序固定 harness 身份、源码 checkout、Web 界面定位,以及解析后的编码 agent persona。Core Web 快照会应用 RL overlay,并固定不含源码提示词段或 Web 提示词段的完整系统提示词。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -30,4 +30,4 @@ Web 提示词段把未限定的「这个页面」「这个 GUI」或「这个应
|
||||
|
||||
## 影响
|
||||
|
||||
Web 请求会增加一段较短且稳定的提示词前缀;部署此变更时,模型提供方的前缀缓存可能失效一次。agent 可以区分 GUI 源码 checkout 与所选 Workspace,并且无需再经过一轮澄清即可解析对当前应用的一般指代。对特定视觉状态的指代仍受「无 DOM/无路由/无截图」这一显式边界约束,必要时仍需用户提供路径、描述或附件。
|
||||
常规 Web 请求会增加一段较短且稳定的提示词前缀;部署此变更时,模型提供方的前缀缓存可能失效一次。agent 可以区分 GUI 源码 checkout 与所选 Workspace,并且无需再经过一轮澄清即可解析对当前应用的一般指代。对特定视觉状态的指代仍受「无 DOM/无路由/无截图」这一显式边界约束,必要时仍需用户提供路径、描述或附件。拥有完整提示词的 profile 可以通过 Web 运行时的组合设置选择退出,而无需检查启动器路径。
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-28-web-gui-feedback-loop.md
|
||||
2026-07-28-web-gui-feedback-loop.md: 039d2aebeeef903d10838a46b48e5172f0195126
|
||||
2026-07-28-web-gui-feedback-loop.zh.md: 34b6b26d4ce7c7e194e641536fffc503c013188b
|
||||
2026-07-28-web-gui-feedback-loop.md: aa488e1df087722c072d98c67d47cdf63a42f6b8
|
||||
2026-07-28-web-gui-feedback-loop.zh.md: d9de988e5a2cc57794ff628b65eb050dee610747
|
||||
@@ -12,7 +12,7 @@ The [incident post-mortem](../../../../docs/postmortem/0003-web-agent-gui-feedba
|
||||
|
||||
## Decision
|
||||
|
||||
`dsh web` publishes one canonical loopback URL and its actual runtime mode as both model-visible orientation and managed shell facts. The `app:web-surface` prompt section says that unqualified references identify this GUI and names the URL; `DSH_WEB_URL` and `DSH_WEB_MODE=production|development` carry the same facts into every foreground or managed background bash call. The section preserves the no-implicit-DOM, route, or screenshot boundary and does not claim that a LAN alias equals the browser's literal address.
|
||||
The ordinary `dsh web` composition mounts the Web bundle's `web-runtime` plugin, which publishes one canonical loopback URL and its actual runtime mode as both model-visible orientation and managed shell facts. The `app:web-surface` prompt section says that unqualified references identify this GUI and names the URL; `DSH_WEB_URL` and `DSH_WEB_MODE=production|development` carry the same facts into every foreground or managed background bash call. The section preserves the no-implicit-DOM, route, or screenshot boundary and does not claim that a LAN alias equals the browser's literal address. A complete-prompt profile sets the row's `surfaceContext` to false and receives neither the prompt section nor the managed variables; the Web launcher uses the same setting to suppress its source-checkout prompt section.
|
||||
|
||||
The mode-specific prompt makes the agent, rather than the user, own the hidden startup contract. Production mode defines acceptance as rebuilding the affected artifacts and refreshing the existing URL. Development mode states that `dsh web --dev` activates only the HMR receiver: automatic client-plugin reload additionally requires a same-checkout `pnpm run dev:web` watcher, which the agent verifies before promising no-refresh updates. Shell and other plain-package changes still require rebuild plus refresh. An agent in production mode explains both commands when a user requests no-refresh updates; it does not launch a replacement GUI unless asked.
|
||||
|
||||
@@ -36,4 +36,4 @@ The keyless fresh-round-trip browser scenario boots the shipped production Web c
|
||||
|
||||
## Consequences
|
||||
|
||||
Web prompts gain a dynamic URL-and-mode paragraph, so provider prefix reuse now varies by bound port and mode. Bash processes gain two non-secret managed environment variables. Bare Vite can no longer be used as a shell-only visual sandbox; developers use the full host or build mode instead. In exchange, GUI work has one mechanically observable target, the agent can teach the user the exact update behavior of the process actually serving their session, and the unsupported startup path fails before a white screen. The URL/mode contract guides the agent away from replacement ports; it does not prohibit arbitrary shell commands from starting one.
|
||||
Ordinary Web prompts gain a dynamic URL-and-mode paragraph, so provider prefix reuse now varies by bound port and mode. Their Bash processes gain two non-secret managed environment variables. Bare Vite can no longer be used as a shell-only visual sandbox; developers use the full host or build mode instead. In exchange, GUI work has one mechanically observable target, the agent can teach the user the exact update behavior of the process actually serving their session, and the unsupported startup path fails before a white screen. The URL/mode contract guides the agent away from replacement ports; it does not prohibit arbitrary shell commands from starting one. Profiles that disable `surfaceContext` also give up this feedback-loop guidance and shell context.
|
||||
@@ -12,7 +12,7 @@ Web agent(智能体)既无法识别承载当前会话的 GUI,也不知道
|
||||
|
||||
## 决策
|
||||
|
||||
`dsh web` 发布一个规范的回环 URL 及其实际运行时模式,同时将二者作为模型可见的界面定位信息和受管 shell 事实。`app:web-surface` 提示词段说明:未加限定的指代指向此 GUI,并给出 URL;`DSH_WEB_URL` 和 `DSH_WEB_MODE=production|development` 会把同样的事实传入每次前台或受管后台 bash 调用。该段保留「不会隐式获得 DOM、路由或截图」这一边界,也不声称局域网别名等于浏览器中的实际地址。
|
||||
常规 `dsh web` 组合会挂载 Web 组合包的 `web-runtime` 插件,由它发布一个规范的回环 URL 及其实际运行时模式,同时将二者作为模型可见的界面定位信息和受管 shell 事实。`app:web-surface` 提示词段说明:未加限定的指代指向此 GUI,并给出 URL;`DSH_WEB_URL` 和 `DSH_WEB_MODE=production|development` 会把同样的事实传入每次前台或受管后台 bash 调用。该段保留「不会隐式获得 DOM、路由或截图」这一边界,也不声称局域网别名等于浏览器中的实际地址。拥有完整提示词的 profile 会把该配置行的 `surfaceContext` 设为 false,并且不会收到该提示词段和这些受管变量中的任何一个;Web 启动器也会使用同一项设置来抑制其源码 checkout 提示词段。
|
||||
|
||||
按模式区分的提示词让 agent 而非用户负责隐藏的启动契约。生产模式将验收定义为重新构建受影响的产物并刷新现有 URL。开发模式说明,`dsh web --dev` 只会启用 HMR(热模块替换)接收端:客户端插件要自动重新加载,还需要在同一检出中运行 `pnpm run dev:web` 监听进程,agent 会在承诺无需刷新即可更新前验证这一点。外壳和其他普通包的变更仍然需要重新构建并刷新。生产模式下的 agent 会在用户要求无需刷新即可更新时说明这两个命令;除非用户要求,否则不会启动替代 GUI。
|
||||
|
||||
@@ -36,4 +36,4 @@ Web agent(智能体)既无法识别承载当前会话的 GUI,也不知道
|
||||
|
||||
## 影响
|
||||
|
||||
Web 提示词会增加一个动态 URL 和模式段落,因此模型提供方的前缀复用会随绑定端口和模式变化。Bash 进程会增加两个非敏感的受管环境变量。裸 Vite 不再能用作只依赖 shell 的视觉沙箱;开发者应改用完整宿主或构建模式。作为交换,GUI 工作有了一个可由机制观察的唯一目标,agent 可以向用户说明实际承载其会话的进程究竟如何更新,不受支持的启动路径也会在出现白屏前失败。URL/模式契约会引导 agent 避免使用替代端口,但不会禁止任意 shell 命令启动替代服务。
|
||||
常规 Web 提示词会增加一个动态 URL 和模式段落,因此模型提供方的前缀复用会随绑定端口和模式变化。相应的 Bash 进程会增加两个非敏感的受管环境变量。裸 Vite 不再能用作只依赖 shell 的视觉沙箱;开发者应改用完整宿主或构建模式。作为交换,GUI 工作有了一个可由机制观察的唯一目标,agent 可以向用户说明实际承载其会话的进程究竟如何更新,不受支持的启动路径也会在出现白屏前失败。URL/模式契约会引导 agent 避免使用替代端口,但不会禁止任意 shell 命令启动替代服务。禁用 `surfaceContext` 的 profile 也会放弃这项反馈闭环指引与 shell 上下文。
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-06-onboarding-step-owned-takeover-chrome.md
|
||||
2026-08-06-onboarding-step-owned-takeover-chrome.md: 4b3bbbc03c4494359297ae6e54bcc9a74c387e80
|
||||
2026-08-06-onboarding-step-owned-takeover-chrome.zh.md: 548285d285939325a26df3b8d70c43fe952813a9
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: onboarding takeover chrome moves into the step
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-06-onboarding-step-owned-takeover-chrome.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The settings shell mounted the onboarding takeover chrome — a body-portaled overlay with an opaque `--dsw-alias-bg-layer-1` stage, a blur mask, and `#root` set inert — the moment a `settings.onboarding` step was registered and not yet locally completed. Every step decides whether it actually needs to show by loading a private fact first (WelcomeNotice: the acknowledgement bit through its settings join; DeepSeekOnboardingDialog: credential readiness through the Models join) and renders `null` while that fact is in flight. Rendering `null` could not suppress the chrome, because the opaque stage was painted by the shell around the slot outlet, not by the step.
|
||||
|
||||
On every reload while the hero (blank or no session) was current, the sessions list turning `ready` therefore popped a full-screen opaque layer — white in the light palette — and blocked all interaction for exactly one credential/settings RPC round-trip, after which the already-configured steps self-completed and the layer vanished. Users saw the app flash white each refresh the moment the workspace/session lists landed.
|
||||
|
||||
## Decision
|
||||
|
||||
The takeover chrome belongs to the step, not the shell. A new zero-cordis primitive, `OnboardingSurface` (ui-primitives), renders the body-portaled overlay/mask/stage — CSS class names and geometry moved verbatim from `SettingsRoot.module.css` — and holds `#root` inert for exactly its own mount lifetime. Both step components wrap only their **visible** branch in it; their existing `null` branches now paint and block nothing by construction, because the chrome is part of the same render decision.
|
||||
|
||||
`SettingsRoot` keeps the coordinator exactly as it was (ordered ledger projection, one mounted step, local completed set, `stepId`/`complete`/`openSection` currency) but renders the elected step bare — no portal, no stage, no inert effect. The `settings.onboarding` slot contract now states that registrants own the surface wrap and must render `null` while their private facts are undecided.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Register steps conditionally (ledger as the has-content signal).** Register the entry only after the private join resolves to "needs intervention". Architecturally clean (publish at the commit point) but a larger change: the join load must move from the dialogs into each plugin's apply, and registration/disposal becomes reactive plumbing in two packages. Rejected as oversized for the defect.
|
||||
|
||||
**Convert `settings.onboarding` to a chain with an externalized completed-set store.** The composer-takeover pattern; prototyped and reverted. Selectors can only judge owner props, so the private readiness facts still had to be resolved inside the components — the chain bought routing generality the two current steps do not need, at the cost of a contract change across three packages.
|
||||
|
||||
**Detect empty slot output at the render site.** `renderSlot` returns an outlet element unconditionally, so the owner cannot branch on a step's `null`; probing rendered DOM emptiness needs a commit-then-retract dance whose dynamic transitions lose the pre-paint guarantee.
|
||||
|
||||
## Consequences
|
||||
|
||||
While a step is mounted but undecided, the application stays visible and interactive: `#root` is no longer inert during the decision window (previously it was inert behind an opaque layer). For a genuinely unconfigured user the takeover now appears one join round-trip later than before — but with its content already present, instead of an empty stage that fills in.
|
||||
|
||||
A future step that registers without wrapping its visible content in `OnboardingSurface` renders bare over the app with no mask; the slot contract JSDoc names the wrap as the registrant's obligation.
|
||||
|
||||
## Testing
|
||||
|
||||
`packages/client/ui-primitives/tests/onboarding-surface.spec.tsx` pins the primitive: body portal around the content, mask/stage class presence, `#root` inert held for exactly the mount lifetime, and the no-`#root` composition. `packages/client/ui-settings/tests/settings-root.spec.tsx` pins the inverted shell contract: no takeover chrome and no inert while a mounted step renders nothing. `apps/web/tests/onboarding-deepseek-config.e2e.ts` gains the defect's assembled regression pin: a configured world reloads while every `settings.describe` response is held open at the browser's network boundary — widening the steps' deciding window from loopback-invisible to hundreds of milliseconds, which is what keeps the assertions non-vacuous — and an 8 ms in-page sampler proves the takeover chrome never mounts and `#root` never turns inert. The file's existing scenarios and the step specs (`ui-settings-general`, `ui-models`) pass unchanged — the mask selector and geometry pins survive because the stylesheet moved verbatim.
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# Agent Note:首次使用引导的接管界面框架移入步骤自身
|
||||
|
||||
状态:已实现
|
||||
|
||||
[English](2026-08-06-onboarding-step-owned-takeover-chrome.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
设置外壳在 `settings.onboarding` 有已注册且本地未完成的步骤时,就立即挂出首次使用引导的接管界面框架——portal 到 body 的浮层,带不透明的 `--dsw-alias-bg-layer-1` 展示层、模糊遮罩,并把 `#root` 置为 `inert`。而每个步骤都要先加载私有事实才能判定自己是否需要出场(WelcomeNotice:经其设置 join 读取确认位;DeepSeekOnboardingDialog:经 Models join 读取凭据就绪状态),判定期间渲染 `null`。渲染 `null` 无法抑制界面框架,因为不透明展示层是外壳画在 slot outlet 外面的,不属于步骤。
|
||||
|
||||
于是每次在 hero(空白或无会话)状态下刷新页面,会话列表一变 `ready` 就弹出整屏不透明层——亮色主题下是白色——并阻断全部交互,时长恰好等于一次凭据/设置 RPC 往返;之后已配置好的步骤自我完成,图层消失。用户看到的就是每次刷新在 workspace/会话列表落地的瞬间闪一下白屏。
|
||||
|
||||
## 决定
|
||||
|
||||
接管界面框架属于步骤,不属于外壳。新增零 cordis 原语 `OnboardingSurface`(ui-primitives):渲染 portal 到 body 的浮层/遮罩/展示层——CSS 类名与几何从 `SettingsRoot.module.css` 逐字迁移——并在自身挂载生命周期内保持 `#root` 为 `inert`。两个步骤组件只把各自的**可见**分支包进该原语;既有的 `null` 分支由此在构造上不绘制、不阻塞任何内容,因为界面框架已是同一次渲染决策的一部分。
|
||||
|
||||
`SettingsRoot` 的协调器原样保留(有序账本投影、每次挂载一个步骤、本地完成集合、`stepId`/`complete`/`openSection` currency),但对当选步骤裸渲染——不再有 portal、展示层和 inert 效果。`settings.onboarding` 的 slot 契约现在写明:注册方持有外层包裹,且在私有事实未决时必须渲染 `null`。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**条件注册(账本即有内容信号)。** 私有 join 解析出「需要介入」后才注册条目。架构上干净(在 commit point 发布),但改动更大:join 的加载必须从对话框上移到各插件的 apply,注册/销毁在两个包里都变成响应式接线。对本缺陷而言过重,否决。
|
||||
|
||||
**把 `settings.onboarding` 改成 chain 并把完成集合外置为 store。** composer takeover 的版型;做过原型后回退。selector 只能判定 owner props,私有就绪事实仍然只能在组件内部解析——chain 买来的是当前两个步骤并不需要的路由通用性,代价却是跨三个包的契约变更。
|
||||
|
||||
**在渲染点探测 slot 输出为空。** `renderSlot` 无条件返回 outlet 元素,owner 无法据步骤的 `null` 分支;探测已渲染 DOM 是否为空需要先提交再撤回的手法,其动态翻转会失去 paint 前的保证。
|
||||
|
||||
## 后果
|
||||
|
||||
步骤已挂载但尚未判定期间,应用保持可见且可交互:判定窗口内 `#root` 不再是 `inert`(此前是在不透明图层背后被置灰)。对真正未配置的用户,接管层比从前晚一个 join 往返出现——但一出现就带着内容,而不是先露出空白展示层再填充。
|
||||
|
||||
未来若有步骤注册后不把可见内容包进 `OnboardingSurface`,会无遮罩地裸渲染在应用之上;slot 契约的 JSDoc 已把包裹写为注册方的义务。
|
||||
|
||||
## 测试
|
||||
|
||||
`packages/client/ui-primitives/tests/onboarding-surface.spec.tsx` 钉住原语行为:内容外的 body portal、遮罩/展示层类名存在、`#root` 的 `inert` 恰好持续挂载生命周期,以及无 `#root` 的组合。`packages/client/ui-settings/tests/settings-root.spec.tsx` 钉住反转后的外壳契约:已挂载步骤什么都不渲染时,无接管界面框架、无 inert。`apps/web/tests/onboarding-deepseek-config.e2e.ts` 新增本缺陷的整装回归钉:已配置世界刷新页面,同时在浏览器网络边界扣住所有 `settings.describe` 响应——把步骤的判定窗口从 loopback 下不可见拉宽到数百毫秒,这正是断言保持非空洞的关键——页内 8ms 采样器证明接管界面框架从未挂载、`#root` 从未变为 inert。该文件的既有场景与步骤 spec(`ui-settings-general`、`ui-models`)原样通过——样式表逐字迁移,遮罩选择器与几何钉子得以幸存。
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md
|
||||
2026-06-21-subagent-capability-seam.md: 043092884731c403a11b71ef8b5a410e9e5af7e0
|
||||
2026-06-21-subagent-capability-seam.zh.md: 6a4a5798199ca7d6d7c011668a319d65a0553208
|
||||
2026-06-21-subagent-capability-seam.md: 752e639b09ea2ac0ba19841ddfe38b15b44d22e9
|
||||
2026-06-21-subagent-capability-seam.zh.md: 6009aeda6773357a4217f8956fb510c2116bbe3f
|
||||
@@ -4,7 +4,7 @@ Status: implemented
|
||||
|
||||
English | [中文](2026-06-21-subagent-capability-seam.zh.md)
|
||||
|
||||
> The full seam is shipped: the `dsh-subagent` interface and `dsh-tool-subagent` consumer; the two in-process backends (`dsh-subagent-spawn`, `dsh-subagent-fork`); the nested-agent snapshot infrastructure ([per-session snapshot replay](../testing/2026-06-22-subagent-snapshot-replay.md)); and the out-of-process `dsh-subagent-acp` backend ([its Agent Note](2026-06-22-acp-subagent-backend.md)).
|
||||
> The full seam is shipped: the `dsh-subagent` interface and `dsh-tool-subagent` consumer; the two in-process backends (`dsh-subagent-spawn`, `dsh-subagent-fork`); the nested-agent snapshot infrastructure ([per-session snapshot replay](../testing/2026-06-22-subagent-snapshot-replay.md)); and the out-of-process ACP, Codex, and Claude Code backends ([ACP Agent Note](2026-06-22-acp-subagent-backend.md), [product-provider Agent Note](2026-08-04-claude-code-and-codex-subagent-backends.md)).
|
||||
|
||||
## Problem
|
||||
|
||||
@@ -14,7 +14,8 @@ The distinctive requirement — the one that shapes the whole design — is that
|
||||
|
||||
- **in-process** — a child concrete `Agent` on the same `Context` (the cheapest, and nearly free given the existing agent factory);
|
||||
- **ACP** — act as an ACP *client* driving another agent process (which can be another instance of ourselves);
|
||||
- later: **A2A**, the **Codex app-server**, and the **Claude Code Agent SDK** — each the same out-of-process "start a child, prompt it, stream updates, cancel" shape as the ACP backend.
|
||||
- **Codex app-server and Claude Code Agent SDK** — current one-shot siblings that apply the same named-provider seam to official product processes ([product-provider Agent Note](2026-08-04-claude-code-and-codex-subagent-backends.md));
|
||||
- later: **A2A** using the same out-of-process "start a child, prompt it, settle, cancel" shape.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -34,6 +35,8 @@ A new package group `packages/subagent/`:
|
||||
| `@deepseek-ai/dsh-subagent-spawn` | implementation: a fresh in-process child via `ctx.agents.create` |
|
||||
| `@deepseek-ai/dsh-subagent-fork` | implementation: an in-process child seeded with a snapshot of the parent's log |
|
||||
| `@deepseek-ai/dsh-subagent-acp` | implementation: an ACP client driving a configured child process |
|
||||
| `@deepseek-ai/dsh-subagent-codex` | implementation: a one-shot official Codex app-server process |
|
||||
| `@deepseek-ai/dsh-subagent-claude-code` | implementation: a one-shot official Claude Code process through the Agent SDK |
|
||||
| `@deepseek-ai/dsh-tool-subagent` | consumer: the model-facing `subagent` tool over `ctx.subagents` |
|
||||
|
||||
### The primitive: async `start → SubagentRun`
|
||||
@@ -51,7 +54,7 @@ Fresh and forked children are separate providers, not a request flag. `dsh-subag
|
||||
|
||||
### Child isolation and the parent log
|
||||
|
||||
Each subagent runs in its **own `Session`** (own id, `parentSession` lineage), persisted independently. The parent's log records only the spawn `tool/call` and its `tool/result` (the child's final output) — the child's internal steps and tool calls stay in the child's own session, never injected into the parent log. This is the only design that is identical across transports: an ACP child's internal events physically cannot be injected into our parent log, so making in-process behave the same keeps the seam transport-agnostic.
|
||||
Each in-process subagent runs in its **own `Session`** (own id, `parentSession` lineage), persisted independently. Remote ACP and one-shot product providers instead mint a parent-scoped lifecycle id and expose no local `Agent` or child `Session`; their internal state remains in the remote process. Across both forms, the parent's log records only the spawn `tool/call` and its `tool/result` (the child's final output), while child steps and tool calls remain outside the parent log.
|
||||
|
||||
### Synchronous collect (first cut)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ Status: implemented
|
||||
|
||||
[English](2026-06-21-subagent-capability-seam.md) | 中文
|
||||
|
||||
> 完整 seam 已交付:`dsh-subagent` 接口与 `dsh-tool-subagent` 消费方;两个进程内后端(`dsh-subagent-spawn`、`dsh-subagent-fork`);嵌套 agent 快照基础设施([逐会话快照回放](../testing/2026-06-22-subagent-snapshot-replay.md));以及进程外后端 `dsh-subagent-acp`([其 Agent Note](2026-06-22-acp-subagent-backend.md))。
|
||||
> 完整 seam 已交付:`dsh-subagent` 接口与 `dsh-tool-subagent` 消费方;两个进程内后端(`dsh-subagent-spawn`、`dsh-subagent-fork`);嵌套 agent 快照基础设施([逐会话快照回放](../testing/2026-06-22-subagent-snapshot-replay.md));以及进程外的 ACP、Codex 与 Claude Code 后端([ACP Agent Note](2026-06-22-acp-subagent-backend.md)、[产品提供方 Agent Note](2026-08-04-claude-code-and-codex-subagent-backends.md))。
|
||||
|
||||
## 问题
|
||||
|
||||
@@ -14,7 +14,8 @@ harness 有一个长期搁置的 seam 用于 **subagent**:一个 agent(智
|
||||
|
||||
- **进程内**:在同一个 `Context` 上创建一个具体的子 `Agent`(最廉价,且鉴于现有 agent 工厂几乎零成本);
|
||||
- **ACP**:作为 ACP *客户端*驱动另一个 agent 进程(可以是自身的另一个实例);
|
||||
- 后续:**A2A**、**Codex app-server** 与 **Claude Code Agent SDK**——每种都与 ACP 后端相同的进程外形状:「启动子 agent、发送提示词、流式接收更新、取消」。
|
||||
- **Codex app-server 与 Claude Code Agent SDK**:当前的一次性兄弟提供方,将同一个命名提供方 seam 应用于官方产品进程([产品提供方 Agent Note](2026-08-04-claude-code-and-codex-subagent-backends.md));
|
||||
- 后续:**A2A**,采用同样的进程外形态:「启动子 agent、发送提示词、结算、取消」。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
@@ -34,6 +35,8 @@ bash seam([能力 seam](../architecture/2026-06-13-capability-seams.md))在
|
||||
| `@deepseek-ai/dsh-subagent-spawn` | 实现:通过 `ctx.agents.create` 创建全新的进程内子 agent |
|
||||
| `@deepseek-ai/dsh-subagent-fork` | 实现:用父 agent 日志快照初始化的进程内子 agent |
|
||||
| `@deepseek-ai/dsh-subagent-acp` | 实现:作为 ACP 客户端驱动已配置的子进程 |
|
||||
| `@deepseek-ai/dsh-subagent-codex` | 实现:一次性官方 Codex app-server 进程 |
|
||||
| `@deepseek-ai/dsh-subagent-claude-code` | 实现:通过 Agent SDK 运行的一次性官方 Claude Code 进程 |
|
||||
| `@deepseek-ai/dsh-tool-subagent` | 消费方:基于 `ctx.subagents` 的面向模型的 `subagent` 工具 |
|
||||
|
||||
### 原语:异步 `start → SubagentRun`
|
||||
@@ -51,7 +54,7 @@ bash seam([能力 seam](../architecture/2026-06-13-capability-seams.md))在
|
||||
|
||||
### 子 agent 隔离与父日志
|
||||
|
||||
每个 subagent 运行在**自己的 `Session`** 中(独立 id、`parentSession` 谱系),独立持久化。父日志仅记录 spawn `tool/call` 及其 `tool/result`(子 agent 的最终输出)——子 agent 的内部步骤和工具调用留在子 agent 自己的会话中,绝不注入父日志。这是唯一在所有传输方式下行为一致的设计:ACP 子 agent 的内部事件在物理上无法注入我们的父日志,因此让进程内行为保持一致,使 seam 真正与传输方式无关。
|
||||
每个进程内 subagent 运行在**自己的 `Session`** 中(独立 id、`parentSession` 谱系),独立持久化。远端 ACP 和一次性产品提供方则会生成一个父级作用域的生命周期 id,且不暴露本地 `Agent` 或子 `Session`;其内部状态留在远端进程中。两种形式下,父日志都仅记录 spawn `tool/call` 及其 `tool/result`(子 agent 的最终输出),而子 agent 的步骤和工具调用均留在父日志之外。
|
||||
|
||||
### 同步收集(首版)
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-22-acp-subagent-backend.md
|
||||
2026-06-22-acp-subagent-backend.md: 5f12aa1c08d4f4cfaa35f2f7f4b09ad341c3eae8
|
||||
2026-06-22-acp-subagent-backend.zh.md: ba9d4255723367ab4afc5ab87e056f12f5c3a285
|
||||
2026-06-22-acp-subagent-backend.md: c994ebfa69649bb9e79d3aa389a0e13c178131c7
|
||||
2026-06-22-acp-subagent-backend.zh.md: 62d631e95b5d1f299bed1fd00353b5ac9349b9a0
|
||||
@@ -57,6 +57,6 @@ Persistent-process pooling (reuse a warm child across runs) is a performance opt
|
||||
|
||||
Every run pays a fresh subprocess (spawn + `initialize` + `newSession`). The parent surfaces only the child's final answer: `session/update` thoughts and tool-call cards are consumed and dropped, and permission prompts never reach a human — the configured policy answers them. The child's environment is credential-scrubbed by default, so its own model key is supplied explicitly via `config.env`.
|
||||
|
||||
## Future providers
|
||||
## Product-provider siblings
|
||||
|
||||
The same out-of-process spawn/prompt/stream/cancel shape generalizes to other transports named in the seam Agent Note — A2A, the Codex app-server, and the Claude Code Agent SDK — each a sibling provider registered by name. The ACP backend is the proof that the seam supports the boundary; those are mechanically similar.
|
||||
The [Codex app-server and Claude Code Agent SDK providers](2026-08-04-claude-code-and-codex-subagent-backends.md) apply the same out-of-process spawn/prompt/settle/cancel boundary as siblings registered by name. A2A remains a future sibling transport; the ACP backend proves that the common seam supports this boundary without owning product-private protocols.
|
||||
@@ -57,6 +57,6 @@ ACP `StopReason` → harness `SubagentStopReason`:`end_turn`→`completed`、`
|
||||
|
||||
每次运行都要付出一个全新子进程的代价(spawn + `initialize` + `newSession`)。父进程仅暴露子 agent 的最终回答:`session/update` 中的思考和工具调用卡片被消费后丢弃,权限提示从不到达人类——由配置的策略应答。子进程环境默认经过凭证清洗,因此其自身的模型密钥需通过 `config.env` 显式提供。
|
||||
|
||||
## 后续提供方
|
||||
## 兄弟产品提供方
|
||||
|
||||
同样的进程外启动/提示词/流式输出/取消形态可泛化到 seam Agent Note 中列出的其他传输方式——A2A、Codex app-server 和 Claude Code Agent SDK——每个都是按名称注册的兄弟提供方。ACP 后端证明了 seam 支持跨进程边界;其余在机制上类似。
|
||||
[Codex app-server 与 Claude Code Agent SDK 提供方](2026-08-04-claude-code-and-codex-subagent-backends.md)作为按名称注册的兄弟提供方,采用同样的进程外启动/提示词/结算/取消边界。A2A 仍是未来的兄弟传输方式;ACP 后端证明了通用 seam 能够支持这项边界,而无需负责产品私有协议。
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-24-workspace-context.md
|
||||
2026-06-24-workspace-context.md: df7ae58f8a42a8c1aac8113a429ef0f50b2fb795
|
||||
2026-06-24-workspace-context.zh.md: aecc57a5c63b4c6282260863f4183944c14b5cf8
|
||||
2026-06-24-workspace-context.md: 3c21b7599e6bb2dd759e4a040c1b999f1b0516cd
|
||||
2026-06-24-workspace-context.zh.md: e913ecb22b4692e1ee7b96d4f5effe2606775cf8
|
||||
@@ -30,9 +30,9 @@ The user-global file is fixed at `$DSH_HOME/AGENTS.md`, is not affected by eithe
|
||||
|
||||
At the first `agent/pre-step` of an agent-loop instance, the plugin composes one sourced user-role baseline. When the downstream decision enters a nonempty first-step batch, the plugin folds the baseline into that final batch right after the claimed prompt, so it becomes durable with the direct prompt and reaches the first request. Rejection or an empty first-step decision leaves the baseline in the next-step inbox for a later wakeup. The plugin loads the user-global file first, then finds the project root by walking upward from `agent.session.header.cwd` to a configured root marker (default `.git`), then loads the configured candidates from each directory from the root to the cwd. A `.git` file and a `.git` directory are both valid markers, covering linked worktrees and submodules. Without a marker, the cwd itself is the root.
|
||||
|
||||
The baseline becomes a durable `user/message` with a typed `workspace-instructions` source. Its `baseline: true` marker distinguishes the complete startup or resume baseline from later deltas, and its change list persists the included scopes and content digests. If a previously queued workspace baseline is still pending, the plugin removes that exact message and prepends its replacement instead of accumulating duplicates.
|
||||
The baseline becomes a durable `user/message` with a typed `workspace-instructions` source. Its `baseline: true` marker distinguishes a complete baseline from later deltas, its `baselineIdentity` records normalized discovery, precedence, project-root, and budget semantics, and its change list persists the included scopes and content digests. If a previously queued workspace baseline is still pending, the plugin removes that exact message and prepends its replacement instead of accumulating duplicates.
|
||||
|
||||
A resumed agent creates a new loop instance and injects a baseline composed from current files before its first request. This permits current baseline content on resume without mutating an earlier history event. A resume and a hot plugin remount both face a log that may already hold a baseline; they are told apart by `agent/session-start`, which a startup or resume emits before the first step while a remount attaches to an already-live session and never sees it. A remount retains the existing baseline only when its typed event remains in the current visible surface, and still rebuilds scope and provider-version tracking from current files. If compaction has shadowed that event, the remount injects a current baseline. A resume always re-composes.
|
||||
A resumed agent creates a new loop instance over persisted history. At its first `agent/pre-step`, a visible baseline with the current identity remains authoritative while the plugin compares its retained scopes with a complete current rendering. Unchanged and budget-omitted files append nothing; offline additions, edits, removals, and files that leave the retained budget set append `set`, `replace`, or `remove` transitions in the entering batch without mutating or duplicating the original baseline. An incompatible visible baseline is superseded by one complete baseline in current precedence order with explicit replacement language; when no current candidate exists, an explicit empty baseline clears the earlier scopes. A hot plugin remount follows the same rule. If compaction has shadowed the typed baseline, the next entering pre-step composes one complete current baseline and carries it in the same request.
|
||||
|
||||
The baseline is a user-role `<system-reminder>` with `Instructions from: <path>` sections and explicit authority and precedence language. This familiar model-facing frame avoids a harness-specific XML vocabulary. Project paths are root-relative and the user-global path is `~/.dsh/AGENTS.md` for the default home or `$DSH_HOME/AGENTS.md` for a configured home. The final rendering boundary escapes a literal `</system-reminder>` anywhere in instruction content or model-visible path, scope, and budget metadata before byte accounting completes. The package README owns the exact current [prompt shape](../../../../packages/context/workspace-context/README.md#prompt-shape).
|
||||
|
||||
@@ -48,15 +48,15 @@ Shell commands are not discovery triggers. Local bash calls start fresh shells,
|
||||
|
||||
### Duplicate Suppression And Change Detection
|
||||
|
||||
Every workspace context event stores versioned metadata with `{ action, scope, path, digest? }`, where `digest` is SHA-1 over the loaded content. Baselines additionally carry `baseline: true`. The model-facing prompt has no HTML comments, hidden markers, or headings that are parsed back into state.
|
||||
Every workspace context event stores versioned metadata with `{ action, scope, path, digest? }`, where `digest` is SHA-1 over the loaded content. Complete baselines additionally carry `baseline: true` and `baselineIdentity`. The model-facing prompt has no HTML comments, hidden markers, or headings that are parsed back into state.
|
||||
|
||||
At reconciliation time the plugin scans workspace-sourced `user/message` events and derives the latest state for each visible scope. A short per-session pending map begins only after the immutable top-level `tools/result` proves an `additionalContexts` entry survived every post-execute listener, then covers the interval before the loop appends that context to the log. Each entry records the open `{ turn, step }`: an equal durable `user/message` at or after its sequence boundary confirms and removes it, while a matching `step/end` arriving first means the loop discarded its context buffer, so the plugin removes both the pending entry and its version-cache fast path. A nested Code Mode result stages its changes under the parent's opaque execution token so repeated sub-dispatches in one run do not duplicate them; the parent result rolls that provisional state back and commits only contexts retained by outer policy.
|
||||
|
||||
An unchanged path and digest is suppressed. A logged removal is a tombstone, so a reappearing candidate becomes a new `set`. Resume works from persisted metadata. If compaction removes an instruction event from the visible surface, that state no longer suppresses a later load, matching the fact that the model can no longer see it. Only changes actually included under the byte budget enter metadata or pending state, so an omitted file remains eligible on a later touch.
|
||||
An unchanged path and digest is suppressed. A logged removal is a tombstone, so a reappearing candidate becomes a new `set`. Resume works from persisted metadata: a compatible visible baseline supplies comparison state rather than causing another complete baseline to be appended. If compaction removes an instruction event from the visible surface, that state no longer suppresses a later load, matching the fact that the model can no longer see it. Only changes actually included under the byte budget enter metadata or pending state, so an omitted file remains eligible on a later touch.
|
||||
|
||||
The initial baseline's typed changes are comparison state only while its event remains in the visible session surface. A later successful filesystem touch re-adds an unchanged baseline scope after compaction, or appends baseline edits or removals as dynamic messages; it never rewrites the original event. The in-memory scope marker and provider-version cache only select and accelerate probes, so neither can suppress context the model no longer sees. During resumed baseline preparation the plugin also reconciles visible dynamic scopes, so nested changes made while the agent was offline can append an update before the first resumed request.
|
||||
The initial baseline's typed changes are comparison state only while its event remains in the visible session surface. Resume retains a compatible baseline and reconciles current baseline and visible dynamic scopes, so changes made while the agent was offline append transitions before the first resumed request. When compaction shadows the baseline event, the next entering pre-step composes the complete current baseline and records it in the same request; a successful filesystem touch can instead re-add an unchanged baseline scope or append baseline edits or removals as dynamic messages. Neither path rewrites the original event. The in-memory scope marker and provider-version cache only select and accelerate probes, so neither can suppress context the model no longer sees.
|
||||
|
||||
There is intentionally no watcher. Detection occurs at the next successful structured filesystem touch or resumed baseline preparation. A provider failure produces no removal; absence is only accepted when all configured candidates in that scope were probed successfully.
|
||||
There is intentionally no watcher. Detection occurs at the next successful structured filesystem touch, resumed-baseline reconciliation, or entering pre-step that restores a shadowed baseline. A provider failure produces no removal; absence is only accepted when all configured candidates in that scope were probed successfully.
|
||||
|
||||
### Byte Budget And Bounded Reads
|
||||
|
||||
@@ -82,7 +82,7 @@ Workspace guidance is isolated per session and shared by the demo front doors, W
|
||||
|
||||
Repository text remains untrusted input. Lower-authority user-role framing, explicit precedence language, and delimiter escaping reduce risk but do not eliminate prompt injection. Following a candidate symlink to its target widens that surface to off-tree content, so the permission and sandbox layers that confine `ctx.fs` to trusted roots are the boundary that treats workspace files as data rather than authority (the [instruction-symlink follow note](2026-07-21-follow-instruction-symlinks.md) owns the residual risk).
|
||||
|
||||
The system is event-driven rather than watch-driven. Edits are not visible at the exact filesystem mutation instant unless that mutation goes through a structured tool; externally changed files are noticed on the next successful structured touch or resume. This keeps the design deterministic and provider-neutral.
|
||||
The system is event-driven rather than watch-driven. Edits are not visible at the exact filesystem mutation instant unless that mutation goes through a structured tool; externally changed files are noticed on the next successful structured touch, resume reconciliation, or restoration of a shadowed baseline. This keeps the design deterministic and provider-neutral.
|
||||
|
||||
## Deferred
|
||||
|
||||
|
||||
@@ -30,9 +30,9 @@ Status: implemented
|
||||
|
||||
在 agent loop(智能体循环)实例的第一个 `agent/pre-step`,插件会组合一条带来源的 user 角色基线。当下游决策让非空的第一步批次进入时,插件会将基线折入最终批次、紧随已领取的直接提示词之后,使其与直接提示词一同成为持久记录并抵达第一次请求。reject 或空的第一步决策会将基线留在 next-step inbox,等待后续唤醒。插件先加载用户全局文件,再从 `agent.session.header.cwd` 向上遍历至配置的根标记(默认为 `.git`)以确定项目根目录,随后从根目录至 cwd 的每级目录加载已配置候选项。`.git` 文件与 `.git` 目录都是有效标记,因而能覆盖链接 worktree 和 submodule。找不到标记时,cwd 本身就是根目录。
|
||||
|
||||
该基线会成为一条持久 `user/message`,并携带带类型的 `workspace-instructions` 来源。其 `baseline: true` 标记将完整的启动或恢复基线与后续增量区分开来,变更列表则持久保存已纳入的作用域和内容 digest。若先前排队的 workspace 基线仍在等待,插件会删除该确切消息并 prepend 替代值,而不会累积副本。
|
||||
该基线会成为一条持久 `user/message`,并携带带类型的 `workspace-instructions` 来源。其 `baseline: true` 标记将完整基线与后续增量区分开来,`baselineIdentity` 记录规范化的发现、优先级、项目根目录和预算语义,变更列表则持久保存已纳入的作用域和内容 digest。若先前排队的 workspace 基线仍在等待,插件会删除该确切消息并 prepend 替代值,而不会累积副本。
|
||||
|
||||
恢复 agent 会创建新的循环实例,并在其第一次请求前注入由当前文件组合的基线。这样,恢复时可以使用当前基线内容,而无需修改先前的历史事件。恢复与插件热重挂都会面对日志中可能已存在基线的情况;二者通过 `agent/session-start` 区分:启动或恢复会在第一步前发出该事件,而热重挂附着到一个已存活的会话、永远不会看到它。只有当基线的类型化事件仍在当前可见表层中时,热重挂才保留既有基线,同时仍会根据当前文件重建 scope 与提供方版本跟踪。如果压缩(compaction)已遮蔽该事件,热重挂会注入当前基线。恢复则始终重新组合。
|
||||
恢复 agent 会基于持久化历史创建新的 loop 实例。在第一个 `agent/pre-step`,具有当前标识的可见基线仍是权威状态;插件会将其保留的 scope 与当前完整渲染进行比较。未变化和被预算省略的文件不追加任何内容;agent 离线期间新增、编辑、移除或不再属于预算保留集的文件,会在进入步骤的批次中追加 `set`、`replace` 或 `remove` 转换,既不改写也不重复原始基线。不兼容的可见基线会被一条按当前优先级排列的完整基线取代,并以明确措辞说明替换关系;如果当前不存在任何候选文件,一条显式空基线会清除先前的 scope。插件热重挂遵循相同规则。如果压缩(compaction)已遮蔽带类型的基线,下一次进入步骤的 pre-step 会组合一条完整的当前基线,并在同一请求中携带它。
|
||||
|
||||
基线是一条 user 角色的 `<system-reminder>`,包含 `Instructions from: <path>` 章节,以及明确的权威性与优先级说明。这种熟悉的模型可见框架避免引入 harness 专用的 XML 词汇。项目路径相对于根目录;使用默认 home 时,用户全局路径为 `~/.dsh/AGENTS.md`,使用已配置 home 时则为 `$DSH_HOME/AGENTS.md`。最终渲染边界会在完成字节核算前,转义指令内容或模型可见的路径、scope 与预算元数据中出现的字面量 `</system-reminder>`。包 README 负责规定当前准确的[提示词形态](../../../../packages/context/workspace-context/README.md#prompt-shape)。
|
||||
|
||||
@@ -48,15 +48,15 @@ shell 命令不会触发发现。本地 bash 调用会启动全新的 shell,
|
||||
|
||||
### 重复抑制与变更检测
|
||||
|
||||
每个工作区上下文事件都会存储带版本的元数据,其形态为 `{ action, scope, path, digest? }`;`digest` 是对已加载内容计算的 SHA-1。基线还会额外携带 `baseline: true`。模型可见提示词中没有 HTML 注释、隐藏标记,也没有会被解析回状态的标题。
|
||||
每个工作区上下文事件都会存储带版本的元数据,其形态为 `{ action, scope, path, digest? }`;`digest` 是对已加载内容计算的 SHA-1。完整基线还会额外携带 `baseline: true` 和 `baselineIdentity`。模型可见提示词中没有 HTML 注释、隐藏标记,也没有会被解析回状态的标题。
|
||||
|
||||
协调时,插件扫描带工作区来源的 `user/message` 事件,并派生每个可见作用域的最新状态。一个简短的逐会话待处理映射只会在不可变的顶层 `tools/result` 证明某个 `additionalContexts` 条目经过所有 post-execute 监听器后仍然保留时开始记录;随后,它覆盖循环将该上下文追加到日志之前的间隔。每个条目记录开启状态的 `{ turn, step }`:如果相同的持久 `user/message` 出现在其序列边界或之后,该条目得到确认并被移除;如果匹配的 `step/end` 先到达,则说明循环丢弃了上下文缓冲区,插件会同时移除待处理条目及其版本缓存快速路径。嵌套的 Code Mode 结果会把变更暂存在父级的不透明执行 token 下,确保一次运行中的重复子分发不会产生重复项;父级结果会回滚这份临时状态,并且只提交外层策略保留的上下文。
|
||||
|
||||
路径和 digest 均未变化时会被抑制。日志中的移除操作是一条墓碑记录,因此重新出现的候选项会成为新的 `set`。恢复操作从持久化元数据继续工作。如果压缩从可见表面移除某条指令事件,该状态不再抑制后续加载,这与模型已经无法看见它的事实一致。只有真正纳入字节预算的变更才会进入元数据或待处理状态,因此被省略的文件在之后的触碰中仍有资格加载。
|
||||
路径和 digest 均未变化时会被抑制。日志中的移除操作是一条墓碑记录,因此重新出现的候选项会成为新的 `set`。恢复操作从持久化元数据继续工作:兼容的可见基线会提供比较状态,而不会导致再次追加完整基线。如果压缩从可见表面移除某条指令事件,该状态不再抑制后续加载,这与模型已经无法看见它的事实一致。只有真正纳入字节预算的变更才会进入元数据或待处理状态,因此被省略的文件在之后的触碰中仍有资格加载。
|
||||
|
||||
只有当初始基线事件仍在可见会话表层中时,其类型化变更才用作比较状态。后续成功的文件系统触碰会在压缩后重新添加未变化的基线 scope,或把基线编辑或移除操作追加为动态消息;它绝不重写原始事件。内存中的 scope 标记和提供方版本 cache 只用于选择探测对象并加速探测,因此二者都不能抑制模型已无法看见的上下文。恢复时准备基线的过程中,插件还会协调可见的动态作用域,因此 agent 离线期间发生的嵌套变更可以在第一次恢复请求前追加更新。
|
||||
只有当初始基线事件仍在可见会话表层中时,其类型化变更才用作比较状态。恢复会保留兼容的基线,并对账当前基线和可见的动态 scope,因此 agent 离线期间的变更会在第一次恢复请求前追加为转换。当压缩遮蔽基线事件时,下一次进入步骤的 pre-step 会组合完整的当前基线,并在同一请求中记录它;也可以改由一次成功的文件系统触碰重新添加未变化的基线 scope,或把基线编辑或移除操作追加为动态消息。两条路径都不会重写原始事件。内存中的 scope 标记和提供方版本 cache 只用于选择探测对象并加速探测,因此二者都不能抑制模型已无法看见的上下文。
|
||||
|
||||
系统刻意不使用文件监视器。检测发生在下一次成功的结构化文件系统触碰或恢复时的基线准备。提供方失败不会产生移除;只有该作用域中的全部已配置候选项都成功完成探测后,系统才接受「不存在」这一结论。
|
||||
系统刻意不使用文件监视器。检测发生在下一次成功的结构化文件系统触碰、恢复时的基线对账,或进入步骤的 pre-step 恢复被遮蔽的基线时。提供方失败不会产生移除;只有该作用域中的全部已配置候选项都成功完成探测后,系统才接受「不存在」这一结论。
|
||||
|
||||
### 字节预算与有界读取
|
||||
|
||||
@@ -82,7 +82,7 @@ shell 命令不会触发发现。本地 bash 调用会启动全新的 shell,
|
||||
|
||||
仓库文本仍是不受信任的输入。低权威 user 角色框架、显式优先级说明和分隔符转义可以降低风险,但无法消除提示词注入。跟随候选符号链接到目标,会把该攻击面扩大至树外内容;因此,把 `ctx.fs` 限制在可信根目录内的权限与沙箱层才是真正的边界,它们让系统把工作区文件当作数据而不是权威([跟随指令符号链接记录](2026-07-21-follow-instruction-symlinks.md)负责说明残余风险)。
|
||||
|
||||
系统由事件驱动,而不是文件监视器驱动。除非文件系统变更通过结构化工具完成,否则编辑不会在确切的文件系统变更时刻可见;外部文件变更会在下一次成功的结构化触碰或恢复时被发现。这使设计保持确定性并且与提供方无关。
|
||||
系统由事件驱动,而不是文件监视器驱动。除非文件系统变更通过结构化工具完成,否则编辑不会在确切的文件系统变更时刻可见;外部文件变更会在下一次成功的结构化触碰、恢复对账,或恢复被遮蔽的基线时被发现。这使设计保持确定性并且与提供方无关。
|
||||
|
||||
## 延后事项
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md
|
||||
2026-07-29-persistent-bash-str-replace-editor.md: 22851078c1cc8fa9d5716afa41c8a2e2b7e7725c
|
||||
2026-07-29-persistent-bash-str-replace-editor.zh.md: 23f80d1a5911f4f3820d526c2221d3002507d774
|
||||
2026-07-29-persistent-bash-str-replace-editor.md: c4750e30370bfd253064c39cb1adc0f5b2baa60d
|
||||
2026-07-29-persistent-bash-str-replace-editor.zh.md: 62073571ce2164d88897d4959caafd9cb1dbdd93
|
||||
@@ -18,7 +18,7 @@ Some deployments need a one-call Bash schema whose shell state survives across m
|
||||
|
||||
Both plugins are included in the Python runtime closure. The persistent Bash closure also includes the PTY service/local backend and the sandbox services required by that backend. Because `node-pty` executes a native `spawn-helper` on macOS, each packaged macOS runtime executable ships with a `-spawn-helper` sibling; Linux uses `forkpty` directly. A pinned `node-pty` patch checks `DSH_NODE_PTY_SPAWN_HELPER` first, so it remains a true override for a current external consumer that supplies a non-sibling helper. When the override is unset, the patch resolves the packaged executable sibling if present and otherwise preserves upstream lookup in ordinary Node runs. The macOS builders fail before publication when the helper is absent or not executable.
|
||||
|
||||
The shipped [`core-web.cordis.yml`](../../../../apps/cli/config/core-web.cordis.yml) overlay composes both plugins over the ordinary Web surface, disables its other model-facing consumers, and leaves the Web host, browser, Workspace, persistence, sandbox, and permission stack in place. The local PTY backend resolves the effective session sandbox mode when it creates the shell. While that owner has an open shell or a spawn in progress, a different permission mode is rejected before its session event commits; the editor continues through the Web filesystem sandbox.
|
||||
The shipped [`core-web.cordis.yml`](../../../../apps/cli/config/core-web.cordis.yml) overlay composes both plugins over the ordinary Web surface for the Claude SWE-compatible RL contract. It pins native tool mode and makes the complete system prompt `DSH_SYSTEM_PROMPT` when set or `You are a helpful software engineer assistant.` otherwise, with no harness identity, source-checkout section, Web orientation, Workspace instructions, or tool-mode guidance. It disables every other model-facing consumer, so the model receives exactly the persistent `bash` and `str_replace_editor` schemas, while the Web host, browser, Workspace, persistence, sandbox, and permission stack remains in place. The local PTY backend resolves the effective session sandbox mode when it creates the shell. While that owner has an open shell or a spawn in progress, a different permission mode is rejected before its session event commits; the editor continues through the Web filesystem sandbox.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ Status: implemented
|
||||
|
||||
两个插件都进入 Python runtime 闭包。持久 Bash 的闭包还包含 PTY 服务/本地后端,以及该后端要求的沙箱服务。由于 `node-pty` 在 macOS 上会执行原生 `spawn-helper`,每个打包后的 macOS 运行时可执行文件都会携带一个 `-spawn-helper` 伴随文件;Linux 直接使用 `forkpty`。固定版本的 `node-pty` 补丁会先检查 `DSH_NODE_PTY_SPAWN_HELPER`,因此对当前提供非伴随 helper 的外部消费方而言,该变量仍是真正的覆盖项。未设置该覆盖时,补丁会在打包可执行文件的伴随文件存在时解析它,否则在普通 Node 运行中保留上游查找方式。若 helper 缺失或不可执行,macOS 构建器会在发布前失败。
|
||||
|
||||
已交付的 [`core-web.cordis.yml`](../../../../apps/cli/config/core-web.cordis.yml) 覆盖层在常规 Web 界面之上组合这两个插件,禁用该界面的其他面向模型的消费方,并保留 Web 宿主、浏览器、Workspace、持久化、沙箱与权限栈。本地 PTY 后端会在创建 shell 时解析会话的有效沙箱模式。只要该所有者仍有打开的 shell 或仍在进行中的 spawn,另一种权限模式就会在对应的会话事件提交前遭到拒绝;编辑器则继续经由 Web 文件系统沙箱运行。
|
||||
已交付的 [`core-web.cordis.yml`](../../../../apps/cli/config/core-web.cordis.yml) overlay 会在常规 Web 界面之上组合这两个插件,以满足与 Claude SWE 兼容的 RL 契约。它固定使用原生工具模式;完整的系统提示词在设置 `DSH_SYSTEM_PROMPT` 时采用其值,否则采用 `You are a helpful software engineer assistant.`,且不包含 harness 身份、源码 checkout 提示词段、Web 界面定位、Workspace 指令或工具模式指引。它会禁用其他所有面向模型的消费方,使模型恰好只收到持久 `bash` 和 `str_replace_editor` 两个 schema,同时保留 Web 宿主、浏览器、Workspace、持久化、沙箱与权限栈。本地 PTY 后端会在创建 shell 时解析会话的有效沙箱模式。只要该所有者仍有打开的 shell 或仍在进行中的 spawn,另一种权限模式就会在对应的会话事件提交前遭到拒绝;编辑器则继续经由 Web 文件系统沙箱运行。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
|
||||
2026-08-04-claude-code-and-codex-subagent-backends.md: e81d1fb14f719331c503dba539d6a5ec0f1eed4f
|
||||
2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 3c78e8d5a4ca1a86942971721ad05c631901f231
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
# Agent Note: Claude Code and Codex subagent backends
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-04-claude-code-and-codex-subagent-backends.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The named [`ctx.subagents`](2026-06-21-subagent-capability-seam.md) registry lets a parent agent delegate work without knowing how the child runs, but the harness needs first-party routes to the real Codex and Claude Code products. Each route must hand the product one self-contained task, let it work in the parent Session's workspace, return a final answer or an explicit failure or cancellation, and leave no managed product process behind.
|
||||
|
||||
The product integrations must not become second owners for task text, cwd, cancellation, result settlement, or process trees. Required evidence therefore separates three facts: a keyless real-product test proves the official integration, native authentication shape, deterministic answer, and teardown; a Loader composition test proves that the public package and documented tool configuration load without starting the product; and a credentialed e2e proves that the production provider and real product can obtain a unique answer from the real DeepSeek service. Direct model HTTP or a product double cannot replace either product-running tier, and a hand-mounted plugin cannot replace the Loader tier.
|
||||
|
||||
## Decision
|
||||
|
||||
The harness publishes two sibling one-shot providers as independently installable, opt-in packages. A user loads a provider and the existing common subagent tool in their own `cordis.yml`: `subagent_codex` binds `codex`, while `subagent_claude_code` binds `claude-code`. The shipped CLI dependency closure and base, Web, and headless configurations load neither provider. Each tool accepts only a standalone text task; product selection and background execution are not model arguments.
|
||||
|
||||
Both providers report `inheritsParentContext: false`, advertise no optional start capabilities, and pass the parent Session cwd without copying the parent conversation. Their documented tools disable background execution and use `maxDepth: 'provider-managed'`, leaving recursion policy with the out-of-process product instead of sending a limit the provider cannot enforce. Every call creates a fresh product process and a non-resumable product conversation. The shared subagent service continues to own request resolution, lifecycle events, result settlement, and foreground collection; the shared subprocess service owns credential scrubbing, process-tree termination, and whole-tree exit observation.
|
||||
|
||||
```text
|
||||
fixed tool → shared subagent service → product provider → official product process
|
||||
← final answer / explicit error / cancellation ← terminal product fact
|
||||
→ foreground disposal → shared process-tree termination → whole-tree exit
|
||||
```
|
||||
|
||||
### Ownership and lifecycle
|
||||
|
||||
| Phase | Shared owner | Product-specific responsibility | Observable result |
|
||||
| --- | --- | --- | --- |
|
||||
| Resolve | `dsh-tool-subagent` and `ctx.subagents` | Validate the product's text-only input and derive native startup parameters | Unsupported context or malformed input fails before a run is published |
|
||||
| Start | `dsh-subprocess` owns every acquired process tree | Reach the smallest native point at which the product conversation and process can both be controlled | `start()` publishes one existing `SubagentRun`, or cleans up and rejects |
|
||||
| Run | The product owns its native protocol facts; the holder owns their mapping | Submit exactly one task and derive an existing shared stop reason; Codex uses `max-tokens` only for explicit context exhaustion | The parent receives only a final answer or an explicit failure |
|
||||
| Dispose | The foreground consumer requests release; `dsh-subprocess` proves exit | Close the native protocol and express any best-effort native cancellation | Disposal is idempotent and returns only after the whole process tree exits |
|
||||
|
||||
## Codex provider
|
||||
|
||||
`@deepseek-ai/dsh-subagent-codex` registers the fixed `codex` provider and starts `codex app-server --stdio` from `PATH`. Its public configuration contains only an explicit `env` overlay and a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`. Installation, login, `CODEX_HOME`, model selection, base URL, sandbox, approval policy, and product-session settings remain native Codex or deployment responsibilities.
|
||||
|
||||
Before publication, the provider validates a non-empty text-only task, starts the managed app-server in the parent workspace, completes `initialize` → `initialized`, and creates an `ephemeral: true` thread. The published run owns exactly one `turn/start`; its thread and turn ids remain private and are never persisted in the parent Session.
|
||||
|
||||
`turn/completed` is the authoritative remote terminal fact. The latest `agentMessage` with `phase: "final_answer"` wins, and that selected message must contain nonblank text. When the product emits no explicit final phase, the latest message with `phase: null` is the compatibility fallback and must likewise be nonblank; commentary never replaces either answer. A failed turn with `error.codexErrorInfo: "contextWindowExceeded"` becomes `max-tokens`. A completed turn without an answer, every other failed or interrupted remote turn, malformed required fields in a recognized app-server frame, protocol closure, early process exit, or unknown server request becomes `error`; this version has no native refusal terminal and therefore produces no `refusal`. Local cancellation wins its race and remains `aborted`.
|
||||
|
||||
For command and file approvals, the unattended wire selects a non-approval decision offered by the request, preferring `cancel`; the stable 0.146.0 request shape without an offered-decision list falls back to `decline`. It grants no requested permissions for the turn, answers user-input requests with no answers, and declines MCP elicitation. A request with no legal unattended response, or any unknown server request, fails the run instead of waiting for a user interface the provider does not supply.
|
||||
|
||||
An unpublished startup failure closes the wire, terminates the acquired process tree, waits for exit, and then rejects `start()`. Published disposal best-effort interrupts a known turn, closes the wire, ends stdin, invokes the shared termination escalation, and waits for whole-tree exit. Result failure and teardown failure stay independently observable.
|
||||
|
||||
Codex 0.146.0 speaks the Responses protocol, while DeepSeek's public OpenAI-compatible endpoint speaks Chat Completions. The credentialed Codex e2e therefore uses a loopback-only, test-private bridge for one no-tool nonce request: real Codex sends Responses to the bridge, the bridge forwards the received bearer credential and extracted task to the fixed official DeepSeek endpoint, and it wraps the real text in the minimal Responses SSE lifecycle. The bridge is neither a production proxy nor evidence that Codex connects to DeepSeek Chat Completions natively.
|
||||
|
||||
## Claude Code provider
|
||||
|
||||
`@deepseek-ai/dsh-subagent-claude-code` registers the fixed `claude-code` provider and invokes `@anthropic-ai/claude-agent-sdk@0.3.220`. The SDK's platform `optionalDependency` supplies the real Claude Code 2.1.220 CLI. The provider uses the official `query()` entrypoint and passes the SDK's `spawnClaudeCodeProcess` command, arguments, cwd, environment, and forwarded signal unchanged to `dsh-subprocess`; its private `SpawnedProcess` adapter exposes only the stream, event, kill, and exit facts the SDK requires.
|
||||
|
||||
The public configuration contains the same two deployment-owned values as the Codex sibling: an explicit `env` overlay and a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`. Each run creates its own `AbortController`, sets `persistSession: false`, and disables `AskUserQuestion`. The provider deliberately omits `settingSources`, so the SDK reads the host's normal user, project, and local Claude settings relative to the parent Session cwd. It neither copies nor filters those settings and does not create or modify login state. It supplies no `canUseTool`, elicitation, or dialog callback, so unattended interactions fail through the SDK rather than waiting for a user interface the provider does not own.
|
||||
|
||||
The provider publishes only after both the SDK `Query` and a live managed CLI handle exist. It consumes the complete SDK stream and completes only when a `result` message has `subtype: "success"`, `is_error: false`, and a nonblank `result`, and the iterator then ends normally. Every SDK error subtype, an error-marked success, a missing result, iterator failure, protocol failure, or process failure becomes `error`. SDK turn, budget, and structured-output limits are not token-window facts, and the SDK exposes no native refusal terminal, so this provider produces neither `max-tokens` nor `refusal`. Local cancellation wins and becomes `aborted`.
|
||||
|
||||
Startup rollback and published disposal close the SDK query, abort the per-run controller, invoke shared process-tree termination, and wait for whole-tree exit. `Query.close()` expresses graceful protocol intent but does not replace the subprocess owner's exit proof. Query-close failure, process failure, and teardown failure remain independently observable.
|
||||
|
||||
The credentialed Claude Code e2e uses the official DeepSeek Claude Code contract directly: the runtime-only DeepSeek key becomes `ANTHROPIC_AUTH_TOKEN`, the fixed official base gains `/anthropic`, and the main and subagent model variables select the documented DeepSeek models. It starts the production provider and real SDK/CLI, requires one random nonce as the complete answer, persists no credential in settings, and waits for every managed handle to exit.
|
||||
|
||||
## Distribution and evidence
|
||||
|
||||
Each product owns branch-complete package tests, a required keyless real-product spec, a Loader composition e2e, and a credentialed DeepSeek e2e. The keyless product tier uses the exact official distribution under test, a non-empty fake product key, an isolated temporary workspace and product home, and a loopback fixed-answer model. Missing product requests, wrong authentication, altered task text, a non-exact answer, a skipped real product, or a surviving managed handle fails the required test. The Loader tier boots the README-shaped user configuration, verifies both fixed foreground-only tools in one context, and starts neither product process. The credentialed tier starts the same production provider and real product with a runtime-only key, requires a unique nonce from the fixed official DeepSeek service, and proves quiescence again; it self-skips only when a local operator supplied no key, while trusted CI preflights the secret.
|
||||
|
||||
The Codex evidence pins `@openai/codex@0.146.0` and `codex-cli 0.146.0`. Its real-product spec observes the exact Bearer key, original task, byte-exact final answer, unattended command rejection with no file side effect, local cancellation, and whole-tree exit. Production still supplies `codex` on `PATH`.
|
||||
|
||||
The Codex credentialed e2e registers the production provider, starts the same real app-server, and requests one random nonce through the test-private bridge described above. It fixes the external endpoint and model, stores no credential or request payload, requires exactly one completed upstream response, compares the trimmed product answer byte-for-byte with the nonce, and waits for every managed handle to exit.
|
||||
|
||||
The Claude Code evidence pins Agent SDK 0.3.220 and its platform-distributed Claude Code 2.1.220 CLI. Its real-product spec observes the exact `x-api-key`, original task, byte-exact final answer, inherited temporary host-setting marker, process failure, local cancellation, and whole-tree exit. The Loader e2e resolves both product packages by name while neither product command is available and records zero child starts.
|
||||
|
||||
The Claude Code credentialed e2e maps the key and fixed official endpoint only in the provider's in-memory environment, uses the documented `deepseek-v4-pro[1m]` and `deepseek-v4-flash` model variables, and traverses the production provider, official SDK, and real CLI. It compares the trimmed result with a random nonce and proves whole-tree exit without calling the Messages API directly from the test.
|
||||
|
||||
The project owner's distribution authorization is scoped to the official `@anthropic-ai/claude-agent-sdk` identity and the official Claude Code CLI/platform payloads each SDK version declares through `optionalDependencies`. [`THIRD_PARTY_NOTICES.md`](../../../../THIRD_PARTY_NOTICES.md) derives and discloses the current payload set without reclassifying its declared terms as permissive. Version, license-field, and payload-set changes still undergo ordinary dependency, lockfile, compatibility, terms, and notices review; unrelated non-permissive runtime packages continue to fail closed.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Direct model HTTP, `codex exec`, or a hand-written Claude CLI protocol.** These paths bypass the products' official extensible integration surfaces and cannot prove native configuration, tools, approvals, result semantics, or teardown. Each provider uses its official product integration instead.
|
||||
|
||||
**A shared product-process helper package.** The existing subagent and subprocess seams already own every shared task, result, environment, and process-tree concern. A new helper would duplicate ownership without deleting either private product adapter, so each adapter calls the existing seams directly.
|
||||
|
||||
**A model-visible product selector.** Product availability and authentication are deployment facts. Two fixed tools keep each schema and provider binding explicit and avoid adding dynamic selection state to the common service.
|
||||
|
||||
**Product doubles as required evidence.** Doubles cover exhaustive private protocol branches but do not prove package exports, official distributions, authentication, or real process behavior. Required evidence drives each official product against a loopback model fixture.
|
||||
|
||||
**Plugin-managed login, product home, models, settings, or permissions.** Those choices would create another authority beside each product's native configuration and enlarge a one-shot provider into account management. The providers expose only an explicit environment overlay and teardown grace; unattended interaction fails closed.
|
||||
|
||||
**Continuation, progress, background collection, and shared parent context.** The delivered user result is one self-contained task and one final answer. Product sessions, resume, follow-up, intermediate messages, parent transcript transfer, structured output, and background collection need separate user contracts and are not prebuilt.
|
||||
|
||||
## Consequences
|
||||
|
||||
Users can install either or both product providers, bind stable foreground tools in their own Cordis configuration, and delegate one self-contained task through the existing subagent contract. Official product integrations preserve native settings and behavior while shared services retain the sole ownership of task settlement and process-tree quiescence.
|
||||
|
||||
Every delegation pays for a fresh product process and independent model context, and only final text reaches the parent. Product-native configuration makes behavior depend on the deployment's installed product, account state, and workspace settings. Credentialed e2e runs also spend external API quota and depend on the official DeepSeek endpoint; deterministic protocol, failure, cancellation, and approval coverage remains in the keyless tier. The providers do not resume sessions, stream progress, accept new human interaction, roll back tool or file side effects, or impose a wall-clock timeout.
|
||||
|
||||
Compatibility is pinned by package-level unit coverage, keyless real-product loopback tests, credentialed DeepSeek nonce tests, public Loader composition, built-package and NodeNext consumer checks, generated documentation and notices, and the repository CI matrix. A supported product or DeepSeek endpoint/model baseline change must refresh those facts; production performs no separate runtime version probe.
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
# Agent Note: Claude Code 与 Codex subagent 后端
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-04-claude-code-and-codex-subagent-backends.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
命名的 [`ctx.subagents`](2026-06-21-subagent-capability-seam.md) 注册表让父 agent(智能体)无需了解子级的运行方式即可委派工作,但 harness 需要通往真实 Codex 与 Claude Code 产品的第一方路径。每条路径都必须向产品交付一项自包含任务,让它在父会话的工作区中执行,返回最终回答或明确的失败或取消结果,并且不留下任何受管的产品进程。
|
||||
|
||||
产品集成不得成为任务文本、cwd、取消、结果结算或进程树的第二责任方。因此,所需证据要区分三个事实:无密钥真实产品测试证明官方集成、原生身份验证形态、确定性答案与资源清理;Loader 组合测试证明公开包(package)和文档所示的工具配置无需启动产品即可加载;带密钥 e2e 证明生产提供方与真实产品能够从真实 DeepSeek 服务取得唯一答案。直接发起模型 HTTP 请求或使用产品替身无法取代上述任一产品运行层级;手工挂载插件无法取代 Loader 层级。
|
||||
|
||||
## 决策
|
||||
|
||||
harness 将两个一次性兄弟提供方作为可独立安装、选择启用的包交付。用户在自己的 `cordis.yml` 中加载提供方与现有的通用 subagent 工具:`subagent_codex` 绑定 `codex`,`subagent_claude_code` 绑定 `claude-code`。随产品交付的 CLI(命令行界面)依赖闭包,以及基础、Web 与 headless 配置都不会加载任一提供方。每个工具只接受独立文本任务;产品选择与后台执行都不作为模型参数。
|
||||
|
||||
这两个提供方都报告 `inheritsParentContext: false`,不声明任何可选的启动能力,并传递父会话 cwd,但不会复制父级对话。文档所示的工具会禁用后台执行,并使用 `maxDepth: 'provider-managed'`,将递归策略留给进程外产品,而不是发送提供方无法强制执行的限制。每次调用都会创建一个全新的产品进程和一次不可续接的产品对话。共享 subagent 服务继续负责请求解析、生命周期事件、结果结算和前台收集;共享子进程服务负责凭证清洗、进程树终止以及整棵进程树的退出观测。
|
||||
|
||||
```text
|
||||
fixed tool → shared subagent service → product provider → official product process
|
||||
← final answer / explicit error / cancellation ← terminal product fact
|
||||
→ foreground disposal → shared process-tree termination → whole-tree exit
|
||||
```
|
||||
|
||||
### 归属与生命周期
|
||||
|
||||
| 阶段 | 共享责任方 | 产品特定职责 | 可观察结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| 解析 | `dsh-tool-subagent` 与 `ctx.subagents` | 验证产品的纯文本输入并推导原生启动参数 | 不受支持的上下文或格式错误的输入会在发布运行前报错 |
|
||||
| 启动 | `dsh-subprocess` 负责每棵已获取的进程树 | 到达能够同时控制产品对话与进程的最小原生控制点 | `start()` 发布一个已存在的 `SubagentRun`,否则清理后拒绝调用 |
|
||||
| 运行 | 产品负责其原生协议事实;持有方负责映射这些事实 | 只提交一项任务,并推导出一种现有的共享停止原因;Codex 仅在明确发生上下文耗尽时使用 `max-tokens` | 父级只会收到最终回答或明确失败 |
|
||||
| dispose(资源释放) | 前台消费方请求释放;`dsh-subprocess` 证明进程已退出 | 关闭原生协议,并发出尽力而为的原生取消请求 | 释放操作具有幂等性,且仅在整棵进程树退出后才返回 |
|
||||
|
||||
## Codex 提供方
|
||||
|
||||
`@deepseek-ai/dsh-subagent-codex` 注册固定的 `codex` 提供方,并启动 `codex app-server --stdio`,该命令从 `PATH` 解析。其公开配置仅包含显式的 `env` 覆盖项和须为正有限值的 `disposeGraceMs`,且后者不得大于仓库共享的 `MAX_TIMER_DELAY_MS`。安装、登录、`CODEX_HOME`、模型选择、基础 URL、沙箱、审批策略和产品会话设置仍由 Codex 原生机制或部署环境负责。
|
||||
|
||||
发布前,提供方会验证非空的纯文本任务,在父级工作区中启动受管的 app-server,完成 `initialize` → `initialized` 握手,并创建一个 `ephemeral: true` 线程。已发布的运行只拥有一次 `turn/start`;其线程 ID 与轮次 ID 保持私有,绝不会持久化到父会话。
|
||||
|
||||
`turn/completed` 是权威的远端终止事实。以最后一条带有 `phase: "final_answer"` 的 `agentMessage` 为准,且选中的消息必须包含非空白文本。若产品没有发出明确的最终阶段,则以最后一条 `phase: null` 的消息作为兼容性回退,该消息也必须包含非空白文本;过程说明绝不会取代上述任一答案。带有 `error.codexErrorInfo: "contextWindowExceeded"` 的失败轮次会成为 `max-tokens`。轮次完成却没有答案、其他任何远端失败或中断轮次、已识别的 app-server 帧中必需字段格式错误、协议关闭、进程提前退出或未知的服务器请求,都会产生 `error`;本版本没有原生的拒绝终止状态,因此不会产生 `refusal`。本地取消在竞态中胜出并保持为 `aborted`。
|
||||
|
||||
对于命令与文件审批,无人值守的协议连接会从请求给出的决策选项中选择一项不予批准的决策,并优先选择 `cancel`;稳定的 0.146.0 请求形态没有决策选项列表,因此回退到 `decline`。它不授予该轮次请求的任何权限,不向用户输入请求提供任何答案,并拒绝 MCP elicitation。若请求在无人值守模式下没有合法响应,或是未知服务器请求,此次运行就会失败,而不会等待本提供方没有提供的用户界面。
|
||||
|
||||
若启动在发布前失败,提供方会关闭协议连接、终止已获取的进程树并等待其退出,然后拒绝 `start()`。对已发布的运行执行资源释放时,提供方会尽力中断已知轮次、关闭协议连接、结束标准输入、调用共享的逐级终止机制,并等待整棵进程树退出。结果失败与清理失败仍可彼此独立地观察。
|
||||
|
||||
Codex 0.146.0 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端点使用 Chat Completions。因此,带密钥 Codex e2e 会采用一个仅限回环、仅供测试内部使用的桥接层来处理一次不使用工具的随机数请求:真实 Codex 将 Responses 发送到桥接层,桥接层把收到的 Bearer 凭据与提取出的任务转发到固定的 DeepSeek 官方端点,再将真实文本包装进最小化的 Responses SSE(Server-Sent Events)生命周期。该桥接层既不是生产代理,也不能作为 Codex 原生连接 DeepSeek Chat Completions 的证据。
|
||||
|
||||
## Claude Code 提供方
|
||||
|
||||
`@deepseek-ai/dsh-subagent-claude-code` 注册固定的 `claude-code` 提供方,并调用 `@anthropic-ai/claude-agent-sdk@0.3.220`。SDK 的平台 `optionalDependency` 提供真实的 Claude Code 2.1.220 CLI。提供方使用官方 `query()` 入口点,并将 SDK 的 `spawnClaudeCodeProcess` 命令、参数、cwd、环境和转发的信号原样传入 `dsh-subprocess`;其私有 `SpawnedProcess` 适配器只公开 SDK 所需的流、事件、终止和退出事实。
|
||||
|
||||
公开配置包含与 Codex 兄弟提供方相同、由部署方负责的两个值:显式的 `env` 覆盖项,以及须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS` 的 `disposeGraceMs`。每次运行都会创建自己的 `AbortController`,设置 `persistSession: false` 并禁用 `AskUserQuestion`。提供方故意省略 `settingSources`,因此 SDK 会相对于父会话 cwd 读取宿主机常规的用户、项目和本地 Claude 设置。它既不复制也不过滤这些设置,也不会创建或修改登录状态。提供方不设置 `canUseTool`、elicitation 或对话回调,因此无人值守交互会经 SDK 失败,而不会等待本提供方不负责的用户界面。
|
||||
|
||||
只有在 SDK `Query` 与受管的活动 CLI 句柄都已存在后,提供方才会发布运行。它会消费完整的 SDK 流;只有 `result` 消息具有 `subtype: "success"`、`is_error: false` 和非空白 `result`,且迭代器随后正常结束时,运行才会完成。所有 SDK 错误子类型、标记为错误的成功消息、结果缺失、迭代器失败、协议失败或进程失败都会成为 `error`。SDK 的轮次、预算和结构化输出限制不表示 token 窗口耗尽,而且 SDK 没有原生的拒绝终止状态,因此本提供方不会产生 `max-tokens` 或 `refusal`。本地取消会胜出并成为 `aborted`。
|
||||
|
||||
启动回滚和已发布运行的资源释放都会关闭 SDK query、中止该次运行的控制器、调用共享的进程树终止机制,并等待整棵进程树退出。`Query.close()` 表达优雅的协议关闭意图,但不能取代子进程责任方的退出证明。Query 关闭失败、进程失败和清理失败仍可彼此独立地观察。
|
||||
|
||||
带密钥 Claude Code e2e 直接使用官方 DeepSeek Claude Code 契约:仅在运行时提供的 DeepSeek 密钥会映射为 `ANTHROPIC_AUTH_TOKEN`,固定的官方基础 URL 会追加 `/anthropic`,主模型与 subagent 模型变量会选择文档所示的 DeepSeek 模型。该测试会启动生产提供方与真实 SDK 和 CLI,要求一个随机数作为完整答案,不会把任何凭据持久化到设置中,并等待所有受管句柄退出。
|
||||
|
||||
## 分发与证据
|
||||
|
||||
每个产品都负责覆盖所有分支的包测试、一项必跑的无密钥真实产品测试、一项 Loader 组合 e2e 和一项带密钥 DeepSeek e2e。无密钥产品层级使用被测的确切官方发行版、非空的伪产品密钥、隔离的临时工作区与产品主目录,以及能返回固定答案的回环模型。产品请求缺失、身份验证错误、任务文本被改动、答案不完全一致、真实产品被跳过或受管句柄仍存活,都会使这项必跑测试失败。Loader 层级会启动 README 所示形态的用户配置,在同一个上下文中验证两个固定且只支持前台执行的工具,并且不会启动任何产品进程。带密钥层级会使用仅在运行时提供的密钥启动同一生产提供方与真实产品,要求从固定的 DeepSeek 官方服务取得唯一随机数,并再次证明完全停稳;仅当本地操作者未提供密钥时才会自行跳过,而受信任的 CI 会预检该 secret。
|
||||
|
||||
Codex 证据锁定 `@openai/codex@0.146.0` 与 `codex-cli 0.146.0`。其真实产品测试会观测确切的 Bearer 密钥、原始任务、逐字节完全一致的最终回答、不会产生文件副作用的无人值守命令拒绝、本地取消以及整棵进程树退出。生产环境仍提供 `codex`,并通过 `PATH` 解析。
|
||||
|
||||
带密钥 Codex e2e 会注册生产提供方,启动同样的真实 app-server,并通过上述测试专用桥接层请求一个随机数。该测试固定外部端点与模型,不存储任何凭据或请求载荷,要求上游恰好完成一次响应,将去除首尾空白后的产品答案与该随机数逐字节比较,并等待所有受管句柄退出。
|
||||
|
||||
Claude Code 证据锁定 Agent SDK 0.3.220 及其平台分发的 Claude Code 2.1.220 CLI。其真实产品测试会观测确切的 `x-api-key`、原始任务、逐字节完全一致的最终回答、继承的临时宿主设置标记、进程失败、本地取消以及整棵进程树退出。Loader e2e 会在两个产品命令均不可用时按名称解析两个产品包,并记录零次子级启动。
|
||||
|
||||
带密钥 Claude Code e2e 仅在提供方的内存环境中映射密钥与固定的官方端点,把模型变量设为文档所示的 `deepseek-v4-pro[1m]` 与 `deepseek-v4-flash`,并实际经过生产提供方、官方 SDK 与真实 CLI。它将去除首尾空白后的结果与一个随机数比较,并证明整棵进程树退出,且测试不会直接调用 Messages API。
|
||||
|
||||
项目所有者的分发授权范围限定为官方 `@anthropic-ai/claude-agent-sdk` 身份,以及每个 SDK 版本通过 `optionalDependencies` 声明的官方 Claude Code CLI 与平台载荷。[`THIRD_PARTY_NOTICES.md`](../../../../THIRD_PARTY_NOTICES.md) 会推导并披露当前载荷集合,但不会将其声明条款重新归类为宽松条款。版本、许可证字段和载荷集合发生变化时,仍须经过常规的依赖、锁文件、兼容性、条款和声明评审;无关的非宽松运行时包继续以默认拒绝方式失败。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**直接模型 HTTP、`codex exec` 或手写的 Claude CLI 协议。** 这些路径会绕过产品的官方可扩展集成接口,无法证明原生配置、工具、审批、结果语义或资源清理。每个提供方都改用相应的官方产品集成。
|
||||
|
||||
**共享产品进程辅助包。** 现有 subagent 与子进程 seam 已负责围绕任务、结果、环境和进程树的全部共享职责。新辅助包无法删除任一私有产品适配器,只会造成责任重复,因此每个适配器都会直接调用现有 seam。
|
||||
|
||||
**面向模型的产品选择器。** 产品可用性和身份验证属于部署事实。两个固定工具使各自的 schema 与提供方绑定保持明确,也避免在通用服务中添加动态选择状态。
|
||||
|
||||
**以产品替身作为强制证据。** 替身可以穷尽覆盖私有协议分支,但无法证明包导出、官方发行版、身份验证或真实进程行为。强制证据会驱动每个官方产品连接回环模型 fixture(测试前置数据)。
|
||||
|
||||
**由插件管理登录、产品主目录、模型、设置或权限。** 这些选择会在每个产品的原生配置之外建立另一套权威来源,并将一次性提供方扩张为账户管理功能。提供方只公开显式环境覆盖项和清理宽限期;无人值守交互会以默认拒绝方式失败。
|
||||
|
||||
**续接、进度、后台收集和共享父级上下文。** 已交付的用户结果是一项自包含任务和一个最终回答。产品会话、恢复、后续交互、中间消息、父级 transcript(文本记录)传递、结构化输出和后台收集都需要独立的用户契约,当前实现不会预先构建这些功能。
|
||||
|
||||
## 后果
|
||||
|
||||
用户可以安装任一或两个产品提供方,在自己的 Cordis 配置中绑定稳定的前台工具,并通过现有 subagent 契约委派一项自包含任务。官方产品集成会保留原生设置与行为,而共享服务继续独占任务结算与进程树完全停稳的责任。
|
||||
|
||||
每次委派都要承担新建产品进程和独立模型上下文的开销,且只有最终文本会到达父级。产品原生配置使行为取决于部署环境中安装的产品、账户状态和工作区设置。带密钥 e2e 运行还会消耗外部 API 配额,并依赖 DeepSeek 官方端点;对协议、失败、取消与审批的确定性覆盖仍由无密钥层级承担。提供方不会恢复会话、以流式方式传送进度、接受新的人工交互、回滚工具或文件副作用,也不会施加按实际经过时间触发的超时。
|
||||
|
||||
兼容性由包级单元测试覆盖率、无密钥真实产品回环测试、带密钥 DeepSeek 随机数测试、公开 Loader 组合、已构建包与 NodeNext 消费方检查、生成的文档与声明以及仓库 CI 矩阵共同锁定。更改受支持的产品基线或 DeepSeek 端点/模型基线时必须刷新这些事实;生产环境不会另行执行运行时版本探测。
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md
|
||||
2026-07-30-generated-third-party-notices.md: e480954d29d5dc09ef8ecd4069059a1f0c8b1043
|
||||
2026-07-30-generated-third-party-notices.zh.md: 78ba7250e797c57048078d1b4f62b7a9a5d9d561
|
||||
2026-07-30-generated-third-party-notices.md: cbabd1142ad292f78a3184723182a834dfb234fc
|
||||
2026-07-30-generated-third-party-notices.zh.md: b135c3626b661f1a5b0317a90b700a26d679bf1e
|
||||
@@ -18,7 +18,7 @@ A hand-written inventory answers none of those durably. Roughly a hundred rows o
|
||||
|
||||
One trigger gap is accepted rather than worked around: lefthook inspects only files present on disk, so **deleting** a manifest runs no job, and removing a package reaches the assertion in the test lane instead. Reconstructing the staged file list to include deletions was tried and does not work — lefthook filters the list against the working tree either way. The assertion is the backstop for exactly this case.
|
||||
|
||||
The file discloses **direct** dependencies only. The complete npm closure with pinned versions already lives in `pnpm-lock.yaml` (`pnpm licenses list` renders it) and the Python closure in `python/sdk/uv.lock`; re-materializing either as prose would be a second, worse copy.
|
||||
The file discloses **direct** dependencies by default. The complete npm closure with pinned versions already lives in `pnpm-lock.yaml` (`pnpm licenses list` renders it) and the Python closure in `python/sdk/uv.lock`; re-materializing either as prose would be a second, worse copy. The one explicit transitive disclosure is the official Claude platform payload set declared by `@anthropic-ai/claude-agent-sdk` through `optionalDependencies`, because those packages carry the distributed Claude Code executable rather than ordinary library implementation detail.
|
||||
|
||||
**Tiering is by declaring area, not by manifest section.** A package is a runtime dependency when any manifest outside `DEV_ONLY_AREAS` — the root manifest, `packages/support/`, `packages/client/test-runtime/`, `website/`, `examples/`, `native/` — names it under `dependencies` or `optionalDependencies`. Section names alone are wrong in both directions: a test-support package declares `vitest` under `dependencies` without shipping it, and the `bin/dsh` launcher execs through `tsx`, which no manifest declares as a runtime dependency at all (the generator marks it runtime explicitly).
|
||||
|
||||
@@ -26,10 +26,14 @@ The runtime tier deliberately covers **every mountable plugin**, not just what t
|
||||
|
||||
The manifest set is derived from the `packages:` members each `pnpm-workspace.yaml` declares — the root one and the nested Landlock workspace's — so a new member area is read the day it is declared rather than the day someone remembers to extend a list. License and repository metadata come from the installed pnpm stores, both the root one and the Landlock workspace's, so the generator requires an installed tree and fails loud when a package resolves to neither, rather than emitting an empty cell. `OVERRIDES` carries the packages whose published manifest cannot answer — Rust-built npm bins that omit `license`, and the `modelcontextprotocol/servers` packages whose repository is mid MIT→Apache-2.0 relicensing, so their effective terms are per-contribution. A runtime dependency whose license is not on the permissive list is a hard error: shipping copyleft is a distribution decision, not something a regenerated table may absorb silently. Vendored packages are cross-checked against `vendor/README.md` and rejected if any is not MIT, and `pnpm-workspace.yaml`'s `patchedDependencies` are listed under the runtime table because pnpm applies those patches at install time — shipped artifacts carry modified copies of `@earendil-works/pi-tui` and `node-pty`, and the patch files are the record of what changed.
|
||||
|
||||
The project owner separately authorizes distribution of every official `@anthropic-ai/claude-agent-sdk` version and the official Claude Code CLI/platform payloads that version declares through `optionalDependencies`. The generator represents this as one exact direct-package identity exception, not as a permissive-license override: `SEE LICENSE IN README.md` and `SEE LICENSE IN LICENSE.md` remain non-permissive classifications, and every unrelated non-permissive runtime still fails closed. When the SDK is present, the generator reads its installed manifest, rejects optional identities outside the official SDK payload prefix, derives the current SDK, CLI, and payload versions, verifies the installed host payload's identity, version, and declared-license field, and renders the complete SDK-declared payload set in a separate notices section. Version, declared-license, and payload-set changes do not require new identity authorization, but they still require ordinary dependency, lockfile, compatibility, terms, and notices review.
|
||||
|
||||
## Testing
|
||||
|
||||
The same spec that asserts freshness pins the tiering rule against fixture manifests — including the two cases that motivate it, a `dependencies` entry of a test-support package and a plugin package no app mounts. It also pins the parsers against the shapes that would otherwise drop a package without a word: a `vendor/README.md` table that stops covering a vendored directory, a requirement array holding extras (`"httpx[http2]"`), a requirement with no version at all, an author-named `[dependency-groups]` table, and a workspace member area absent from any hardcoded list. Each of those is a silent-omission path, which is the failure mode a disclosure file cannot afford.
|
||||
|
||||
The Claude distribution tests prove that only the exact direct SDK identity bypasses the ordinary non-permissive-runtime rejection, that the bypass does not change license classification, and that the payload set comes from the SDK manifest rather than a version or platform allowlist. Wrong SDK identities, missing payloads, and unrelated optional package identities all fail.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep the hand-written file and review it at release time.** Reviewing a hundred derived rows by eye is exactly the work a generator does correctly, and the file's own claim — that it lists every direct dependency — would be unverified between releases.
|
||||
@@ -42,6 +46,8 @@ The same spec that asserts freshness pins the tiering rule against fixture manif
|
||||
|
||||
**Tier by reachability from the shipped assemblies only** (`apps/*` plus `python/sdk-runtime`). This produces a tighter runtime tier, but classifies the MCP client and the OpenTelemetry exporter as development-only even though a user running the installed repository can mount them. It understates the disclosure, which is the wrong direction to err for a legal notice.
|
||||
|
||||
**Treat the Claude SDK terms as permissive or add a reusable non-permissive allowlist.** Either shape would misstate the upstream declaration and let an unrelated runtime inherit authorization it was never granted. The narrow exception keys only the official direct SDK identity, while its optional payload identities are accepted solely as data declared by that SDK and remain visibly non-permissive.
|
||||
|
||||
**Emit the notices as a bilingual pair.** Every other root document is paired, but the file is a table of upstream package names, SPDX identifiers, and URLs; the translatable surface is a handful of section blurbs. `scripts/translation-pairing.ts` scopes discovery to `README*`, `.agents/notes/**`, `docs/**`, and `python/**`, so a root non-README file is outside the bilingual corpus by construction, and the README pair carries the bilingual entry points into it.
|
||||
|
||||
## Consequences
|
||||
@@ -51,3 +57,5 @@ A dependency edit now carries a regenerated notices file into the same commit. C
|
||||
The generator needs an installed tree, which makes it heavier than a pure-source generator, and a new package with unusable published metadata needs an `OVERRIDES` entry rather than silently rendering a blank license. Both failures are loud and name the remedy.
|
||||
|
||||
The tiering rule is a policy encoded in one constant. Adding a workspace area that never ships — a second test-infrastructure tier, another site — requires extending `DEV_ONLY_AREAS`, or its dependencies will be disclosed as runtime.
|
||||
|
||||
The Claude identity exception is deliberately narrower than the payload disclosure it activates. Upgrading the SDK needs no new owner authorization, but regeneration fails unless the installed SDK exposes its version, CLI version, and at least one official platform payload, and unless the current host payload matches the SDK declaration. Maintainers still review changed terms and compatibility; the generator prevents the authorization from silently widening to another package.
|
||||
@@ -18,7 +18,7 @@ Status: implemented
|
||||
|
||||
有一处触发缺口是接受而非绕过的:lefthook 只检视磁盘上存在的文件,因此**删除**清单文件不会触发任何任务,移除一个包会落到测试 lane 的断言上。重构暂存文件列表以纳入删除的做法试过,不成立——无论怎么给列表,lefthook 都会拿工作树过滤一遍。这个场景正由断言兜底。
|
||||
|
||||
文件只披露**直接**依赖。完整的 npm 闭包连同锁定版本已记录在 `pnpm-lock.yaml`(`pnpm licenses list` 可渲染),Python 闭包记录在 `python/sdk/uv.lock`;再用散文誊一遍只会得到一份更差的副本。
|
||||
文件默认只披露**直接**依赖。完整的 npm 闭包连同锁定版本已记录在 `pnpm-lock.yaml`(`pnpm licenses list` 可渲染),Python 闭包记录在 `python/sdk/uv.lock`;再用散文誊一遍只会得到一份更差的副本。唯一明确披露的传递依赖,是 `@anthropic-ai/claude-agent-sdk` 通过 `optionalDependencies` 声明的官方 Claude 平台载荷集合,因为这些包承载随产品分发的 Claude Code 可执行文件,而非普通的库实现细节。
|
||||
|
||||
**分层依据是声明方所在区域,而非清单字段名。** 只要 `DEV_ONLY_AREAS` 之外的任一清单——即根清单、`packages/support/`、`packages/client/test-runtime/`、`website/`、`examples/`、`native/` 之外——在 `dependencies` 或 `optionalDependencies` 里点名某个包,它就是运行时依赖。单看字段名在两个方向上都会出错:测试支撑包把 `vitest` 写在 `dependencies` 里却并不交付它;而 `bin/dsh` 启动器 exec 经过的 `tsx`,根本没有任何清单把它声明为运行时依赖,只能由生成器显式标记。
|
||||
|
||||
@@ -26,10 +26,14 @@ Status: implemented
|
||||
|
||||
清单集合由两个 `pnpm-workspace.yaml`——根工作区与嵌套的 Landlock 工作区——各自声明的 `packages:` 成员派生,因此新增成员区域在声明当天就会被读取,而不必等谁想起来去补一份列表。许可证与仓库地址取自已安装的 pnpm store,根 store 与 Landlock 工作区的 store 都会查;某个包两处都解析不到时直接失败,而不是留下空单元格。`OVERRIDES` 收录已发布清单答不上来的包:用 Rust 构建、发布时省略 `license` 字段的 npm 可执行包,以及 `modelcontextprotocol/servers` 系列——该仓库正处在 MIT 向 Apache-2.0 的重新许可过程中,实际条款按贡献逐条而定。运行时依赖的许可证若不在宽松清单内即为硬失败:交付 copyleft 是一项分发决策,不该被一次重新生成悄悄吸收。被源码收编的包会与 `vendor/README.md` 交叉核对,出现非 MIT 即报错;`pnpm-workspace.yaml` 的 `patchedDependencies` 列在运行时表格之后,因为 pnpm 在安装期就会打上这些补丁——交付产物携带的是改动过的 `@earendil-works/pi-tui` 与 `node-pty`,补丁文件本身就是改动的完整记录。
|
||||
|
||||
项目所有者另行授权分发每个官方 `@anthropic-ai/claude-agent-sdk` 版本,以及该版本通过 `optionalDependencies` 声明的官方 Claude Code CLI 与平台载荷。生成器将其表示为一项精确匹配直接包身份的例外,而非宽松许可证覆盖项:`SEE LICENSE IN README.md` 与 `SEE LICENSE IN LICENSE.md` 仍归类为非宽松,所有无关的非宽松运行时依赖仍以默认拒绝方式失败。存在该 SDK 时,生成器会读取其已安装清单,拒绝不符合官方 SDK 载荷前缀的可选包身份,推导当前 SDK、CLI 与载荷版本,核验已安装宿主载荷的身份、版本和声明许可证字段,并在单独的声明章节中渲染 SDK 声明的完整载荷集合。版本、声明许可证和载荷集合发生变化时无需新的身份授权,但仍须经过常规的依赖、锁文件、兼容性、条款和声明评审。
|
||||
|
||||
## Testing
|
||||
|
||||
断言新鲜度的同一个 spec 也用夹具清单钉住分层规则,覆盖促成该规则的两个场景:测试支撑包的 `dependencies` 条目,以及没有任何应用挂载的插件包。它还把各解析器钉在那些原本会让某个包无声消失的形态上:不再覆盖全部收编目录的 `vendor/README.md` 表、含 extras 的依赖数组(`"httpx[http2]"`)、完全不带版本的依赖、作者自取名字的 `[dependency-groups]` 表,以及任何硬编码列表都不含的工作区成员区域。这些都是静默漏报路径——正是披露文件最担不起的失败方式。
|
||||
|
||||
Claude 分发测试证明:只有精确匹配的直接 SDK 身份会绕过通常的非宽松运行时拒绝;该绕过不会改变许可证分类;载荷集合来自 SDK 清单,而非版本或平台允许列表。SDK 身份错误、载荷缺失或存在无关的可选包身份时,测试都会失败。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**保留手写文件,发版时人工过一遍。** 用肉眼审阅上百行推导数据,恰恰是生成器能做对的活;而且在两次发版之间,文件自称「列出全部直接依赖」这句话无人验证。
|
||||
@@ -42,6 +46,8 @@ Status: implemented
|
||||
|
||||
**只按已交付装配的可达性分层**(`apps/*` 加 `python/sdk-runtime`)。这样得到的运行时层更紧凑,但会把 MCP 客户端与 OpenTelemetry 导出器判为仅开发用途——而运行已安装仓库的用户完全可以挂载它们。这会低估披露,对法务通告来说错在了更危险的一侧。
|
||||
|
||||
**将 Claude SDK 条款视为宽松条款,或新增可复用的非宽松允许列表。** 两种方案都会误述上游声明,并让无关运行时依赖继承从未授予它的授权。这项窄例外只匹配官方直接 SDK 身份;其可选载荷身份仅作为该 SDK 声明的数据被接受,并继续明确归类为非宽松。
|
||||
|
||||
**把披露文件做成双语对。** 其他根文档都是成对的,但这份文件是上游包名、SPDX 标识与网址构成的表格,可翻译的只有寥寥几段章节导语。`scripts/translation-pairing.ts` 的发现范围限定在 `README*`、`.agents/notes/**`、`docs/**` 与 `python/**`,根目录下的非 README 文件在构造上就不属于双语语料;双语入口由 README 对承担。
|
||||
|
||||
## Consequences
|
||||
@@ -51,3 +57,5 @@ Status: implemented
|
||||
生成器需要已安装的工作树,因此比纯源码生成器更重;发布元数据不可用的新包需要补一条 `OVERRIDES`,而不是默默渲染出空白许可证。这两类失败都会明确报错并指出补救方式。
|
||||
|
||||
分层规则是编码在一个常量里的政策。若新增了不参与交付的工作区区域——第二层测试基础设施、另一个站点——就要同步扩展 `DEV_ONLY_AREAS`,否则其依赖会被当作运行时依赖披露出去。
|
||||
|
||||
Claude 身份例外刻意比其启用的载荷披露范围更窄。升级 SDK 无需新的所有者授权,但如果已安装的 SDK 未公开自身版本、CLI 版本和至少一个官方平台载荷,或当前宿主载荷与 SDK 声明不符,重新生成就会失败。维护者仍须评审发生变化的条款与兼容性;生成器会阻止授权悄然扩大到其他包。
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.md
|
||||
2026-07-07-claude-code-and-codex-subagent-backends.md: ee8576f97a9fdef8c88dcad3a73f28b63ca3ebe1
|
||||
2026-07-07-claude-code-and-codex-subagent-backends.zh.md: bd76a8f34d86b704494b56331c47ea89bfc8a0aa
|
||||
@@ -1,89 +0,0 @@
|
||||
# Agent Note: Claude Code and Codex subagent backends (out-of-process delegation to external coding agents)
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-07-07-claude-code-and-codex-subagent-backends.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The subagent seam ([the seam Agent Note](../../implemented/feature/2026-06-21-subagent-capability-seam.md)) hosts multiple named providers on `ctx.subagents`, and the ACP backend ([the ACP backend Agent Note](../../implemented/feature/2026-06-22-acp-subagent-backend.md)) proved the seam generalizes across a process boundary; its Future-providers section explicitly named the Codex app-server and the Claude Code Agent SDK as mechanically similar siblings. Those two are the engines actually worth delegating to today: a harness turn should be able to hand a self-contained task to a real Claude Code or a real Codex — a separate product with its own model, tools, and sandbox — and get back one final answer, without the parent deployment leaking its secrets into the child or the child's behavior silently depending on whatever `~/.claude` / `~/.codex` state exists on the host machine.
|
||||
|
||||
## Proposal
|
||||
|
||||
Two sibling provider packages, structural variants of the ACP backend, plus one extraction:
|
||||
|
||||
- `@deepseek-ai/dsh-subagent-claude-code` — drives a Claude Code child through `@anthropic-ai/claude-agent-sdk`'s `query()` (the SDK runs in the parent process and spawns its bundled `claude` CLI as the subprocess). Provider name `claude-code`: the child is the Claude Code *product*, not an Anthropic model adapter — "claude" stays reserved for a future `dsh-llm` adapter.
|
||||
- `@deepseek-ai/dsh-subagent-codex` — spawns `codex app-server` and drives one thread/turn over its JSON-RPC-over-stdio protocol with a hand-rolled newline-JSON client (~200–300 lines) in the package.
|
||||
- `@deepseek-ai/dsh-subagent-process` — a pure library (the `subagent-inprocess` precedent) extracting what `dsh-subagent-acp` already carries and both new backends need: the credential env scrub (`buildChildEnv`), the EOF → SIGTERM → SIGKILL dispose ladder, and new isolated-config-dir helpers (`mkdtemp` create, best-effort remove). The ACP backend migrates onto it; `bash-local`'s sibling copy is left alone to bound the change.
|
||||
|
||||
Both providers copy the ACP backend's seam posture verbatim: fresh child per `start`, exactly one prompt round-trip, capabilities all `false`, `inheritsParentContext: false`, `request.parent`/`request.agentOptions` ignored, `id = SessionId(randomUUID())`, `result` never rejects — child-level failure flattens to a stop reason and the original error goes to `ctx.logger` via an `onError` spec callback. Model exposure is zero new code: `dsh-tool-subagent` is loaded once per provider with a distinct `toolName` (`subagent_claude_code`, `subagent_codex`). No new session events are needed — the only model-visible artifact is the tool result, so reconstructability holds exactly as it did for ACP. To be explicit about the boundary: the session log reconstructs the model-visible transcript, not workspace mutation history — a child granted write access mutates files as an ambient side effect outside the log, exactly as the bash tools and the ACP backend already do; replay reproduces requests, not the disk.
|
||||
|
||||
## Verified interface facts (pinned versions)
|
||||
|
||||
Both integration surfaces were verified against pinned implementations before this proposal — types and bundled source read, keyless spikes run — not from vendor docs alone. The pins are the verification baseline, not a runtime contract: the backends perform no runtime version probe (no `codex --version` gate, no SDK version sniffing). Compatibility is enforced at development time — every dependency bump re-runs the keyless suites against the real load path — and at runtime by failing loudly: a protocol-level surprise settles `error` via `onError`, never a silent misbehavior.
|
||||
|
||||
**`@anthropic-ai/claude-agent-sdk` 0.3.202.** `options.env` REPLACES the child environment (no merge with `process.env`), which is exactly what the scrub needs. `settingSources` defaults to loading ALL filesystem settings — isolation requires explicitly passing `[]`. Result subtypes are `success` | `error_during_execution` | `error_max_turns` | `error_max_budget_usd` | `error_max_structured_output_retries`. On abort the SDK escalates the CLI child itself: stdin EOF immediately, SIGTERM ~2s later if the child ignores it (observed; no leftover processes) — no bespoke kill fallback needed. `outputFormat: {type: 'json_schema'}` and an `agents` option exist, giving future landing points for the seam's `outputSchema` capability and named subagent types; both are out of scope here.
|
||||
|
||||
**codex CLI 0.142.5, `codex app-server` (v2 vocabulary).** LF-delimited JSON, JSON-RPC 2.0 shapes with the `"jsonrpc"` header omitted.
|
||||
|
||||
- Lifecycle: `initialize{clientInfo}` + `initialized` → `thread/start` (accepts `cwd`, `model`, `sandbox`, `approvalPolicy`, `ephemeral`; succeeds unauthenticated) → `turn/start{threadId, input:[{type:'text',text}]}` returns an `inProgress` turn immediately; the terminal signal is the `turn/completed` notification carrying `Turn{status: completed|interrupted|failed|inProgress, error}`.
|
||||
- Approvals are server-initiated requests — `item/commandExecution/requestApproval`, `item/fileChange/requestApproval`, `item/permissions/requestApproval`, `item/tool/requestUserInput`, `mcpServer/elicitation/request` — answered with `accept`/`decline`-family decisions.
|
||||
- Auth: `account/login/start{type:'apiKey', apiKey}` is a first-class RPC and `account/read` reports `requiresOpenaiAuth` — and an unauthenticated `turn/start` does NOT fail fast (it hangs in retry), so the backend MUST pre-check auth and settle `error` loudly instead of waiting on the turn.
|
||||
- Isolation: `CODEX_HOME` redirection is honored (the `initialize` response echoes it, so tests can assert isolation), and `ephemeral: true` threads leave no session files at all.
|
||||
|
||||
## Isolation and credentials
|
||||
|
||||
Deployments authenticate with API keys only, and the child must not see the host user's Claude Code / Codex configuration: behavior has to be a function of `cordis.yml` alone. Each run gets a fresh `mkdtemp` config dir — `CLAUDE_CONFIG_DIR` for Claude Code (paired with an explicit `settingSources: []`), `CODEX_HOME` for Codex — removed best-effort on dispose; a config field can pin a persistent dir instead. The child env reuses the ACP backend's `buildChildEnv` semantics verbatim via the extraction: the ambient env is forwarded MINUS credential-shaped vars (`/KEY|SECRET|TOKEN/i`), with `config.env` layered on top — so `PATH`, `HOME`, `TMPDIR`, locale, and proxy vars survive and the CLIs run normally, while only credential-shaped ambient vars are scrubbed (`ANTHROPIC_API_KEY` enters explicitly through `config.env` for Claude Code), and the Codex key travels via the `account/login/start` RPC into the isolated `CODEX_HOME` rather than a hand-written `auth.json`.
|
||||
|
||||
## Permission and approval policy
|
||||
|
||||
Instead of collapsing to ACP's single `permission: allow|reject` knob, each backend exposes its engine's native vocabulary as config, with conservative defaults: Claude Code gets `permissionMode` (default `default`) plus `permission: allow|reject` (default `reject`) as the `canUseTool` auto-answer for whatever falls through; Codex gets `sandboxMode` (default `read-only`) and `approvalPolicy` (default `never`) plus the same `permission` fallback for approval requests that still arrive. Defaults are deliberately do-no-harm (the out-of-box child cannot write files); examples demonstrate opening up (`acceptEdits` / `workspace-write`). The mechanical rule: EVERY server-initiated request is settled programmatically and promptly — the enumerated approval/user-input/elicitation requests by the configured policy, an unknown request method with a JSON-RPC method-not-found error response (never left pending), unknown notifications consumed — so no child request can wedge a turn waiting on an answer that will never come. Prompts never reach a human in this cut, matching ACP.
|
||||
|
||||
## StopReason mapping
|
||||
|
||||
Claude Code: `success` → `completed`; `error_max_turns`, `error_during_execution`, `error_max_budget_usd`, `error_max_structured_output_retries` → `error` (aligning with the ACP call on `max_turn_requests`: an unfinished task is not success); generator abort → `aborted`; anything unknown → `error`. Codex: `Turn.status` `completed` → `completed`; `interrupted` → `aborted`; `failed` with `codexErrorInfo: 'contextWindowExceeded'` → `max-tokens`, any other `failed` → `error`; transport/spawn/auth-precheck failure → `error` (or `aborted` if cancel was requested). In both, `cancel()` is the ACP shape: flag + abort/interrupt + a cancel-settled race arm so an uncooperative child cannot stall the result.
|
||||
|
||||
Liveness posture, stated explicitly: teardown timing is config, turn duration is not. Both backends take the dispose ladder's grace periods as defaulted validated config fields (the ACP backend's `disposeEofGraceMs`/`disposeGraceMs` shape, carried by the extraction), but there is deliberately NO turn-duration or startup timeout — matching ACP, liveness during a turn belongs to the caller via `cancel()`/the abort signal, a subagent turn is legitimately minutes long, and the Codex auth precheck removes the one verified guaranteed-hang; a deployment wanting a wall-clock bound cancels from the parent.
|
||||
|
||||
## Testing
|
||||
|
||||
Named at every tier per the root AGENTS.md rule, and de-risked up front:
|
||||
|
||||
- **Keyless unit/integration**, mirroring the ACP spec list per backend (round-trip and output accumulation, every stop mapping, both cancel paths, already-aborted, permission auto-answer under both policies, unknown-message tolerance, bad-command spawn failure, HMR provider cleanup, export shape, isolation assertions on child env and temp-dir removal; Codex adds the auth-precheck failure path). Claude Code's harness is a scripted fake `claude` executable behind `pathToClaudeCodeExecutable` driven by the REAL SDK — a spike already passed end-to-end keyless in 24ms (the fake CLI answers one `control_request/initialize` and speaks plain stream-json, ~40 lines). Codex's harness is a scripted mock app-server subprocess speaking the verified wire protocol, the `mock-acp-server.ts` shape.
|
||||
- **With-key e2e** per backend: the real engine does real file work verified on disk, under a pinned opened-up config so acceptance and the do-no-harm defaults don't collide — `permissionMode: 'acceptEdits'` for Claude Code, `sandboxMode: 'workspace-write'` + `approvalPolicy: 'never'` for Codex; self-skips report exactly what is missing (binary vs key). CI has no secrets, so these run locally per the with-key policy.
|
||||
- **Snapshot**: deferred as `TODO(claude-code-subagent-replay)` / `TODO(codex-subagent-replay)` — the same distinct replay shape the ACP backend deferred ([the per-session replay Agent Note](../../implemented/testing/2026-06-22-subagent-snapshot-replay.md)); the keyless suites carry deterministic coverage meanwhile.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
### Why not the official `@openai/codex-sdk` instead of a hand-rolled client?
|
||||
|
||||
The dispose ladder and env scrub require owning the child process (spawn args, env, signals, exit await); the SDK hides the process. The wire format is trivial to frame (LF JSON), the shapes are generatable per pinned version (`codex app-server generate-json-schema`), and the repo precedent (`hook-protocol`) is to own thin protocol cores rather than wrap someone's runtime. The SDK would save protocol-evolution maintenance but costs the exact control this backend exists to have.
|
||||
|
||||
### Why not a model-visible `subagent_type` parameter (one Task-style tool)?
|
||||
|
||||
Claude Code's own Task tool puts the subagent type in the model-facing schema, selecting a prompt-plus-toolset persona. Here the choice is between EXECUTION ENGINES, and only the deployer knows which engines have credentials configured — so selection stays deployment config, preserving `dsh-tool-subagent`'s documented one-provider-per-tool contract. A persona-style type selector would be a separate Agent Note against the tool, not the backends.
|
||||
|
||||
### Why not login-state credentials and the user's own config?
|
||||
|
||||
Inheriting `~/.claude` / `~/.codex` (subscription login, user settings, skills, MCP servers) would make child behavior depend on host-machine state and punch an implicit exception through the "credentials enter explicitly via `config.env`, never ambiently" rule the ACP backend and bash executor established. API-key-only plus forced config-dir isolation keeps runs reproducible; deployments wanting shared state can point the config-dir field at a persistent directory deliberately.
|
||||
|
||||
### Why not a driver-injection seam for the Claude Code keyless tests?
|
||||
|
||||
Injecting a fake `query()` would mock our own boundary and leave the real SDK load path untested (the real-over-mock policy in docs/testing.md). The risk that justified considering it — the SDK↔CLI stream-json control protocol being internal — was retired by the spike: the fake-CLI harness works against the real pinned SDK today. If an SDK upgrade breaks the mock, the keyless suite fails the upgrade PR, which is the gate working.
|
||||
|
||||
### Why not ACP adapters (e.g. `claude-code-acp`) reusing the existing backend?
|
||||
|
||||
Community shims wrap both engines in ACP, which would make them "just config" on `dsh-subagent-acp`. But that inserts an unofficial third-party layer between the harness and the engine, erases the native control surfaces this Agent Note exposes (permissionMode, sandboxMode/approvalPolicy, config-dir isolation, apiKey RPC), and trades first-party protocol stability for a shim's release cadence. First-party surfaces — the Agent SDK and the app-server — are the supported integration points.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
On a machine with both engines and keys configured: a REPL-driven model completes one real file task through `subagent_claude_code` and one through `subagent_codex`, the tool result being the child's final answer, with only `tool/call` + `tool/result` in the parent session log. Keyless suites pass at 100% per-file coverage in a credential-less environment, asserting isolation (scrubbed child env, no temp config dirs left after dispose) and that child behavior is unchanged by the presence or absence of `~/.claude` / `~/.codex`. Cancelling a parent turn quiesces both backends in bounded time with no leftover child processes. E2e suites self-skip cleanly, naming the missing prerequisite.
|
||||
|
||||
## Risks
|
||||
|
||||
- `codex app-server` is CLI-flagged experimental and its v1/v2 vocabularies coexist; the client pins 0.142.5, implements v2 only, and consumes unknown methods/notifications without crashing, but a future codex bump can still force rework (regenerate schemas and re-run the keyless suite on every bump — the development-time enforcement behind the no-runtime-version-probe stance above).
|
||||
- The Claude Code fake-CLI mock rides an internal protocol: any SDK upgrade must go through the keyless suite, and a breaking control-protocol change means reworking the mock (fallback: the driver-injection seam rejected above becomes the escape hatch).
|
||||
- The SDK's optionalDependencies weigh ~280MB per platform — accepted, and confined to the one backend package.
|
||||
- The SDK's SIGKILL branch beyond EOF→SIGTERM was not observed and is trusted; e2e keeps a no-leftover-process assertion.
|
||||
- Codex is a deployment prerequisite (no npm-bundled binary); a missing or incompatible binary surfaces as a loud spawn/protocol `error`, not a version probe.
|
||||
- Every run pays a fresh child process and only the final answer surfaces — thoughts, tool cards, and usage are consumed and dropped; pooling, intermediate-progress surfacing, `sendMessage`/`resume`, `outputSchema` via the SDK's `outputFormat`, and named subagent types via the SDK's `agents` option are all deliberate deferrals.
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
# Agent Note: Claude Code 与 Codex subagent 后端(向外部编码 agent 的进程外委派)
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-07-07-claude-code-and-codex-subagent-backends.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
subagent seam([seam Agent Note](../../implemented/feature/2026-06-21-subagent-capability-seam.md))在 `ctx.subagents` 上托管多个命名提供方,ACP(Agent Client Protocol)后端([ACP 后端 Agent Note](../../implemented/feature/2026-06-22-acp-subagent-backend.md))证明了该 seam 能跨越进程边界泛化;其「未来提供方」一节明确将 Codex app-server 与 Claude Code Agent SDK 列为实现机制相似的同类方案。如今真正值得委派的就是这两个引擎:harness 的一个轮次应能把一个自包含任务交给真实的 Claude Code 或真实的 Codex——一个拥有自身模型、工具与沙箱的独立产品——并取回一个最终答案,同时父部署不向子进程泄漏密钥,子进程行为也不静默依赖宿主机上碰巧存在的 `~/.claude` / `~/.codex` 状态。
|
||||
|
||||
## 提案
|
||||
|
||||
两个兄弟提供方包,作为 ACP 后端的结构变体,另加一次提取:
|
||||
|
||||
- `@deepseek-ai/dsh-subagent-claude-code`:通过 `@anthropic-ai/claude-agent-sdk` 的 `query()` 驱动一个 Claude Code 子进程(SDK 在父进程中运行,并将其内置的 `claude` CLI(命令行界面)作为子进程 spawn)。提供方名称为 `claude-code`:子进程是 Claude Code 这个*产品*,而非 Anthropic 模型适配器——「claude」保留给未来的 `dsh-llm` 适配器。
|
||||
- `@deepseek-ai/dsh-subagent-codex`:spawn `codex app-server`,通过其 JSON-RPC-over-stdio 协议驱动一个 thread 及其中的一个轮次,使用包内一个手写的换行 JSON 客户端(约 200–300 行)。
|
||||
- `@deepseek-ai/dsh-subagent-process`:纯库(沿用 `subagent-inprocess` 的先例),提取 `dsh-subagent-acp` 已有且两个新后端都需要的内容:凭证环境清洗(`buildChildEnv`)、EOF → SIGTERM → SIGKILL 的 dispose(资源释放)阶梯,以及新的隔离配置目录辅助函数(`mkdtemp` 创建、尽力删除)。ACP 后端迁移到该库上;`bash-local` 的兄弟副本保持不动以限制变更范围。
|
||||
|
||||
两个提供方逐字复制 ACP 后端的 seam 姿态:每次 `start` 创建全新子进程、恰好一次提示词往返、所有能力均为 `false`、`inheritsParentContext: false`、忽略 `request.parent`/`request.agentOptions`、`id = SessionId(randomUUID())`,且 `result` 从不 reject——子进程级失败扁平化为 stop reason,原始错误则通过 `onError` spec 回调送到 `ctx.logger`。模型暴露无需新代码:每个提供方各加载一次 `dsh-tool-subagent`,使用不同的 `toolName`(`subagent_claude_code`、`subagent_codex`)。无需新的会话事件——唯一的模型可见产物是工具结果,因此可重建性与 ACP 完全相同。明确边界:会话日志重建模型可见的 transcript(文本记录),而不是工作区变更历史——获准写入的子进程将文件作为日志之外的环境副作用进行修改,与 bash 工具和 ACP 后端现有行为完全一致;回放复现请求,而非磁盘。
|
||||
|
||||
## 已验证的接口事实(固定版本)
|
||||
|
||||
两个集成面在本提案之前均已针对固定版本进行了验证——阅读类型与打包源码、运行无需密钥的 spike——而非仅依赖厂商文档。固定版本是验证基线,不是运行时契约:后端不执行运行时版本探测(无 `codex --version` 门禁、无 SDK 版本嗅探)。兼容性在开发时强制执行——每次依赖升级都会针对真实加载路径重跑无密钥套件——在运行时则通过显式失败来保障:协议层面的意外通过 `onError` 结算为 `error`,绝不静默异常。
|
||||
|
||||
**`@anthropic-ai/claude-agent-sdk` 0.3.202。** `options.env` 会替换子进程环境(不与 `process.env` 合并),恰好满足清洗需求。`settingSources` 默认加载所有文件系统设置——隔离要求显式传入 `[]`。结果子类型为 `success` | `error_during_execution` | `error_max_turns` | `error_max_budget_usd` | `error_max_structured_output_retries`。中止时 SDK 自行逐级加强对 CLI 子进程的终止措施:立即关闭 stdin,约 2 秒后若子进程未退出则发送 SIGTERM(已观察到;无残留进程)——无需自定义 kill 回退。`outputFormat: {type: 'json_schema'}` 和 `agents` 选项已存在,为 seam 的 `outputSchema` 能力和命名 subagent 类型提供了未来着陆点;两者均不在本 Agent Note 范围内。
|
||||
|
||||
**codex CLI 0.142.5,`codex app-server`(v2 词汇)。** LF 分隔的 JSON,JSON-RPC 2.0 形状但省略 `"jsonrpc"` 头。
|
||||
|
||||
- 生命周期:`initialize{clientInfo}` + `initialized` → `thread/start`(接受 `cwd`、`model`、`sandbox`、`approvalPolicy`、`ephemeral`;未认证即可成功)→ `turn/start{threadId, input:[{type:'text',text}]}` 立即返回一个 `inProgress` 的轮次;终止信号是携带 `Turn{status: completed|interrupted|failed|inProgress, error}` 的 `turn/completed` 通知。
|
||||
- 审批是服务端发起的请求——`item/commandExecution/requestApproval`、`item/fileChange/requestApproval`、`item/permissions/requestApproval`、`item/tool/requestUserInput`、`mcpServer/elicitation/request`——以 `accept`/`decline` 系列决策应答。
|
||||
- 认证:`account/login/start{type:'apiKey', apiKey}` 是一等 RPC,`account/read` 报告 `requiresOpenaiAuth`——且未认证的 `turn/start` 不会快速失败(它会挂在重试中),因此后端必须预检认证状态,并在失败时明确结算为 `error`,而非等待轮次。
|
||||
- 隔离:`CODEX_HOME` 重定向被尊重(`initialize` 响应会回显它,测试可据此断言隔离),`ephemeral: true` 的 thread 不留任何会话文件。
|
||||
|
||||
## 隔离与凭证
|
||||
|
||||
部署只使用 API key 认证,子进程不得看到宿主用户的 Claude Code / Codex 配置:行为必须只由 `cordis.yml` 决定。每次运行获得一个全新的 `mkdtemp` 配置目录——Claude Code 使用 `CLAUDE_CONFIG_DIR`(并显式设置 `settingSources: []`),Codex 使用 `CODEX_HOME`——dispose 时尽力删除;配置字段也可以固定一个持久目录。子进程环境通过提取逐字复用 ACP 后端的 `buildChildEnv` 语义:转发环境变量,但移除凭证形态的变量(`/KEY|SECRET|TOKEN/i`),再叠加 `config.env`——因此 `PATH`、`HOME`、`TMPDIR`、locale 和代理变量保留,CLI 正常运行;只有环境中的凭证形态变量被清洗(Claude Code 的 `ANTHROPIC_API_KEY` 通过 `config.env` 显式进入),Codex key 则通过 `account/login/start` RPC 进入隔离的 `CODEX_HOME`,而非手写 `auth.json`。
|
||||
|
||||
## 权限与审批策略
|
||||
|
||||
每个后端不压缩为 ACP 单一的 `permission: allow|reject` 旋钮,而把引擎原生词汇作为配置暴露,并采用保守默认值:Claude Code 获得 `permissionMode`(默认 `default`)以及 `permission: allow|reject`(默认 `reject`),后者作为所有未被前者处理的请求的 `canUseTool` 自动应答;Codex 获得 `sandboxMode`(默认 `read-only`)和 `approvalPolicy`(默认 `never`),以及同一个 `permission` 后备值,用来应答仍然到达的审批请求。默认值刻意做到不造成损害(开箱即用的子进程无法写文件);示例演示如何开放权限(`acceptEdits` / `workspace-write`)。机械规则是:每一个服务端发起的请求都由程序迅速结算——枚举出的审批/用户输入/elicitation 请求按配置策略应答,未知请求方法用 JSON-RPC method-not-found 错误响应(绝不保持 pending),未知通知被消费——因此任何子进程请求都不会因等待永远不会到来的应答而卡住轮次。这一版中提示词不会到达人类,与 ACP 一致。
|
||||
|
||||
## StopReason 映射
|
||||
|
||||
Claude Code:`success` → `completed`;`error_max_turns`、`error_during_execution`、`error_max_budget_usd`、`error_max_structured_output_retries` → `error`(与 ACP 对 `max_turn_requests` 的处理对齐:未完成的任务不是成功);生成器中止 → `aborted`;未知值 → `error`。Codex:`Turn.status` 为 `completed` → `completed`;`interrupted` → `aborted`;`failed` 且 `codexErrorInfo: 'contextWindowExceeded'` → `max-tokens`,其他 `failed` → `error`;传输/spawn/认证预检失败 → `error`(若已请求取消则为 `aborted`)。两者中,`cancel()` 采用 ACP 形状:标志位 + abort/interrupt + 一个 cancel-settled 竞争分支,使不合作的子进程无法阻塞结果。
|
||||
|
||||
活性姿态,明确声明:teardown 时序是配置项,轮次时长不是。两个后端将 dispose 阶梯的宽限期作为带默认值的已验证配置字段(ACP 后端的 `disposeEofGraceMs`/`disposeGraceMs` 形状,由提取库承载),但刻意不设轮次时长或启动超时——与 ACP 一致:轮次期间的活性由调用方通过 `cancel()`/abort signal 掌控,subagent 轮次持续数分钟也属合理,而 Codex 认证预检消除了唯一已验证的必然挂起场景;需要墙钟上限的部署从父侧取消即可。
|
||||
|
||||
## 测试
|
||||
|
||||
依照根 AGENTS.md 规则在每个层级明确命名,并预先消除风险:
|
||||
|
||||
- **无密钥单元/集成测试**:每个后端都镜像 ACP spec 清单(往返和输出累积、每种 stop 映射、两条取消路径、已中止、两种策略下的权限自动应答、未知消息容错、错误命令的 spawn 失败、HMR(热模块替换)提供方清理、导出形状、对子进程环境隔离和临时目录删除的断言;Codex 另加认证预检失败路径)。Claude Code harness 是通过 `pathToClaudeCodeExecutable` 接入真实 SDK 的脚本化假 `claude` 可执行文件——一个 spike 已在 24 ms 内完成端到端无密钥验证(假 CLI 应答一次 `control_request/initialize`,并使用普通 stream-json 通信,约 40 行)。Codex harness 是讲已验证协议格式的脚本化 mock app-server 子进程,沿用 `mock-acp-server.ts` 形状。
|
||||
- **有密钥 e2e 测试**:每个后端的真实引擎执行真实文件操作,并通过磁盘状态进行验证,固定使用开放后的配置,以免验收与不造成损害的默认值冲突——Claude Code 使用 `permissionMode: 'acceptEdits'`,Codex 使用 `sandboxMode: 'workspace-write'` + `approvalPolicy: 'never'`;自跳过会准确报告缺失的是二进制还是 key。CI 没有密钥,因此依照有密钥策略在本地运行。
|
||||
- **快照测试**:以 `TODO(claude-code-subagent-replay)` / `TODO(codex-subagent-replay)` 推迟——即 ACP 后端也推迟的独立回放形状([按会话回放 Agent Note](../../implemented/testing/2026-06-22-subagent-snapshot-replay.md));在此期间由无密钥套件提供确定性覆盖。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
### 为什么不用官方 `@openai/codex-sdk` 而手写客户端?
|
||||
|
||||
dispose 阶梯和环境清洗要求拥有子进程(spawn 参数、env、信号、exit 等待);SDK 隐藏了进程。协议格式(wire format)极其简单(LF JSON),形状可按固定版本生成(`codex app-server generate-json-schema`),仓库先例(`hook-protocol`)是拥有薄协议核心而非包装他人的运行时。SDK 能节省协议演进的维护成本,但代价是失去本后端存在的意义所在的精确控制。
|
||||
|
||||
### 为什么不用模型可见的 `subagent_type` 参数(单一 Task 风格工具)?
|
||||
|
||||
Claude Code 自身的 Task 工具将 subagent 类型放在模型可见的 schema 中,选择一个提示词 + 工具集人格。这里的选择是在执行引擎之间做出的,而只有部署者知道哪些引擎配置了凭证——因此选择留在部署配置层,保持 `dsh-tool-subagent` 文档中的「一个提供方对应一个工具」契约。人格风格的类型选择器应是针对工具的另一个 Agent Note,而非针对后端。
|
||||
|
||||
### 为什么不用登录态凭证和用户自身的配置?
|
||||
|
||||
继承 `~/.claude` / `~/.codex`(订阅登录、用户设置、skill(技能)、MCP 服务器)会使子进程行为依赖宿主机状态,并在 ACP 后端和 bash 执行器确立的「凭证通过 `config.env` 显式进入,绝不隐式继承」规则上打开一个隐式例外。仅 API key 加强制配置目录隔离使运行可复现;需要共享状态的部署可以有意将配置目录字段指向一个持久目录。
|
||||
|
||||
### 为什么不为 Claude Code 无密钥测试注入驱动层 seam?
|
||||
|
||||
注入假的 `query()` 会 mock 我们自己的边界,使真实 SDK 加载路径未被测试(docs/testing.md 中的 real-over-mock 策略)。曾考虑此方案的风险——SDK↔CLI 的 stream-json 控制协议是内部实现——已被 spike 消除:假 CLI harness 今天能对真实固定版本的 SDK 正常工作。如果 SDK 升级破坏了 mock,无密钥套件会让升级 PR(Pull Request)失败,这正是门禁在发挥作用。
|
||||
|
||||
### 为什么不用 ACP 适配器(如 `claude-code-acp`)复用既有后端?
|
||||
|
||||
社区 shim 将两个引擎包装为 ACP,这会使它们在 `dsh-subagent-acp` 上变成「仅配置」。但这在 harness 与引擎之间插入了一个非官方的第三方层,抹去了本 Agent Note 暴露的原生控制面(permissionMode、sandboxMode/approvalPolicy、配置目录隔离、apiKey RPC),并以 shim 的发布节奏替换了第一方协议的稳定性。第一方接口——Agent SDK 和 app-server——才是受支持的集成点。
|
||||
|
||||
## 验收标准
|
||||
|
||||
在两个引擎和密钥均已配置的机器上:一个 REPL 驱动的模型通过 `subagent_claude_code` 完成一个真实文件任务,通过 `subagent_codex` 完成另一个,工具结果为子进程的最终答案,父会话日志中仅有 `tool/call` + `tool/result`。无密钥套件在无凭证环境下以逐文件 100% 覆盖率通过,断言隔离(清洗后的子进程环境、dispose 后无残留临时配置目录),并断言 `~/.claude` / `~/.codex` 的存在与否不影响子进程行为。取消父轮次后,两个后端在有界时间内完全停稳,无残留子进程。e2e 套件干净地自跳过,命名缺失的前置条件。
|
||||
|
||||
## 风险
|
||||
|
||||
- `codex app-server` 被 CLI 标记为实验性,其 v1/v2 词汇共存;客户端固定 0.142.5、仅实现 v2、对未知方法/通知消费而不崩溃,但未来 codex 升级仍可能迫使返工(每次升级重新生成 schema 并重跑无密钥套件——这是上述「不做运行时版本探测」立场背后的开发时强制执行)。
|
||||
- Claude Code 假 CLI mock 依赖一个内部协议:任何 SDK 升级都必须通过无密钥套件,控制协议的破坏性变更意味着返工 mock(回退方案:上面否决的驱动注入 seam 成为逃生舱口)。
|
||||
- SDK 的 optionalDependencies 每平台约 280 MB——已接受,限制在单个后端包内。
|
||||
- SDK 的 SIGKILL 分支(EOF→SIGTERM 之后)未被观察到,信任其实现;e2e 保留无残留进程断言。
|
||||
- Codex 是部署前置条件(无 npm 内置二进制);缺失或不兼容的二进制会明确报出 spawn/协议 `error`,而非版本探测。
|
||||
- 每次运行付出一个全新子进程的代价,且仅最终答案浮出——思考、工具卡片和用量被消费后丢弃;池化、中间进度浮出、`sendMessage`/`resume`、通过 SDK 的 `outputFormat` 实现 `outputSchema`、以及通过 SDK 的 `agents` 选项实现命名 subagent 类型,均为刻意推迟。
|
||||
+23
-2
@@ -3,9 +3,9 @@
|
||||
|
||||
# Third-Party Notices
|
||||
|
||||
DeepSeek Harness is licensed under [BSD 3-Clause](LICENSE). It depends on the third-party open-source software listed below. Each project remains under its own license; nothing in this file changes those terms.
|
||||
DeepSeek Harness is licensed under [BSD 3-Clause](LICENSE). It depends on the third-party software listed below. Each project remains under its own license; nothing in this file changes those terms.
|
||||
|
||||
This file lists **direct** dependencies declared by the workspace. It is generated from the workspace manifests by `scripts/gen-third-party-notices.ts`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and `scripts/gen-third-party-notices.spec.ts` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run `pnpm run verify-third-party-notices` for the standalone check.
|
||||
This file lists **direct** dependencies declared by the workspace and the explicitly disclosed official Claude platform payload closure. It is generated from the workspace manifests by `scripts/gen-third-party-notices.ts`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and `scripts/gen-third-party-notices.spec.ts` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run `pnpm run verify-third-party-notices` for the standalone check.
|
||||
|
||||
The complete npm transitive closure, with exact pinned versions, is recorded in [`pnpm-lock.yaml`](pnpm-lock.yaml) — inspect it with `pnpm licenses list`. The Python closure is recorded in [`python/sdk/uv.lock`](python/sdk/uv.lock), and the Landlock launcher workspace keeps its own in [`native/landlock-run/pnpm-lock.yaml`](native/landlock-run/pnpm-lock.yaml).
|
||||
|
||||
@@ -32,6 +32,8 @@ External packages that a workspace package resolves at runtime. `scripts/install
|
||||
| Package | License |
|
||||
| --- | --- |
|
||||
| [`@agentclientprotocol/sdk`](https://github.com/agentclientprotocol/typescript-sdk) | Apache-2.0 |
|
||||
| [`@anthropic-ai/claude-agent-sdk`](https://github.com/anthropics/claude-agent-sdk-typescript) | SEE LICENSE IN README.md |
|
||||
| [`@anthropic-ai/sdk`](https://github.com/anthropics/anthropic-sdk-typescript) | MIT |
|
||||
| [`@babel/code-frame`](https://github.com/babel/babel) | MIT |
|
||||
| [`@clack/core`](https://github.com/bombshell-dev/clack) | MIT |
|
||||
| [`@clack/prompts`](https://github.com/bombshell-dev/clack) | MIT |
|
||||
@@ -95,6 +97,24 @@ pnpm applies local patches to the following packages at install time, so shipped
|
||||
|
||||
- `node-pty@1.1.0` — [`patches/node-pty@1.1.0.patch`](patches/node-pty@1.1.0.patch)
|
||||
|
||||
## Official Claude Code platform payloads
|
||||
|
||||
The project owner authorizes distribution of every version of the official `@anthropic-ai/claude-agent-sdk` package and the official Claude Code CLI/platform payloads that each version declares through `optionalDependencies`. This identity-scoped authorization does not classify their declared terms as permissive and does not cover any unrelated runtime package; version, declared-license, and payload-set changes still require the ordinary dependency, lockfile, compatibility, terms, and notices review.
|
||||
|
||||
The installed SDK 0.3.220 declares the following optional platform packages. Each carries the official Claude Code 2.1.220 executable; the package identities and versions come from the SDK manifest, while the declared license field is verified against the platform payload installed for the current host.
|
||||
|
||||
| Optional platform package | Version | Declared license |
|
||||
| --- | --- | --- |
|
||||
| [`@anthropic-ai/claude-agent-sdk-darwin-arm64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-darwin-arm64) | 0.3.220 | SEE LICENSE IN LICENSE.md |
|
||||
| [`@anthropic-ai/claude-agent-sdk-darwin-x64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-darwin-x64) | 0.3.220 | SEE LICENSE IN LICENSE.md |
|
||||
| [`@anthropic-ai/claude-agent-sdk-linux-arm64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-linux-arm64) | 0.3.220 | SEE LICENSE IN LICENSE.md |
|
||||
| [`@anthropic-ai/claude-agent-sdk-linux-arm64-musl`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-linux-arm64-musl) | 0.3.220 | SEE LICENSE IN LICENSE.md |
|
||||
| [`@anthropic-ai/claude-agent-sdk-linux-x64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-linux-x64) | 0.3.220 | SEE LICENSE IN LICENSE.md |
|
||||
| [`@anthropic-ai/claude-agent-sdk-linux-x64-musl`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-linux-x64-musl) | 0.3.220 | SEE LICENSE IN LICENSE.md |
|
||||
| [`@anthropic-ai/claude-agent-sdk-win32-arm64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-win32-arm64) | 0.3.220 | SEE LICENSE IN LICENSE.md |
|
||||
| [`@anthropic-ai/claude-agent-sdk-win32-x64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-win32-x64) | 0.3.220 | SEE LICENSE IN LICENSE.md |
|
||||
|
||||
|
||||
## Development-only npm dependencies
|
||||
|
||||
External packages **directly declared** only by repository tooling, test infrastructure, the documentation site, the demo leaves, or the native launcher's build workspace. No shipped surface names them itself. A package here may still be pulled in transitively by a runtime dependency — `pnpm-lock.yaml` is the authority on the full closure — so this tier records who declares a package, not what a build ultimately bundles.
|
||||
@@ -104,6 +124,7 @@ External packages **directly declared** only by repository tooling, test infrast
|
||||
| [`@braintree/sanitize-url`](https://github.com/braintree/sanitize-url) | MIT |
|
||||
| [`@modelcontextprotocol/server-everything`](https://github.com/modelcontextprotocol/servers) | MIT / Apache-2.0 |
|
||||
| [`@modelcontextprotocol/server-filesystem`](https://github.com/modelcontextprotocol/servers) | MIT / Apache-2.0 |
|
||||
| [`@openai/codex`](https://github.com/openai/codex) | Apache-2.0 |
|
||||
| [`@stylistic/eslint-plugin`](https://github.com/eslint-stylistic/eslint-stylistic) | MIT |
|
||||
| [`@testing-library/dom`](https://github.com/testing-library/dom-testing-library) | MIT |
|
||||
| [`@testing-library/react`](https://github.com/testing-library/react-testing-library) | MIT |
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
# Opt-in two-tool profile over the shipped Web composition. The default native
|
||||
# model surface is exactly persistent `bash` plus `str_replace_editor`; the
|
||||
# Web host, browser shell, workspace, persistence, and permission stack remain.
|
||||
# Opt-in Web shell for the RL core agent contract. The model receives exactly
|
||||
# the configured persona plus the native `bash` and `str_replace_editor`
|
||||
# schemas; the Web host, browser shell, persistence, and permission stack stay.
|
||||
|
||||
# Match the Claude SWE-compatible RL core prompt. Disabling the Web runtime's
|
||||
# surface context removes its GUI orientation, managed shell variables, and the
|
||||
# launcher's source-checkout section through one configuration contract.
|
||||
# Workspace instructions are model-visible user context rather than a system
|
||||
# section, but RL core disables them as part of the same prompt contract.
|
||||
- id: system-prompt
|
||||
config:
|
||||
includeHarnessIdentity: false
|
||||
persona: !!js process.env.DSH_SYSTEM_PROMPT ?? 'You are a helpful software engineer assistant.'
|
||||
|
||||
- id: web-runtime
|
||||
config:
|
||||
surfaceContext: false
|
||||
|
||||
- id: workspace-context
|
||||
disabled: true
|
||||
|
||||
- id: tools
|
||||
config:
|
||||
mode: native
|
||||
|
||||
# Disable every model-facing consumer in the base/Web tree. plan-mode owns the
|
||||
# always-registered exit_plan_mode tool even while the session is not planning.
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write apps/cli/reference/README.md
|
||||
README.md: 62275139809364d80296433e804ade3d447f37e6
|
||||
README.zh.md: e508caaf05622c0c177eaacd070978368ff84af3
|
||||
README.md: 8b8a0e7dbebafedd6a4f8d988adb3fd11c7bd026
|
||||
README.zh.md: d1d6d5a594596a8be5db30021163f0fcea4a95bf
|
||||
@@ -53,7 +53,9 @@ All modes treat the invoking directory as the default workspace root, load appli
|
||||
|
||||
New sessions default to the `workspace-write` permission preset. Bash and filesystem mutations are restricted to the session workspace and platform temporary roots; reads, network access, and process visibility are not confined. `DSH_PERMISSION_MODE` changes the process fallback. Stored General-settings permissions affect later Web sessions, not an already-open one.
|
||||
|
||||
`DSH_TOOLS_MODE` selects `native`, `code`, or `both` for the process; another value fails at boot. [`config/core-web.cordis.yml`](../config/core-web.cordis.yml) is an optional `--patch` overlay that reduces the native model surface to persistent `bash` and `str_replace_editor` while retaining the shipped host, browser, workspace, persistence, and permission composition.
|
||||
`DSH_TOOLS_MODE` selects `native`, `code`, or `both` for the process; another value fails at boot. [`config/core-web.cordis.yml`](../config/core-web.cordis.yml) is an optional RL-compatible `--patch` overlay that pins native mode, renders only `DSH_SYSTEM_PROMPT` or `You are a helpful software engineer assistant.` as the system prompt, disables Workspace instructions and every Web runtime prompt contribution, and exposes only persistent `bash` and `str_replace_editor` while retaining the shipped host, browser, workspace, persistence, and permission composition.
|
||||
|
||||
`DSH_SYSTEM_PROMPT` is passed as the system-prompt [`persona`](../../../packages/core/system-prompt/README.md#config): complete `{{…}}` groups use that contract's strict variable interpolation and have no literal-brace escape; any set value, including an empty string, is authoritative and an empty value therefore removes the system prompt, while only an unset variable selects the fallback.
|
||||
|
||||
## Shared deployment behavior
|
||||
|
||||
|
||||
@@ -53,7 +53,9 @@ dsh web --dump-config
|
||||
|
||||
新会话默认使用 `workspace-write` 权限预设。Bash 和文件系统修改仅限于会话 workspace 与平台临时根目录;读取、网络访问和进程可见性不受限制。`DSH_PERMISSION_MODE` 更改进程后备值。General settings 中存储的权限影响后续 Web 会话,不改变已打开的会话。
|
||||
|
||||
`DSH_TOOLS_MODE` 为进程选择 `native`、`code` 或 `both`;其他值会导致启动失败。[`config/core-web.cordis.yml`](../config/core-web.cordis.yml) 是可选的 `--patch` overlay:它在保留随附宿主、浏览器、workspace、持久化和权限组合的同时,把原生模型 surface 缩减为持久 `bash` 和 `str_replace_editor`。
|
||||
`DSH_TOOLS_MODE` 为进程选择 `native`、`code` 或 `both`;其他值会导致启动失败。[`config/core-web.cordis.yml`](../config/core-web.cordis.yml) 是可选的 RL 兼容 `--patch` overlay:它固定使用 `native` 模式,仅将 `DSH_SYSTEM_PROMPT` 或 `You are a helpful software engineer assistant.` 渲染为系统提示词,禁用 Workspace 指令与所有 Web 运行时提示词贡献,并且在保留随附宿主、浏览器、workspace、持久化和权限组合的同时,仅暴露持久 `bash` 和 `str_replace_editor`。
|
||||
|
||||
`DSH_SYSTEM_PROMPT` 会传给系统提示词的 [`persona`](../../../packages/core/system-prompt/README.md#config):完整的 `{{…}}` 分组遵循该契约的严格变量插值规则,且无法转义为字面花括号;任何已设置的值(包括空字符串)都具有权威性,因此空值会移除系统提示词,只有未设置该变量时才会选择后备值。
|
||||
|
||||
## 共享部署行为
|
||||
|
||||
|
||||
@@ -95,6 +95,9 @@ export function prepareProfile(name: string, userLayer = true): Profile {
|
||||
return profile
|
||||
}
|
||||
|
||||
/** Read-only row index of a profile composition before launcher flag patches. */
|
||||
export type ProfileRows = ReadonlyMap<string, { name?: string; config?: unknown }>
|
||||
|
||||
/** One profile's patch layers (application order) and the row index of its pre-flag composition. */
|
||||
interface ComposedProfile {
|
||||
profile: Profile
|
||||
@@ -109,7 +112,7 @@ interface ComposedProfile {
|
||||
* for flag merges and row checks. Flag patches must not insert rows the
|
||||
* launcher consults here (they only override values and insert dev glue).
|
||||
*/
|
||||
rows: Map<string, { name?: string; config?: unknown }>
|
||||
rows: ProfileRows
|
||||
}
|
||||
|
||||
/** The full patch stack of one composed profile, in application order. */
|
||||
@@ -155,11 +158,11 @@ export interface RunProfileOptions {
|
||||
/** `--patch` overlay paths, in argv order. */
|
||||
patchFiles: readonly string[]
|
||||
/** Launcher hook turning the pre-flag composed rows into flag patches (the web alias's flag family). */
|
||||
deriveFlagPatches?: (rows: ComposedProfile['rows']) => PatchOptions[]
|
||||
deriveFlagPatches?: (rows: ProfileRows) => PatchOptions[]
|
||||
/** One-shot task text; requires the composition to mount the headless runner row. */
|
||||
task?: string
|
||||
/** Surface setup registered after Loader installation and before any config-tree entry mounts. */
|
||||
prepare?: (ctx: Context) => Promise<void> | void
|
||||
prepare?: (ctx: Context, rows: ProfileRows) => Promise<void> | void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -231,7 +234,7 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
}
|
||||
hostCtx.provide('headlessIo', io)
|
||||
}
|
||||
await options.prepare?.(hostCtx)
|
||||
await options.prepare?.(hostCtx, composed.rows)
|
||||
})
|
||||
app.current = ctx
|
||||
// A surface can dispose the whole tree while startup was still in flight
|
||||
|
||||
+16
-3
@@ -13,7 +13,7 @@ import { fileURLToPath } from 'node:url'
|
||||
import type { Context } from 'cordis'
|
||||
import type { PatchOptions } from '@cordisjs/plugin-include'
|
||||
import { addHarnessSourceSection } from '@deepseek-ai/dsh-app-boot'
|
||||
import { runProfile } from './profile-boot.ts'
|
||||
import { runProfile, type ProfileRows } from './profile-boot.ts'
|
||||
|
||||
const SOURCE_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
|
||||
|
||||
@@ -71,7 +71,7 @@ export interface WebFlags {
|
||||
* @returns the flag patch list, in application order.
|
||||
*/
|
||||
function deriveWebFlagPatches(
|
||||
rows: Map<string, { name?: string; config?: unknown }>,
|
||||
rows: ProfileRows,
|
||||
flags: WebFlags,
|
||||
): PatchOptions[] {
|
||||
const overrides = new Map<string, Record<string, unknown>>()
|
||||
@@ -105,6 +105,18 @@ function deriveWebFlagPatches(
|
||||
return patches
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the composed Web runtime keeps its model- and shell-visible surface
|
||||
* context. The bundle schema defaults the field to true, so only an explicit
|
||||
* false suppresses both the bundle contributions and the launcher-owned
|
||||
* source-checkout section.
|
||||
* @param rows - the composed Web profile rows before launcher flag patches.
|
||||
* @returns true unless the web-runtime row explicitly disables surface context.
|
||||
*/
|
||||
export function webSurfaceContextEnabled(rows: ProfileRows): boolean {
|
||||
return (rows.get('web-runtime')?.config as { surfaceContext?: boolean } | undefined)?.surfaceContext !== false
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve the browser UI from the web profile. Host/port/workspace-root flags
|
||||
* are passed through only when given (absent, the composed profile values
|
||||
@@ -118,7 +130,8 @@ export async function runWeb(flags: WebFlags): Promise<void> {
|
||||
profile: 'web',
|
||||
patchFiles: flags.patches,
|
||||
deriveFlagPatches: rows => deriveWebFlagPatches(rows, flags),
|
||||
prepare: (ctx: Context) => {
|
||||
prepare: (ctx: Context, rows: ProfileRows) => {
|
||||
if (!webSurfaceContextEnabled(rows)) return
|
||||
ctx.inject(['systemPrompt'], (promptCtx) => {
|
||||
addHarnessSourceSection(promptCtx, SOURCE_ROOT)
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
/** Published-entry acceptance for argument errors, profile lifecycle, and boot-free config dumps. */
|
||||
const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
|
||||
const dshBin = join(repoRoot, 'apps/cli/lib/bin.js')
|
||||
const coreWebOverlay = fileURLToPath(new URL('../config/core-web.cordis.yml', import.meta.url))
|
||||
const invalidProvider = fileURLToPath(new URL('./fixtures/invalid-provider.cordis.yml', import.meta.url))
|
||||
|
||||
async function runBuiltBin(
|
||||
@@ -367,5 +368,16 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
expect(stdout).toContain(`patched by ${profilePatch}, ${overlay}`)
|
||||
expect(stderr).toContain('patch: entry "absent-row" not found')
|
||||
}, 30_000)
|
||||
|
||||
it('shows the RL Web patch disabling runtime surface context', async () => {
|
||||
const { stdout, code, stderr } = await runBuiltBin(
|
||||
['web', '--patch', coreWebOverlay, '--dump-config'],
|
||||
{ DSH_HOME: home },
|
||||
)
|
||||
expect(code).toBe(0)
|
||||
expect(stderr).toBe('')
|
||||
expect(stdout).toContain("name: '@deepseek-ai/dsh-web-app'")
|
||||
expect(stdout).toContain('surfaceContext: false')
|
||||
}, 30_000)
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
/** Single-sample LAN-trust resolution for the /api browser-trust fence (`resolveLanTrust`). */
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { resolveLanTrust } from '../src/web.ts'
|
||||
import { resolveLanTrust, webSurfaceContextEnabled } from '../src/web.ts'
|
||||
|
||||
vi.mock('node:os', () => ({
|
||||
networkInterfaces: () => ({
|
||||
@@ -31,3 +31,15 @@ describe('resolveLanTrust', () => {
|
||||
expect(resolveLanTrust(undefined, ['lab.internal'])).toEqual({ lanAddresses: [], trustedHosts: ['lab.internal'] })
|
||||
})
|
||||
})
|
||||
|
||||
describe('webSurfaceContextEnabled', () => {
|
||||
it('defaults to enabled and honors an explicit complete-prompt disable', () => {
|
||||
expect(webSurfaceContextEnabled(new Map())).toBe(true)
|
||||
expect(webSurfaceContextEnabled(new Map([
|
||||
['web-runtime', { config: { mode: 'production' } }],
|
||||
]))).toBe(true)
|
||||
expect(webSurfaceContextEnabled(new Map([
|
||||
['web-runtime', { config: { surfaceContext: false } }],
|
||||
]))).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -3,21 +3,27 @@ import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||
import type { AgentHandle } from '@deepseek-ai/dsh-agent'
|
||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import { CallId, createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { launchWebScaffold, type WebScaffold } from './scaffold.ts'
|
||||
import { assertFixtureInventory, launchWebScaffold, type WebScaffold } from './scaffold.ts'
|
||||
|
||||
const CORE_WEB_OVERLAY = fileURLToPath(new URL('../../cli/config/core-web.cordis.yml', import.meta.url))
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/core-web-profile', import.meta.url))
|
||||
const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
|
||||
const PROMPT = 'Reply exactly CORE_WEB_REQUEST_OK and stop.'
|
||||
|
||||
describe('core Web profile', () => {
|
||||
let scaffold: WebScaffold
|
||||
let agentHandle: AgentHandle
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({
|
||||
extraOverlayPath: CORE_WEB_OVERLAY,
|
||||
toolsMode: 'native',
|
||||
})
|
||||
const systemPrompt = process.env.DSH_SYSTEM_PROMPT
|
||||
Reflect.deleteProperty(process.env, 'DSH_SYSTEM_PROMPT')
|
||||
try {
|
||||
scaffold = await launchWebScaffold({ extraOverlayPath: CORE_WEB_OVERLAY, replayFixture: FIXTURE })
|
||||
} finally {
|
||||
if (systemPrompt !== undefined) process.env.DSH_SYSTEM_PROMPT = systemPrompt
|
||||
}
|
||||
agentHandle = await scaffold.ctx.agents.create({
|
||||
sessionId: SessionId('core-web-profile-smoke'),
|
||||
meta: { cwd: scaffold.workspaceCwd },
|
||||
@@ -33,7 +39,16 @@ describe('core Web profile', () => {
|
||||
if (failures.length > 1) throw new AggregateError(failures, 'core Web profile smoke teardown failed')
|
||||
})
|
||||
|
||||
it('boots and executes both tools through the shipped Web composition', async () => {
|
||||
it('sends the RL prompt and tool schemas through a real request, then executes both tools', async () => {
|
||||
agentHandle.agent.followup(createUserMessage({
|
||||
content: [{ type: 'text', text: PROMPT }],
|
||||
source: { kind: 'user' },
|
||||
}))
|
||||
await agentHandle.agent.whenIdle()
|
||||
|
||||
const requestHeader = agentHandle.agent.session.requestHeader()
|
||||
if (requestHeader === undefined) throw new Error('the core Web agent issued no model request')
|
||||
|
||||
const seedPath = join(scaffold.workspaceCwd, 'profile-smoke.txt')
|
||||
await writeFile(seedPath, 'CORE_WEB_EDITOR_OK\n')
|
||||
const signal = new AbortController().signal
|
||||
@@ -60,7 +75,8 @@ describe('core Web profile', () => {
|
||||
.trimEnd()
|
||||
|
||||
expect({
|
||||
tools: scaffold.ctx.tools.schemas().map(tool => tool.name),
|
||||
prompt: requestHeader.system,
|
||||
tools: requestHeader.tools?.map(tool => tool.name),
|
||||
bash: text(bash),
|
||||
editor: text(editor),
|
||||
}).toMatchInlineSnapshot(`
|
||||
@@ -69,16 +85,53 @@ describe('core Web profile', () => {
|
||||
"editor": "Here's the content of {{cwd}}/profile-smoke.txt with line numbers (which has a total of 2 lines):
|
||||
1 CORE_WEB_EDITOR_OK
|
||||
2",
|
||||
"prompt": "You are a helpful software engineer assistant.",
|
||||
"tools": [
|
||||
"bash",
|
||||
"str_replace_editor",
|
||||
],
|
||||
}
|
||||
`)
|
||||
expect(requestHeader.tools).toEqual(scaffold.ctx.tools.schemas(agentHandle.agent))
|
||||
|
||||
const entries = [...scaffold.ctx.loader.entries()]
|
||||
expect(entries.find(entry => entry.options.id === 'persistent-bash')?.fiber).toBeDefined()
|
||||
expect(entries.find(entry => entry.options.id === 'pty-local')?.fiber).toBeDefined()
|
||||
expect(entries.find(entry => entry.options.id === 'str-replace-editor')?.fiber).toBeDefined()
|
||||
expect(entries.find(entry => entry.options.id === 'web-runtime')?.fiber).toBeDefined()
|
||||
expect(entries.find(entry => entry.options.id === 'workspace-context')?.fiber).toBeUndefined()
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['session.jsonl'])
|
||||
})
|
||||
|
||||
it('uses DSH_SYSTEM_PROMPT as the complete prompt when configured', async () => {
|
||||
const previous = process.env.DSH_SYSTEM_PROMPT
|
||||
process.env.DSH_SYSTEM_PROMPT = 'RL prompt override'
|
||||
let overrideScaffold: WebScaffold | undefined
|
||||
let overrideAgent: AgentHandle | undefined
|
||||
try {
|
||||
overrideScaffold = await launchWebScaffold({ extraOverlayPath: CORE_WEB_OVERLAY, replayFixture: FIXTURE })
|
||||
overrideAgent = await overrideScaffold.ctx.agents.create({
|
||||
sessionId: SessionId('core-web-profile-override'),
|
||||
meta: { cwd: overrideScaffold.workspaceCwd },
|
||||
agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
||||
})
|
||||
overrideAgent.agent.followup(createUserMessage({
|
||||
content: [{ type: 'text', text: PROMPT }],
|
||||
source: { kind: 'user' },
|
||||
}))
|
||||
await overrideAgent.agent.whenIdle()
|
||||
expect(overrideAgent.agent.session.requestHeader()?.system).toBe('RL prompt override')
|
||||
} finally {
|
||||
try {
|
||||
await overrideAgent?.dispose()
|
||||
} finally {
|
||||
try {
|
||||
await overrideScaffold?.close()
|
||||
} finally {
|
||||
if (previous === undefined) Reflect.deleteProperty(process.env, 'DSH_SYSTEM_PROMPT')
|
||||
else process.env.DSH_SYSTEM_PROMPT = previous
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -161,6 +161,58 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('never paints the takeover chrome on a configured reload, even with the settings join held open', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-configured-reload'))
|
||||
// Regression pin for the reload white flash: both steps are satisfied
|
||||
// (welcome acknowledged, credential configured), yet each must LOAD its
|
||||
// private join before it can decide not to show. The chrome lives inside
|
||||
// the step (OnboardingSurface), so the deciding window paints and blocks
|
||||
// nothing. Holding settings.describe widens that window from loopback
|
||||
// RTT scale to a deterministic hundreds of milliseconds, removing all
|
||||
// timing dependence from the sampler assertions below.
|
||||
//
|
||||
// The sampler init script persists across this shared page's later
|
||||
// navigations (init scripts re-run per navigation); that stays harmless
|
||||
// because no later scenario in this file legitimately shows the
|
||||
// takeover, and only this test reads __takeoverSightings.
|
||||
await page.addInitScript(() => {
|
||||
const sightings: string[] = []
|
||||
;(window as unknown as { __takeoverSightings: string[] }).__takeoverSightings = sightings
|
||||
setInterval(() => {
|
||||
if (document.querySelector('[class*="onboardingStage"], [class*="onboardingMask"]') !== null) {
|
||||
sightings.push('chrome')
|
||||
}
|
||||
if (document.getElementById('root')?.inert === true) sightings.push('inert')
|
||||
}, 8)
|
||||
})
|
||||
// EVERY settings.describe issued before the release is held — not just
|
||||
// the first — so the pin cannot silently collapse back to loopback
|
||||
// timing if a second boot-time consumer of the join ever appears.
|
||||
let released = false
|
||||
const heldRoutes: Array<() => void> = []
|
||||
const releaseDescribe = (): void => {
|
||||
released = true
|
||||
for (const resolve of heldRoutes.splice(0)) resolve()
|
||||
}
|
||||
await page.route('**/api/settings.describe', async (route) => {
|
||||
if (!released) await new Promise<void>((resolve) => { heldRoutes.push(resolve) })
|
||||
await route.continue()
|
||||
})
|
||||
const warningsBefore = tripwire.warnings.length
|
||||
await page.reload({ waitUntil: 'commit' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 15_000 })
|
||||
// The app is painted and interactive while the steps are still deciding.
|
||||
await page.waitForTimeout(600)
|
||||
releaseDescribe()
|
||||
await page.waitForTimeout(400)
|
||||
await page.unroute('**/api/settings.describe')
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningsBefore)
|
||||
expect(await page.evaluate(() =>
|
||||
(window as unknown as { __takeoverSightings: string[] }).__takeoverSightings)).toEqual([])
|
||||
expect(await page.locator('[class*="onboardingStage"]').count()).toBe(0)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('configures arbitrary DeepSeek models and prompts after the selected model is removed', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-deepseek-models'))
|
||||
// Opened here rather than inherited: the credential test reloads the page
|
||||
|
||||
@@ -33,7 +33,13 @@ import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import Include, { type PatchOptions } from '@cordisjs/plugin-include'
|
||||
import { scrubRequestHeaders } from '@deepseek-ai/dsh-acp-snapshot'
|
||||
import { assertEntriesLoaded, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
|
||||
import {
|
||||
addHarnessSourceSection,
|
||||
assertEntriesLoaded,
|
||||
composeEntries,
|
||||
healProfilesModuleFallback,
|
||||
loadOverlayPatches,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { dshHomePath } from '@deepseek-ai/dsh-paths'
|
||||
import {
|
||||
WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_SETTINGS_NAMESPACE, WELCOME_NOTICE_VERSION,
|
||||
@@ -54,7 +60,6 @@ import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
|
||||
// Empty type imports carry the httpServer/agents/sessionPersistence Context merges.
|
||||
import type {} from '@deepseek-ai/dsh-host-webserver'
|
||||
import type {} from '@deepseek-ai/dsh-agent'
|
||||
import { addHarnessSourceSection, healProfilesModuleFallback } from '@deepseek-ai/dsh-app-boot'
|
||||
import { REPO_ROOT, requireDist } from './support.ts'
|
||||
|
||||
/** Snapshot mode for the lane, from $DSH_SNAPSHOT (same vocabulary as the other snapshot suites). */
|
||||
@@ -252,6 +257,11 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
const extraOverlayPatches = options.extraOverlayPath === undefined
|
||||
? []
|
||||
: loadOverlayPatches('web e2e scaffold', options.extraOverlayPath)
|
||||
const composedRows = composeEntries([basePatches, surfacePatches, extraOverlayPatches])
|
||||
const webRuntimeConfig = composedRows.find(row => row.id === 'web-runtime')?.config as {
|
||||
surfaceContext?: boolean
|
||||
} | undefined
|
||||
const surfaceContext = webRuntimeConfig?.surfaceContext !== false
|
||||
const patches: PatchOptions[] = [
|
||||
...basePatches,
|
||||
...surfacePatches,
|
||||
@@ -290,7 +300,9 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
},
|
||||
// The bundle's web-runtime row resolves the same built dist under test
|
||||
// (apps/web IS @deepseek-ai/dsh-frontend); only the URL line is silenced.
|
||||
{ id: 'web-runtime', config: { mode: 'production', printUrl: false } },
|
||||
// Preserve the composed surface-context choice because a patch replaces
|
||||
// the row's complete config.
|
||||
{ id: 'web-runtime', config: { mode: 'production', printUrl: false, surfaceContext } },
|
||||
...options.remoteAuthority === undefined
|
||||
? []
|
||||
: [{ id: 'connection', config: { trustedHosts: [options.remoteAuthority] } }],
|
||||
@@ -346,7 +358,9 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
// The shipped CLI deliberately has no dependency on this opt-in package.
|
||||
// Keep the Loader row real without broadening the product installation.
|
||||
if (options.cordisTools === true) ctx.loader.builtins['tool-cordis'] = ToolCordis
|
||||
ctx.inject(['systemPrompt'], (promptCtx) => { addHarnessSourceSection(promptCtx, REPO_ROOT) })
|
||||
if (surfaceContext) {
|
||||
ctx.inject(['systemPrompt'], (promptCtx) => { addHarnessSourceSection(promptCtx, REPO_ROOT) })
|
||||
}
|
||||
await ctx.loader.create({
|
||||
name: 'cordis:include',
|
||||
config: { path: pathToFileURL(rootConfig).href, patches },
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785974400000,"cwd":"{{cwd}}"}
|
||||
{"type":"user/message","seq":0,"time":1785974400001,"data":{"content":[{"type":"text","text":"Reply exactly CORE_WEB_REQUEST_OK and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
|
||||
{"type":"assistant/chunk","seq":1,"time":1785974400002,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":2,"time":1785974400003,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"CORE_WEB_REQUEST_OK"}}}
|
||||
{"type":"assistant/chunk","seq":3,"time":1785974400004,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CORE_WEB_REQUEST_OK"}}}}
|
||||
{"type":"assistant/chunk","seq":4,"time":1785974400005,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":4}}}}
|
||||
{"type":"assistant/chunk","seq":5,"time":1785974400006,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/architecture.md
|
||||
architecture.md: 40c20a1c9eeabe5ecbbc6edacde81c20071b8a04
|
||||
architecture.zh.md: 6fddaa883775cf8345aba01af52575c0f0e1aaa0
|
||||
architecture.md: 81464d9c8800556565c84d33239882dc750180a8
|
||||
architecture.zh.md: c02bca4f12c3758723b0fc818c89080dccf435d9
|
||||
@@ -156,9 +156,9 @@ Streaming uses raw chunks and `BlockAssembler`. Each `LlmAdapter.stream()` is on
|
||||
|
||||
A swappable capability usually has **interface / implementation / consumer** layers: service/events, backend, and model-facing tools/prompts. Bash is the reference; the [capability graph](capability-seams.md) maps each family.
|
||||
|
||||
Exceptions combine LLM interface/consumer, filesystem policy, web registries, and named skill/subagent providers. Subagents spawn fresh, fork a completed-turn prefix, or use ACP children ([subagent.md](core-data-structures/subagent.md)).
|
||||
Exceptions combine LLM interface/consumer, filesystem policy, web registries, and named skill/subagent providers. Subagents spawn fresh, fork a completed-turn prefix, use ACP children, or delegate one self-contained turn to a real product provider such as Codex ([subagent.md](core-data-structures/subagent.md)).
|
||||
|
||||
`dsh-workspace-context` composes its baseline on the first `agent/pre-step` and folds it into the final entering batch right after the claimed prompt, so it reaches the first request with the direct prompt; rejection keeps it in the next-step inbox. Filesystem changes projected after tools are likewise folded into the next entering pre-step instead of creating a later context-only step ([decision](../.agents/notes/implemented/feature/2026-06-24-workspace-context.md)). `dsh-paths` owns shared paths.
|
||||
`dsh-workspace-context` composes its baseline on the first `agent/pre-step` and folds it into the final entering batch right after the claimed prompt, so it reaches the first request with the direct prompt; rejection keeps it in the next-step inbox. When compaction removes that baseline from the visible surface, the next entering pre-step composes the current baseline and carries it in the same request. Filesystem changes projected after tools are likewise folded into the next entering pre-step instead of creating a later context-only step ([decision](../.agents/notes/implemented/feature/2026-06-24-workspace-context.md)). `dsh-paths` owns shared paths.
|
||||
|
||||
### Bundles And Apps
|
||||
|
||||
|
||||
@@ -156,9 +156,9 @@ idle inject:
|
||||
|
||||
可替换功能通常具有**接口/实现/消费方**三层:服务和事件、后端、面向模型的工具和提示词。Bash 是参考实现;[功能图](capability-seams.md)映射了每个包族。
|
||||
|
||||
例外情况包括 LLM(大语言模型)合并接口和消费方、文件系统整合策略、web 使用注册表、skill 和 subagent 使用具名提供方。subagent 可以通过 spawn 创建全新实例、fork 一个已完成轮次的前缀,或使用 ACP(Agent Client Protocol)子 agent([subagent.md](core-data-structures/subagent.md))。
|
||||
例外情况包括 LLM(大语言模型)合并接口和消费方、文件系统整合策略、web 使用注册表、skill 和 subagent 使用具名提供方。subagent 可以通过 spawn 创建全新实例、fork 一个已完成轮次的前缀、使用 ACP(Agent Client Protocol)子 agent,或将一个独立完整的轮次委派给 Codex 等真实产品提供方([subagent.md](core-data-structures/subagent.md))。
|
||||
|
||||
`dsh-workspace-context` 在第一次 `agent/pre-step` 组合基线并将它折入最终进入的批次、紧随已领取的直接提示词之后,使其与直接提示词一同抵达第一次请求;reject 则将它留在 next-step inbox。工具执行后投影的文件系统变更也会折入下一次进入步骤的 pre-step,而不会另外创建稍后的纯上下文步骤([决策](../.agents/notes/implemented/feature/2026-06-24-workspace-context.md))。`dsh-paths` 负责共享路径。
|
||||
`dsh-workspace-context` 在第一次 `agent/pre-step` 组合基线并将它折入最终进入的批次、紧随已领取的直接提示词之后,使其与直接提示词一同抵达第一次请求;reject 则将它留在 next-step inbox。当压缩从可见表层移除该基线时,下一次进入步骤的 pre-step 会组合当前基线,并在同一请求中携带它。工具执行后投影的文件系统变更也会折入下一次进入步骤的 pre-step,而不会另外创建稍后的纯上下文步骤([决策](../.agents/notes/implemented/feature/2026-06-24-workspace-context.md))。`dsh-paths` 负责共享路径。
|
||||
|
||||
### 组合包与应用
|
||||
|
||||
|
||||
@@ -105,6 +105,8 @@ flowchart LR
|
||||
pkg_bash_sandbox["bash-sandbox"]
|
||||
pkg_lsp_local["lsp-local"]
|
||||
pkg_subagent_acp["subagent-acp"]
|
||||
pkg_subagent_codex["subagent-codex"]
|
||||
pkg_subagent_claude_code["subagent-claude-code"]
|
||||
pkg_bash["bash"]
|
||||
svc_bash["ctx.bash<br/>Bash executor seam"]
|
||||
pkg_pwsh_local["pwsh-local"]
|
||||
@@ -137,6 +139,7 @@ flowchart LR
|
||||
svc_subagents["ctx.subagents<br/>Subagent provider and continuation service"]
|
||||
pkg_subagent_spawn["subagent-spawn"]
|
||||
pkg_subagent_fork["subagent-fork"]
|
||||
pkg_subagent_dsh_sdk["subagent-dsh-sdk"]
|
||||
pkg_tool_subagent_control["tool-subagent-control"]
|
||||
pkg_tool_ralph["tool-ralph"]
|
||||
pkg_tasks["tasks"]
|
||||
@@ -231,6 +234,9 @@ flowchart LR
|
||||
pkg_storage_sqlite --> svc_storage
|
||||
pkg_subagent --> svc_subagents
|
||||
pkg_subagent_acp --> svc_subagents
|
||||
pkg_subagent_claude_code --> svc_subagents
|
||||
pkg_subagent_codex --> svc_subagents
|
||||
pkg_subagent_dsh_sdk --> svc_subagents
|
||||
pkg_subagent_fork --> svc_subagents
|
||||
pkg_subagent_spawn --> svc_subagents
|
||||
pkg_subprocess --> svc_subprocess
|
||||
@@ -321,6 +327,8 @@ flowchart LR
|
||||
svc_subprocess --> pkg_bash_sandbox
|
||||
svc_subprocess --> pkg_lsp_local
|
||||
svc_subprocess --> pkg_subagent_acp
|
||||
svc_subprocess --> pkg_subagent_claude_code
|
||||
svc_subprocess --> pkg_subagent_codex
|
||||
svc_systemPrompt --> pkg_agent_loop
|
||||
svc_systemPrompt --> pkg_tool_fs
|
||||
svc_systemPrompt --> pkg_tool_pty
|
||||
@@ -381,7 +389,7 @@ flowchart LR
|
||||
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`cli-demo`](../packages/examples/cli-demo), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. |
|
||||
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. |
|
||||
| `ctx.goals` | `core` | [`goal`](../packages/goal/goal) | - | - | - | Folds revisioned objective state from the session log and keeps live continuation activation process-local. |
|
||||
| `ctx.subprocess` | `seam` | [`subprocess`](../packages/subprocess/subprocess) | [`subprocess-local`](../packages/subprocess/subprocess-local) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox), [`lsp-local`](../packages/lsp/lsp-local), [`subagent-acp`](../packages/subagent/subagent-acp) | - | The bash executors, the LSP host, and the ACP subagent backend spawn their children through ctx.subprocess; the service owns tree lifetime, stdio dispositions (pipes, inherit, bounded spill-backed collection), and kill escalation. |
|
||||
| `ctx.subprocess` | `seam` | [`subprocess`](../packages/subprocess/subprocess) | [`subprocess-local`](../packages/subprocess/subprocess-local) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox), [`lsp-local`](../packages/lsp/lsp-local), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | - | The bash executors, the LSP host, and the out-of-process ACP, Codex, and Claude Code subagent backends spawn their children through ctx.subprocess; the service owns tree lifetime, stdio dispositions (pipes, inherit, bounded spill-backed collection), and kill escalation. |
|
||||
| `ctx.bash` | `seam` | [`bash`](../packages/bash/bash) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox), [`pwsh-local`](../packages/bash/pwsh-local) | [`tool-bash`](../packages/bash/tool-bash), [`tool-pwsh`](../packages/bash/tool-pwsh), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | - | The model-facing shell tools and hook bridges consume this seam; sandboxed, remote, or PowerShell executors replace bash-local without touching them. |
|
||||
| `ctx.bashEnv` | `core` | [`bash-env`](../packages/bash/bash-env) | - | [`tool-bash`](../packages/bash/tool-bash), [`tool-pwsh`](../packages/bash/tool-pwsh) | - | Plugins declare effect-scoped DSH_* facts; each shell tool collects one trusted snapshot per execution and its executor rebuilds the namespace. |
|
||||
| `ctx.pty` | `seam` | [`pty`](../packages/pty/pty) | [`pty-local`](../packages/pty/pty-local) | [`tool-pty`](../packages/pty/tool-pty) | - | The registry owns exact-Agent session identity and cleanup; backends own terminal mechanics, while tool-pty exposes the owner-scoped model surface. |
|
||||
@@ -392,7 +400,7 @@ flowchart LR
|
||||
| `ctx.codeRuntime` | `seam` | [`code-runtime`](../packages/code-runtime/code-runtime) | [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | [`tools`](../packages/core/tools) | - | Runs one model-written program against host-provided async bindings; backends differ by substrate and language (the tool registry consumes it for Code Mode). |
|
||||
| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-policy`](../packages/fs/fs-policy) | tool-fs executes read/write/edit through ctx.fs; fs-sandbox fences mutations by the shared sandbox mode; fs-policy contributes observed-state checks through the fs/* event gate. |
|
||||
| `ctx.compact` | `seam` | [`compact`](../packages/compact/compact) | [`compact-basic`](../packages/compact/compact-basic) | [`compact-basic`](../packages/compact/compact-basic) | - | The basic backend consumes post-step pressure and request-error recovery events; a model-facing compact tool remains deferred. |
|
||||
| `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn`](../packages/subagent/subagent-spawn), [`subagent-fork`](../packages/subagent/subagent-fork), [`subagent-acp`](../packages/subagent/subagent-acp) | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/workflow/tool-ralph) | - | Providers implement transports; the service also owns optional Activation-based continuation orchestration, tool-subagent selects one-shot or continuable delegation, tool-subagent-control delivers follow-ups, and tool-ralph requires one fresh structured-output route. |
|
||||
| `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn`](../packages/subagent/subagent-spawn), [`subagent-fork`](../packages/subagent/subagent-fork), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code), [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/workflow/tool-ralph) | - | Providers implement transports; the service also owns optional Activation-based continuation orchestration, tool-subagent selects one-shot or continuable delegation, tool-subagent-control delivers follow-ups, and tool-ralph requires one fresh structured-output route. |
|
||||
| `ctx.tasks` | `seam` | [`tasks`](../packages/tasks/tasks) | [`tasks-local`](../packages/tasks/tasks-local) | [`tool-bash`](../packages/bash/tool-bash), [`tool-pty`](../packages/pty/tool-pty), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-tasks`](../packages/tasks/tool-tasks) | - | Producers (background bash, PTY sends, and subagent delegations) register running work; tool-tasks is the model-facing control surface that reads, lists, and kills it; tasks-local is the process-local registry. |
|
||||
| `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-local`](../packages/web/web-fetch-local) | [`tool-web`](../packages/web/tool-web) | - | Search and fetch providers register into one ctx.web seam; tool-web owns the stable model-facing names. |
|
||||
| `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | The backend saves oversized tool text and returns a model-facing locator plus retrieval hint; spill-policy is the tools/post-execute consumer that decides when to spill. |
|
||||
|
||||
+49
-10
@@ -254,7 +254,7 @@ export interface Config {
|
||||
maxOutputBytes?: number
|
||||
/** Per-stream spill-file cap; larger streams retain only their in-memory tail. */
|
||||
maxSpillBytes?: number
|
||||
/** Grace period for kill escalation and for inherited pipes after shell exit. */
|
||||
/** Grace period for kill escalation and inherited pipes; at most `MAX_TIMER_DELAY_MS`. */
|
||||
graceMs?: number
|
||||
}
|
||||
```
|
||||
@@ -1096,7 +1096,7 @@ export interface Config {
|
||||
maxOutputBytes?: number
|
||||
/** Per-stream spill-file cap; larger streams retain only their in-memory tail. */
|
||||
maxSpillBytes?: number
|
||||
/** Grace period for kill escalation and for inherited pipes after shell exit. */
|
||||
/** Grace period for kill escalation and inherited pipes; at most `MAX_TIMER_DELAY_MS`. */
|
||||
graceMs?: number
|
||||
/**
|
||||
* Explicit pwsh executable. When omitted, well-known Windows install
|
||||
@@ -1659,10 +1659,11 @@ export interface Config {
|
||||
/**
|
||||
* Grace period (ms) for the child's EOF-driven quiesce on dispose — its
|
||||
* window to flush persistence and tear down its own nested subprocesses
|
||||
* before the parent escalates to a signal.
|
||||
* before the parent escalates to a signal. Must not exceed
|
||||
* `MAX_TIMER_DELAY_MS`.
|
||||
*/
|
||||
disposeEofGraceMs?: number
|
||||
/** Termination confirmation window (ms), including forced exit on every platform. */
|
||||
/** Termination-escalation grace (ms); must not exceed `MAX_TIMER_DELAY_MS`. */
|
||||
disposeGraceMs?: number
|
||||
}
|
||||
|
||||
@@ -1670,7 +1671,45 @@ export interface Config {
|
||||
export type PermissionPolicy = 'allow' | 'reject'
|
||||
```
|
||||
|
||||
Source: [`packages/subagent/subagent-acp/src/index.ts:26`](../packages/subagent/subagent-acp/src/index.ts)
|
||||
Source: [`packages/subagent/subagent-acp/src/index.ts:27`](../packages/subagent/subagent-acp/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-subagent-claude-code`
|
||||
|
||||
Requires: `subagents` · `subprocess`
|
||||
|
||||
```ts config-catalog
|
||||
/** Deployment-owned environment and process-release bound. */
|
||||
export interface Config {
|
||||
/**
|
||||
* Explicit environment entries layered over the subprocess seam's
|
||||
* credential-scrubbed parent environment.
|
||||
*/
|
||||
env?: Record<string, string>
|
||||
/** Grace in milliseconds for Claude Code process-tree termination. */
|
||||
disposeGraceMs?: number
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/subagent/subagent-claude-code/src/index.ts:32`](../packages/subagent/subagent-claude-code/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-subagent-codex`
|
||||
|
||||
Requires: `subagents` · `subprocess`
|
||||
|
||||
```ts config-catalog
|
||||
/** Deployment-owned environment and process-release bound. */
|
||||
export interface Config {
|
||||
/**
|
||||
* Explicit environment entries layered over the subprocess seam's
|
||||
* credential-scrubbed parent environment.
|
||||
*/
|
||||
env?: Record<string, string>
|
||||
/** Grace in milliseconds for app-server process-tree termination. */
|
||||
disposeGraceMs?: number
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/subagent/subagent-codex/src/index.ts:30`](../packages/subagent/subagent-codex/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-subagent-dsh-sdk`
|
||||
|
||||
@@ -1904,7 +1943,7 @@ export interface Config {
|
||||
searchMetaMaxBytes?: number
|
||||
/** Max complete raw `rg` stdout bytes a search will parse; larger raw output fails with `SEARCH_RAW_OUTPUT_OVERFLOW`. */
|
||||
rawOutputMaxBytes?: number
|
||||
/** Terminate-escalation grace period (ms) for one search process, handed to the subprocess seam. */
|
||||
/** Terminate-escalation grace (ms), handed to the subprocess seam and bounded by `MAX_TIMER_DELAY_MS`. */
|
||||
graceMs?: number
|
||||
/** Max bytes retained for one search's stderr tail; the excerpt is embedded in `SEARCH_*` error messages, never shown on success. */
|
||||
stderrMaxBytes?: number
|
||||
@@ -1913,7 +1952,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/fs/tool-fs-search/src/index.ts:72`](../packages/fs/tool-fs-search/src/index.ts)
|
||||
Source: [`packages/fs/tool-fs-search/src/index.ts:73`](../packages/fs/tool-fs-search/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-tool-goal`
|
||||
|
||||
@@ -2094,8 +2133,8 @@ export interface Config {
|
||||
* requires the provider's `depthLimit` capability (mount fails loud
|
||||
* otherwise). The provider checks the calling agent's current depth at every
|
||||
* start; the tool remains model-visible so runtime policy owns rejection.
|
||||
* `'provider-managed'` is for an out-of-process provider (ACP) whose
|
||||
* recursion budget belongs to the child harness's own deployment.
|
||||
* `'provider-managed'` is for an out-of-process provider whose recursion
|
||||
* budget belongs to the child runtime or its own deployment.
|
||||
*/
|
||||
maxDepth?: number | 'provider-managed'
|
||||
}
|
||||
@@ -2454,7 +2493,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/context/workspace-context/src/config.ts:17`](../packages/context/workspace-context/src/config.ts)
|
||||
Source: [`packages/context/workspace-context/src/config.ts:18`](../packages/context/workspace-context/src/config.ts)
|
||||
|
||||
## Loadable plugins with no config
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/cookbook/extension-cookbook.md
|
||||
extension-cookbook.md: 1d5705672396d66d5625567aefec5006ae126e67
|
||||
extension-cookbook.zh.md: 2f8e0ccb745baefb857f1065040f3225142d264f
|
||||
extension-cookbook.md: 26f20b8f6cb57103b8e7340fcd21089ff1b0e5f6
|
||||
extension-cookbook.zh.md: b8bafdb73e91b56dd411cc5e96c21ccf2c5db123
|
||||
@@ -117,7 +117,7 @@ Every product feature maps to a listener on a documented extension seam — the
|
||||
| Subprocess sandbox (landlock / sandbox-exec) | use a `ctx.sandbox` backend through `dsh-bash-sandbox`; use `tools/pre-execute` for capability-level denial |
|
||||
| Permission system / AskUserQuestion | return `ask` from `tools/pre-execute` and answer through `ctx.approval`; register a separate model-facing ask tool for ordinary user questions |
|
||||
| Plan mode | Shipped: [`@deepseek-ai/dsh-plan-mode`](../../packages/plan/plan-mode/README.md) — logged `plan/mode` state, the `plan:policy` guidance section, `/plan [message]` entry, `/plan off` direct exit, and the user-reviewed `exit_plan_mode` exit; enforcement stays on the independent sandbox/approval axes |
|
||||
| Sub-agent delegation | the `ctx.subagents` provider registry (`dsh-subagent-spawn`/`-fork`/`-acp`) + `dsh-tool-subagent` exposing one configured provider to the model |
|
||||
| Sub-agent delegation | the `ctx.subagents` provider registry (`dsh-subagent-spawn`/`-fork`/`-acp`/`-codex`/`-claude-code`/`-dsh-sdk`) + `dsh-tool-subagent` exposing one configured provider to the model |
|
||||
| MCP | one plugin per server: discover tools → `ctx.tools.register()` |
|
||||
| Skills | section + tool registration; `inject()` skill content on invocation |
|
||||
| Memory | section provider + tool |
|
||||
|
||||
@@ -117,7 +117,7 @@ export function apply(ctx: Context) {
|
||||
| 子进程沙箱(landlock / sandbox-exec) | 通过 `dsh-bash-sandbox` 使用 `ctx.sandbox` 后端;能力级别的拒绝使用 `tools/pre-execute` |
|
||||
| 权限系统 / AskUserQuestion | 从 `tools/pre-execute` 返回 `ask` 并通过 `ctx.approval` 应答;为普通用户提问注册一个独立的面向模型的 ask 工具 |
|
||||
| Plan mode | 已交付:[`@deepseek-ai/dsh-plan-mode`](../../packages/plan/plan-mode/README.md) — 落日志的 `plan/mode` 状态、`plan:policy` 引导段、`/plan [message]` 入口、`/plan off` 直接退出,以及经用户评审的 `exit_plan_mode` 出口;强制约束留在独立的沙箱/审批轴上 |
|
||||
| 子 agent 委派 | `ctx.subagents` 提供方注册表(`dsh-subagent-spawn`/`-fork`/`-acp`)+ `dsh-tool-subagent` 向模型暴露一个已配置的提供方 |
|
||||
| 子 agent 委派 | `ctx.subagents` 提供方注册表(`dsh-subagent-spawn`/`-fork`/`-acp`/`-codex`/`-claude-code`/`-dsh-sdk`)+ `dsh-tool-subagent` 向模型暴露一个已配置的提供方 |
|
||||
| MCP | 每个服务器一个插件:发现工具 → `ctx.tools.register()` |
|
||||
| Skill(技能) | section + 工具注册;调用时通过 `inject()` 注入 skill 内容 |
|
||||
| 记忆 | section provider + 工具 |
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/core-data-structures/subagent.md
|
||||
subagent.md: 956b47cfa85efe7826fbde47d4405d20a6abed6c
|
||||
subagent.zh.md: 467fcd35bcde5fd01a2e18efbad862c72d7a4253
|
||||
subagent.md: 315051fafaa0bb291a0f7525d2de142d8570961b
|
||||
subagent.zh.md: 5e147b85b1b9a57fb604145bef66e560c443c1de
|
||||
@@ -4,7 +4,7 @@ English | [中文](subagent.zh.md)
|
||||
|
||||
The subagent seam — an agent delegating work to a child agent. Like [bash](bash.md) it is **one optional capability**, not part of the agent-loop spine, so its vocabulary lives here rather than in [core.md](core.md). But it differs from every other seam on one axis: **multiple provider implementations coexist** in one context, registered by name (`ctx.subagents`), where bash allows only one executor. The registry shape mirrors the [LLM adapter registry](llm-streaming.md), not the single-service bash executor.
|
||||
|
||||
Interface: [dsh-subagent](../../packages/subagent/subagent) (`ctx.subagents` + the vocabulary below). Implementations are sibling packages (`dsh-subagent-spawn`, `-fork`, `-acp`); the model-facing consumers are [dsh-tool-subagent](../../packages/subagent/tool-subagent) (per-provider delegation), [dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control) (the optional global `send_message` and `list_agents` controls), and [dsh-tool-subagent-report](../../packages/subagent/tool-subagent-report) (the optional child-scoped `report` return channel). The same `ctx.subagents` service owns continuable-child orchestration through an internal activation manager and read-only direct-child discovery through optional session query. The rationale lives in [the subagent Agent Note](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md), [the continuable subagents Agent Note](../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md), [the report-tool Agent Note](../../.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.md), [the durable catalog Agent Note](../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md), and [the merged-service Agent Note](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md).
|
||||
Interface: [dsh-subagent](../../packages/subagent/subagent) (`ctx.subagents` + the vocabulary below). Implementations are sibling packages (`dsh-subagent-spawn`, `-fork`, `-acp`, `-codex`, `-claude-code`, `-dsh-sdk`); the model-facing consumers are [dsh-tool-subagent](../../packages/subagent/tool-subagent) (per-provider delegation), [dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control) (the optional global `send_message` and `list_agents` controls), and [dsh-tool-subagent-report](../../packages/subagent/tool-subagent-report) (the optional child-scoped `report` return channel). The same `ctx.subagents` service owns continuable-child orchestration through an internal activation manager and read-only direct-child discovery through optional session query. Product-provider rationale lives in [the Codex and Claude Code Agent Note](../../.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md); common-seam rationale lives in [the subagent Agent Note](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md), [the continuable subagents Agent Note](../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md), [the report-tool Agent Note](../../.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.md), [the durable catalog Agent Note](../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md), and [the merged-service Agent Note](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md).
|
||||
|
||||
Sources: [`packages/subagent/subagent/src/types.ts`](../../packages/subagent/subagent/src/types.ts), [`packages/subagent/subagent/src/index.ts`](../../packages/subagent/subagent/src/index.ts), and [`packages/subagent/subagent/src/continuation.ts`](../../packages/subagent/subagent/src/continuation.ts)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
subagent seam:一个 agent(智能体)将工作委派给子 agent。与 [bash](bash.md) 一样,它是**一项可选能力**,不属于 agent loop(智能体循环)主干,因此其词汇定义在此而非 [core.md](core.md) 中。但它在一个维度上与其他所有 seam 不同:**同一上下文中可共存多个提供方实现**,按名称注册(`ctx.subagents`),而 bash 只允许一个执行器。注册表的形状参照 [LLM(大语言模型)适配器注册表](llm-streaming.md),而非单服务的 bash 执行器。
|
||||
|
||||
接口:[dsh-subagent](../../packages/subagent/subagent)(`ctx.subagents` + 下文词汇)。实现为三个兄弟包(package):`dsh-subagent-spawn`、`-fork`、`-acp`;面向模型的消费方包括 [dsh-tool-subagent](../../packages/subagent/tool-subagent)(按提供方委派)、[dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control)(可选的全局 `send_message` 与 `list_agents` 控制工具)和 [dsh-tool-subagent-report](../../packages/subagent/tool-subagent-report)(可选的 child 作用域 `report` 返回通道)。同一个 `ctx.subagents` 服务通过内部激活管理器负责可继续子 agent 编排,并通过可选的会话查询负责只读的直接 child 发现。设计理由见 [subagent Agent Note(agent 决策记录)](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)、[可继续 subagent Agent Note](../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md)、[report 工具 Agent Note](../../.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.md)、[持久化目录 Agent Note](../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md)和[服务合并 Agent Note](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md)。
|
||||
接口:[dsh-subagent](../../packages/subagent/subagent)(`ctx.subagents` + 下文词汇)。实现为六个兄弟包(package):`dsh-subagent-spawn`、`-fork`、`-acp`、`-codex`、`-claude-code`、`-dsh-sdk`;面向模型的消费方包括 [dsh-tool-subagent](../../packages/subagent/tool-subagent)(按提供方委派)、[dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control)(可选的全局 `send_message` 与 `list_agents` 控制工具)和 [dsh-tool-subagent-report](../../packages/subagent/tool-subagent-report)(可选的 child 作用域 `report` 返回通道)。同一个 `ctx.subagents` 服务通过内部激活管理器负责可继续子 agent 编排,并通过可选的会话查询负责只读的直接 child 发现。产品提供方设计理由见 [Codex 与 Claude Code Agent Note(agent 决策记录)](../../.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md);通用 seam 的设计理由见 [subagent Agent Note](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)、[可继续 subagent Agent Note](../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md)、[report 工具 Agent Note](../../.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.md)、[持久化目录 Agent Note](../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md)和[服务合并 Agent Note](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md)。
|
||||
|
||||
源码:[`packages/subagent/subagent/src/types.ts`](../../packages/subagent/subagent/src/types.ts)、[`packages/subagent/subagent/src/index.ts`](../../packages/subagent/subagent/src/index.ts)和 [`packages/subagent/subagent/src/continuation.ts`](../../packages/subagent/subagent/src/continuation.ts)
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/core-data-structures/subprocess.md
|
||||
subprocess.md: 922e7ad0ee8b5c0dbcd0a6a4553c9d2a580f3ee2
|
||||
subprocess.zh.md: ec9f41cfa9e3121f630bab2b6f1e921c81ad55b7
|
||||
subprocess.md: 8189795c5acf2fc1882d9e5bc1c006f49c327e2f
|
||||
subprocess.zh.md: 370a1a05807eefbe09a896ea8bcca1e4ab0a6b83
|
||||
@@ -8,7 +8,7 @@ Source: [`packages/subprocess/subprocess/src/types.ts`](../../packages/subproces
|
||||
|
||||
## Managed environment namespace and captured output
|
||||
|
||||
`DSH_*` variables are Harness-owned child-process facts; implementations discard ambient `DSH_*` names before the caller's explicit `env` merges, so a current fact arrives only as a deliberate entry, and each collected stream reports its truncation and spill-recovery state through `CollectedOutput`.
|
||||
`DSH_*` variables are Harness-owned child-process facts; implementations discard ambient `DSH_*` names before the caller's explicit `env` merges, so a current fact arrives only as a deliberate string entry, while an explicit `undefined` tombstone removes an ordinary ambient value. Each collected stream reports its truncation and spill-recovery state through `CollectedOutput`.
|
||||
|
||||
```ts type-equiv
|
||||
/** One environment key inside the managed {@link DSH_ENV_PREFIX} namespace. */
|
||||
@@ -101,10 +101,11 @@ interface SubprocessSpawnSpec {
|
||||
/** Per-stream stdio dispositions. */
|
||||
stdio: SubprocessStdio
|
||||
/**
|
||||
* Grace period in milliseconds for the {@link SubprocessHandle.terminate}
|
||||
* escalation and for draining still-open collected pipes after the process
|
||||
* exits (an inherited descriptor held by a surviving descendant cannot hold
|
||||
* the outcome open indefinitely).
|
||||
* Positive finite grace period in milliseconds, no greater than
|
||||
* `MAX_TIMER_DELAY_MS`, for the {@link SubprocessHandle.terminate} escalation
|
||||
* and for draining still-open collected pipes after the process exits (an
|
||||
* inherited descriptor held by a surviving descendant cannot hold the
|
||||
* outcome open indefinitely).
|
||||
*/
|
||||
graceMs: number
|
||||
/**
|
||||
@@ -115,13 +116,12 @@ interface SubprocessSpawnSpec {
|
||||
signal?: AbortSignal | undefined
|
||||
/**
|
||||
* Explicit environment entries merged onto the implementation's scrubbed
|
||||
* parent base (see `scrubbedParentEnv`), with no namespace validation:
|
||||
* every entry is a deliberate caller opt-in, so a forwarded
|
||||
* credential-shaped entry or a current `DSH_*` fact survives precisely
|
||||
* because this layer merges after the scrub that drops its ambient
|
||||
* namesake.
|
||||
* parent base (see `scrubbedParentEnv`), with no namespace validation. A
|
||||
* string is a deliberate caller opt-in, so a forwarded credential-shaped
|
||||
* entry or current `DSH_*` fact survives the scrub; `undefined` is a
|
||||
* tombstone that removes an ordinary ambient entry from the child.
|
||||
*/
|
||||
env?: Record<string, string> | undefined
|
||||
env?: NodeJS.ProcessEnv | undefined
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## 受管环境命名空间与捕获的输出
|
||||
|
||||
`DSH_*` 变量是归 Harness 所有的子进程事实;实现会在合并调用方显式 `env` 之前丢弃环境中已有的 `DSH_*` 名称,因此当前事实只会以有意提供的条目形式到达,每条被收集的流都通过 `CollectedOutput` 报告自身的截断与 spill 恢复状态。
|
||||
`DSH_*` 变量是归 Harness 所有的子进程事实;实现会在合并调用方显式 `env` 之前丢弃环境中已有的 `DSH_*` 名称,因此当前事实只会以有意提供的字符串条目形式到达,而显式的 `undefined` tombstone 会删除普通环境中已有的值。每条被收集的流都通过 `CollectedOutput` 报告自身的截断与 spill 恢复状态。
|
||||
|
||||
```ts type-equiv
|
||||
/** One environment key inside the managed {@link DSH_ENV_PREFIX} namespace. */
|
||||
@@ -101,10 +101,11 @@ interface SubprocessSpawnSpec {
|
||||
/** Per-stream stdio dispositions. */
|
||||
stdio: SubprocessStdio
|
||||
/**
|
||||
* Grace period in milliseconds for the {@link SubprocessHandle.terminate}
|
||||
* escalation and for draining still-open collected pipes after the process
|
||||
* exits (an inherited descriptor held by a surviving descendant cannot hold
|
||||
* the outcome open indefinitely).
|
||||
* Positive finite grace period in milliseconds, no greater than
|
||||
* `MAX_TIMER_DELAY_MS`, for the {@link SubprocessHandle.terminate} escalation
|
||||
* and for draining still-open collected pipes after the process exits (an
|
||||
* inherited descriptor held by a surviving descendant cannot hold the
|
||||
* outcome open indefinitely).
|
||||
*/
|
||||
graceMs: number
|
||||
/**
|
||||
@@ -115,13 +116,12 @@ interface SubprocessSpawnSpec {
|
||||
signal?: AbortSignal | undefined
|
||||
/**
|
||||
* Explicit environment entries merged onto the implementation's scrubbed
|
||||
* parent base (see `scrubbedParentEnv`), with no namespace validation:
|
||||
* every entry is a deliberate caller opt-in, so a forwarded
|
||||
* credential-shaped entry or a current `DSH_*` fact survives precisely
|
||||
* because this layer merges after the scrub that drops its ambient
|
||||
* namesake.
|
||||
* parent base (see `scrubbedParentEnv`), with no namespace validation. A
|
||||
* string is a deliberate caller opt-in, so a forwarded credential-shaped
|
||||
* entry or current `DSH_*` fact survives the scrub; `undefined` is a
|
||||
* tombstone that removes an ordinary ambient entry from the child.
|
||||
*/
|
||||
env?: Record<string, string> | undefined
|
||||
env?: NodeJS.ProcessEnv | undefined
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+23
-3
@@ -68,6 +68,8 @@ flowchart TD
|
||||
subgraph group_subagent["packages/subagent"]
|
||||
pkg_subagent["subagent"]
|
||||
pkg_subagent_acp["subagent-acp"]
|
||||
pkg_subagent_claude_code["subagent-claude-code"]
|
||||
pkg_subagent_codex["subagent-codex"]
|
||||
pkg_subagent_dsh_sdk["subagent-dsh-sdk"]
|
||||
pkg_subagent_fork["subagent-fork"]
|
||||
pkg_subagent_inprocess["subagent-inprocess"]
|
||||
@@ -357,6 +359,7 @@ flowchart TD
|
||||
pkg_storage_sqlite --> pkg_storage
|
||||
pkg_subprocess_local --> pkg_invariants
|
||||
pkg_subprocess_local --> pkg_subprocess
|
||||
pkg_subprocess_local --> pkg_timeout
|
||||
pkg_typert_loader --> pkg_invariants
|
||||
pkg_typert_loader --> pkg_typert_registry
|
||||
pkg_llm_deepseek --> pkg_credentials
|
||||
@@ -733,6 +736,7 @@ flowchart TD
|
||||
pkg_tool_fs_search --> pkg_spill
|
||||
pkg_tool_fs_search --> pkg_subprocess
|
||||
pkg_tool_fs_search --> pkg_system_prompt
|
||||
pkg_tool_fs_search --> pkg_timeout
|
||||
pkg_tool_fs_search --> pkg_tools
|
||||
pkg_tool_str_replace_editor --> pkg_fs
|
||||
pkg_tool_str_replace_editor --> pkg_invariants
|
||||
@@ -932,6 +936,13 @@ flowchart TD
|
||||
pkg_subagent_acp --> pkg_session
|
||||
pkg_subagent_acp --> pkg_subagent
|
||||
pkg_subagent_acp --> pkg_subprocess
|
||||
pkg_subagent_acp --> pkg_timeout
|
||||
pkg_subagent_claude_code --> pkg_invariants
|
||||
pkg_subagent_claude_code --> pkg_llm
|
||||
pkg_subagent_claude_code --> pkg_session
|
||||
pkg_subagent_claude_code --> pkg_subagent
|
||||
pkg_subagent_claude_code --> pkg_subprocess
|
||||
pkg_subagent_claude_code --> pkg_timeout
|
||||
pkg_subagent_inprocess --> pkg_agent
|
||||
pkg_subagent_inprocess --> pkg_invariants
|
||||
pkg_subagent_inprocess --> pkg_llm
|
||||
@@ -1027,6 +1038,13 @@ flowchart TD
|
||||
pkg_workflow_workerthread --> pkg_subagent
|
||||
pkg_workflow_workerthread --> pkg_tools
|
||||
pkg_workflow_workerthread --> pkg_workflow
|
||||
pkg_subagent_codex --> pkg_invariants
|
||||
pkg_subagent_codex --> pkg_llm
|
||||
pkg_subagent_codex --> pkg_sdk_protocol
|
||||
pkg_subagent_codex --> pkg_session
|
||||
pkg_subagent_codex --> pkg_subagent
|
||||
pkg_subagent_codex --> pkg_subprocess
|
||||
pkg_subagent_codex --> pkg_timeout
|
||||
pkg_subagent_fork --> pkg_agent
|
||||
pkg_subagent_fork --> pkg_invariants
|
||||
pkg_subagent_fork --> pkg_session
|
||||
@@ -1145,7 +1163,7 @@ flowchart TD
|
||||
| [`storage-domain`](../packages/storage/storage-domain) | `storage` | [`invariants`](../packages/support/invariants), [`storage`](../packages/storage/storage) |
|
||||
| [`storage-json`](../packages/storage/storage-json) | `storage` | [`invariants`](../packages/support/invariants), [`storage`](../packages/storage/storage) |
|
||||
| [`storage-sqlite`](../packages/storage/storage-sqlite) | `storage` | [`invariants`](../packages/support/invariants), [`storage`](../packages/storage/storage) |
|
||||
| [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess) |
|
||||
| [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`typert-loader`](../packages/typert/loader) | `typert` | [`invariants`](../packages/support/invariants), [`typert-registry`](../packages/typert/registry) |
|
||||
| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
|
||||
| [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
|
||||
@@ -1230,7 +1248,7 @@ flowchart TD
|
||||
| [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`bash-env`](../packages/bash/bash-env) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`session-persistence`](../packages/session-persistence/session-persistence), [`tools`](../packages/core/tools) |
|
||||
| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) |
|
||||
| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
|
||||
| [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) |
|
||||
| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
|
||||
| [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
|
||||
@@ -1262,7 +1280,8 @@ flowchart TD
|
||||
| [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
|
||||
| [`tool-bash`](../packages/bash/tool-bash) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) |
|
||||
| [`tool-pwsh`](../packages/bash/tool-pwsh) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
|
||||
| [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
|
||||
| [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) |
|
||||
| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
|
||||
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
||||
@@ -1277,6 +1296,7 @@ flowchart TD
|
||||
| [`sdk-protocol`](../packages/sdk/sdk-protocol) | `sdk` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
||||
| [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
|
||||
| [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
|
||||
| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/sdk-protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`subagent-fork`](../packages/subagent/subagent-fork) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
|
||||
| [`subagent-spawn`](../packages/subagent/subagent-spawn) | `subagent` | [`invariants`](../packages/support/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
|
||||
| [`jsonrpc`](../packages/ui/jsonrpc) | `ui` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/sdk-protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
||||
|
||||
@@ -276,6 +276,8 @@ const SCENARIOS: Scenario[] = [
|
||||
// symlinked instruction file to its target's content. A second nested path
|
||||
// containing a literal closing tag is created at runtime: Git cannot check
|
||||
// that name out on Windows, so this delimiter-injection case is POSIX-only.
|
||||
// The fixture also shadows the baseline after the first touch finishes its
|
||||
// projection; the next entering pre-step restores it before request 2.
|
||||
// The scenario-specific config keeps home/root discovery hermetic, and the
|
||||
// resulting prefix needs its own pinned header class.
|
||||
{
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Test-only composition of both public opt-in providers and foreground tools.
|
||||
# The owning e2e boots this tree but never invokes a model or product process.
|
||||
- id: fixture
|
||||
name: './fixture.ts'
|
||||
|
||||
- id: subagent
|
||||
name: '@deepseek-ai/dsh-subagent'
|
||||
|
||||
- id: subprocess
|
||||
name: '@deepseek-ai/dsh-subprocess-local'
|
||||
|
||||
- id: subagent-codex
|
||||
name: '@deepseek-ai/dsh-subagent-codex'
|
||||
|
||||
- id: subagent-claude-code
|
||||
name: '@deepseek-ai/dsh-subagent-claude-code'
|
||||
|
||||
- id: tool-subagent-codex
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: codex
|
||||
toolName: subagent_codex
|
||||
enableRunInBackground: false
|
||||
maxDepth: 'provider-managed'
|
||||
|
||||
- id: tool-subagent-claude-code
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: claude-code
|
||||
toolName: subagent_claude_code
|
||||
enableRunInBackground: false
|
||||
maxDepth: 'provider-managed'
|
||||
|
||||
- id: cli-agent
|
||||
name: '@deepseek-ai/dsh-cli-demo'
|
||||
config:
|
||||
provider: mock
|
||||
model: mock-delegate
|
||||
persona: 'This composition test must not start a model turn.'
|
||||
workspaceContext: false
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env node
|
||||
/** Inspect both public product-provider compositions without invoking them. */
|
||||
|
||||
import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
|
||||
import type {} from '@deepseek-ai/dsh-subagent'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
|
||||
const configPath = process.argv[2]
|
||||
if (configPath === undefined) {
|
||||
throw new Error('product-provider Loader composition driver requires a config path')
|
||||
}
|
||||
|
||||
let starts = 0
|
||||
const ctx = await boot(
|
||||
'product-provider-loader-composition',
|
||||
resolveConfigPath(configPath, undefined),
|
||||
undefined,
|
||||
(hostCtx) => {
|
||||
hostCtx.on('subagent/start', () => {
|
||||
starts += 1
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
try {
|
||||
const providerNames = ['codex', 'claude-code'] as const
|
||||
const toolNames = ['subagent_codex', 'subagent_claude_code'] as const
|
||||
const providers = providerNames.map((providerName) => {
|
||||
const provider = ctx.subagents.getProvider(providerName)
|
||||
if (provider === undefined) {
|
||||
throw new Error(`${providerName} provider was not registered`)
|
||||
}
|
||||
return {
|
||||
name: provider.name,
|
||||
capabilities: provider.capabilities,
|
||||
inheritsParentContext: provider.inheritsParentContext,
|
||||
}
|
||||
})
|
||||
const tools = toolNames.map((toolName) => {
|
||||
const tool = ctx.tools.schemas().find(schema => schema.name === toolName)
|
||||
if (tool === undefined) throw new Error(`${toolName} tool was not registered`)
|
||||
const properties = tool.parameters.properties
|
||||
if (
|
||||
typeof properties !== 'object'
|
||||
|| properties === null
|
||||
|| Array.isArray(properties)
|
||||
) {
|
||||
throw new Error(`${toolName} has invalid parameter properties`)
|
||||
}
|
||||
return {
|
||||
name: tool.name,
|
||||
parameterNames: Object.keys(properties).sort(),
|
||||
required: tool.parameters.required,
|
||||
}
|
||||
})
|
||||
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
registeredProviders: ctx.subagents.list(),
|
||||
providers,
|
||||
tools,
|
||||
starts,
|
||||
})}\n`)
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/** Reuse the composition-only parent adapter shared by the product providers. */
|
||||
|
||||
export {
|
||||
apply,
|
||||
inject,
|
||||
name,
|
||||
} from '../subagent-codex/fixture.ts'
|
||||
@@ -0,0 +1,29 @@
|
||||
# Test-only composition of the public opt-in provider and foreground tool.
|
||||
# The owning e2e boots this tree but never invokes the model or Codex.
|
||||
- id: fixture
|
||||
name: './fixture.ts'
|
||||
|
||||
- id: subagent
|
||||
name: '@deepseek-ai/dsh-subagent'
|
||||
|
||||
- id: subprocess
|
||||
name: '@deepseek-ai/dsh-subprocess-local'
|
||||
|
||||
- id: subagent-codex
|
||||
name: '@deepseek-ai/dsh-subagent-codex'
|
||||
|
||||
- id: tool-subagent-codex
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: codex
|
||||
toolName: subagent_codex
|
||||
enableRunInBackground: false
|
||||
maxDepth: 'provider-managed'
|
||||
|
||||
- id: cli-agent
|
||||
name: '@deepseek-ai/dsh-cli-demo'
|
||||
config:
|
||||
provider: mock
|
||||
model: mock-delegate
|
||||
persona: 'This composition test must not start a model turn.'
|
||||
workspaceContext: false
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env node
|
||||
/** Inspect the public Codex provider composition without invoking the product. */
|
||||
|
||||
import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
|
||||
import type {} from '@deepseek-ai/dsh-subagent'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
|
||||
const configPath = process.argv[2]
|
||||
if (configPath === undefined) {
|
||||
throw new Error('subagent-codex Loader composition driver requires a config path')
|
||||
}
|
||||
|
||||
let starts = 0
|
||||
const ctx = await boot(
|
||||
'subagent-codex-loader-composition',
|
||||
resolveConfigPath(configPath, undefined),
|
||||
undefined,
|
||||
(hostCtx) => {
|
||||
hostCtx.on('subagent/start', () => {
|
||||
starts += 1
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
try {
|
||||
const provider = ctx.subagents.getProvider('codex')
|
||||
if (provider === undefined) throw new Error('Codex provider was not registered')
|
||||
const tool = ctx.tools.schemas().find(schema => schema.name === 'subagent_codex')
|
||||
if (tool === undefined) throw new Error('subagent_codex tool was not registered')
|
||||
const properties = tool.parameters.properties
|
||||
if (typeof properties !== 'object' || properties === null || Array.isArray(properties)) {
|
||||
throw new Error('subagent_codex tool has invalid parameter properties')
|
||||
}
|
||||
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
providers: ctx.subagents.list(),
|
||||
provider: {
|
||||
name: provider.name,
|
||||
capabilities: provider.capabilities,
|
||||
inheritsParentContext: provider.inheritsParentContext,
|
||||
},
|
||||
tool: {
|
||||
name: tool.name,
|
||||
parameterNames: Object.keys(properties).sort(),
|
||||
required: tool.parameters.required,
|
||||
},
|
||||
starts,
|
||||
})}\n`)
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/** Parent adapter that fails if the composition-only Loader test starts a turn. */
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
class CompositionOnlyAdapter extends LlmAdapter {
|
||||
async * stream(_options: GenerateOptions): AsyncIterable<StreamChunk> {
|
||||
throw new Error('subagent-codex Loader composition must not invoke a model')
|
||||
}
|
||||
}
|
||||
|
||||
export const name = 'codex-loader-composition-fixture'
|
||||
export const inject = ['llm']
|
||||
|
||||
/**
|
||||
* Register a parent adapter solely so the host composition is complete.
|
||||
* @param ctx - Loader context supplying the LLM seam.
|
||||
*/
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.llm.registerAdapter(['mock'], new CompositionOnlyAdapter())
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-agent'
|
||||
import { COMPACT_CHECKPOINT_SOURCE } from '@deepseek-ai/dsh-compact'
|
||||
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'workspace-context-compaction'
|
||||
|
||||
/** Replace the visible workspace baseline after the first touch is fully projected. */
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.on('tools/post-execute', async (exec, result, next) => {
|
||||
const downstream = await next()
|
||||
if (result.isError
|
||||
|| exec.agent === undefined
|
||||
|| exec.name !== 'read'
|
||||
|| typeof exec.arguments !== 'object'
|
||||
|| exec.arguments === null
|
||||
|| !('file_path' in exec.arguments)
|
||||
|| exec.arguments.file_path !== 'nested/task.txt') return downstream
|
||||
const agent = exec.agent
|
||||
const baseline = agent.session.surface.nodes
|
||||
.map(seq => agent.session.events[seq])
|
||||
.find(event => event?.type === 'user/message'
|
||||
&& event.data.source.kind === 'workspace-instructions'
|
||||
&& event.data.source.baseline === true)
|
||||
if (baseline === undefined) throw new Error('workspace baseline missing before snapshot compaction')
|
||||
agent.session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'Earlier context was compacted for this snapshot.' }],
|
||||
source: COMPACT_CHECKPOINT_SOURCE,
|
||||
}), {
|
||||
surfaceOp: { op: 'replace', start: baseline.seq, end: baseline.seq },
|
||||
sourceEventSeqs: [baseline.seq],
|
||||
})
|
||||
return downstream
|
||||
})
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
{"type":"agent/inbox/spliced","seq":2,"time":1785498825916,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","seq":3,"time":1785901435161,"data":{"turn":1,"step":1}}
|
||||
{"type":"user/message","seq":4,"time":1785498825917,"data":{"content":[{"type":"text","text":"Using ONE run_code program, call tools.read on nested/task.txt. After the program finishes, answer the workspace handshake question using the newly discovered instructions: What is the Code Mode workspace handshake?"}],"source":{"kind":"user"},"role":"user","id":"3b04578e-7b22-4b44-b4cd-ef9d4d26fe8b"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":5,"time":1785901435161,"data":{"content":[{"type":"text","text":"<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nWorkspace snapshot root instruction.\n\n</system-reminder>"}],"source":{"kind":"workspace-instructions","form":"instructions","baseline":true,"changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"2119a7072358cc727f8d9c4cb7388e905b075fe6"}]},"role":"user","id":"ac92e76e-4861-47a6-87f8-4e9ca904eb24"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":5,"time":1785901435161,"data":{"content":[{"type":"text","text":"<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nWorkspace snapshot root instruction.\n\n</system-reminder>"}],"source":{"kind":"workspace-instructions","form":"instructions","baseline":true,"baselineIdentity":"{\"projectRoot\":\"\",\"projectRootMarkers\":[\".git\"],\"maxBytes\":65536,\"maxSourceBytes\":1048576,\"instructionFileCandidates\":[\"AGENTS.md\",\"CLAUDE.md\"],\"localInstructionFileCandidates\":[\"AGENTS.local.md\",\"CLAUDE.local.md\"]}","changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"2119a7072358cc727f8d9c4cb7388e905b075fe6"}]},"role":"user","id":"ac92e76e-4861-47a6-87f8-4e9ca904eb24"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":6,"time":1785730478198,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"d6d78330-05c0-4ebd-9e29-595df6440250"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":7,"time":1785730478198,"data":{"title":"Using ONE run_code program, call","messageSeqs":[4],"source":{"kind":"fallback"}}}
|
||||
{"type":"request/header","seq":8,"time":1785498825920,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0}
|
||||
{"type":"agent/inbox/spliced","seq":0,"time":1785498790330,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Read nested/task.txt, then read scope</system-reminder>/task.txt with the read tool, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"22938d3b-c065-46c8-acb7-18f758285842"}]}}
|
||||
{"type":"agent/inbox/spliced","seq":0,"time":1785498790330,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Read nested/task.txt, then read scope</system-reminder>/task.txt with the read tool, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"81078e7a-6837-45c2-a6b4-a5a3dfce0d4a"}]}}
|
||||
{"type":"turn/start","seq":1,"time":1785821400350,"data":{"turn":1}}
|
||||
{"type":"agent/inbox/spliced","seq":2,"time":1785498790356,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","seq":3,"time":1785901433981,"data":{"turn":1,"step":1}}
|
||||
{"type":"user/message","seq":4,"time":1785498790356,"data":{"content":[{"type":"text","text":"Read nested/task.txt, then read scope</system-reminder>/task.txt with the read tool, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"22938d3b-c065-46c8-acb7-18f758285842"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":5,"time":1785901433982,"data":{"content":[{"type":"text","text":"<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nRoot snapshot instruction.\n\n</system-reminder>"}],"source":{"kind":"workspace-instructions","form":"instructions","baseline":true,"changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"2e18766c26603608f321508caae00ea8f4434d59"}]},"role":"user","id":"ec039e95-6864-49ef-ad23-4f65b331dc29"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":6,"time":1785730689193,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"cbea9bd9-3e08-48bf-951f-fe3e5aa4b5d9"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":4,"time":1785498790356,"data":{"content":[{"type":"text","text":"Read nested/task.txt, then read scope</system-reminder>/task.txt with the read tool, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"81078e7a-6837-45c2-a6b4-a5a3dfce0d4a"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":5,"time":1785901433982,"data":{"content":[{"type":"text","text":"<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nRoot snapshot instruction.\n\n</system-reminder>"}],"source":{"kind":"workspace-instructions","form":"instructions","baseline":true,"baselineIdentity":"{\"projectRoot\":\"\",\"projectRootMarkers\":[\".dsh-project\"],\"maxBytes\":65536,\"maxSourceBytes\":1048576,\"instructionFileCandidates\":[\"AGENTS.md\",\"CLAUDE.md\"],\"localInstructionFileCandidates\":[\"AGENTS.local.md\",\"CLAUDE.local.md\"]}","changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"2e18766c26603608f321508caae00ea8f4434d59"}]},"role":"user","id":"4cba1848-cbb7-46fd-8cea-8497d54d0e63"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":6,"time":1785730689193,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"e4406554-e400-49c6-b8a3-0fe36841160b"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":7,"time":1785730689193,"data":{"title":"Read nested/task.txt, then read scope</s","messageSeqs":[4],"source":{"kind":"fallback"}}}
|
||||
{"type":"request/header","seq":8,"time":1785498790358,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","seq":9,"time":1785730689194,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
@@ -14,32 +14,33 @@
|
||||
{"type":"assistant/chunk","seq":12,"time":1784903339801,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_workspace_read","name":"read","arguments":"{\"file_path\":\"nested/task.txt\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":13,"time":1785498790358,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
|
||||
{"type":"assistant/chunk","seq":14,"time":1785730689194,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":15,"time":1785730689194,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_workspace_read","name":"read","arguments":"{\"file_path\":\"nested/task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"cb229812-91a1-4aa6-8bce-d3b8d032f6dd"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[10,11,12,13,14],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":15,"time":1785730689194,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_workspace_read","name":"read","arguments":"{\"file_path\":\"nested/task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"9e9648c4-949e-4cf1-b9ef-0eb65897d36b"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[10,11,12,13,14],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":16,"time":1785730689195,"data":{"turn":1,"step":1,"callId":"call_workspace_read","name":"read","arguments":"{\"file_path\":\"nested/task.txt\"}"}}
|
||||
{"type":"tool/result","seq":17,"time":1785730689204,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_workspace_read"},"content":[{"type":"tool-result","toolCallId":"call_workspace_read","content":[{"type":"text","text":"<path>{{cwd}}/nested/task.txt</path>\n<type>file</type>\n<content>\n1: snapshot task\n\n(End of file - total 1 lines)\n</content>"}],"isError":false}],"role":"user","id":"260bbd5d-4496-40cf-b987-d1d944d93cf1"},"meta":{"path":"{{cwd}}/nested/task.txt","offset":1,"lines":[{"number":1,"text":"snapshot task"}],"totalLines":1}},"sourceEventSeqs":[16],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":18,"time":1785498790369,"data":{"turn":1,"step":1}}
|
||||
{"type":"agent/inbox/spliced","seq":19,"time":1785498790369,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"<system-reminder>\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nNested snapshot instruction.\n\n</system-reminder>"}],"source":{"kind":"workspace-instructions","form":"instructions","changes":[{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"c446df9a85c7e73a3055f394a4822a19ac9ead5a"}]},"role":"user","id":"10cdaa4b-9654-420e-afca-3cfb07e26754"}]}}
|
||||
{"type":"agent/inbox/spliced","seq":20,"time":1785730689207,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[],"outcome":"canceled"}}
|
||||
{"type":"step/start","seq":21,"time":1785730689212,"data":{"turn":1,"step":2}}
|
||||
{"type":"user/message","seq":22,"time":1785498790377,"data":{"content":[{"type":"text","text":"<system-reminder>\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nNested snapshot instruction.\n\n</system-reminder>"}],"source":{"kind":"workspace-instructions","form":"instructions","changes":[{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"c446df9a85c7e73a3055f394a4822a19ac9ead5a"}]},"role":"user","id":"10cdaa4b-9654-420e-afca-3cfb07e26754"},"surfaceOp":"append"}
|
||||
{"type":"assistant/chunk","seq":23,"time":1784903339821,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":24,"time":1785498790377,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"call_workspace_delimiter_read","name":"read","argumentsDelta":"{\"file_path\":\"scope</system-reminder>/task.txt\"}"}}}
|
||||
{"type":"assistant/chunk","seq":25,"time":1785498790377,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_workspace_delimiter_read","name":"read","arguments":"{\"file_path\":\"scope</system-reminder>/task.txt\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":26,"time":1785498790378,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
|
||||
{"type":"assistant/chunk","seq":27,"time":1785498790378,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":28,"time":1785498790378,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_workspace_delimiter_read","name":"read","arguments":"{\"file_path\":\"scope</system-reminder>/task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"43385fc1-54b4-4a8a-82ca-d9111c766f48"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[23,24,25,26,27],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":29,"time":1785498790378,"data":{"turn":1,"step":2,"callId":"call_workspace_delimiter_read","name":"read","arguments":"{\"file_path\":\"scope</system-reminder>/task.txt\"}"}}
|
||||
{"type":"tool/result","seq":30,"time":1785498790388,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_workspace_delimiter_read"},"content":[{"type":"tool-result","toolCallId":"call_workspace_delimiter_read","content":[{"type":"text","text":"<path>{{cwd}}/scope</system-reminder>/task.txt</path>\n<type>file</type>\n<content>\n1: delimiter path snapshot task\n\n(End of file - total 1 lines)\n</content>"}],"isError":false}],"role":"user","id":"3a62b23b-d165-4c6d-a028-e171c4b2d7fc"},"meta":{"path":"{{cwd}}/scope</system-reminder>/task.txt","offset":1,"lines":[{"number":1,"text":"delimiter path snapshot task"}],"totalLines":1}},"sourceEventSeqs":[29],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":31,"time":1785730689220,"data":{"turn":1,"step":2}}
|
||||
{"type":"agent/inbox/spliced","seq":32,"time":1785730689220,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"<system-reminder>\nAdditional instructions from: scope<\\/system-reminder>/AGENTS.md\n\nThese instructions apply to work under `scope<\\/system-reminder>`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nDelimiter path snapshot instruction.\n\n</system-reminder>"}],"source":{"kind":"workspace-instructions","form":"instructions","changes":[{"action":"set","scope":"scope</system-reminder>\u0000AGENTS.md","path":"scope</system-reminder>/AGENTS.md","digest":"38803cd13e2dff9105ba5fbbc703fe27e989e26e"}]},"role":"user","id":"01c3af61-4567-4be2-b776-21f04ddc9cba"}]}}
|
||||
{"type":"agent/inbox/spliced","seq":33,"time":1785498790389,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[],"outcome":"canceled"}}
|
||||
{"type":"step/start","seq":34,"time":1785498790396,"data":{"turn":1,"step":3}}
|
||||
{"type":"user/message","seq":35,"time":1785498790396,"data":{"content":[{"type":"text","text":"<system-reminder>\nAdditional instructions from: scope<\\/system-reminder>/AGENTS.md\n\nThese instructions apply to work under `scope<\\/system-reminder>`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nDelimiter path snapshot instruction.\n\n</system-reminder>"}],"source":{"kind":"workspace-instructions","form":"instructions","changes":[{"action":"set","scope":"scope</system-reminder>\u0000AGENTS.md","path":"scope</system-reminder>/AGENTS.md","digest":"38803cd13e2dff9105ba5fbbc703fe27e989e26e"}]},"role":"user","id":"01c3af61-4567-4be2-b776-21f04ddc9cba"},"surfaceOp":"append"}
|
||||
{"type":"assistant/chunk","seq":36,"time":1785498790396,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":37,"time":1785498790396,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":0,"text":"DONE"}}}
|
||||
{"type":"assistant/chunk","seq":38,"time":1785498790396,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}}
|
||||
{"type":"assistant/chunk","seq":39,"time":1785498790396,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}}
|
||||
{"type":"assistant/chunk","seq":40,"time":1785498790397,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":41,"time":1785498790397,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"f82f5200-090f-4e68-a016-963ab7166d4f"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[36,37,38,39,40],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":42,"time":1785498790397,"data":{"turn":1,"step":3}}
|
||||
{"type":"turn/end","seq":43,"time":1785901434023,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
{"type":"user/message","seq":17,"time":1785982371865,"data":{"content":[{"type":"text","text":"Earlier context was compacted for this snapshot."}],"source":{"kind":"plugin","plugin":"compact"},"role":"user","id":"162c764f-f01d-484d-ad81-1481dc29792a"},"sourceEventSeqs":[5],"surfaceOp":{"op":"replace","start":5,"end":5}}
|
||||
{"type":"tool/result","seq":18,"time":1785982371865,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_workspace_read"},"content":[{"type":"tool-result","toolCallId":"call_workspace_read","content":[{"type":"text","text":"<path>{{cwd}}/nested/task.txt</path>\n<type>file</type>\n<content>\n1: snapshot task\n\n(End of file - total 1 lines)\n</content>"}],"isError":false}],"role":"user","id":"a46fded2-333a-4fb2-b01e-28520bffbc21"},"meta":{"path":"{{cwd}}/nested/task.txt","offset":1,"lines":[{"number":1,"text":"snapshot task"}],"totalLines":1}},"sourceEventSeqs":[16],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":19,"time":1785982371865,"data":{"turn":1,"step":1}}
|
||||
{"type":"agent/inbox/spliced","seq":20,"time":1785730689207,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nRoot snapshot instruction.\n\n</system-reminder>"},{"type":"text","text":"<system-reminder>\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nNested snapshot instruction.\n\n</system-reminder>"}],"source":{"kind":"workspace-instructions","form":"instructions","baseline":true,"baselineIdentity":"{\"projectRoot\":\"\",\"projectRootMarkers\":[\".dsh-project\"],\"maxBytes\":65536,\"maxSourceBytes\":1048576,\"instructionFileCandidates\":[\"AGENTS.md\",\"CLAUDE.md\"],\"localInstructionFileCandidates\":[\"AGENTS.local.md\",\"CLAUDE.local.md\"]}","changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"2e18766c26603608f321508caae00ea8f4434d59"},{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"c446df9a85c7e73a3055f394a4822a19ac9ead5a"}]},"role":"user","id":"09640903-80ea-4eb6-8635-90ddfb4e24e4"}]}}
|
||||
{"type":"agent/inbox/spliced","seq":21,"time":1785982371873,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[],"outcome":"canceled"}}
|
||||
{"type":"step/start","seq":22,"time":1785982371873,"data":{"turn":1,"step":2}}
|
||||
{"type":"user/message","seq":23,"time":1785982371873,"data":{"content":[{"type":"text","text":"<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nRoot snapshot instruction.\n\n</system-reminder>"},{"type":"text","text":"<system-reminder>\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nNested snapshot instruction.\n\n</system-reminder>"}],"source":{"kind":"workspace-instructions","form":"instructions","baseline":true,"baselineIdentity":"{\"projectRoot\":\"\",\"projectRootMarkers\":[\".dsh-project\"],\"maxBytes\":65536,\"maxSourceBytes\":1048576,\"instructionFileCandidates\":[\"AGENTS.md\",\"CLAUDE.md\"],\"localInstructionFileCandidates\":[\"AGENTS.local.md\",\"CLAUDE.local.md\"]}","changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"2e18766c26603608f321508caae00ea8f4434d59"},{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"c446df9a85c7e73a3055f394a4822a19ac9ead5a"}]},"role":"user","id":"09640903-80ea-4eb6-8635-90ddfb4e24e4"},"surfaceOp":"append"}
|
||||
{"type":"assistant/chunk","seq":24,"time":1785498790377,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":25,"time":1785498790377,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"call_workspace_delimiter_read","name":"read","argumentsDelta":"{\"file_path\":\"scope</system-reminder>/task.txt\"}"}}}
|
||||
{"type":"assistant/chunk","seq":26,"time":1785498790378,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_workspace_delimiter_read","name":"read","arguments":"{\"file_path\":\"scope</system-reminder>/task.txt\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":27,"time":1785498790378,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
|
||||
{"type":"assistant/chunk","seq":28,"time":1785982371874,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":29,"time":1785982371874,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_workspace_delimiter_read","name":"read","arguments":"{\"file_path\":\"scope</system-reminder>/task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"ba85f14b-5ff0-4b71-a3f8-0d9ea7f4893d"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[24,25,26,27,28],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":30,"time":1785982371875,"data":{"turn":1,"step":2,"callId":"call_workspace_delimiter_read","name":"read","arguments":"{\"file_path\":\"scope</system-reminder>/task.txt\"}"}}
|
||||
{"type":"tool/result","seq":31,"time":1785982371882,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_workspace_delimiter_read"},"content":[{"type":"tool-result","toolCallId":"call_workspace_delimiter_read","content":[{"type":"text","text":"<path>{{cwd}}/scope</system-reminder>/task.txt</path>\n<type>file</type>\n<content>\n1: delimiter path snapshot task\n\n(End of file - total 1 lines)\n</content>"}],"isError":false}],"role":"user","id":"c0fad80c-59c3-41bf-b662-84f87ee1420c"},"meta":{"path":"{{cwd}}/scope</system-reminder>/task.txt","offset":1,"lines":[{"number":1,"text":"delimiter path snapshot task"}],"totalLines":1}},"sourceEventSeqs":[30],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":32,"time":1785982371882,"data":{"turn":1,"step":2}}
|
||||
{"type":"agent/inbox/spliced","seq":33,"time":1785498790389,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"<system-reminder>\nAdditional instructions from: scope<\\/system-reminder>/AGENTS.md\n\nThese instructions apply to work under `scope<\\/system-reminder>`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nDelimiter path snapshot instruction.\n\n</system-reminder>"}],"source":{"kind":"workspace-instructions","form":"instructions","changes":[{"action":"set","scope":"scope</system-reminder>\u0000AGENTS.md","path":"scope</system-reminder>/AGENTS.md","digest":"38803cd13e2dff9105ba5fbbc703fe27e989e26e"}]},"role":"user","id":"cd19663e-c8b5-46a5-9eeb-1386dcb1c609"}]}}
|
||||
{"type":"agent/inbox/spliced","seq":34,"time":1785982371889,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[],"outcome":"canceled"}}
|
||||
{"type":"step/start","seq":35,"time":1785982371889,"data":{"turn":1,"step":3}}
|
||||
{"type":"user/message","seq":36,"time":1785982371889,"data":{"content":[{"type":"text","text":"<system-reminder>\nAdditional instructions from: scope<\\/system-reminder>/AGENTS.md\n\nThese instructions apply to work under `scope<\\/system-reminder>`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nDelimiter path snapshot instruction.\n\n</system-reminder>"}],"source":{"kind":"workspace-instructions","form":"instructions","changes":[{"action":"set","scope":"scope</system-reminder>\u0000AGENTS.md","path":"scope</system-reminder>/AGENTS.md","digest":"38803cd13e2dff9105ba5fbbc703fe27e989e26e"}]},"role":"user","id":"cd19663e-c8b5-46a5-9eeb-1386dcb1c609"},"surfaceOp":"append"}
|
||||
{"type":"assistant/chunk","seq":37,"time":1785498790396,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":38,"time":1785498790396,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":0,"text":"DONE"}}}
|
||||
{"type":"assistant/chunk","seq":39,"time":1785498790396,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}}
|
||||
{"type":"assistant/chunk","seq":40,"time":1785498790397,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}}
|
||||
{"type":"assistant/chunk","seq":41,"time":1785982371890,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":42,"time":1785982371890,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"81b25d58-fa4a-4eb6-9b87-1c33baf90053"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[37,38,39,40,41],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":43,"time":1785982371890,"data":{"turn":1,"step":3}}
|
||||
{"type":"turn/end","seq":44,"time":1785982371891,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
@@ -28,3 +28,5 @@
|
||||
- insert:
|
||||
- id: llm-replay
|
||||
name: '@deepseek-ai/dsh-llm-replay'
|
||||
- id: workspace-context-compaction
|
||||
name: './tests/fixtures/workspace-context-compaction.ts'
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Loader fixture that resumes the seeded workspace-context session.
|
||||
* @module workspace-context-resume-agent
|
||||
*/
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
|
||||
/** Fixture plugin name. */
|
||||
export const name = 'workspace-context-resume-agent'
|
||||
/** Services that must exist before the fixture resumes its agent. */
|
||||
export const inject = ['agents', 'agentLoop', 'sessionPersistence']
|
||||
|
||||
/**
|
||||
* Resume the seeded session and bind its handle to this fixture's lifetime.
|
||||
* @param ctx - settled agent and persistence services from the Loader tree.
|
||||
* @returns after the resumed agent is published.
|
||||
*/
|
||||
export async function apply(ctx: Context): Promise<void> {
|
||||
const handle = await ctx.agents.resume({
|
||||
resumeSessionId: 'workspace-context-resume' as SessionId,
|
||||
agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
||||
})
|
||||
ctx.effect(() => () => handle.dispose(), 'workspace-context-resume-agent.handle')
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"type":"session","version":0,"id":"workspace-context-resume-replay","createdAt":1,"delegationDepth":0}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"kind": "chunks",
|
||||
"chunks": [
|
||||
{ "type": "block-start", "index": 0, "blockType": "text" },
|
||||
{ "type": "text-delta", "index": 0, "text": "RESUME_DONE" },
|
||||
{ "type": "block-end", "index": 0, "block": { "type": "text", "text": "RESUME_DONE" } },
|
||||
{ "type": "finish", "reason": { "kind": "stop" } }
|
||||
]
|
||||
}
|
||||
]
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0}
|
||||
{"type":"turn/start","seq":0,"time":0,"data":{"turn":1}}
|
||||
{"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Remember the workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":2,"time":0,"data":{"content":[{"type":"text","text":"<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nOld workspace instruction.\n</system-reminder>"}],"source":{"kind":"workspace-instructions","form":"instructions","baseline":true,"baselineIdentity":"{\"projectRoot\":\"\",\"projectRootMarkers\":[\".git\"],\"maxBytes\":65536,\"maxSourceBytes\":1048576,\"instructionFileCandidates\":[\"AGENTS.md\",\"CLAUDE.md\"],\"localInstructionFileCandidates\":[\"AGENTS.local.md\",\"CLAUDE.local.md\"]}","changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"ba65bdb41810f4d0129129dcbd6cadcd643c069d"}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"turn/end","seq":3,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
{"type":"session/end-seed","seq":4,"time":0,"data":{}}
|
||||
{"type":"agent/inbox/spliced","seq":5,"time":0,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Acknowledge the current workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}
|
||||
{"type":"turn/start","seq":6,"time":0,"data":{"turn":2}}
|
||||
{"type":"agent/inbox/spliced","seq":7,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","seq":8,"time":0,"data":{"turn":2,"step":1}}
|
||||
{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Acknowledge the current workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":10,"time":0,"data":{"content":[{"type":"text","text":"<system-reminder>\nUpdated instructions from: AGENTS.md\n\nThis file changed after it was loaded. Use the following content instead of the previously loaded instructions from this file.\n\nNew workspace instruction after offline edit.\n\n</system-reminder>"}],"source":{"kind":"workspace-instructions","form":"instructions","changes":[{"action":"replace","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"d8375b516f158718bd3463bc8eb7ed42c011b29f"}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":11,"time":0,"data":{"title":"Remember the workspace instruction.","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"request/header","seq":12,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}"},"reason":"initial"}}
|
||||
{"type":"request/context","seq":13,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"assistant/chunk","seq":14,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":15,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":0,"text":"RESUME_DONE"}}}
|
||||
{"type":"assistant/chunk","seq":16,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"RESUME_DONE"}}}}
|
||||
{"type":"assistant/chunk","seq":17,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":18,"time":0,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"RESUME_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"}},"sourceEventSeqs":[14,15,16,17],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":19,"time":0,"data":{"turn":2,"step":1}}
|
||||
{"type":"turn/end","seq":20,"time":0,"data":{"turn":2,"reason":{"kind":"completed"}}}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0}
|
||||
{"type":"turn/start","seq":0,"time":0,"data":{"turn":1}}
|
||||
{"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Remember the workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":2,"time":0,"data":{"content":[{"type":"text","text":"<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: CLAUDE.md\n\nOld CLAUDE rule.\n\nInstructions from: AGENTS.md\n\nOld AGENTS rule.\n</system-reminder>"}],"source":{"kind":"workspace-instructions","form":"instructions","baseline":true,"baselineIdentity":"{\"projectRoot\":\"\",\"projectRootMarkers\":[\".git\"],\"maxBytes\":65536,\"maxSourceBytes\":1048576,\"instructionFileCandidates\":[\"CLAUDE.md\",\"AGENTS.md\"],\"localInstructionFileCandidates\":[\"AGENTS.local.md\",\"CLAUDE.local.md\"]}","changes":[{"action":"set","scope":".\u0000CLAUDE.md","path":"CLAUDE.md","digest":"b525eb8a6d3660b732dad4b0aff1b7c63ab32890"},{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"3113bd093ae91976207dcef7390bdc0b2bfcfa10"}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"turn/end","seq":3,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
{"type":"session/end-seed","seq":4,"time":0,"data":{}}
|
||||
{"type":"agent/inbox/spliced","seq":5,"time":0,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Acknowledge the current workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}
|
||||
{"type":"turn/start","seq":6,"time":0,"data":{"turn":2}}
|
||||
{"type":"agent/inbox/spliced","seq":7,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","seq":8,"time":0,"data":{"turn":2,"step":1}}
|
||||
{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Acknowledge the current workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":10,"time":0,"data":{"content":[{"type":"text","text":"<system-reminder>\nThis complete workspace instruction baseline replaces all earlier workspace instruction baselines. The following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nCurrent AGENTS rule.\n\n\nInstructions from: CLAUDE.md\n\nCurrent CLAUDE rule.\n\n</system-reminder>"}],"source":{"kind":"workspace-instructions","form":"instructions","baseline":true,"baselineIdentity":"{\"projectRoot\":\"\",\"projectRootMarkers\":[\".git\"],\"maxBytes\":65536,\"maxSourceBytes\":1048576,\"instructionFileCandidates\":[\"AGENTS.md\",\"CLAUDE.md\"],\"localInstructionFileCandidates\":[\"AGENTS.local.md\",\"CLAUDE.local.md\"]}","changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"7f53d2327837129750aef117f9754a001c46cf68"},{"action":"set","scope":".\u0000CLAUDE.md","path":"CLAUDE.md","digest":"5b1e9e3fd759eee6b43ceff899e47fb10c64701a"}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":11,"time":0,"data":{"title":"Remember the workspace instruction.","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"request/header","seq":12,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}"},"reason":"initial"}}
|
||||
{"type":"request/context","seq":13,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"assistant/chunk","seq":14,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":15,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":0,"text":"RESUME_DONE"}}}
|
||||
{"type":"assistant/chunk","seq":16,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"RESUME_DONE"}}}}
|
||||
{"type":"assistant/chunk","seq":17,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":18,"time":0,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"RESUME_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"}},"sourceEventSeqs":[14,15,16,17],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":19,"time":0,"data":{"turn":2,"step":1}}
|
||||
{"type":"turn/end","seq":20,"time":0,"data":{"turn":2,"reason":{"kind":"completed"}}}
|
||||
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* Assembled-app regression for persisted workspace-instruction resume state.
|
||||
* @module workspace-context-resume-snapshot
|
||||
*/
|
||||
|
||||
import { createHash } from 'node:crypto'
|
||||
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { Context } from 'cordis'
|
||||
import { normalizeSessionLog, scrubRequestHeaders, type NormalizeContext } from '@deepseek-ai/dsh-acp-snapshot'
|
||||
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
|
||||
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, {
|
||||
SESSION_FORMAT_VERSION,
|
||||
SessionId,
|
||||
type SessionEvent,
|
||||
type SessionHeader,
|
||||
} from '@deepseek-ai/dsh-session'
|
||||
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
import { renderWorkspaceContext } from '@deepseek-ai/dsh-workspace-context'
|
||||
import { resolveConfig, workspaceBaselineIdentity } from '@deepseek-ai/dsh-workspace-context/src/config.ts'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const fixtureDir = join(dirname(fileURLToPath(import.meta.url)), 'workspace-context-resume-snapshots/offline-edit')
|
||||
const replayFixture = join(fixtureDir, 'replay.jsonl')
|
||||
const replayOverride = join(fixtureDir, 'replay.override.json')
|
||||
const sessionExpected = join(fixtureDir, 'session.expected.jsonl')
|
||||
const precedenceExpected = join(dirname(fixtureDir), 'precedence-change/session.expected.jsonl')
|
||||
const configPath = fileURLToPath(new URL('../workspace-context-resume.cordis.snapshot.yml', import.meta.url))
|
||||
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url))
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
const sessionId = SessionId('workspace-context-resume')
|
||||
const refreshing = process.env.DSH_SNAPSHOT === 'refresh'
|
||||
const oldInstruction = 'Old workspace instruction.'
|
||||
const newInstruction = 'New workspace instruction after offline edit.'
|
||||
|
||||
interface SeedBaselineOptions {
|
||||
files?: Array<{ name: string; content: string }>
|
||||
instructionFileCandidates?: string[]
|
||||
}
|
||||
|
||||
async function seedVisibleBaseline(
|
||||
root: string,
|
||||
cwd: string,
|
||||
options: SeedBaselineOptions = {},
|
||||
): Promise<string> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SessionPersistenceJsonl, { root, compression: 'none' })
|
||||
const meta: SessionHeader = {
|
||||
version: SESSION_FORMAT_VERSION,
|
||||
id: sessionId,
|
||||
createdAt: 1,
|
||||
cwd,
|
||||
delegationDepth: 0,
|
||||
}
|
||||
const files = options.files ?? [{ name: 'AGENTS.md', content: oldInstruction }]
|
||||
const baseline = renderWorkspaceContext(files.map(file => ({
|
||||
absolutePath: join(cwd, file.name),
|
||||
displayPath: file.name,
|
||||
content: file.content,
|
||||
})), { maxBytes: 65536 })
|
||||
const config = resolveConfig({
|
||||
dshHome: join(cwd, '.dsh'),
|
||||
maxBytes: 65536,
|
||||
...options.instructionFileCandidates === undefined
|
||||
? {}
|
||||
: { instructionFileCandidates: options.instructionFileCandidates },
|
||||
})
|
||||
const events: SessionEvent[] = [
|
||||
{ type: 'turn/start', seq: 0, time: 10, data: { turn: 1 } },
|
||||
{
|
||||
type: 'user/message',
|
||||
seq: 1,
|
||||
time: 11,
|
||||
data: createUserMessage({ content: [{ type: 'text', text: 'Remember the workspace instruction.' }], source: { kind: 'user' } }),
|
||||
surfaceOp: 'append',
|
||||
},
|
||||
{
|
||||
type: 'user/message',
|
||||
seq: 2,
|
||||
time: 12,
|
||||
data: createUserMessage({
|
||||
content: [{ type: 'text', text: baseline.text }],
|
||||
source: {
|
||||
kind: 'workspace-instructions',
|
||||
form: 'instructions',
|
||||
baseline: true,
|
||||
baselineIdentity: workspaceBaselineIdentity(config, cwd, cwd),
|
||||
changes: files.map(file => ({
|
||||
action: 'set',
|
||||
scope: `.\0${file.name}`,
|
||||
path: file.name,
|
||||
digest: createHash('sha1').update(file.content).digest('hex'),
|
||||
})),
|
||||
},
|
||||
}),
|
||||
surfaceOp: 'append',
|
||||
},
|
||||
{ type: 'turn/end', seq: 3, time: 13, data: { turn: 1, reason: { kind: 'completed' } } },
|
||||
]
|
||||
try {
|
||||
await ctx.sessionPersistence.create(meta)
|
||||
await ctx.sessionPersistence.append(sessionId, events)
|
||||
const location = ctx.sessionPersistence.locate(meta)
|
||||
if (location === undefined) throw new Error('JSONL backend did not locate the seeded session')
|
||||
return location.path
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
describe('workspace-context resume snapshot', () => {
|
||||
it('appends an offline replacement without duplicating the visible baseline', async () => {
|
||||
let cwd = ''
|
||||
let sessionPath = ''
|
||||
const result = await runLoaderSmoke({
|
||||
label: 'workspace-context resume headless stream-json snapshot',
|
||||
tempDirPrefix: 'dsh-workspace-context-resume-',
|
||||
binScript,
|
||||
configPath,
|
||||
binArgs: ['--config', configPath, '--output-format', 'stream-json', 'Acknowledge the current workspace instruction.'],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
DSH_SNAPSHOT_FILE: replayFixture,
|
||||
DSH_SNAPSHOT_OVERRIDE: replayOverride,
|
||||
},
|
||||
prepare: async (runCwd) => {
|
||||
cwd = runCwd
|
||||
await mkdir(join(runCwd, '.git'), { recursive: true })
|
||||
await writeFile(join(runCwd, 'AGENTS.md'), `${newInstruction}\n`)
|
||||
sessionPath = await seedVisibleBaseline(join(runCwd, '.sessions'), runCwd)
|
||||
},
|
||||
inspect: async () => {
|
||||
const normalization: NormalizeContext = { sessionIds: [sessionId], cwd }
|
||||
const session = scrubRequestHeaders(normalizeSessionLog(await readFile(sessionPath, 'utf8'), normalization))
|
||||
if (refreshing) await writeFile(sessionExpected, session)
|
||||
expect(session).toBe(await readFile(sessionExpected, 'utf8'))
|
||||
|
||||
const records = session.trimEnd().split('\n').map(line => JSON.parse(line) as {
|
||||
type?: string
|
||||
data?: {
|
||||
source?: { kind?: string; baseline?: boolean; changes?: Array<Record<string, unknown>> }
|
||||
content?: Array<{ type?: string; text?: string }>
|
||||
}
|
||||
})
|
||||
const workspaceEvents = records.filter(record => record.type === 'user/message'
|
||||
&& record.data?.source?.kind === 'workspace-instructions')
|
||||
expect(workspaceEvents.filter(record => record.data?.source?.baseline === true)).toHaveLength(1)
|
||||
expect(workspaceEvents.filter(record => record.data?.source?.baseline !== true)).toHaveLength(1)
|
||||
expect(workspaceEvents.at(-1)?.data?.source?.changes).toMatchObject([{
|
||||
action: 'replace', scope: '.\0AGENTS.md', path: 'AGENTS.md',
|
||||
}])
|
||||
expect(JSON.stringify(workspaceEvents.at(-1)?.data?.content)).toContain(newInstruction)
|
||||
|
||||
const files = await readdir(join(cwd, '.sessions'), { recursive: true })
|
||||
expect(files.filter(file => file.endsWith('.jsonl'))).toHaveLength(1)
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.stderr).toBe('')
|
||||
const records = result.stdout.trimEnd().split('\n').map(line => JSON.parse(line) as Record<string, unknown>)
|
||||
expect(records.at(-1)).toMatchObject({
|
||||
type: 'result',
|
||||
sessionId,
|
||||
output: 'RESUME_DONE',
|
||||
})
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('supersedes an incompatible baseline when precedence changed offline', async () => {
|
||||
let cwd = ''
|
||||
let sessionPath = ''
|
||||
const result = await runLoaderSmoke({
|
||||
label: 'workspace-context precedence-change resume snapshot',
|
||||
tempDirPrefix: 'dsh-workspace-context-precedence-',
|
||||
binScript,
|
||||
configPath,
|
||||
binArgs: ['--config', configPath, '--output-format', 'stream-json', 'Acknowledge the current workspace instruction.'],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
DSH_SNAPSHOT_FILE: replayFixture,
|
||||
DSH_SNAPSHOT_OVERRIDE: replayOverride,
|
||||
},
|
||||
prepare: async (runCwd) => {
|
||||
cwd = runCwd
|
||||
await mkdir(join(runCwd, '.git'), { recursive: true })
|
||||
await writeFile(join(runCwd, 'AGENTS.md'), 'Current AGENTS rule.\n')
|
||||
await writeFile(join(runCwd, 'CLAUDE.md'), 'Current CLAUDE rule.\n')
|
||||
sessionPath = await seedVisibleBaseline(join(runCwd, '.sessions'), runCwd, {
|
||||
files: [
|
||||
{ name: 'CLAUDE.md', content: 'Old CLAUDE rule.' },
|
||||
{ name: 'AGENTS.md', content: 'Old AGENTS rule.' },
|
||||
],
|
||||
instructionFileCandidates: ['CLAUDE.md', 'AGENTS.md'],
|
||||
})
|
||||
},
|
||||
inspect: async () => {
|
||||
const normalization: NormalizeContext = { sessionIds: [sessionId], cwd }
|
||||
const session = scrubRequestHeaders(normalizeSessionLog(await readFile(sessionPath, 'utf8'), normalization))
|
||||
if (refreshing) {
|
||||
await mkdir(dirname(precedenceExpected), { recursive: true })
|
||||
await writeFile(precedenceExpected, session)
|
||||
}
|
||||
expect(session).toBe(await readFile(precedenceExpected, 'utf8'))
|
||||
|
||||
const records = session.trimEnd().split('\n').map(line => JSON.parse(line) as {
|
||||
type?: string
|
||||
data?: {
|
||||
source?: { kind?: string; baseline?: boolean }
|
||||
content?: Array<{ type?: string; text?: string }>
|
||||
}
|
||||
})
|
||||
const baselines = records.filter(record => record.type === 'user/message'
|
||||
&& record.data?.source?.kind === 'workspace-instructions'
|
||||
&& record.data.source.baseline === true)
|
||||
expect(baselines).toHaveLength(2)
|
||||
const replacement = JSON.stringify(baselines.at(-1)?.data?.content)
|
||||
expect(replacement).toContain('replaces all earlier workspace instruction baselines')
|
||||
expect(replacement.indexOf('Instructions from: AGENTS.md'))
|
||||
.toBeLessThan(replacement.indexOf('Instructions from: CLAUDE.md'))
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.stderr).toBe('')
|
||||
expect(result.stdout.trimEnd().split('\n').map(line => JSON.parse(line) as Record<string, unknown>).at(-1))
|
||||
.toMatchObject({
|
||||
type: 'result',
|
||||
sessionId,
|
||||
output: 'RESUME_DONE',
|
||||
})
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
})
|
||||
@@ -0,0 +1,37 @@
|
||||
# Keyless real-Loader composition for workspace-instruction resume
|
||||
# reconciliation. The test seeds one persisted baseline, changes AGENTS.md
|
||||
# while the session is offline, then resumes through the public agent service.
|
||||
|
||||
- id: persistence
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: './.sessions'
|
||||
compression: none
|
||||
|
||||
- id: replay
|
||||
name: '@deepseek-ai/dsh-llm-replay'
|
||||
config:
|
||||
file: !!js process.env.DSH_SNAPSHOT_FILE
|
||||
overrideFile: !!js process.env.DSH_SNAPSHOT_OVERRIDE
|
||||
|
||||
- id: fs-local
|
||||
name: '@deepseek-ai/dsh-fs-local'
|
||||
config:
|
||||
cwd: !!js process.cwd()
|
||||
|
||||
- id: agent
|
||||
name: '@deepseek-ai/dsh-agent-spine-demo'
|
||||
config:
|
||||
agents: []
|
||||
workspaceContext:
|
||||
maxBytes: 65536
|
||||
dshHome: !!js process.cwd() + '/.dsh'
|
||||
skills:
|
||||
enabled: false
|
||||
toolBash: false
|
||||
toolTasks: false
|
||||
goals: false
|
||||
|
||||
# Await the persisted resume before the headless driver inspects root agents.
|
||||
- id: resumed-agent
|
||||
name: './tests/fixtures/workspace-context-resume-agent.ts'
|
||||
@@ -21,6 +21,7 @@
|
||||
"@deepseek-ai/dsh-code-runtime-worker": "workspace:*",
|
||||
"@deepseek-ai/dsh-command-goal": "workspace:*",
|
||||
"@deepseek-ai/dsh-commands": "workspace:*",
|
||||
"@deepseek-ai/dsh-compact": "workspace:*",
|
||||
"@deepseek-ai/dsh-compact-basic": "workspace:*",
|
||||
"@deepseek-ai/dsh-compact-tool-result-prune": "workspace:*",
|
||||
"@deepseek-ai/dsh-credentials-local": "workspace:*",
|
||||
@@ -66,6 +67,8 @@
|
||||
"@deepseek-ai/dsh-spill-policy": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent-acp": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent-claude-code": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent-codex": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent-dsh-sdk": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent-fork": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent-spawn": "workspace:*",
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"headless-agent/tests/fixtures/cli-mock-llm.ts",
|
||||
"headless-agent/tests/fixtures/semantic-checkpoint-agent.ts",
|
||||
"headless-agent/tests/fixtures/subagent-inheritance-agent.ts",
|
||||
"headless-agent/tests/fixtures/workspace-context-resume-agent.ts",
|
||||
"headless-agent/tests/fixtures/goal-domain/seed-goal.ts",
|
||||
"headless-agent/tests/fixtures/time-context-driver.ts",
|
||||
"headless-agent/tests/fixtures/time-context-mock-llm.ts",
|
||||
@@ -46,8 +47,13 @@
|
||||
"acp-agent/tests/fixtures/partial-landlock-sandbox.ts",
|
||||
"acp-agent/tests/fixtures/subagent-durability-failure.ts",
|
||||
"acp-agent/tests/fixtures/subagent-settlement-marker.ts",
|
||||
"acp-agent/tests/fixtures/workspace-context-compaction.ts",
|
||||
"acp-agent/tests/fixtures/subagent/subagent-acp/mock-delegating-llm.ts",
|
||||
"acp-agent/tests/fixtures/subagent/subagent-acp/driver.ts",
|
||||
"acp-agent/tests/fixtures/subagent/subagent-claude-code/fixture.ts",
|
||||
"acp-agent/tests/fixtures/subagent/subagent-claude-code/driver.ts",
|
||||
"acp-agent/tests/fixtures/subagent/subagent-codex/fixture.ts",
|
||||
"acp-agent/tests/fixtures/subagent/subagent-codex/driver.ts",
|
||||
"jsonrpc-agent/tests/fixtures/subagent/subagent-dsh-sdk/driver.ts",
|
||||
"jsonrpc-agent/tests/fixtures/subagent/subagent-dsh-sdk/child-mock-llm.ts",
|
||||
"jsonrpc-agent/tests/fixtures/subagent/subagent-dsh-sdk/mock-delegating-llm.ts",
|
||||
@@ -553,6 +559,29 @@
|
||||
"tests/**/*.ts"
|
||||
]
|
||||
},
|
||||
"packages/subagent/subagent-codex": {
|
||||
"entry": [
|
||||
"tests/**/*.spec.ts",
|
||||
"tests/**/*.e2e.ts"
|
||||
],
|
||||
"project": [
|
||||
"src/**/*.ts",
|
||||
"tests/**/*.ts"
|
||||
],
|
||||
"ignoreDependencies": [
|
||||
"@openai/codex"
|
||||
]
|
||||
},
|
||||
"packages/subagent/subagent-claude-code": {
|
||||
"entry": [
|
||||
"tests/**/*.spec.ts",
|
||||
"tests/**/*.e2e.ts"
|
||||
],
|
||||
"project": [
|
||||
"src/**/*.ts",
|
||||
"tests/**/*.ts"
|
||||
]
|
||||
},
|
||||
"packages/fs/tool-fs": {
|
||||
"entry": [
|
||||
"tests/**/*.spec.ts",
|
||||
@@ -597,6 +626,22 @@
|
||||
"@deepseek-ai/dsh-client-ui-theme"
|
||||
]
|
||||
},
|
||||
"packages/client/ui-settings": {
|
||||
"entry": [
|
||||
"tests/**/*.spec.ts",
|
||||
"tests/**/*.spec.tsx"
|
||||
],
|
||||
"project": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"tests/**/*.ts",
|
||||
"tests/**/*.tsx"
|
||||
],
|
||||
"ignoreDependencies": [
|
||||
"@types/react-dom",
|
||||
"react-dom"
|
||||
]
|
||||
},
|
||||
"apps/web": {
|
||||
"entry": [
|
||||
"tests/**/*.e2e.ts",
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/bash/bash-local/README.md
|
||||
README.md: bb87ad6fe021e3144cef4adced3d798bf3d94d67
|
||||
README.zh.md: d2f8c9091072bbf3d75909f6826432601001ab88
|
||||
README.md: cb40cb8fa40d95d5b4589b7c804f450a2bf38c8e
|
||||
README.zh.md: bd4f73babdb47ff92e87e20eb7d60657ed515ec4
|
||||
@@ -23,7 +23,7 @@ The package root exports the default and named `LocalBashExecutor` plugin plus i
|
||||
## Behavior
|
||||
|
||||
- **Spawn per call, no shell state** — every call is a fresh non-login `bash -c` with no rc files.
|
||||
- **Configured budgets over managed groups** — `resolve()` fills `workdir`/`timeoutMs`/`stdoutMaxBytes` from config, and every spawn hands the service explicit byte caps, spill cap, and `graceMs`. Process-group kills, post-exit pipe draining, tail retention, and bounded spill files are [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md) mechanics. A foreground `BashExecRequest.stdoutMaxBytes` can raise stdout's capture budget for one trusted caller; stderr and background runs still use `maxOutputBytes`.
|
||||
- **Configured budgets over managed groups** — `resolve()` fills `workdir`/`timeoutMs`/`stdoutMaxBytes` from config, and every spawn hands the service explicit byte caps, spill cap, and `graceMs`. The grace must be positive, finite, and no greater than [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md), so Node can represent it with one timer. Process-group kills, post-exit pipe draining, tail retention, and bounded spill files are [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md) mechanics. A foreground `BashExecRequest.stdoutMaxBytes` can raise stdout's capture budget for one trusted caller; stderr and background runs still use `maxOutputBytes`.
|
||||
- **Timeout and cancel classification** — `run()` fuses its config-clamped timeout with the caller's signal through one deadline; only the executor's own timeout reports `timedOut`, an upstream cancel reports `aborted`, and a self-signaled command reports neither ([timeout-library Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md)).
|
||||
- **Model-friendly terminal env** — `NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat` prevents pagers and ANSI color from garbling results. These values merge as ordinary env under the service's credential scrub and `DSH_*` channel rules; an explicit caller entry still wins. See the [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) and [managed environment Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md).
|
||||
- **Background processes** — `start()` returns a live `BashProcess` handle immediately with no timeout, and `readOutput()` merges offset-based stdout/stderr reads into one consuming delta, placing stderr under a `[stderr]` marker when present. A running process belongs to the subprocess service, survives executor reloads, and is killed and joined on service disposal. Task ids, ownership, polling, and notices belong to the generic [`ctx.tasks` runtime](../../tasks/tasks/README.md), which the tool layer registers the handle with.
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
## 行为
|
||||
|
||||
- **每次调用都 spawn,不保留 shell 状态**:每次调用都启动新的非登录 `bash -c`,且不读取 rc 文件。
|
||||
- **在受管进程组之上应用配置预算**:`resolve()` 从配置补全 `workdir`/`timeoutMs`/`stdoutMaxBytes`,每次 spawn 都向服务传入显式的字节上限、spill 上限与 `graceMs`。进程组终止、退出后管道排空、尾部保留与有界 spill 文件是 [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md) 的机制。前台 `BashExecRequest.stdoutMaxBytes` 可为某个受信任调用方提高单次 stdout 捕获预算;stderr 和后台运行仍使用 `maxOutputBytes`。
|
||||
- **在受管进程组之上应用配置预算**:`resolve()` 从配置补全 `workdir`/`timeoutMs`/`stdoutMaxBytes`,每次 spawn 都向服务传入显式的字节上限、spill 上限与 `graceMs`。该宽限期须为正有限值,且不得大于 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md),这样 Node 就能用一个定时器表示它。进程组终止、退出后管道排空、尾部保留与有界 spill 文件是 [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md) 的机制。前台 `BashExecRequest.stdoutMaxBytes` 可为某个受信任调用方提高单次 stdout 捕获预算;stderr 和后台运行仍使用 `maxOutputBytes`。
|
||||
- **超时与取消分类**:`run()` 通过同一个 deadline 把经配置钳位的超时与调用方的信号融合;只有执行器自身的超时报告 `timedOut`,上游取消报告 `aborted`,自身因信号终止的命令两者皆不报告(见[超时库 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md))。
|
||||
- **适合模型的终端环境**:`NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat` 防止分页器与 ANSI 颜色破坏结果。这些值作为普通 env 合并,遵循服务的凭据清除与 `DSH_*` 通道规则;调用方的显式条目依旧优先。详见 [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) 与 [受管环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md)。
|
||||
- **后台进程**:`start()` 会立即返回活动的 `BashProcess` 句柄且不应用超时;`readOutput()` 把基于偏移量的 stdout/stderr 读取合并为一条消费式增量,并在存在 stderr 时将其置于 `[stderr]` 标记下。运行中的进程属于 subprocess 服务,可在执行器重载后存活,并在服务 dispose 时被终止且等待退出。task id、所有权、轮询和通知属于通用 [`ctx.tasks` 运行时](../../tasks/tasks/README.md),工具层会在其中注册该句柄。
|
||||
|
||||
@@ -14,7 +14,7 @@ import z from 'schemastery'
|
||||
import { BashExecutor } from '@deepseek-ai/dsh-bash'
|
||||
import type { BashExecRequest, BashExecSpec, BashProcess, BashProcessRead, BashRunResult, CollectedOutput } from '@deepseek-ai/dsh-bash'
|
||||
import type { SubprocessCollect, SubprocessHandle, SubprocessOutputReader, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
|
||||
import { clampTimeout, deadline, timeoutOf } from '@deepseek-ai/dsh-timeout'
|
||||
import { clampTimeout, deadline, MAX_TIMER_DELAY_MS, timeoutOf } from '@deepseek-ai/dsh-timeout'
|
||||
|
||||
/**
|
||||
* Model-friendly environment overrides: disable colors, pagers, and
|
||||
@@ -48,7 +48,7 @@ export interface Config {
|
||||
maxOutputBytes?: number
|
||||
/** Per-stream spill-file cap; larger streams retain only their in-memory tail. */
|
||||
maxSpillBytes?: number
|
||||
/** Grace period for kill escalation and for inherited pipes after shell exit. */
|
||||
/** Grace period for kill escalation and inherited pipes; at most `MAX_TIMER_DELAY_MS`. */
|
||||
graceMs?: number
|
||||
}
|
||||
|
||||
@@ -102,6 +102,9 @@ export class LocalBashExecutor extends BashExecutor {
|
||||
assertPositiveFinite('maxOutputBytes', this.config.maxOutputBytes)
|
||||
assertPositiveFinite('maxSpillBytes', this.config.maxSpillBytes)
|
||||
assertPositiveFinite('graceMs', this.config.graceMs)
|
||||
if (this.config.graceMs > MAX_TIMER_DELAY_MS) {
|
||||
throw new Error(`bash-local: graceMs must be no greater than ${MAX_TIMER_DELAY_MS}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
|
||||
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
|
||||
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
|
||||
import type { BashProcess } from '@deepseek-ai/dsh-bash'
|
||||
|
||||
const spillDir = mkdtempSync(join(tmpdir(), 'dsh-bash-exec-spec-'))
|
||||
@@ -70,6 +71,8 @@ describe('LocalBashExecutor.run', () => {
|
||||
await expect(setup({ maxOutputBytes: -1 })).rejects.toThrow(/maxOutputBytes/)
|
||||
await expect(setup({ maxSpillBytes: 0 })).rejects.toThrow(/maxSpillBytes/)
|
||||
await expect(setup({ graceMs: 0 })).rejects.toThrow(/graceMs/)
|
||||
await expect(setup({ graceMs: MAX_TIMER_DELAY_MS + 1 }))
|
||||
.rejects.toThrow(`graceMs must be no greater than ${MAX_TIMER_DELAY_MS}`)
|
||||
|
||||
const { bash } = await setup()
|
||||
expect(() => bash.resolve({ command: 'true', timeoutMs: Number.NaN })).toThrow(/request\.timeoutMs/)
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/bash/pwsh-local/README.md
|
||||
README.md: 2914c46ab91dd9555dab04551e52321f6eac05bf
|
||||
README.zh.md: ce9696b276a2e60acf116d7124cd5cd256d7ebde
|
||||
README.md: 3e38ea3830cb651a80eaee744a42f68891767358
|
||||
README.zh.md: 8d32ce865d299bac37704e3e8730a7faa63ee108
|
||||
@@ -30,7 +30,7 @@ The Windows counterpart of `dsh-bash-local`, deliberately mirroring its semantic
|
||||
- **Spawn per call, no shell state** — every call is a fresh non-interactive `pwsh -Command` (deterministic; no profile files). The `-NoLogo -NoProfile -NonInteractive` flags disable startup banners, profile loading, and prompts that would garble tool output.
|
||||
- **UTF-8 output pinned** — every command runs with `[Console]::OutputEncoding` and `$OutputEncoding` set to UTF-8 first, so the Windows PowerShell 5.1 fallback (or any host whose console code page is not UTF-8) cannot garble non-ASCII output: the subprocess collector decodes bytes as UTF-8. Input encoding is left at the host default; pwsh 7 defaults to UTF-8 and is unaffected.
|
||||
- **Executable resolution** — `resolvePwshPath` prefers an explicit `pwshPath`, then on Windows probes PowerShell 7's install location, every PATH entry (Microsoft Store installs; surrounding quotes stripped), and Windows PowerShell 5.1 as a legacy last resort, checking `existsSync` on each; elsewhere it falls back to a bare `pwsh` resolved through PATH. Resolution is a pure function of `(configured, env, platform)` and happens once at construction.
|
||||
- **Configured budgets over managed groups** — `resolve()` fills `workdir`/`timeoutMs`/`stdoutMaxBytes` from config, and every spawn hands the service explicit byte caps, spill cap, and `graceMs`. Tree termination (taskkill on Windows, process-group signals on POSIX), the post-exit pipe-drain grace, tail-keep truncation, and bounded spill files are [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md) mechanics. A foreground `BashExecRequest.stdoutMaxBytes` can raise stdout's capture budget for one trusted caller; stderr and background runs still use `maxOutputBytes`.
|
||||
- **Configured budgets over managed groups** — `resolve()` fills `workdir`/`timeoutMs`/`stdoutMaxBytes` from config, and every spawn hands the service explicit byte caps, spill cap, and `graceMs`. The grace must be positive, finite, and no greater than [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md), so Node can represent it with one timer. Tree termination (taskkill on Windows, process-group signals on POSIX), the post-exit pipe-drain grace, tail-keep truncation, and bounded spill files are [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md) mechanics. A foreground `BashExecRequest.stdoutMaxBytes` can raise stdout's capture budget for one trusted caller; stderr and background runs still use `maxOutputBytes`.
|
||||
- **Timeout and cancel classification** — `run()` fuses its config-clamped timeout with the caller's signal through one deadline; only the executor's own timeout reports `timedOut`, an upstream cancel reports `aborted`, and a self-terminated command reports neither ([timeout-library Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md)). Windows reports forced termination as exit 1 without a signal, so signal-stamped facts (`signal`, `killed` status) are POSIX-only there; the timeout/abort classification is platform-independent.
|
||||
- **Model-friendly terminal env** — `NO_COLOR=1 PAGER=cat GIT_PAGER=cat` (no `TERM=dumb`: that is a POSIX concept; `NO_COLOR` is honored by modern PowerShell renderers) merged as ordinary env under the service's credential scrub and `DSH_*` channel rules; an explicit caller entry still wins.
|
||||
- **Background processes** — `start()` returns a live `BashProcess` handle immediately, no timeout applies, and the handle's `readOutput()` merges the service's offset-based stdout/stderr reads into one marked-section delta with a consuming cursor. A still-running process belongs to the subprocess service, so it survives executor reloads and dies (killed and joined) with the service's disposal. Everything task-shaped (ids, ownership, polling, notices) lives in the generic [`ctx.tasks` runtime](../../tasks/tasks/README.md), which the tool layer registers the handle with — this executor never sees a session or a registry.
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
- **每次调用新建进程,无 shell 状态**——每次调用都是全新的非交互 `pwsh -Command`(确定性;不加载 profile 文件)。`-NoLogo -NoProfile -NonInteractive` 关闭启动横幅、profile 加载与会干扰工具输出的提示符。
|
||||
- **UTF-8 输出固定**——每条命令都先以 UTF-8 设置 `[Console]::OutputEncoding` 与 `$OutputEncoding`,因此 Windows PowerShell 5.1 兜底(或任何控制台代码页非 UTF-8 的主机)不会破坏非 ASCII 输出:subprocess collector 以 UTF-8 解码字节。输入编码保持宿主默认;pwsh 7 默认为 UTF-8,不受影响。
|
||||
- **可执行文件解析**——`resolvePwshPath` 优先显式 `pwshPath`,然后在 Windows 上依次探测 PowerShell 7 安装位置、每个 PATH 条目(Microsoft Store 安装;剥离两端引号)以及作为遗留兜底的 Windows PowerShell 5.1,逐一检查 `existsSync`;其他平台回退为通过 PATH 解析的裸 `pwsh`。解析是 `(configured, env, platform)` 的纯函数,在构造时执行一次。
|
||||
- **受管进程组之上的配置预算**——`resolve()` 从配置填充 `workdir`/`timeoutMs`/`stdoutMaxBytes`,每次 spawn 都向服务提供显式字节上限、spill 上限与 `graceMs`。进程树终止(Windows 用 taskkill,POSIX 用进程组信号)、退出后管道排空宽限、保尾截断与有界 spill 文件是 [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md) 的机制。前台 `BashExecRequest.stdoutMaxBytes` 可为单个受信调用方提高 stdout 捕获预算;stderr 与后台运行仍使用 `maxOutputBytes`。
|
||||
- **受管进程组之上的配置预算**——`resolve()` 从配置填充 `workdir`/`timeoutMs`/`stdoutMaxBytes`,每次 spawn 都向服务提供显式字节上限、spill 上限与 `graceMs`。该宽限期须为正有限值,且不得大于 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md),这样 Node 就能用一个定时器表示它。进程树终止(Windows 用 taskkill,POSIX 用进程组信号)、退出后管道排空宽限、保尾截断与有界 spill 文件是 [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md) 的机制。前台 `BashExecRequest.stdoutMaxBytes` 可为单个受信调用方提高 stdout 捕获预算;stderr 与后台运行仍使用 `maxOutputBytes`。
|
||||
- **超时与取消分类**——`run()` 通过一个 deadline 融合配置夹取的超时与调用方信号;只有执行器自身超时报告 `timedOut`,上游取消报告 `aborted`,自我终止的命令两者都不报告(见 [timeout 库 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md))。Windows 将强制终止报告为退出码 1 且无信号,因此基于信号的实情(`signal`、`killed` 状态)在那里仅限 POSIX;超时/取消分类与平台无关。
|
||||
- **面向模型的终端环境**——`NO_COLOR=1 PAGER=cat GIT_PAGER=cat`(没有 `TERM=dumb`:那是 POSIX 概念;现代 PowerShell 渲染器遵循 `NO_COLOR`),作为普通 env 在服务的凭据清理与 `DSH_*` 通道规则之下合并;显式调用方条目仍然优先。
|
||||
- **后台进程**——`start()` 立即返回存活的 `BashProcess` 句柄,不设超时;句柄的 `readOutput()` 把服务基于偏移的 stdout/stderr 读取合并为带标记分段的增量与消费游标。仍在运行的进程属于 subprocess 服务,因此它跨执行器重载存活,并随服务销毁(被终止并 join)。一切任务形状的职责(id、所有权、轮询、通知)都在通用 [`ctx.tasks` 运行时](../../tasks/tasks/README.md) 中,由工具层把句柄注册进去——本执行器从不接触会话或注册表。
|
||||
|
||||
@@ -18,7 +18,7 @@ import z from 'schemastery'
|
||||
import { BashExecutor } from '@deepseek-ai/dsh-bash'
|
||||
import type { BashExecRequest, BashExecSpec, BashProcess, BashProcessRead, BashRunResult, CollectedOutput } from '@deepseek-ai/dsh-bash'
|
||||
import type { SubprocessCollect, SubprocessHandle, SubprocessOutputReader, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
|
||||
import { clampTimeout, deadline, timeoutOf } from '@deepseek-ai/dsh-timeout'
|
||||
import { clampTimeout, deadline, MAX_TIMER_DELAY_MS, timeoutOf } from '@deepseek-ai/dsh-timeout'
|
||||
import { resolvePwshPath } from './resolve.ts'
|
||||
|
||||
/* jscpd:ignore-start -- deliberate call-for-call mirror of dsh-bash-local (Agent Note: pwsh-tool-and-executor). */
|
||||
@@ -62,7 +62,7 @@ export interface Config {
|
||||
maxOutputBytes?: number
|
||||
/** Per-stream spill-file cap; larger streams retain only their in-memory tail. */
|
||||
maxSpillBytes?: number
|
||||
/** Grace period for kill escalation and for inherited pipes after shell exit. */
|
||||
/** Grace period for kill escalation and inherited pipes; at most `MAX_TIMER_DELAY_MS`. */
|
||||
graceMs?: number
|
||||
/**
|
||||
* Explicit pwsh executable. When omitted, well-known Windows install
|
||||
@@ -129,6 +129,9 @@ export class PwshLocalExecutor extends BashExecutor {
|
||||
assertPositiveFinite('maxOutputBytes', this.config.maxOutputBytes)
|
||||
assertPositiveFinite('maxSpillBytes', this.config.maxSpillBytes)
|
||||
assertPositiveFinite('graceMs', this.config.graceMs)
|
||||
if (this.config.graceMs > MAX_TIMER_DELAY_MS) {
|
||||
throw new Error(`pwsh-local: graceMs must be no greater than ${MAX_TIMER_DELAY_MS}`)
|
||||
}
|
||||
this.pwshPath = resolvePwshPath(this.config.pwshPath)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import { PwshLocalExecutor, ENCODING_PREAMBLE, candidatePwshPaths, resolvePwshPa
|
||||
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
|
||||
import SubprocessService from '@deepseek-ai/dsh-subprocess'
|
||||
import type { SubprocessHandle, SubprocessOutputReader, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
|
||||
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
|
||||
import type { BashProcess } from '@deepseek-ai/dsh-bash'
|
||||
|
||||
const spillDir = mkdtempSync(join(tmpdir(), 'dsh-pwsh-exec-spec-'))
|
||||
@@ -187,6 +188,8 @@ describe.skipIf(!hasPwsh)('PwshLocalExecutor.run', () => {
|
||||
await expect(setup({ maxOutputBytes: -1 })).rejects.toThrow(/maxOutputBytes/)
|
||||
await expect(setup({ maxSpillBytes: 0 })).rejects.toThrow(/maxSpillBytes/)
|
||||
await expect(setup({ graceMs: 0 })).rejects.toThrow(/graceMs/)
|
||||
await expect(setup({ graceMs: MAX_TIMER_DELAY_MS + 1 }))
|
||||
.rejects.toThrow(`graceMs must be no greater than ${MAX_TIMER_DELAY_MS}`)
|
||||
|
||||
const { bash } = await setup()
|
||||
expect(() => bash.resolve({ command: 'Write-Output ok', timeoutMs: Number.NaN })).toThrow(/request\.timeoutMs/)
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/bundle/web-app/README.md
|
||||
README.md: 95cdbc9694b44539742e5b871157eefa7cb4c290
|
||||
README.zh.md: b8d6e9d80bac82a7798cc07d3a34c01d219f2174
|
||||
README.md: a835497982e41c29c5dbc287c43b0515a61f77c9
|
||||
README.zh.md: 00d8636e848a7927792dfc8ad3a460015c9e7fef
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The dsh browser-surface bundle. [`cordis.patch.yml`](cordis.patch.yml) rides over [`dsh-base`](../base/README.md): it sets the coding persona, inserts the Web host rows (webserver, API gateway, workspace, projection, storage) and the browser plugin roster, and mounts this package's own `web-runtime` glue plugin (config `{mode, printUrl, lanAddresses}`). That plugin owns what used to be launcher code: it resolves the built frontend dist through `@deepseek-ai/dsh-frontend`'s exports (workspace knowledge of this bundle, never user config), mounts the [`frontend-static`](../../host/frontend-static/README.md) fallback owner over it, registers the web-surface prompt section and the bash-visible `DSH_WEB_URL`/`DSH_WEB_MODE` runtime variables, and prints the `dsh web:` URL line when `printUrl` is true. The `dsh web` launcher alias patches `mode`/`lanAddresses`/`printUrl` and the flag family over these rows; [`dsh-headless`](../headless/README.md) layers on top and silences the URL line.
|
||||
The dsh browser-surface bundle. [`cordis.patch.yml`](cordis.patch.yml) rides over [`dsh-base`](../base/README.md): it sets the coding persona, inserts the Web host rows (webserver, API gateway, workspace, projection, storage) and the browser plugin roster, and mounts this package's own `web-runtime` glue plugin (config `{mode, printUrl, surfaceContext, lanAddresses}`). That plugin owns what used to be launcher code: it resolves the built frontend dist through `@deepseek-ai/dsh-frontend`'s exports (workspace knowledge of this bundle, never user config), mounts the [`frontend-static`](../../host/frontend-static/README.md) fallback owner over it, registers the web-surface prompt section and the bash-visible `DSH_WEB_URL`/`DSH_WEB_MODE` runtime variables when `surfaceContext` is true, and prints the `dsh web:` URL line when `printUrl` is true. The `dsh web` launcher alias patches `mode`/`lanAddresses` and the flag family over these rows; [`dsh-headless`](../headless/README.md) layers on top, silences the URL line, and disables the surface context.
|
||||
|
||||
## Model Experience
|
||||
|
||||
@@ -10,7 +10,7 @@ The dsh browser-surface bundle. [`cordis.patch.yml`](cordis.patch.yml) rides ove
|
||||
|
||||
#### What the model sees
|
||||
|
||||
The `app:web-surface` global section (order −98) orients the model to the GUI: the canonical local URL, the "this page" referent, the HMR/rebuild update contract for the active mode, and the instruction not to start replacement servers. `DSH_WEB_URL` and `DSH_WEB_MODE` additionally appear in the managed bash environment with their descriptions, resolved per invocation from the live server.
|
||||
When `surfaceContext` is true, the `app:web-surface` global section (order −98) orients the model to the GUI: the canonical local URL, the "this page" referent, the HMR/rebuild update contract for the active mode, and the instruction not to start replacement servers. `DSH_WEB_URL` and `DSH_WEB_MODE` additionally appear in the managed bash environment with their descriptions, resolved per invocation from the live server. When it is false, neither the section nor the variables are registered.
|
||||
|
||||
#### Token effect
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
dsh 浏览器表层组合包。[`cordis.patch.yml`](cordis.patch.yml) 叠加在 [`dsh-base`](../base/README.md) 之上:设置 coding persona,插入 Web 宿主行(webserver、API 网关、workspace、投影、存储)与浏览器插件名录,并挂载本包自己的 `web-runtime` 粘合插件(配置为 `{mode, printUrl, lanAddresses}`)。该插件接管了原先属于启动器的代码:它通过 `@deepseek-ai/dsh-frontend` 的 exports 解析已构建的前端 dist(这是本组合包的 workspace 知识,绝不是用户配置),在其上挂载 [`frontend-static`](../../host/frontend-static/README.md) 回退席位所有者,注册 web 表层提示词段落和 bash 可见的 `DSH_WEB_URL`/`DSH_WEB_MODE` 运行时变量,并在 `printUrl` 为 true 时打印 `dsh web:` URL 行。`dsh web` 启动器别名把 `mode`/`lanAddresses`/`printUrl` 与相应 flag 家族 patch 到这些行上;[`dsh-headless`](../headless/README.md) 再叠加一层并关闭 URL 行。
|
||||
dsh 浏览器表层组合包。[`cordis.patch.yml`](cordis.patch.yml) 叠加在 [`dsh-base`](../base/README.md) 之上:设置 coding persona,插入 Web 宿主行(webserver、API 网关、workspace、投影、存储)与浏览器插件名录,并挂载本包自己的 `web-runtime` 粘合插件(配置为 `{mode, printUrl, surfaceContext, lanAddresses}`)。该插件接管了原先属于启动器的代码:它通过 `@deepseek-ai/dsh-frontend` 的 exports 解析已构建的前端 dist(这是本组合包的 workspace 知识,绝不是用户配置),在其上挂载 [`frontend-static`](../../host/frontend-static/README.md) 回退席位所有者,在 `surfaceContext` 为 true 时注册 web 表层提示词段落和 bash 可见的 `DSH_WEB_URL`/`DSH_WEB_MODE` 运行时变量,并在 `printUrl` 为 true 时打印 `dsh web:` URL 行。`dsh web` 启动器别名把 `mode`/`lanAddresses` 与相应 flag 家族 patch 到这些行上;[`dsh-headless`](../headless/README.md) 再叠加一层,关闭 URL 行并禁用表层上下文。
|
||||
|
||||
## 模型体验
|
||||
|
||||
@@ -10,7 +10,7 @@ dsh 浏览器表层组合包。[`cordis.patch.yml`](cordis.patch.yml) 叠加在
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
全局段落 `app:web-surface`(顺序 −98)向模型说明 GUI:规范的本地 URL、「this page」指代什么、当前模式下 HMR(热模块替换)/重建的更新契约,以及不要启动替代服务器的指令。`DSH_WEB_URL` 与 `DSH_WEB_MODE` 还会连同各自描述出现在受管 bash 环境中,每次调用时从运行中的服务器解析。
|
||||
当 `surfaceContext` 为 true 时,全局段落 `app:web-surface`(顺序 −98)向模型说明 GUI:规范的本地 URL、「this page」指代什么、当前模式下 HMR(热模块替换)/重建的更新契约,以及不要启动替代服务器的指令。`DSH_WEB_URL` 与 `DSH_WEB_MODE` 还会连同各自描述出现在受管 bash 环境中,每次调用时从运行中的服务器解析。当它为 false 时,该提示词段和这些变量都不会注册。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
|
||||
@@ -103,12 +103,15 @@
|
||||
# assembly fact of dsh-web-app, never user config), mounts the
|
||||
# frontend-static fallback owner, registers the web-surface prompt
|
||||
# section and bash runtime variables, and prints the URL line. `dsh web`
|
||||
# patches mode/lanAddresses over these defaults.
|
||||
# patches mode/lanAddresses over these defaults; complete-prompt overlays
|
||||
# set surfaceContext false to suppress every model- and shell-visible Web
|
||||
# runtime contribution.
|
||||
- id: web-runtime
|
||||
name: '@deepseek-ai/dsh-web-app'
|
||||
config:
|
||||
mode: production
|
||||
printUrl: true
|
||||
surfaceContext: true
|
||||
|
||||
# ── browser plugin roster (dshClient rows; node halves are layer-2 hosts) ──
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { BrandWordmark, Button } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { BrandWordmark, Button, OnboardingSurface } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-web-react'
|
||||
import type { ModelsSettingsState, ModelsSettingsStore } from './store.ts'
|
||||
import { deepSeekReadiness } from './store.ts'
|
||||
@@ -66,6 +66,9 @@ export function DeepSeekOnboardingDialog(props: DeepSeekOnboardingDialogProps):
|
||||
openSection('models')
|
||||
}
|
||||
|
||||
// Null covers the still-deciding and nothing-to-do states alike: the
|
||||
// takeover chrome below is part of THIS render, so declining paints and
|
||||
// blocks nothing while the shared join is in flight.
|
||||
switch (readiness.kind) {
|
||||
case 'loading':
|
||||
case 'adapter-absent':
|
||||
@@ -80,25 +83,27 @@ export function DeepSeekOnboardingDialog(props: DeepSeekOnboardingDialogProps):
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={styles['page']} role="region" aria-labelledby="deepseek-onboarding-title">
|
||||
<div className={styles['brand']} aria-hidden="true"><BrandWordmark size={24} /></div>
|
||||
<h2
|
||||
ref={titleRef}
|
||||
id="deepseek-onboarding-title"
|
||||
className={styles['title']}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{t('onboardingTitle')}
|
||||
</h2>
|
||||
<p className={styles['description']}>{t('onboardingDescription')}</p>
|
||||
<div className={styles['actions']}>
|
||||
<Button variant="ghost" className={styles['later']} onClick={complete}>
|
||||
{t('onboardingLater')}
|
||||
</Button>
|
||||
<Button variant="primary" className={styles['primary']} onClick={openModels}>
|
||||
{t('onboardingGoToSettings')}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
<OnboardingSurface>
|
||||
<section className={styles['page']} role="region" aria-labelledby="deepseek-onboarding-title">
|
||||
<div className={styles['brand']} aria-hidden="true"><BrandWordmark size={24} /></div>
|
||||
<h2
|
||||
ref={titleRef}
|
||||
id="deepseek-onboarding-title"
|
||||
className={styles['title']}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{t('onboardingTitle')}
|
||||
</h2>
|
||||
<p className={styles['description']}>{t('onboardingDescription')}</p>
|
||||
<div className={styles['actions']}>
|
||||
<Button variant="ghost" className={styles['later']} onClick={complete}>
|
||||
{t('onboardingLater')}
|
||||
</Button>
|
||||
<Button variant="primary" className={styles['primary']} onClick={openModels}>
|
||||
{t('onboardingGoToSettings')}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</OnboardingSurface>
|
||||
)
|
||||
}
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-primitives/README.md
|
||||
README.md: 385730c94831d2fd4af83f9eca0f55941551c796
|
||||
README.zh.md: b8a75dbffc6549f6294dfda5988c67d6569386c9
|
||||
README.md: 7571cb48424b650a1aaa5222b33a3ee14faa69b4
|
||||
README.zh.md: fa0c3f24023ec8c1eb77553bfe191801b6698687
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Pure React atoms (zero cordis): StateDot, ic_ds_* icons, Button/Pill/Menu/Modal/Input, the markdown family (MessageText/MarkdownText/JsonBlock), the read-only JsonTree inspector, the `useAnchoredMaxHeight` hook that clamps a bottom-anchored overlay to the viewport space above its anchor (re-measured on resize, scroll, and a caller-supplied dependency), TerminalBlock, DiffBlock, ReadBlock, SearchBlock, and WebBlock. Contract: api-contracts v3 §8.
|
||||
Pure React atoms (zero cordis): StateDot, ic_ds_* icons, Button/Pill/Menu/Modal/Input, the OnboardingSurface first-run takeover (body-portaled mask + opaque stage that holds `#root` inert for exactly its own lifetime), the markdown family (MessageText/MarkdownText/JsonBlock), the read-only JsonTree inspector, the `useAnchoredMaxHeight` hook that clamps a bottom-anchored overlay to the viewport space above its anchor (re-measured on resize, scroll, and a caller-supplied dependency), TerminalBlock, DiffBlock, ReadBlock, SearchBlock, and WebBlock. Contract: api-contracts v3 §8.
|
||||
|
||||
## Hover cards
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
纯 React 原子组件(零 cordis):StateDot、ic_ds_* 图标、Button/Pill/Menu/Modal/Input、markdown 家族(MessageText/MarkdownText/JsonBlock)、只读 JsonTree 检查器、`useAnchoredMaxHeight` hook(把底部锚定的浮层高度收敛到锚点上方的视口空间,并在 resize、scroll 与调用方提供的依赖变化时重新测量)、TerminalBlock、DiffBlock、ReadBlock、SearchBlock,以及 WebBlock。契约:api-contracts v3 §8。
|
||||
纯 React 原子组件(零 cordis):StateDot、ic_ds_* 图标、Button/Pill/Menu/Modal/Input、OnboardingSurface 首次使用接管层(portal 到 body 的遮罩加不透明展示层,在自身生命周期内保持 `#root` 为 `inert`)、markdown 家族(MessageText/MarkdownText/JsonBlock)、只读 JsonTree 检查器、`useAnchoredMaxHeight` hook(把底部锚定的浮层高度收敛到锚点上方的视口空间,并在 resize、scroll 与调用方提供的依赖变化时重新测量)、TerminalBlock、DiffBlock、ReadBlock、SearchBlock,以及 WebBlock。契约:api-contracts v3 §8。
|
||||
|
||||
## 悬浮卡片
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/* First-run stage: keep the product top bar visible, then let onboarding own
|
||||
the complete workspace instead of presenting another settings modal. */
|
||||
.onboardingOverlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1100;
|
||||
}
|
||||
|
||||
/* Mask */
|
||||
.onboardingMask {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
top: 80px;
|
||||
bottom: 0px;
|
||||
background: rgba(0, 0, 0, 0.24);
|
||||
/* Mask-blur */
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.onboardingStage {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
background: var(--dsw-alias-bg-layer-1);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user