fix(subagent): close stacked catalog integration gaps

This commit is contained in:
Dudu-0223
2026-08-02 12:51:09 +08:00
committed by Tianyi Cui
parent a967f5174a
commit 8b0a7a5d8d
16 changed files with 102 additions and 53 deletions
@@ -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: b288c113d38e067d323513a3c73afb15163ef76e
2026-07-22-durable-subagent-catalog-and-list-agents.zh.md: b6050239b45d375f554b6b0f5e488fbe1a5c045b
2026-07-22-durable-subagent-catalog-and-list-agents.md: c150b7417ac80505d74f8ea477e3bf92f0762720
2026-07-22-durable-subagent-catalog-and-list-agents.zh.md: 49f2fbffc854dd4301f05d90c8f39b1625869dad
@@ -33,7 +33,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 catches missing service wiring at plugin load, 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.
`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.
@@ -93,7 +93,7 @@ The first version has no child deletion operation. If later product behavior del
- `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. Delegation-tool tests pin propagation of their existing display description.
- `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, the no-agent rejection, load-time `sessionQuery` injection, and HMR disposal.
- The keyless ACP snapshot scenario `subagent-list-agents` (examples/acp-agent) pins the model-visible transcript: a background delegation settles, and `list_agents` executes for real against the subagent service, session query, and JSONL persistence, rendering `<id> [complete] — <label>`.
- 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>`.
## Consequences
@@ -104,5 +104,5 @@ The first version has no child deletion operation. If later product behavior del
- The service returns every direct session-backed subagent and diagnostic without a cursor or candidate cap. Stable ordering makes the result deterministic; the model projection avoids one-shot context growth but remains unbounded in the number of continuable children.
- `running` and `inactive` are process-local corpus snapshots, not outcomes or delivery promises. Another process may activate a persisted child while this process reports it as `inactive`; cross-process accuracy requires a shared lease.
- Persisting lifecycle mode is a pre-release descriptor-format change: version 2 rejects old version-1 descriptors as unsupported. Every local session-backed start now pays one small log event so UI and other service consumers can classify one-shot history without replaying model-visible tool results.
- One-shot session persistence remains best-effort. A one-shot child is visible while live and remains discoverable after disposal only when its session checkpoint reached persistence; unlike continuable activation, catalog participation does not add a required final flush or turn a persistence failure into a run failure.
- One-shot session persistence remains best-effort. A one-shot child is visible while live and remains discoverable after disposal only when its session checkpoint reached persistence; unlike continuable activation, catalog participation does not add a required final flush or turn a persistence failure into a run failure. Continuable starts need no eager descriptor flush because the descriptor rides in the creation seed and every Activation disposal path performs the required final checkpoint, including after blocked prompt admission.
- Remote ACP one-shot runs remain outside the catalog because they publish no local child session for `traceSession()` to discover. Enumerating them would require a separate durable local record rather than pretending remote lifecycle ids are session ids.
@@ -33,7 +33,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`;列表工具会在插件加载时捕获服务未接入的问题,而其他直接服务消费方会收到同一项明确的调用时契约。
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 会在必需服务可用前保持未激活状态,而其他直接服务消费方会收到同一项明确的调用时契约。
`listChildren(parentSessionId, signal?)` 会把调用方的取消信号转发给 `traceSession()` 和条件性精确 `readEvent()` 操作。`listEvents()` 不接受取消参数,因此列表查询路径会在等待该操作的前后,以及每个候选处理完成后检查信号。如果取消信号触发后有查询操作以拒绝结算,服务会将结果归一化为 `SubagentError`,并携带稳定错误码 `CANCELLED`;后端中止错误或可映射为 diagnostic 的查询错误均不会逃逸,也不会使调用以成功的部分列表返回。
@@ -93,7 +93,7 @@ diagnostic 是瞬时查询结果,不属于会话事件或目录状态。推导
- `packages/subagent/subagent/tests/service.spec.ts` 固定两种模式下的描述符 v2 解析,并证明无标签的底层启动会在分发给提供方之前解析出一次性描述符。`packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts` 证明本地驱动会在初始轮次内追加该描述符。委派工具测试固定其现有显示说明的传递。
- `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 的投影、childdiagnostic/空结果的固定文本形式、带持久化 label 的已结束 child 端到端列表、无调用 agent 时的拒绝、加载时的 `sessionQuery` 注入,以及 HMR dispose。
- 无密钥 ACP 快照场景 `subagent-list-agents`examples/acp-agent固定模型可见的转写:一次后台委派结束后,`list_agents` 针对 subagent 服务、会话查询和 JSONL 持久化真实执行,渲染 `<id> [complete] — <label>`
- 无密钥 ACP 快照场景 `subagent-list-agents`examples/acp-agent使用仅限快照的 `subagent/end` 标记为第二个 parent 轮次设置边界,随后针对 subagent 服务、会话查询和 JSONL 持久化真实执行 `list_agents`,渲染 `<id> [complete] — <label>`
## 影响
@@ -104,5 +104,5 @@ diagnostic 是瞬时查询结果,不属于会话事件或目录状态。推导
- 服务会返回每个直接且由会话支撑的 subagent 和 diagnostic,不设 cursor 或候选数量上限。稳定排序可使结果确定;模型投影避免了一次性 child 带来的上下文增长,但可继续 child 的数量仍无上限。
- `running``inactive` 是进程本地语料快照,而非结果或消息送达承诺。另一个进程可能在当前进程将某个持久化 child 报告为 `inactive` 时激活它;跨进程准确性需要共享租约。
- 持久化生命周期模式是一次发布前的描述符格式变更:版本 2 会将旧版版本 1 描述符拒绝为不受支持。现在,每次由本地会话支撑的启动都会产生一个小型日志事件,使 UI 和其他服务消费方无需重放模型可见的工具结果即可对一次性历史进行分类。
- 一次性会话持久化仍为尽力执行。一次性 child 在存活期间可见,dispose 后只有在其会话检查点到达持久化存储时才可继续被发现;目录参与不会像可继续激活那样增加必需的最终 flush,也不会把持久化失败变为 run 失败。
- 一次性会话持久化仍为尽力执行。一次性 child 在存活期间可见,dispose 后只有在其会话检查点到达持久化存储时才可继续被发现;目录参与不会像可继续激活那样增加必需的最终 flush,也不会把持久化失败变为 run 失败。可继续启动无需提前 flush 描述符,因为描述符会随创建 seed 一同携带,并且每条 Activation dispose 路径都会执行必需的最终检查点,包括提示词准入受阻之后。
- 远端 ACP 一次性运行仍不在目录中,因为它们不会发布可供 `traceSession()` 发现的本地 child 会话。若要枚举这些运行,需要单独的持久化本地记录,不能假装远端生命周期 id 就是会话 id。
+4
View File
@@ -55,3 +55,7 @@
models:
- id: deepseek-v4-flash
- id: deepseek-v4-pro
# Authored scenarios can fence a later parent action on the real
# child settlement edge without exposing a test-only model tool.
- id: subagent-settlement-marker
name: './tests/fixtures/subagent-settlement-marker.ts'
+5 -3
View File
@@ -228,9 +228,11 @@ const SCENARIOS: Scenario[] = [
recorded: false,
configPath: SUBAGENT_DURABILITY_FAILURE_CONFIG,
},
// Authored durable-catalog transcript: after a background delegation settles,
// `list_agents` executes for real against the control service, session query,
// and JSONL persistence, and renders the child's durable label as complete.
// Authored durable-catalog transcript: the snapshot-only lifecycle marker
// fences the second parent turn behind the child's Activation end, so
// `list_agents` deterministically reads the persisted child as complete.
// The tool itself executes for real against the control service, session
// query, and JSONL persistence; the marker is not model-visible.
{ name: 'subagent-list-agents', hasModelTurn: true, recorded: false },
{
name: 'subagent-depth-two-rejection',
@@ -0,0 +1,13 @@
import { writeFileSync } from 'node:fs'
import { join } from 'node:path'
import type { Context } from 'cordis'
import type {} from '@deepseek-ai/dsh-subagent'
export const name = 'subagent-settlement-marker'
/** Publish a workspace marker after a subagent lifecycle end. */
export function apply(ctx: Context): void {
ctx.on('subagent/end', () => {
writeFileSync(join(process.cwd(), '.dsh-snapshot-subagent-settled'), '')
})
}
@@ -8,7 +8,15 @@
},
{
"op": "prompt",
"text": "Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. 2. Collect its result with task_output using the task id from the acknowledgement and wait: true. 3. Call list_agents and observe the subagent you started. 4. Reply with the single word DONE. Do not use the bash tool."
"text": "Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. Then reply with the single word STARTED. Do not call any other tool."
},
{
"op": "waitForFile",
"path": ".dsh-snapshot-subagent-settled"
},
{
"op": "prompt",
"text": "Call list_agents once and observe the subagent you started. Then reply with the single word DONE. Do not call any other tool."
}
]
}
@@ -1,8 +1,8 @@
{"type":"session","version":0,"id":"11111111-1111-4111-8111-111111111111","createdAt":1789000000000,"cwd":"{{cwd}}","delegationDepth":0}
{"type":"turn/start","seq":0,"time":1789000000000,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
{"type":"user/message","seq":1,"time":1789000000001,"data":{"content":[{"type":"text","text":"Follow these steps exactly, then stop. 1. Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. 2. Collect its result with task_output using the task id from the acknowledgement and wait: true. 3. Call list_agents and observe the subagent you started. 4. Reply with the single word DONE. Do not use the bash tool."}],"source":{"kind":"user"},"role":"user","id":"f0bb39a7-3fc0-44e9-b3b9-5df1fc3a2a32"},"surfaceOp":"append"}
{"type":"session/title","seq":2,"time":1789000000001,"data":{"title":"Follow these steps exactly, then","messageSeqs":[1],"source":{"kind":"fallback"}}}
{"type":"user/message","seq":3,"time":1785531795622,"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"},"role":"user","id":"00d07016-a9d1-4733-a714-d4eaeac5df87"},"surfaceOp":"append"}
{"type":"user/message","seq":1,"time":1789000000001,"data":{"content":[{"type":"text","text":"Call the subagent tool once with run_in_background set to true, description 'Reply with CHILD_OK', and prompt 'Reply with exactly the word CHILD_OK and nothing else.'. Then reply with the single word STARTED. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"437d4bb4-a21e-442e-924e-a968795aec27"},"surfaceOp":"append"}
{"type":"session/title","seq":2,"time":1789000000001,"data":{"title":"Call the subagent tool once","messageSeqs":[1],"source":{"kind":"fallback"}}}
{"type":"user/message","seq":3,"time":1785531795622,"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"},"role":"user","id":"84603d0e-6f38-49c1-a622-2eaeee3ebad7"},"surfaceOp":"append"}
{"type":"step/start","seq":4,"time":1785531795622,"data":{"turn":1,"step":1}}
{"type":"request/header","seq":5,"time":1785531795623,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":6,"time":1785531795623,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
@@ -11,36 +11,37 @@
{"type":"assistant/chunk","seq":9,"time":1789000000008,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}}}
{"type":"assistant/chunk","seq":10,"time":1785531795632,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":11,"time":1785531795632,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":12,"time":1785531795632,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"75877603-3b80-4c08-8c55-4ee9899ffeb5"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[7,8,9,10,11],"surfaceOp":"append"}
{"type":"assistant/message","seq":12,"time":1785531795632,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"12b10b33-c694-4f08-adcd-7ffc229746ca"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[7,8,9,10,11],"surfaceOp":"append"}
{"type":"tool/call","seq":13,"time":1785531795632,"data":{"turn":1,"step":1,"callId":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}
{"type":"tool/result","seq":14,"time":1785531795642,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"c702b49d-200f-44ca-8112-e6f44b9f9c54"}},"sourceEventSeqs":[13],"surfaceOp":"append"}
{"type":"tool/result","seq":14,"time":1785531795642,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent 33333333-3333-4333-8333-333333333333"}],"isError":false}],"role":"user","id":"cac5aed0-bf6b-48a4-89b2-43a382c00b35"}},"sourceEventSeqs":[13],"surfaceOp":"append"}
{"type":"step/end","seq":15,"time":1785531795642,"data":{"turn":1,"step":1}}
{"type":"step/start","seq":16,"time":1785531795650,"data":{"turn":1,"step":2}}
{"type":"assistant/chunk","seq":17,"time":1789000000016,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":18,"time":1789000000017,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"call_collect_1","name":"task_output","argumentsDelta":"{\"task_id\": \"subagent-1\", \"wait\": true}"}}}
{"type":"assistant/chunk","seq":19,"time":1789000000018,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_collect_1","name":"task_output","arguments":"{\"task_id\": \"subagent-1\", \"wait\": true}"}}}}
{"type":"assistant/chunk","seq":17,"time":1789000000016,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":18,"time":1789000000017,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"STARTED"}}}
{"type":"assistant/chunk","seq":19,"time":1789000000018,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"STARTED"}}}}
{"type":"assistant/chunk","seq":20,"time":1785531795656,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":21,"time":1785531795656,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":22,"time":1785531795656,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_collect_1","name":"task_output","arguments":"{\"task_id\": \"subagent-1\", \"wait\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"18452edf-194a-4372-9851-8b1a7738360e"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[17,18,19,20,21],"surfaceOp":"append"}
{"type":"tool/call","seq":23,"time":1785531795656,"data":{"turn":1,"step":2,"callId":"call_collect_1","name":"task_output","arguments":"{\"task_id\": \"subagent-1\", \"wait\": true}"}}
{"type":"tool/result","seq":24,"time":1785531795668,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_collect_1"},"content":[{"type":"tool-result","toolCallId":"call_collect_1","content":[{"type":"text","text":"Error: unknown task subagent-1"}],"isError":true}],"role":"user","id":"7df847e3-c725-4ba4-94cc-85079e25b501"}},"sourceEventSeqs":[23],"surfaceOp":"append"}
{"type":"step/end","seq":25,"time":1785531795668,"data":{"turn":1,"step":2}}
{"type":"step/start","seq":26,"time":1785531795680,"data":{"turn":1,"step":3}}
{"type":"assistant/chunk","seq":27,"time":1789000000026,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":28,"time":1789000000027,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"call_list","name":"list_agents","argumentsDelta":"{}"}}}
{"type":"assistant/chunk","seq":29,"time":1789000000028,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_list","name":"list_agents","arguments":"{}"}}}}
{"type":"assistant/chunk","seq":30,"time":1785531795686,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":31,"time":1785531795686,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":32,"time":1785531795686,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_list","name":"list_agents","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"f720c7a5-97fb-49dc-aefa-99c89f2d37bc"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[27,28,29,30,31],"surfaceOp":"append"}
{"type":"tool/call","seq":33,"time":1785531795686,"data":{"turn":1,"step":3,"callId":"call_list","name":"list_agents","arguments":"{}"}}
{"type":"tool/result","seq":34,"time":1785531795703,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_list"},"content":[{"type":"tool-result","toolCallId":"call_list","content":[{"type":"text","text":"33333333-3333-4333-8333-333333333333 [complete] — Reply with CHILD_OK"}],"isError":false}],"role":"user","id":"0a8e97a2-a206-4cee-b449-59f3ce21f67c"}},"sourceEventSeqs":[33],"surfaceOp":"append"}
{"type":"step/end","seq":35,"time":1785531795703,"data":{"turn":1,"step":3}}
{"type":"step/start","seq":36,"time":1785531795710,"data":{"turn":1,"step":4}}
{"type":"assistant/chunk","seq":37,"time":1789000000036,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":38,"time":1789000000037,"data":{"turn":1,"step":4,"chunk":{"type":"text-delta","index":0,"text":"DONE"}}}
{"type":"assistant/chunk","seq":39,"time":1789000000038,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}}
{"type":"assistant/chunk","seq":40,"time":1785531795715,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}}
{"type":"assistant/chunk","seq":41,"time":1785531795715,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":42,"time":1785531795715,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"4a4c63b7-4ecf-4efc-8f4a-c0894b211475"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[37,38,39,40,41],"surfaceOp":"append"}
{"type":"step/end","seq":43,"time":1785531795716,"data":{"turn":1,"step":4}}
{"type":"turn/end","seq":44,"time":1785531795716,"data":{"turn":1,"reason":{"kind":"completed"}}}
{"type":"assistant/chunk","seq":21,"time":1785531795656,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":22,"time":1785531795656,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"740e57f2-7e00-4611-b3ef-8a9f003df4ab"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[17,18,19,20,21],"surfaceOp":"append"}
{"type":"step/end","seq":23,"time":1785536135021,"data":{"turn":1,"step":2}}
{"type":"turn/end","seq":24,"time":1785536135021,"data":{"turn":1,"reason":{"kind":"completed"}}}
{"type":"turn/start","seq":25,"time":1785536135057,"data":{"turn":2,"trigger":{"kind":"message","source":{"kind":"user"}}}}
{"type":"user/message","seq":26,"time":1785536135057,"data":{"content":[{"type":"text","text":"Call list_agents once and observe the subagent you started. Then reply with the single word DONE. Do not call any other tool."}],"source":{"kind":"user"},"role":"user","id":"ec732d1e-a1ab-4fff-bd39-2d2cf6b88851"},"surfaceOp":"append"}
{"type":"step/start","seq":27,"time":1785536135061,"data":{"turn":2,"step":1}}
{"type":"assistant/chunk","seq":28,"time":1789000000027,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":29,"time":1789000000028,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_list","name":"list_agents","argumentsDelta":"{}"}}}
{"type":"assistant/chunk","seq":30,"time":1785531795686,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_list","name":"list_agents","arguments":"{}"}}}}
{"type":"assistant/chunk","seq":31,"time":1785531795686,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":32,"time":1785536135065,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":33,"time":1785536135065,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_list","name":"list_agents","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"13ce9c4f-11b1-4cc2-8e8b-68e731911890"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[28,29,30,31,32],"surfaceOp":"append"}
{"type":"tool/call","seq":34,"time":1785536135065,"data":{"turn":2,"step":1,"callId":"call_list","name":"list_agents","arguments":"{}"}}
{"type":"tool/result","seq":35,"time":1785536135092,"data":{"turn":2,"step":1,"message":{"source":{"kind":"tool","callId":"call_list"},"content":[{"type":"tool-result","toolCallId":"call_list","content":[{"type":"text","text":"33333333-3333-4333-8333-333333333333 [complete] — Reply with CHILD_OK"}],"isError":false}],"role":"user","id":"a12be6a6-6aab-4bfa-bb79-1bf7d5dfb964"}},"sourceEventSeqs":[34],"surfaceOp":"append"}
{"type":"step/end","seq":36,"time":1785536135093,"data":{"turn":2,"step":1}}
{"type":"step/start","seq":37,"time":1785536135101,"data":{"turn":2,"step":2}}
{"type":"assistant/chunk","seq":38,"time":1789000000037,"data":{"turn":2,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":39,"time":1789000000038,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":0,"text":"DONE"}}}
{"type":"assistant/chunk","seq":40,"time":1785531795715,"data":{"turn":2,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}}
{"type":"assistant/chunk","seq":41,"time":1785531795715,"data":{"turn":2,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}}
{"type":"assistant/chunk","seq":42,"time":1785536135106,"data":{"turn":2,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":43,"time":1785536135106,"data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"61f09125-7a29-4420-b2d7-49ecf0b07ea4"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[38,39,40,41,42],"surfaceOp":"append"}
{"type":"step/end","seq":44,"time":1785536135106,"data":{"turn":2,"step":2}}
{"type":"turn/end","seq":45,"time":1785536135106,"data":{"turn":2,"reason":{"kind":"completed"}}}
@@ -1,4 +1,6 @@
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}}
{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"DONE"}}}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"STARTED"}}}}
{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"DONE"}}}}
{"jsonrpc":"2.0","id":4,"result":{"stopReason":"end_turn"}}
@@ -10,6 +10,7 @@
import type { Context } from 'cordis'
import type { SessionId } from '@deepseek-ai/dsh-session'
import type { SessionQueryService, SessionRecord } from '@deepseek-ai/dsh-session-query'
import type SubagentService from './index.ts'
import { SubagentError } from './error.ts'
import { foldSubagentDescriptor } from './descriptor.ts'
@@ -67,10 +68,11 @@ export type SubagentListEntry =
/**
* Interpret one parent's direct session descendants as session-backed subagents
* without loading or resuming an Agent.
* @see {@link SubagentService.listChildren} for the public cancellation and
* failure contract.
* @param ctx - context carrying the optional session-query service.
* @param parentSessionId - parent session whose direct children are listed.
* @param signal - caller-owned cancellation forwarded where supported and
* observed around every query await.
* @param signal - caller-owned cancellation.
* @returns children and per-child diagnostics in stable trace order.
* @throws {@link SubagentError} when session query is unavailable or
* the caller cancels the scan.
@@ -79,7 +81,7 @@ export async function listChildren(
ctx: Context,
parentSessionId: SessionId,
signal?: AbortSignal,
): Promise<SubagentListEntry[]> {
): ReturnType<SubagentService['listChildren']> {
const query = ctx.get('sessionQuery')
if (query === undefined) {
throw new SubagentError(
@@ -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/subagent/tool-subagent-control/README.md
README.md: 78621701cd4fe8ac9ad8b3f0985271ee6e2a7e8c
README.zh.md: f6ed8144dcfc8df8f2e6edad89c4d42b641d666e
README.md: 5babd9a34fd63b0152543eca4467cd3afa3ff927
README.zh.md: bfeede644e7ffdd8ba5b01b2b62c2974937133df
@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
The optional, globally named `send_message` and `list_agents` tools are thin adapters over `ctx.subagents`. Provider-bound `@deepseek-ai/dsh-tool-subagent` instances register distinct delegation tools per transport; this separately loaded package registers shared control tools once, so multiple delegation tools never register duplicate global controls. The root plugin registers `send_message` and requires only `subagents`; the separately loadable `./list-agents` plugin registers `list_agents` and additionally requires `sessionQuery` at load. A deployment without session query keeps `send_message` and omits the list tool. Neither tool's presence determines whether a delegation tool starts continuable work.
The optional, globally named `send_message` and `list_agents` tools are thin adapters over `ctx.subagents`. Provider-bound `@deepseek-ai/dsh-tool-subagent` instances register distinct delegation tools per transport; this separately loaded package registers shared control tools once, so multiple delegation tools never register duplicate global controls. The root plugin registers `send_message` and requires only `subagents`; the separately loadable `./list-agents` plugin registers `list_agents`, declares `sessionQuery` as a load-time dependency, and remains inactive until that service is available. A deployment without session query keeps `send_message` and omits the list tool. Neither tool's presence determines whether a delegation tool starts continuable work.
The tool performs no lifecycle routing — residency and cold resume belong to the subagent service. It passes `exec.agent` as the exact live parent that authorizes delivery and attributes every message as durable provenance `{ kind: 'coordinator', senderSessionId: parent.id }`, which the service retains but never treats as authority. Every message becomes the subagent's next FIFO turn through `Agent.followup()`: if the child is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. The tool forwards its execution signal, which owns admission only until inbox acceptance; once the child accepts the message the accepted turn cannot be cancelled through this tool. The child does not reply to the sender — its transcript by that id is the source of what it did. A delivery failure becomes an errored tool result stating the message was not delivered.
@@ -2,7 +2,7 @@
[English](README.md) | 中文
可选的全局具名 `send_message``list_agents` 工具是 `ctx.subagents` 之上的轻量适配器。绑定提供方的 `@deepseek-ai/dsh-tool-subagent` 实例会为每种传输注册不同的委派工具;这个单独加载的包只注册一次共享控制工具,因此多个委派工具绝不会重复注册全局控制工具。根插件注册 `send_message`,且只要求 `subagents`;可单独加载的 `./list-agents` 插件注册 `list_agents`并在加载时额外要求 `sessionQuery`。没有会话查询服务的部署可保留 `send_message` 并省略列表工具。是否加载这些工具不会决定委派工具是否启动可继续工作。
可选的全局具名 `send_message``list_agents` 工具是 `ctx.subagents` 之上的轻量适配器。绑定提供方的 `@deepseek-ai/dsh-tool-subagent` 实例会为每种传输注册不同的委派工具;这个单独加载的包只注册一次共享控制工具,因此多个委派工具绝不会重复注册全局控制工具。根插件注册 `send_message`,且只要求 `subagents`;可单独加载的 `./list-agents` 插件注册 `list_agents` `sessionQuery` 声明为加载时依赖,并在该服务可用前保持未激活状态。没有会话查询服务的部署可保留 `send_message` 并省略列表工具。是否加载这些工具不会决定委派工具是否启动可继续工作。
本工具不执行生命周期路由——驻留与冷恢复归 subagent 服务所有。它将 `exec.agent` 作为授权投递的准确实时父级传入,并把每条消息的来源标记为持久化来源 `{ kind: 'coordinator', senderSessionId: parent.id }`;服务会保留该来源,但绝不将其视为权限。每条消息都会通过 `Agent.followup()` 成为子 agent(智能体)的下一个 FIFO 轮次:如果子 agent 仍在工作,该消息会等待其当前轮次结束,因此无法重定向已经在进行的工作。本工具会转发其执行信号,该信号只在 inbox 接受之前掌管准入;一旦子 agent 接受消息,已接受的轮次便无法再通过本工具取消。子 agent 不会回复发送方——通过该 id 查看其 transcript 即是其所做工作的来源。投递失败会变为出错的工具结果,并明确说明消息未送达。
@@ -4,7 +4,7 @@
* separately loadable from the
* root `send_message` plugin because it additionally requires the session
* query service — a deployment may use `send_message` without loading session
* query, and this plugin catches that misconfiguration at load.
* query, and this plugin remains inactive until that service is available.
* @module @deepseek-ai/dsh-tool-subagent-control/list-agents
*/
@@ -66,6 +66,7 @@ export type InputStep =
text: string
waitForFile?: { path: string; timeoutMs?: number }
}
| { op: 'waitForFile'; path: string; timeoutMs?: number }
| { op: 'waitForTurnStart'; minimumTurn?: number; timeoutMs?: number }
| { op: 'waitForTurnEnd'; timeoutMs?: number }
| { op: 'waitForTitleAfterTurnEnd'; timeoutMs?: number }
@@ -436,6 +437,9 @@ async function runStep(
await promptDone
return
}
case 'waitForFile':
await waitForWorkspaceFile(cwd, step.path, step.timeoutMs)
return
case 'waitForTurnEnd': {
const sessionId = getSessionId()
if (sessionId === undefined) throw new Error('snapshot-harness: waitForTurnEnd before newSession')
@@ -865,6 +865,19 @@ describe('runScenario', () => {
expect(result.sessionId).toBeDefined()
})
it('waitForFile holds the next input step behind cwd-relative readiness', { timeout: 20_000 }, async () => {
const { dir, fixtureFile } = await scenario({})
const workspaceDir = join(dir, 'workspace')
const { mkdir } = await import('node:fs/promises')
await mkdir(workspaceDir, { recursive: true })
await writeFile(join(workspaceDir, 'ready'), '')
const result = await runScenario(
{ steps: [...boot, { op: 'waitForFile', path: 'ready' }, { op: 'cancel' }] },
{ agent: AGENT, mode: 'replay', fixtureFile, workspaceDir },
)
expect(result.sessionId).toBeDefined()
})
it.each([
[{ op: 'prompt', text: 'x' }, /prompt before newSession/],
[{ op: 'promptAndWaitForAgentMessage', text: 'x', waitForText: 'later' }, /promptAndWaitForAgentMessage before newSession/],