Merge remote-tracking branch 'origin/master' into worktree/turn-tail-actions

This commit is contained in:
creatixchu
2026-08-07 10:50:49 +08:00
378 changed files with 15733 additions and 3622 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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 的步骤和工具调用均留在父日志之外。
### 同步收集(首版)

View File

@@ -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

View File

@@ -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.

View File

@@ -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 能够支持这项边界,而无需负责产品私有协议。

View File

@@ -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

View File

@@ -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

View File

@@ -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)负责说明残余风险)。
系统由事件驱动,而不是文件监视器驱动。除非文件系统变更通过结构化工具完成,否则编辑不会在确切的文件系统变更时刻可见;外部文件变更会在下一次成功的结构化触碰或恢复时被发现。这使设计保持确定性并且与提供方无关。
系统由事件驱动,而不是文件监视器驱动。除非文件系统变更通过结构化工具完成,否则编辑不会在确切的文件系统变更时刻可见;外部文件变更会在下一次成功的结构化触碰、恢复对账,或恢复被遮蔽的基线时被发现。这使设计保持确定性并且与提供方无关。
## 延后事项

View File

@@ -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-20-dsh-cli-personal-config.md
2026-07-20-dsh-cli-personal-config.md: 1fa8cda2b34b58cc7a28b722872520b68a9b7009
2026-07-20-dsh-cli-personal-config.zh.md: e70b8914cf005e0a2e54ba2b29d3b7def84b00db
2026-07-20-dsh-cli-personal-config.md: 10f16a1cbabdd8cd383c59ad8e09787c02d0109a
2026-07-20-dsh-cli-personal-config.zh.md: 22435efbec8ea661c546ffd0c1aa9bb0ff2ebbb2

View File

@@ -10,6 +10,8 @@ A developer's own preferences — which provider and model the TUI uses, persona
## Decision
The entry modes and the personal file's name and location below are superseded by the [profile plugin bundles decision](../architecture/2026-08-05-profile-plugin-bundles.md): `dsh` boots profiles, and the personal layer became the per-profile and home-level `cordis.patch.yml`. What survives unchanged is this note's substance — the Harness home as the machine-level layer's root, patch semantics over a shipped composition, and fail-loud parsing.
Two coupled pieces, aligned with the `apps/` assembly tier proposed by the `dsh web` PR (#443):
**The `dsh` CLI (`apps/cli`, npm name `@deepseek-ai/dsh`).** `apps/*` is the product-assembly tier over `packages/*` libraries. One bin dispatches the default interactive TUI, `-p`/`--prompt` headless turns, and the `web` surface. The TUI boots `examples/tui-agent/cordis.yml` (or `--config`) with the invoking directory as the workspace. The committed `bin/dsh` launcher resolves the checkout through its own real path and runs the app with tsx's ESM hook; the [source-launch decision](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md) owns that contract. `pnpm run demo:tui` runs the same entry.
@@ -46,4 +48,4 @@ The TUI and Web register the exact personal path through Cordis HMR after boot.
## Testing
`packages/ui/app-boot/tests/personal-config.spec.ts` pins parsing, startup application, exact-path add/failure/recovery/removal, last-good rollback, failure broadcast, and preservation of app-owned patches. `examples/tui-agent/tests/tui-keyless-smoke.e2e.ts` boots the real dsh bin with no overlay, a personal environment and UI patch, a config-only cached repository skill, and invalid personal YAML. Test launchers isolate `$DSH_HOME`, so a developer's real overlay cannot leak into fixtures.
`packages/ui/app-boot/tests/user-patches.spec.ts` pins parsing, startup application, exact-path add/failure/recovery/removal, last-good rollback, failure broadcast, and preservation of app-owned patches. `apps/cli/tests/built-bin.e2e.ts` boots the real dsh bin over a profile and exercises the live patch layer end to end. Test launchers isolate `$DSH_HOME`, so a developer's real overlay cannot leak into fixtures.

View File

@@ -10,6 +10,8 @@ Status: implemented
## Decision
下文的各入口模式,以及个人文件的名称与位置,已被 [profile 插件组合包决策](../architecture/2026-08-05-profile-plugin-bundles.md)取代:`dsh` 启动 profile,个人层变成逐 profile 与 home 级的 `cordis.patch.yml`。保留不变的是本笔记的实质:以 Harness home 作为机器级层的根目录、在随附组合之上使用 patch 语义,以及解析时的大声失败。
两个耦合的部分,与 `dsh web` PR(#443)提出的 `apps/` 装配层对齐:
**`dsh` CLI(`apps/cli`,npm 名 `@deepseek-ai/dsh`)。** `apps/*` 是位于 `packages/*` 库之上的产品组装层。一个 bin 负责分发默认交互式 TUI、`-p`/`--prompt` 无头轮次和 `web` 界面。TUI 以调用目录为 workspace,启动 `examples/tui-agent/cordis.yml`(或 `--config` 指定的配置)。已提交的 `bin/dsh` 启动器通过自身真实路径解析 checkout,并使用 tsx 的 ESM hook 运行应用;该契约由[源码启动决策](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md)维护。`pnpm run demo:tui` 运行同一入口。
@@ -46,4 +48,4 @@ TUI 和 Web 启动后通过 Cordis HMR(热模块替换)注册确切的个人
## Testing
`packages/ui/app-boot/tests/personal-config.spec.ts` 固定解析、启动时应用、确切路径的新增/失败/恢复/移除、最后可用状态回滚、失败广播以及应用自有 patch 的保留。`examples/tui-agent/tests/tui-keyless-smoke.e2e.ts` 启动真实 dsh bin,覆盖无 overlay、个人环境与 UI patch、纯配置的缓存 repository skill,以及无效个人 YAML。测试启动器会隔离 `$DSH_HOME`,因此开发者的真实 overlay 不会泄漏进 fixture。
`packages/ui/app-boot/tests/user-patches.spec.ts` 固定解析、启动时应用、确切路径的新增/失败/恢复/移除、最后可用状态回滚、失败广播以及应用自有 patch 的保留。`apps/cli/tests/built-bin.e2e.ts` 启动真实 dsh bin 并基于 profile 端到端验证实时 patch 层。测试启动器会隔离 `$DSH_HOME`,因此开发者的真实 overlay 不会泄漏进 fixture。

View File

@@ -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-22-durable-subagent-catalog-and-list-agents.md
2026-07-22-durable-subagent-catalog-and-list-agents.md: 0dd7eebac74689004014248c7178dba540ef4662
2026-07-22-durable-subagent-catalog-and-list-agents.zh.md: 33b0296cf9914d1975fb1dc84564b498a09bd511
2026-07-22-durable-subagent-catalog-and-list-agents.md: b96d6e1dd36c58af67c8e93e62515672790ad009
2026-07-22-durable-subagent-catalog-and-list-agents.zh.md: 856bac615db84bfe2898ec0838094c6bc29f77b2

View File

@@ -12,6 +12,8 @@ Enumeration must cross-check immutable session lineage, descriptor validity, and
## Decision
**Superseded read path.** [Subagent list identity via the projection unit](../architecture/2026-08-06-subagent-list-identity-projection.md) replaces this note's enumeration and per-child read design: `listChildren` now merges the live session store with optional session persistence directly and serves each child's mode/label from the registered `subagent` projection unit — no session-query dependency, no list-time descriptor scan — and that note owns the current listing semantics, including the diagnostic mapping. This note remains the authority for descriptor persistence, the mode-discriminated descriptor as durable identity, direct-parent authorization, and the model-facing `list_agents` projection; the trace-based read mechanics below are decision context, not current behavior.
Parent-to-child enumeration is a service capability with consumer-specific projections. `SubagentService.listChildren(parentSessionId: SessionId)` ([subagent/src/index.ts](../../../../packages/subagent/subagent/src/index.ts)) does the following:
- use `ctx.sessionQuery.traceSession(parentSessionId)` to obtain the parent's direct live-preferred child sessions;
@@ -33,7 +35,7 @@ Session lineage is broader than subagent identity: an ordinary `ctx.sessions.for
The published logical record is also the activity source: `SessionRecord.live` means `running`, while `live: false, persisted: true` means `inactive`. Activity comes directly from the trace and causes no additional child-log load. `inactive` encodes neither successful completion nor resumability: it may describe settled one-shot history or a continuable child for which `send_message` can materialize another Activation. Conversely, `running` says only that the session is live: a live continuable Agent outside the continuation manager's matching Activation still appears as `running`, but `send_message` rejects it as an ownership conflict. A child is not visible before its session is published, and no process-local Activation entry is added as a second candidate or activity source. Listing is a snapshot that may race publication, disposal, or a later message; `send_message` remains the authoritative delivery-time operation.
The subagent service keeps `sessionQuery` optional so start and follow-up remain available without it. Its public `listChildren(parentSessionId: SessionId)` method resolves the optional service and dynamically loads the optional session-query runtime only when called; ordinary subagent imports, start, and follow-up therefore do not evaluate that package. Listing belongs directly to `SubagentService`: it interprets the query's lineage, events, and live state without resolving the Activation-based continuation manager or consulting Agent registrations, Activations, or providers, so a deployment with sessions, `subagents`, and `sessionQuery` can list even when `agents` is absent. The method throws `SubagentError` with stable code `SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE` before loading the runtime or doing query work when the query service is absent. `@deepseek-ai/dsh-tool-subagent-control` exports separately loadable tool plugins: the `send_message` adapter requires only `subagents`, while the `list_agents` adapter requires both `subagents` and `sessionQuery` at load. A deployment may therefore use `send_message` without installing or loading session query; the list-tool fiber remains inactive until the required service is available, while another direct service consumer receives the same explicit call-time contract.
The subagent service keeps `sessionQuery` optional so start and follow-up remain available without it. Its public `listChildren(parentSessionId: SessionId)` method resolves the optional service and dynamically loads the optional session-query runtime only when called; ordinary subagent imports, start, and follow-up therefore do not evaluate that package. Listing belongs directly to `SubagentService`: it interprets the query's lineage, events, and live state without resolving the Activation-based continuation manager or consulting Agent registrations, Activations, or providers, so a deployment with sessions, `subagents`, and `sessionQuery` can list even when `agents` is absent. The method throws `SubagentError` with stable code `SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE` before loading the runtime or doing query work when the query service is absent. `@deepseek-ai/dsh-tool-subagent-control` exports separately loadable tool plugins: the `send_message` adapter requires only `subagents`, while the `list_agents` adapter requires both `subagents` and `sessionQuery` at load. A deployment may therefore use `send_message` without installing or loading session query; the list-tool fiber remains inactive until the required service is available, while another direct service consumer receives the same explicit call-time contract. This dependency posture — the optional `sessionQuery`, its error code, and the list tool's load requirement — is part of the superseded read path: the current codes (`SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE`, `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE`) and the narrowed load requirement live in the [superseding note](../architecture/2026-08-06-subagent-list-identity-projection.md).
`listChildren(parentSessionId, signal?)` forwards the caller's signal to `traceSession()` and the conditional exact `readEvent()` operation. `listEvents()` has no cancellation parameter, so the listing path checks the signal before and after that await and after each candidate settles. If any query operation rejects after the signal aborts, the service normalizes the result to `SubagentError` with stable code `CANCELLED`; a backend abort error or a diagnostic-mapped query error cannot escape or become a successful partial listing.
@@ -91,9 +93,10 @@ The first version has no child deletion operation. If later product behavior del
## Testing
- `packages/subagent/subagent/tests/service.spec.ts` pins descriptor v2 parsing for both modes and proves an unlabeled raw start resolves a one-shot descriptor before provider dispatch. `packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts` proves the local driver appends that descriptor inside the initial turn, returns the published id when cancellation lands in the factory-to-run handoff, and keeps result and handle-disposal failures on separate channels. Delegation-tool tests pin propagation of their existing display description and preserve independent result and disposal diagnostics.
- `packages/subagent/subagent/tests/list-children.spec.ts` pins a query-only composition with sessions, `subagents`, and `sessionQuery` but no `agents`, then drives the full real stack (agent loop, JSONL persistence, spawn/fork providers, the subagent service, and a concrete session-query service) keylessly: one-shot and continuable children from one real trace; a persisted (restart-shaped) parent target; `createdAt`-then-id ordering with authored ties; ordinary-fork and fork-seed ancestor-descriptor exclusion without diagnostics; live `running` vs persisted `inactive`; duplicate-descriptor, malformed-payload, invalid-surface, mismatched-header, and changed-read-target corruption diagnostics that leave healthy siblings visible; unsupported-version and per-child unavailable diagnostics; provider absence without child omission; compacted/uncompacted twins listing identically; grandchild exclusion; trace-phase failure failing the whole call while candidate-phase failures isolate to one child; configuration/window and unrecognized failures propagating as operation failures; forwarded trace/exact-read cancellation with stable `CANCELLED` normalization; and the `SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE` no-service contract. `packages/subagent/subagent/tests/optional-session-query.spec.ts` rejects eager evaluation of the optional runtime while importing the ordinary subagent surface.
- `packages/subagent/tool-subagent-control/tests/list-agents.spec.ts` pins the `list_agents` schema (no parameters), the continuable-only projection that omits a healthy one-shot sibling while preserving diagnostics, the fixed child/diagnostic/empty text forms, an end-to-end settled-child listing with its durable label, forwarding of the tool cancellation signal, the no-agent rejection, load-time `sessionQuery` injection, and HMR disposal.
- The keyless ACP snapshot scenario `subagent-list-agents` (examples/acp-agent) fences its second parent turn on a snapshot-only `subagent/end` marker, then executes `list_agents` for real against the subagent service, session query, and JSONL persistence, rendering `<id> [complete] — <label>`.
- `packages/subagent/subagent/tests/list-children.spec.ts` pins the current read path against a real composition of the session store, JSONL persistence, spawn/fork providers, the subagent service, and the projection registry — no query service — keylessly: live-only listing without persistence; loud `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE` and `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE` even with zero children; the three-rung ladder (a live child never inspected, a cold child inspected exactly once, and the cache-hit, absent-key, absent-service, and poisoned-row second-rung cases); last-wins over multiple descriptors; malformed payloads and unknown versions diagnosed as `corrupt`; a failed cold inspection as one `unavailable` diagnostic retried on the next listing; a fork seed's ancestor descriptor listed under that identity; foreign-unit fold failures contained per child as `corrupt` on both the live and cold paths; `createdAt`-then-id ordering without ordinary forks; provider absence without child omission; compacted/uncompacted twins listing identically; a persisted-listing failure failing the whole enumeration; cancellation normalized to stable `CANCELLED`; and typed stable error codes. A companion spec (retired together with the query-backed read path) rejected eager evaluation of the optional session-query runtime while importing the ordinary subagent surface.
- `packages/subagent/tool-subagent-control/tests/list-agents.spec.ts` pins the `list_agents` schema (no parameters), the continuable-only projection that omits a healthy one-shot sibling while preserving diagnostics, the fixed child/diagnostic/empty text forms, an end-to-end settled-child listing with its durable label, forwarding of the tool cancellation signal, the no-agent rejection, the narrowed load requirement without `sessionQuery`, and HMR disposal.
- The keyless ACP snapshot scenario `subagent-list-agents` (examples/acp-agent) fences its second parent turn on a snapshot-only `subagent/end` marker, then executes `list_agents` for real against the subagent service, the projection registry, and JSONL persistence, rendering `<id> [complete] — <label>`.
- The keyless snapshot scenario `subagent-diagnostic` (examples/headless-agent) pins the current listing's model-visible diagnostic classification, including a descriptor-less settled child surfacing as a `corrupt` diagnostic.
- The keyless ACP snapshot scenario `subagent-published-run-failure` publishes a real one-shot child, injects independent run-result and handle-disposal failures, and preserves both diagnostics in the parent tool result.
## Consequences

View File

@@ -12,6 +12,8 @@ Status: implemented
## 决策
**列表读路径已被取代。**[subagent 列表经投影单元读取身份](../architecture/2026-08-06-subagent-list-identity-projection.md)取代了本记录的枚举与逐 child 读取设计:`listChildren` 现在直接合并存活会话存储与可选的会话持久化,并从注册的 `subagent` projection unit 读取每个 child 的 mode/label——不依赖会话查询,也不在列表时扫描描述符;当前的列表语义(含 diagnostic 映射)以该记录为准。本记录仍是描述符持久化、以 mode 判别的描述符作为持久身份、直接 parent 鉴权与面向模型的 `list_agents` 投影的权威;下文基于追踪的读取机制是决策背景,不再是当前行为。
parent 到 child 的枚举是一项带消费方专用投影的服务功能。`SubagentService.listChildren(parentSessionId: SessionId)`([subagent/src/index.ts](../../../../packages/subagent/subagent/src/index.ts))执行以下操作:
- 使用 `ctx.sessionQuery.traceSession(parentSessionId)` 获取 parent 的直接且实时优先的 child 会话;
@@ -33,7 +35,7 @@ parent 到 child 的枚举是一项带消费方专用投影的服务功能。`Su
已发布的逻辑记录同时也是活动状态来源:`SessionRecord.live` 表示 `running`,而 `live: false, persisted: true` 表示 `inactive`。活动状态直接来自追踪结果,不会导致额外加载 child 日志。`inactive` 既不表示执行成功,也不表示可恢复:它可能表示已结算的一次性历史,也可能表示 `send_message` 可以为其物化另一次 Activation 的可继续 child。反过来,`running` 只表示会话存活:位于继续执行管理器对应 Activation 之外的存活可继续 Agent 仍会显示为 `running`,但 `send_message` 会将其作为所有权冲突拒绝。child 会话发布前不可见,也不会添加进程内 Activation 条目作为第二个候选来源或活动状态来源。列表查询是一份快照,可能与发布、dispose 或后续消息发生竞态;`send_message` 仍是消息送达时的权威操作。
subagent 服务将 `sessionQuery` 保持为可选依赖,因此没有该服务时仍可执行 start 和 follow-up。其公开的 `listChildren(parentSessionId: SessionId)` 方法只在被调用时才会解析这个可选服务,并动态加载可选的会话查询运行时;因此,普通 subagent 导入、start 和 follow-up 都不会触发该包求值。列表查询直接由 `SubagentService` 负责:它解释查询返回的谱系、事件和存活状态,无需解析基于 Activation 的继续执行管理器,也不会查询 Agent 注册信息、Activation 或提供方;因此,仅包含会话、`subagents` 和 `sessionQuery` 的部署即使缺少 `agents` 也能执行列表查询。如果查询服务缺失,该方法会在加载运行时或执行查询工作前抛出 `SubagentError`,并携带稳定错误码 `SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE`。`@deepseek-ai/dsh-tool-subagent-control` 导出可分别加载的工具插件:`send_message` 适配器只要求 `subagents`,而 `list_agents` 适配器在加载时同时要求 `subagents` 和 `sessionQuery`。因此,部署可以在既不安装也不加载会话查询的情况下使用 `send_message`;列表工具 fiber 会在必需服务可用前保持未激活状态,而其他直接服务消费方会收到同一项明确的调用时契约。
subagent 服务将 `sessionQuery` 保持为可选依赖,因此没有该服务时仍可执行 start 和 follow-up。其公开的 `listChildren(parentSessionId: SessionId)` 方法只在被调用时才会解析这个可选服务,并动态加载可选的会话查询运行时;因此,普通 subagent 导入、start 和 follow-up 都不会触发该包求值。列表查询直接由 `SubagentService` 负责:它解释查询返回的谱系、事件和存活状态,无需解析基于 Activation 的继续执行管理器,也不会查询 Agent 注册信息、Activation 或提供方;因此,仅包含会话、`subagents` 和 `sessionQuery` 的部署即使缺少 `agents` 也能执行列表查询。如果查询服务缺失,该方法会在加载运行时或执行查询工作前抛出 `SubagentError`,并携带稳定错误码 `SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE`。`@deepseek-ai/dsh-tool-subagent-control` 导出可分别加载的工具插件:`send_message` 适配器只要求 `subagents`,而 `list_agents` 适配器在加载时同时要求 `subagents` 和 `sessionQuery`。因此,部署可以在既不安装也不加载会话查询的情况下使用 `send_message`;列表工具 fiber 会在必需服务可用前保持未激活状态,而其他直接服务消费方会收到同一项明确的调用时契约。这一段的依赖姿态——可选 `sessionQuery`、其错误码与列表工具的加载要求——同属被取代的读路径:现行错误码(`SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE`、`SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE`)与收窄后的加载要求以[取代记录](../architecture/2026-08-06-subagent-list-identity-projection.md)为准。
`listChildren(parentSessionId, signal?)` 会把调用方的取消信号转发给 `traceSession()` 和条件性精确 `readEvent()` 操作。`listEvents()` 不接受取消参数,因此列表查询路径会在等待该操作的前后,以及每个候选处理完成后检查信号。如果取消信号触发后有查询操作以拒绝结算,服务会将结果归一化为 `SubagentError`,并携带稳定错误码 `CANCELLED`;后端中止错误或可映射为 diagnostic 的查询错误均不会逃逸,也不会使调用以成功的部分列表返回。
@@ -91,9 +93,10 @@ diagnostic 是瞬时查询结果,不属于会话事件或目录状态。推导
## 测试
- `packages/subagent/subagent/tests/service.spec.ts` 固定两种模式下的描述符 v2 解析,并证明无标签的底层启动会在分发给提供方之前解析出一次性描述符。`packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts` 证明本地驱动会在初始轮次内追加该描述符,在取消落入工厂到 run 的交接窗口时返回已发布 id,并让结果与句柄释放失败保留在独立通道中。委派工具测试固定其现有显示说明的传递,并保留相互独立的结果与 dispose diagnostic。
- `packages/subagent/subagent/tests/list-children.spec.ts` 先固定一个只有会话、`subagents` 和 `sessionQuery` 而没有 `agents` 的纯查询组合,再以无密钥方式驱动完整真实栈(agent loop、JSONL 持久化、spawn/fork 提供方、subagent 服务,以及一个具体的会话查询服务):来自同一真实追踪的一次性与可继续 child;只存在于持久化存储中(重启形态)的 parent 目标;带有人工构造并列项的按 `createdAt` 再按 id 排序;排除普通 fork 和 fork seed 中祖先描述符且不产生 diagnostic;存活 `running` 与持久化 `inactive` 的对比;重复描述符、载荷格式错误、无效 surface、header 不匹配和读取目标已变化的损坏 diagnostic 均不隐藏健康的 sibling;不受支持版本与逐 child unavailable diagnostic;提供方缺失时不排除 child;压缩与未压缩的孪生 child 列表结果一致;排除孙代会话;追踪阶段失败导致整次调用失败而候选阶段失败只隔离到单个 child;配置/窗口错误和无法识别的失败作为操作失败向上传播;转发 trace/精确读取取消并稳定归一化为 `CANCELLED`;以及 `SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE` 缺服务契约。`packages/subagent/subagent/tests/optional-session-query.spec.ts` 会在导入普通 subagent surface 时拒绝对可选运行时的 eager 求值。
- `packages/subagent/tool-subagent-control/tests/list-agents.spec.ts` 固定 `list_agents` 的 schema(无参数)、只保留可继续 child 且排除健康的一次性 sibling、同时保留 diagnostic 的投影、child/diagnostic/空结果的固定文本形式、带持久化 label 的已结束 child 端到端列表、工具取消信号的转发、无调用 agent 时的拒绝、加载时的 `sessionQuery` 注入,以及 HMR dispose。
- 无密钥 ACP 快照场景 `subagent-list-agents`(examples/acp-agent)使用仅限快照的 `subagent/end` 标记为第二个 parent 轮次设置边界,随后针对 subagent 服务、会话查询和 JSONL 持久化真实执行 `list_agents`,渲染 `<id> [complete] — <label>`。
- `packages/subagent/subagent/tests/list-children.spec.ts` 针对由会话存储、JSONL 持久化、spawn/fork 提供方、subagent 服务与投影注册表构成的真实组合——不含查询服务——以无密钥方式钉住现行读取路径:无持久化时的仅存活列表;零 children 也响亮报 `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE` 与 `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE`;三级阶梯(存活 child 从不检查、冷 child 恰好检查一次,以及缓存命中、key 缺席、服务缺席、行中毒四个第二级用例);多描述符 last-wins 取末者;载荷格式错误与未知版本诊断为 `corrupt`;冷检查失败成一条 `unavailable` diagnostic 并在下次列表重试;fork seed 中的祖先描述符按该身份列出;外部 unit 折叠失败在存活与冷两条路径上按 child 收纳为 `corrupt`;按 `createdAt` 再按 id 排序且不列普通 fork;提供方缺失时不排除 child;压缩与未压缩的孪生 child 列表结果一致;持久化列表失败使整次枚举失败;取消稳定归一化为 `CANCELLED`;以及带类型的稳定错误码。一个伴随规格(已随查询式读取路径一起退役)曾在导入普通 subagent surface 时拒绝对可选 session-query 运行时的 eager 求值。
- `packages/subagent/tool-subagent-control/tests/list-agents.spec.ts` 固定 `list_agents` 的 schema(无参数)、只保留可继续 child 且排除健康的一次性 sibling、同时保留 diagnostic 的投影、child/diagnostic/空结果的固定文本形式、带持久化 label 的已结束 child 端到端列表、工具取消信号的转发、无调用 agent 时的拒绝、收窄后的加载要求(不再注入 `sessionQuery`),以及 HMR dispose。
- 无密钥 ACP 快照场景 `subagent-list-agents`(examples/acp-agent)使用仅限快照的 `subagent/end` 标记为第二个 parent 轮次设置边界,随后针对 subagent 服务、投影注册表和 JSONL 持久化真实执行 `list_agents`,渲染 `<id> [complete] — <label>`。
- 无密钥快照场景 `subagent-diagnostic`(examples/headless-agent)钉住现行列表的模型可见诊断分类,包括无描述符的定局 child 以 `corrupt` diagnostic 出现。
- 无密钥 ACP 快照场景 `subagent-published-run-failure` 会发布一个真实的一次性 child,注入相互独立的 run result 与 handle dispose 失败,并在 parent 工具结果中保留两项 diagnostic。
## 影响

View File

@@ -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

View File

@@ -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

View File

@@ -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 文件系统沙箱运行。
## 考虑过的替代方案

View File

@@ -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-31-even-out-shipped-tool-rosters.md
2026-07-31-even-out-shipped-tool-rosters.md: 312e61d017abad1a6ac57f2ba491a715f8fd92d0
2026-07-31-even-out-shipped-tool-rosters.zh.md: c77370c312004052bc4f8ee9545ed287a6d92554
2026-07-31-even-out-shipped-tool-rosters.md: d12db993654d9f2b41a16a4663dc64aefe8e3a2f
2026-07-31-even-out-shipped-tool-rosters.zh.md: 8381a457def1a909b300de52dc011e2437c7e9f3

View File

@@ -12,7 +12,7 @@ The result was a user-visible difference nobody had decided: the same model, ask
## Decision
The rows that are not surface-specific move into [`base.cordis.yml`](../../../../apps/cli/config/base.cordis.yml), and three more join them: `tool-session-query`, `tool-str-replace-editor`, and `repeat-tool-guard`. Web search moves there too; its [deployment decision](2026-07-31-web-default-search.md) owns the security boundary while the shared base owns its surface-neutral mount. Both surfaces assemble the same roster: twenty-two tools on every host — the twenty shared rows plus `glob` and `grep`, which are fixed members because `dsh-tool-fs-search` spawns the [packaged ripgrep binary](../architecture/2026-08-01-packaged-ripgrep-search.md). `tool-session-query` joined and then left again — the [session-search-not-shipped-default decision](2026-08-02-session-search-not-shipped-default.md) keeps the model-facing consumer opt-in — while the rest of this roster stands.
The rows that are not surface-specific move into [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml), and three more join them: `tool-session-query`, `tool-str-replace-editor`, and `repeat-tool-guard`. Web search moves there too; its [deployment decision](2026-07-31-web-default-search.md) owns the security boundary while the shared base owns its surface-neutral mount. Both surfaces assemble the same roster: twenty-two tools on every host — the twenty shared rows plus `glob` and `grep`, which are fixed members because `dsh-tool-fs-search` spawns the [packaged ripgrep binary](../architecture/2026-08-01-packaged-ripgrep-search.md). `tool-session-query` joined and then left again — the [session-search-not-shipped-default decision](2026-08-02-session-search-not-shipped-default.md) keeps the model-facing consumer opt-in — while the rest of this roster stands.
Two rows stay surface-specific. `tmux-context` is TUI-only because a browser surface has no terminal multiplexer to describe. `session-reference` is TUI-only because it drives the shared session-query index from the launcher's process-local path, and the browser sidebar reconciles that index on its own first search.

View File

@@ -12,7 +12,7 @@ Status: implemented
## 决策
那些并非 surface 专属的行移入 [`base.cordis.yml`](../../../../apps/cli/config/base.cordis.yml),另有三行加入:`tool-session-query`、`tool-str-replace-editor` 和 `repeat-tool-guard`。Web 搜索也一并移入;其[部署决策](2026-07-31-web-default-search.md)负责安全边界,共享 base 则负责与 surface 无关的挂载。两个 surface 组装同一份清单:每台宿主上都有二十二个工具——二十个共享行加上 `glob` 和 `grep`,它们成为固定成员,因为 `dsh-tool-fs-search` 直接 spawn [打包的 ripgrep 二进制](../architecture/2026-08-01-packaged-ripgrep-search.md)。`tool-session-query` 加入后又退出了——[session-search-not-shipped-default 决策](2026-08-02-session-search-not-shipped-default.md)让面向模型的消费方保持需显式启用——而这份清单的其余部分保持不变。
那些并非 surface 专属的行移入 [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml),另有三行加入:`tool-session-query`、`tool-str-replace-editor` 和 `repeat-tool-guard`。Web 搜索也一并移入;其[部署决策](2026-07-31-web-default-search.md)负责安全边界,共享 base 则负责与 surface 无关的挂载。两个 surface 组装同一份清单:每台宿主上都有二十二个工具——二十个共享行加上 `glob` 和 `grep`,它们成为固定成员,因为 `dsh-tool-fs-search` 直接 spawn [打包的 ripgrep 二进制](../architecture/2026-08-01-packaged-ripgrep-search.md)。`tool-session-query` 加入后又退出了——[session-search-not-shipped-default 决策](2026-08-02-session-search-not-shipped-default.md)让面向模型的消费方保持需显式启用——而这份清单的其余部分保持不变。
有两行仍是 surface 专属。`tmux-context` 只在 TUI,因为浏览器 surface 没有终端复用器可描述。`session-reference` 只在 TUI,因为它以 launcher 的进程本地路径驱动共享的 session-query 索引,而浏览器侧边栏会在自己的首次搜索里重建该索引。

View File

@@ -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-31-workspace-write-surface-default.md
2026-07-31-workspace-write-surface-default.md: a0b216122e301b5332ed761155d743dc78fa3bab
2026-07-31-workspace-write-surface-default.zh.md: 4391daa32b142ea976e3b04833163936913c17bc
2026-07-31-workspace-write-surface-default.md: e6a36ad4ee7179cabf958114681728c3ac7340b4
2026-07-31-workspace-write-surface-default.zh.md: 5ced928b167892d9abe1f4423da72a0243590735

View File

@@ -10,7 +10,7 @@ The shipped terminal and browser surfaces exposed the same coding tools under di
## Decision
[`base.cordis.yml`](../../../../apps/cli/config/base.cordis.yml) owns one sandbox and permission stack for every shipped TUI, Web, and browser-backed headless session: `dsh-sandbox-local`, `dsh-sandbox-policy`, `dsh-bash-sandbox`, `dsh-fs-sandbox`, `dsh-user-approval`, and `dsh-permission`. The composition fallback is the `workspace-write` preset, which bundles `workspace-write` file effects with the `ask` approval policy. `DSH_PERMISSION_MODE` remains an explicit process override; a stored `permission.defaultPreset` remains the user preference for later sessions and outranks the fallback through the Settings seam.
[`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml) owns one sandbox and permission stack for every shipped TUI, Web, and browser-backed headless session: `dsh-sandbox-local`, `dsh-sandbox-policy`, `dsh-bash-sandbox`, `dsh-fs-sandbox`, `dsh-user-approval`, and `dsh-permission`. The composition fallback is the `workspace-write` preset, which bundles `workspace-write` file effects with the `ask` approval policy. `DSH_PERMISSION_MODE` remains an explicit process override; a stored `permission.defaultPreset` remains the user preference for later sessions and outranks the fallback through the Settings seam.
A genuinely fresh session pins `permission/preset: workspace-write`, `sandbox/mode: workspace-write`, and `approval/policy: ask` before execution. Existing and resumed sessions retain their logged permission, and changing the General-settings default affects only sessions created afterward. The browser keeps its Access picker, answerable approval cards, and risk confirmation for Full access. The TUI gains the existing `/permission` command because the shared Permission service activates its command child there.

View File

@@ -10,7 +10,7 @@ Status: implemented
## 决策
[`base.cordis.yml`](../../../../apps/cli/config/base.cordis.yml) 为所有已交付的 TUI、Web 以及由浏览器支撑的无头会话统一持有一套沙箱与权限栈:`dsh-sandbox-local`、`dsh-sandbox-policy`、`dsh-bash-sandbox`、`dsh-fs-sandbox`、`dsh-user-approval` 和 `dsh-permission`。组合回退值为 `workspace-write` preset,其中包含 `workspace-write` 文件效果模式与 `ask` 审批策略。`DSH_PERMISSION_MODE` 仍是显式的进程级覆盖;已存储的 `permission.defaultPreset` 仍是面向后续会话的用户偏好,并通过 Settings seam 优先于该回退值。
[`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml) 为所有已交付的 TUI、Web 以及由浏览器支撑的无头会话统一持有一套沙箱与权限栈:`dsh-sandbox-local`、`dsh-sandbox-policy`、`dsh-bash-sandbox`、`dsh-fs-sandbox`、`dsh-user-approval` 和 `dsh-permission`。组合回退值为 `workspace-write` preset,其中包含 `workspace-write` 文件效果模式与 `ask` 审批策略。`DSH_PERMISSION_MODE` 仍是显式的进程级覆盖;已存储的 `permission.defaultPreset` 仍是面向后续会话的用户偏好,并通过 Settings seam 优先于该回退值。
真正的新会话会在执行前固定 `permission/preset: workspace-write`、`sandbox/mode: workspace-write` 和 `approval/policy: ask`。现有会话和恢复的会话保留日志中记录的权限,更改「通用」设置中的默认值只影响之后创建的会话。浏览器保留 Access 选择器、可应答的审批卡片,以及选择 Full access 时的风险确认。共享 Permission 服务在 TUI 中激活其命令子件,因此 TUI 会获得现有的 `/permission` 命令。

View File

@@ -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-08-02-session-search-not-shipped-default.md
2026-08-02-session-search-not-shipped-default.md: ba7299712c0ba3db5e807e928f6f5d98ac917187
2026-08-02-session-search-not-shipped-default.zh.md: 1678ebfb5514003eabe0221e460c619bab1aa444
2026-08-02-session-search-not-shipped-default.md: 65bd72fff76210b726e7562fb8e88e5f8802434a
2026-08-02-session-search-not-shipped-default.zh.md: 5e42a2c2323904117f9322b5c4a53c43c6ed3f2a

View File

@@ -6,11 +6,11 @@ English | [中文](2026-08-02-session-search-not-shipped-default.zh.md)
## Problem
The [shipped-roster decision](2026-07-31-even-out-shipped-tool-rosters.md) made `tool-session-query` a default row of the shared [`base.cordis.yml`](../../../../apps/cli/config/base.cordis.yml), so the shipped TUI and Web surfaces put the five session-search tools (`session_search`, `session_event_search`, `session_trace`, `session_event_trace`, `session_event_read`) in front of the model. That contradicted the [model-facing session-query-tools decision](2026-07-24-model-facing-session-query-tools.md), whose opt-in stance the package README recorded as "shipped host compositions do not mount it by default". The default also shipped a prompt section teaching a prior-work search workflow that no user had asked for.
The [shipped-roster decision](2026-07-31-even-out-shipped-tool-rosters.md) made `tool-session-query` a default row of the shared [`cordis.patch.yml`](../../../../packages/bundle/base/cordis.patch.yml), so the shipped TUI and Web surfaces put the five session-search tools (`session_search`, `session_event_search`, `session_trace`, `session_event_trace`, `session_event_read`) in front of the model. That contradicted the [model-facing session-query-tools decision](2026-07-24-model-facing-session-query-tools.md), whose opt-in stance the package README recorded as "shipped host compositions do not mount it by default". The default also shipped a prompt section teaching a prior-work search workflow that no user had asked for.
## Decision
The shipped TUI, Web, and headless surfaces no longer mount `@deepseek-ai/dsh-tool-session-query`: the row is removed from the shared `base.cordis.yml`, the now-dangling `disabled` patch in the opt-in [`core-web.cordis.yml`](../../../../apps/cli/config/core-web.cordis.yml) profile goes with it, and the workspace dependency drops from `apps/cli/package.json`. The consumer stays opt-in exactly as the model-facing-session-query-tools note describes: the ACP example's [`session-query.cordis.yml`](../../../../examples/acp-agent/session-query.cordis.yml) and its snapshot counterpart remain the mounted reference, and a custom composition can mount the package with the timeout and spill policies.
The shipped TUI, Web, and headless surfaces no longer mount `@deepseek-ai/dsh-tool-session-query`: the row is removed from the shared `cordis.patch.yml`, the now-dangling `disabled` patch in the opt-in [`core-web.cordis.yml`](../../../../apps/cli/config/core-web.cordis.yml) profile goes with it, and the workspace dependency drops from `apps/cli/package.json`. The consumer stays opt-in exactly as the model-facing-session-query-tools note describes: the ACP example's [`session-query.cordis.yml`](../../../../examples/acp-agent/session-query.cordis.yml) and its snapshot counterpart remain the mounted reference, and a custom composition can mount the package with the timeout and spill policies.
The `ctx.sessionQuery` service itself stays mounted. `session-query-sqlite` remains a base row — the TUI's `session-reference` consumes it for `/resume` — and the Web overlay keeps patching it to an in-memory index for the browser content search. Only the model-facing consumer is removed.

View File

@@ -6,11 +6,11 @@ Status: implemented
## 问题
[交付清单决策](2026-07-31-even-out-shipped-tool-rosters.md)把 `tool-session-query` 设为共享 [`base.cordis.yml`](../../../../apps/cli/config/base.cordis.yml) 的默认行,于是交付的 TUI 与 Web surface 把这五个会话搜索工具(`session_search`、`session_event_search`、`session_trace`、`session_event_trace`、`session_event_read`)呈现给了模型。这与[面向模型的会话查询工具决策](2026-07-24-model-facing-session-query-tools.md)相抵触,该决策持需显式启用的立场,包 README 将其记录为「shipped host compositions do not mount it by default」。这份默认还交付了一个提示词段,向模型讲授一套既往工作搜索工作流,而没有任何用户要求过。
[交付清单决策](2026-07-31-even-out-shipped-tool-rosters.md)把 `tool-session-query` 设为共享 [`cordis.patch.yml`](../../../../packages/bundle/base/cordis.patch.yml) 的默认行,于是交付的 TUI 与 Web surface 把这五个会话搜索工具(`session_search`、`session_event_search`、`session_trace`、`session_event_trace`、`session_event_read`)呈现给了模型。这与[面向模型的会话查询工具决策](2026-07-24-model-facing-session-query-tools.md)相抵触,该决策持需显式启用的立场,包 README 将其记录为「shipped host compositions do not mount it by default」。这份默认还交付了一个提示词段,向模型讲授一套既往工作搜索工作流,而没有任何用户要求过。
## 决策
交付的 TUI、Web 与无头 surface 不再挂载 `@deepseek-ai/dsh-tool-session-query`:该行从共享的 `base.cordis.yml` 移除,opt-in 的 [`core-web.cordis.yml`](../../../../apps/cli/config/core-web.cordis.yml) profile 中那条已悬空的 `disabled` patch 也随之删除,workspace 依赖也从 `apps/cli/package.json` 中移除。该消费方仍保持 opt-in,与面向模型的会话查询工具决策所述完全一致:ACP 示例的 [`session-query.cordis.yml`](../../../../examples/acp-agent/session-query.cordis.yml) 及其快照对侧文件仍是挂载参考,自定义组合也可以连同超时与 spill 策略一起挂载该包。
交付的 TUI、Web 与无头 surface 不再挂载 `@deepseek-ai/dsh-tool-session-query`:该行从共享的 `cordis.patch.yml` 移除,opt-in 的 [`core-web.cordis.yml`](../../../../apps/cli/config/core-web.cordis.yml) profile 中那条已悬空的 `disabled` patch 也随之删除,workspace 依赖也从 `apps/cli/package.json` 中移除。该消费方仍保持 opt-in,与面向模型的会话查询工具决策所述完全一致:ACP 示例的 [`session-query.cordis.yml`](../../../../examples/acp-agent/session-query.cordis.yml) 及其快照对侧文件仍是挂载参考,自定义组合也可以连同超时与 spill 策略一起挂载该包。
`ctx.sessionQuery` 服务本身保持挂载。`session-query-sqlite` 仍是 base 的一行,TUI 的 `session-reference` 消费它来实现 `/resume`,Web overlay 也继续把它 patch 成内存索引,供浏览器内容搜索使用。被移除的只有面向模型的消费方。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .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

View File

@@ -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.

View File

@@ -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 端点/模型基线时必须刷新这些事实;生产环境不会另行执行运行时版本探测。