From 88f913a9aeb3e84490e995510fe29696995166ed Mon Sep 17 00:00:00 2001 From: Dudu <3414513905@qq.com> Date: Mon, 27 Jul 2026 00:00:14 +0800 Subject: [PATCH] refactor(subagent): merge continuation control service --- ...continuable-background-subagents.i18n.yaml | 4 +- ...-07-21-continuable-background-subagents.md | 10 +- ...-21-continuable-background-subagents.zh.md | 10 +- ...6-merge-subagent-control-service.i18n.yaml | 6 + ...26-07-26-merge-subagent-control-service.md | 37 ++++++ ...07-26-merge-subagent-control-service.zh.md | 37 ++++++ apps/cli/composition.md | 3 - apps/cli/config/base.cordis.yml | 10 +- apps/cli/package.json | 1 - docs/architecture.i18n.yaml | 4 +- docs/architecture.md | 3 +- docs/architecture.zh.md | 3 +- docs/capability-seams.md | 12 +- docs/config-catalog.md | 11 +- docs/cordis-catalog/events.md | 8 +- docs/cordis-catalog/services.md | 60 +++------ docs/core-data-structures/subagent.md | 14 +- docs/event-producer-consumer.md | 8 +- docs/module-graph.md | 40 +++--- docs/tool-catalog.md | 6 +- examples/acp-agent/composition.md | 3 - examples/acp-agent/cordis.yml | 10 +- .../system-prompt.expected.md | 8 +- .../tool-schemas.expected.json | 8 +- .../both-mode-turn/tool-schemas.expected.json | 8 +- .../code-mode-turn/system-prompt.expected.md | 8 +- .../lsp-definition/tool-schemas.expected.json | 8 +- .../pty-tools/tool-schemas.expected.json | 8 +- .../tool-schemas.expected.json | 8 +- .../text-turn/tool-schemas.expected.json | 8 +- .../web-fetch/tool-schemas.expected.json | 8 +- examples/headless-agent/composition.md | 3 - examples/headless-agent/cordis.yml | 10 +- examples/package.json | 1 - .../cordis/tool-cordis/src/api-catalog.ts | 18 +-- .../sdk/helper/src/features/builtin/index.ts | 13 +- packages/subagent/README.i18n.yaml | 4 +- packages/subagent/README.md | 9 +- packages/subagent/README.zh.md | 9 +- packages/subagent/subagent-control/README.md | 37 ------ .../subagent/subagent-control/package.json | 55 -------- .../subagent-control/src/invariant.ts | 32 ----- .../subagent/subagent-control/tsconfig.json | 39 ------ packages/subagent/subagent/README.md | 13 +- packages/subagent/subagent/package.json | 12 ++ .../index.ts => subagent/src/continuation.ts} | 54 ++++---- packages/subagent/subagent/src/descriptor.ts | 2 +- packages/subagent/subagent/src/index.ts | 83 ++++++++++-- packages/subagent/subagent/src/types.ts | 8 +- .../tests/continuation.spec.ts} | 103 +++++++-------- .../subagent/subagent/tests/service.spec.ts | 11 ++ packages/subagent/subagent/tsconfig.json | 6 + .../subagent/tool-subagent-control/README.md | 4 +- .../tool-subagent-control/package.json | 5 +- .../tool-subagent-control/src/index.ts | 12 +- .../tool-subagent-control/src/invariant.ts | 2 +- .../tests/tool-subagent-control.spec.ts | 13 +- .../tool-subagent-control/tsconfig.json | 2 +- .../subagent/tool-subagent/README.i18n.yaml | 4 +- packages/subagent/tool-subagent/README.md | 5 +- packages/subagent/tool-subagent/README.zh.md | 5 +- packages/subagent/tool-subagent/package.json | 2 - packages/subagent/tool-subagent/src/index.ts | 53 ++++---- .../tool-subagent/tests/tool-subagent.spec.ts | 120 +++++++++--------- packages/subagent/tool-subagent/tsconfig.json | 3 - pnpm-lock.yaml | 66 +--------- python/sdk-runtime/package.json | 1 - scripts/gen-doc-graphs.ts | 14 +- scripts/gen-tool-catalog.ts | 8 +- scripts/type-equiv.manifest.json | 2 +- tsconfig.host.json | 1 - 71 files changed, 543 insertions(+), 673 deletions(-) create mode 100644 .agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.i18n.yaml create mode 100644 .agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md create mode 100644 .agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.zh.md delete mode 100644 packages/subagent/subagent-control/README.md delete mode 100644 packages/subagent/subagent-control/package.json delete mode 100644 packages/subagent/subagent-control/src/invariant.ts delete mode 100644 packages/subagent/subagent-control/tsconfig.json rename packages/subagent/{subagent-control/src/index.ts => subagent/src/continuation.ts} (92%) rename packages/subagent/{subagent-control/tests/subagent-control.spec.ts => subagent/tests/continuation.spec.ts} (90%) diff --git a/.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.i18n.yaml b/.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.i18n.yaml index 956acaae45..d9be549a27 100644 --- a/.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.i18n.yaml @@ -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-21-continuable-background-subagents.md -2026-07-21-continuable-background-subagents.md: 2ee8a7ce19bba3f44a5bd58429e323e0eb818d36 -2026-07-21-continuable-background-subagents.zh.md: 53a4797dbafb7c69a45b99646be6bb58b465469b +2026-07-21-continuable-background-subagents.md: 42e9f6dd653dee8df4b8c068e3a33b7c69f8dc4b +2026-07-21-continuable-background-subagents.zh.md: e643e81f40074c3cc0cd6398b3f1d24e0d9bc988 diff --git a/.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md b/.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md index 2ee8a7ce19..42e9f6dd65 100644 --- a/.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md +++ b/.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md @@ -4,6 +4,8 @@ Status: implemented English | [中文](2026-07-21-continuable-background-subagents.zh.md) +The service-placement and provider-capability policy in this record are superseded by [Merge subagent control into the subagent service](../simplification/2026-07-26-merge-subagent-control-service.md). The continuation, persistence, Task, routing, authorization, and durability semantics remain in force; references below to the control service describe the internal continuation manager now exposed through `ctx.subagents`. + ## Problem The subagent tool treats each delegation as one owned `SubagentRun`: foreground calls and background Tasks collect the result and then dispose the run. Disposal bounds the number of live child Agents and releases their scoped services, listeners, and provider resources. The persisted child session may survive, but the parent has no durable catalog or tool path for discovering that child and starting another turn on it. @@ -101,15 +103,15 @@ Task records and active-run associations are process-local. Persistence makes th **Keep `resume?()` on the disposed run.** Retaining a disposed `SubagentRun` only to call `resume()` makes the old run double as a durable child handle and cannot reconstruct that object after restart. Service dispatch plus provider reconstruction makes the persistence boundary explicit. -**Put control orchestration on `SubagentService`.** This would let one service look up descriptors, associate Tasks, and dispatch providers, but would make the collection-agnostic provider seam depend on one consumer's persistence and Task policy. A separate control service keeps start/resume transport reusable by foreground and non-Task consumers while giving tools and UI one orchestration path. +**Put control orchestration on `SubagentService`.** This service-placement alternative was later adopted by the [merged-service decision](../simplification/2026-07-26-merge-subagent-control-service.md), which keeps raw start/resume transport reusable while isolating optional Task and persistence work in an injected internal manager. **Add explicit activation phases.** Public `starting`/`running`/`settling` states could describe admission and cleanup precisely, but would add a lifecycle protocol the implementation does not otherwise need. The synchronous association install closes duplicate process-local cold resume without exposing those phases. ## Testing -- `packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts` pins the continuable durability boundary: a permanent flush failure rejects with `DURABILITY_FAILED` and its cause, a transient loop-checkpoint failure can succeed on the final confirmation, cancellation owns either final-checkpoint outcome, resume also confirms durability, and foreground runs remain best-effort. `packages/subagent/subagent-control/tests/subagent-control.spec.ts` drives the real stack (agent loop, JSONL persistence, spawn/fork providers, Task service and surface, control service) keylessly: initial and resumed activations create fresh Tasks and dispose their runs before terminal; the descriptor event is turn-enclosed, model-hidden, versioned, and durable under the control-allocated child id; `task_kill` during a run, a final durability checkpoint, or cold-resume lookup settles `killed` after quiescence with no child work; steering joins the running Task without a second Task and retains the caller source; cold follow-ups accumulate turns in one durable transcript with their source and declared composition reconstructed; fork resume keeps the persisted seed boundary and never re-forks newer parent history; resumed depth uses the persisted header floor; foreign-parent, descriptor-less, and unmaterialized ids fail their started Task with the id unavailable; ownership conflicts and steering-settlement races report not-delivered without cold-resume fallthrough; competing sends during resume load are admitted once. +- `packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts` pins the continuable durability boundary: a permanent flush failure rejects with `DURABILITY_FAILED` and its cause, a transient loop-checkpoint failure can succeed on the final confirmation, cancellation owns either final-checkpoint outcome, resume also confirms durability, and foreground runs remain best-effort. `packages/subagent/subagent/tests/continuation.spec.ts` drives the real stack (agent loop, JSONL persistence, spawn/fork providers, Task service, and `ctx.subagents`) keylessly: initial and resumed activations create fresh Tasks and dispose their runs before terminal; the descriptor event is turn-enclosed, model-hidden, versioned, and durable under the service-allocated child id; cancellation, steering, cold follow-up, authorization, ownership conflicts, and resume races retain the contracts described above. - `packages/subagent/tool-subagent-control/tests/tool-subagent-control.spec.ts` pins the `send_message` schema, coordinator attribution, both route renderings, the not-delivered failure, the no-agent rejection, and HMR disposal. -- `packages/subagent/tool-subagent/tests/tool-subagent.spec.ts` covers the capability-branched background route: a resumable provider returns both ids through the control service and advertises `send_message`, a one-shot provider keeps the plain task acknowledgement, and a resumable provider without the control service fails loud. +- `packages/subagent/tool-subagent/tests/tool-subagent.spec.ts` covers the configured background route: continuable mode requires a resumable provider and returns both ids without requiring `send_message`, while one-shot mode keeps the plain Task acknowledgement even when the provider can resume. - `packages/sdk/helper/tests/project.spec.ts` pins the Task service and model-facing Task controls in generated spawn and fork compositions. - The keyless ACP snapshot scenario `subagent-continuable` (examples/acp-agent) pins the model-visible transcript: the two-id acknowledgement, a final durability-confirmation failure rendered through `task_output` without unconfirmed child output, and a `send_message` follow-up whose started Task fails with the id unavailable. @@ -117,7 +119,7 @@ Task records and active-run associations are process-local. Persistence makes th - Every follow-up after settlement pays persistence load and scoped setup cost; in exchange, live children stay bounded by concurrent work rather than historical session count. Continuable creation fails clearly when persistence is unavailable or the stored composition cannot be reconstructed. - Two callers may still race a stopped child through paths outside the control service. The Agent registry prevents duplicate same-session publication; a losing Task fails and its message is not delivered. A message may also race cancellation, terminal status publication, or run disposal. Admission is not claimed to be atomic or exactly-once; the synchronous process-local association install closes duplicate cold resume through the control service without a public lifecycle state machine. -- Driving a continuable child through the ordinary Agent API bypasses its Task association. The control service rejects that live child as an ownership conflict; adapters must display persisted transcripts without loading an Agent and submit human input through `SubagentControlService.sendMessage()`. +- Driving a continuable child through the ordinary Agent API bypasses its Task association. `ctx.subagents` rejects that live child as an ownership conflict; adapters must display persisted transcripts without loading an Agent and submit human input through `SubagentService.sendMessage()`. - The active-run association coordinates only one runtime. Concurrent resume from multiple processes is not serialized; that deployment requires a persistence-level lease or compare-and-set operation. - Human interaction requires the exact parent Agent instance to remain live because owner disposal cancels and removes its Tasks. It also requires an attached Task control surface. Standalone child interaction requires a future separation between Task access ownership and durable notification targeting. - The background tool returns child and Task ids before child publication and descriptor durability. Startup failure, a failed final durability confirmation, or process exit before the first child flush leaves the Task failed and may leave an unmaterialized or stale child id; by-id control reports missing state as unavailable rather than retroactively changing the tool acknowledgement. diff --git a/.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.zh.md b/.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.zh.md index 53a4797dba..e643e81f40 100644 --- a/.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.zh.md +++ b/.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.zh.md @@ -4,6 +4,8 @@ Status: implemented [English](2026-07-21-continuable-background-subagents.md) | 中文 +本记录中的服务放置与提供方功能策略已由[将 subagent 控制合并到 subagent 服务](../simplification/2026-07-26-merge-subagent-control-service.md)取代。继续执行、持久化、Task、路由、授权和持久性语义仍然有效;下文所提控制服务,是指现已通过 `ctx.subagents` 公开的内部继续执行管理器。 + ## 问题 subagent 工具将每次委派视为一个独占的 `SubagentRun`:前台调用和后台 Task 收集结果后 dispose(资源释放)该 run。这种所有权关系能够限制存活 child agent(智能体)的数量,并释放其作用域服务、监听器及提供方资源。持久化的 child 会话可能继续存在,但 parent 缺少持久化目录和工具路径,无法发现该 child 并为其启动另一轮次。 @@ -101,15 +103,15 @@ Task 记录和活跃 run 关联都位于进程内。持久化使 child 会话可 **在已 dispose 的 run 上保留 `resume?()`。** 如果仅为调用 `resume()` 而保留已 dispose 的 `SubagentRun`,旧 run 会同时充当持久化 child handle,而且进程重启后无法重建该对象。由服务分发、提供方重建,可明确表达持久化边界。 -**将控制编排放在 `SubagentService` 上。** 这样一个服务就能查找描述符、关联 Task 并分发提供方,但会迫使不感知集合的提供方 seam 依赖某个消费方的持久化与 Task 策略。单独的控制服务让前台及不使用 Task 的消费方可以复用 start/resume 传输,同时为工具和 UI 提供统一的编排路径。 +**将控制编排放在 `SubagentService` 上。** 后来的[服务合并决策](../simplification/2026-07-26-merge-subagent-control-service.md)采用了这一服务放置方案;该方案保持底层 start/resume 传输可复用,同时将可选的 Task 与持久化工作隔离在注入的内部管理器中。 **增加显式激活阶段。** 公开的 `starting`/`running`/`settling` 状态可以准确描述准入和清理,但会引入实现本身并不需要的生命周期协议。同步安装关联无需暴露这些阶段,即可消除进程内重复的 cold resume。 ## 测试 -- `packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts` 固定可继续执行的持久性边界:flush 持续失败时会以 `DURABILITY_FAILED` 拒绝并保留失败原因,循环检查点的瞬时失败可在最终确认成功后继续完成,发生取消时最终检查点无论成功还是失败都由取消优先决定结果,resume 同样会确认持久性,而前台运行仍采用尽力而为策略。`packages/subagent/subagent-control/tests/subagent-control.spec.ts` 以无密钥方式驱动真实栈(agent loop、JSONL 持久化、spawn/fork 提供方、Task 服务与控制面、控制服务):初始及恢复后的激活都会创建新 Task,并在进入终态前 dispose 各自的 run;描述符事件位于轮次内、对模型隐藏、带版本,并在控制服务分配的 child id 下持久化;在 run 运行期间、最终持久性检查点执行期间或 cold resume 查找期间执行 `task_kill`,会在完全停稳后结算为 `killed`,且不产生任何 child 工作;steering 会加入运行中的 Task,不创建第二个 Task,并保留调用方来源;cold follow-up 会在一份持久化 transcript 中累积轮次,并重建其来源和声明的组合配置;恢复 fork 会保持持久化 seed 边界,绝不重新 fork parent 更新后的历史;恢复后的深度以持久化 header 为下界;外来 parent、无描述符及 unmaterialized 的 id 会带着「id 不可用」使其已启动的 Task 失败;所有权冲突和 steering 与结算的竞态会报告未送达,且不改用从持久化存储恢复路径;resume 加载期间竞争的发送只准入一次。 +- `packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts` 固定可继续执行的持久性边界:flush 持续失败时会以 `DURABILITY_FAILED` 拒绝并保留失败原因,循环检查点的瞬时失败可在最终确认成功后继续完成,发生取消时最终检查点无论成功还是失败都由取消优先决定结果,resume 同样会确认持久性,而前台运行仍采用尽力而为策略。`packages/subagent/subagent/tests/continuation.spec.ts` 以无密钥方式驱动真实栈(agent loop、JSONL 持久化、spawn/fork 提供方、Task 服务和 `ctx.subagents`):初始及恢复后的激活都会创建新 Task,并在进入终态前 dispose 各自的 run;描述符事件位于轮次内、对模型隐藏、带版本,并在服务分配的 child id 下持久化;取消、steering、cold follow-up、授权、所有权冲突与 resume 竞态保留上述契约。 - `packages/subagent/tool-subagent-control/tests/tool-subagent-control.spec.ts` 固定 `send_message` 的 schema、coordinator 来源标记、两种路由渲染、未送达失败、无 agent 时的拒绝,以及 HMR(热模块替换)dispose。 -- `packages/subagent/tool-subagent/tests/tool-subagent.spec.ts` 覆盖按功能分支的后台路由:可恢复的提供方会通过控制服务返回两个 id 并公开 `send_message`,一次性提供方保持普通的 task 确认消息,而缺少控制服务的可恢复提供方会明确失败。 +- `packages/subagent/tool-subagent/tests/tool-subagent.spec.ts` 覆盖配置的后台路由:可继续模式要求提供方可恢复,并在不要求 `send_message` 的情况下返回两个 id;即使提供方可以恢复,一次性模式仍保持普通的 Task 确认消息。 - `packages/sdk/helper/tests/project.spec.ts` 固定生成的 spawn 与 fork 组合中的 Task 服务及面向模型的 Task 控制工具。 - 无密钥 ACP 快照场景 `subagent-continuable`(examples/acp-agent)固定模型可见的 transcript:双 id 确认消息、最终持久性确认失败(该失败通过 `task_output` 呈现,且不包含未经确认的 child 输出),以及一次 `send_message` 后续操作——其已启动的 Task 会带着「id 不可用」失败。 @@ -117,7 +119,7 @@ Task 记录和活跃 run 关联都位于进程内。持久化使 child 会话可 - 每次完成结算后的后续轮次都需要承担持久化加载和作用域 setup 成本;作为交换,存活 child 的数量受并发工作量限制,而不是随历史会话数量增长。持久化不可用或存储的组合配置无法重建时,可继续 child 的创建会明确失败。 - 两个调用方仍可能通过控制服务外部的路径争抢已停止的 child。Agent 注册表会阻止相同会话的重复发布;失败的 Task 会失败,且其消息不会送达。消息也可能与取消、终态状态发布或 run dispose 发生竞态。准入不承诺原子或恰好执行一次;在进程内同步安装的关联无需公开生命周期状态机,即可通过控制服务消除重复的 cold resume。 -- 通过普通 Agent API 驱动可继续 child 会绕过其 Task 关联。控制服务会将该存活 child 视为所有权冲突并拒绝;适配器必须在不加载 Agent 的情况下展示持久化 transcript,并通过 `SubagentControlService.sendMessage()` 提交用户输入。 +- 通过普通 Agent API 驱动可继续 child 会绕过其 Task 关联。`ctx.subagents` 会将该存活 child 视为所有权冲突并拒绝;适配器必须在不加载 Agent 的情况下展示持久化 transcript,并通过 `SubagentService.sendMessage()` 提交用户输入。 - 活跃 run 关联只能协调一个运行时。多个进程同时恢复时不会串行化;此类部署需要持久化层的租约或 compare-and-set 操作。 - 用户交互要求作为 owner 的那个精确 parent Agent 实例保持存活,因为 dispose owner 会取消并移除其 Task。用户交互还要求附加 Task 控制面。若要单独与 child 交互,后续必须将 Task 访问所有权与持久化通知目标分离。 - 后台工具会在 child 发布和描述符持久化之前返回 child id 和 Task id。启动失败、最终持久性确认失败,或进程在 child 首次 flush 之前退出,都会使 Task 失败,并可能留下 unmaterialized 或陈旧的 child id;按 id 的控制操作会将缺失状态报告为不可用,而不会追溯修改工具确认消息。 diff --git a/.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.i18n.yaml b/.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.i18n.yaml new file mode 100644 index 0000000000..1068f5f578 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md +2026-07-26-merge-subagent-control-service.md: a5ce673997502ef6fbd7c66ff4e94e301d4192ba +2026-07-26-merge-subagent-control-service.zh.md: cf867ab444438b7ee62cde68a3d53e83c3d049d1 diff --git a/.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md b/.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md new file mode 100644 index 0000000000..a5ce673997 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md @@ -0,0 +1,37 @@ +# Agent Note: Merge subagent control into the subagent service + +Status: implemented + +English | [中文](2026-07-26-merge-subagent-control-service.zh.md) + +## Problem + +Continuable-child orchestration originally lived in a separate `ctx.subagentControl` service above the raw `ctx.subagents` provider seam. That split kept provider dispatch independent of Tasks and persistence, and gave model and human adapters one orchestration contract. In practice the two services described one capability family, every continuable caller needed both, and the provider-bound delegation tool had to infer policy from `provider.resume` and inspect whether the control service and `send_message` tool happened to be loaded. This made sibling plugin presence decide execution semantics and coupled starting continuable work to an optional follow-up surface. + +## Decision + +`SubagentService` is the only public service. It retains raw `start(name, request)` and `resume(name, request)` for callers that own run collection, and exposes `startContinuable(spec)` and `sendMessage(...)` for durable Task-backed activations. The standalone `@deepseek-ai/dsh-subagent-control` package and `ctx.subagentControl` key are deleted; the optional `@deepseek-ai/dsh-tool-subagent-control` package injects `ctx.subagents` directly. + +The continuation implementation remains an internal manager rather than expanding the provider registry's core state. `SubagentService` creates it through `ctx.inject(['tasks', 'agents'], ...)`, so the injected Cordis child fiber owns its Task completion listener and teardown effects. Loading the provider registry does not require Tasks or persistence. The manager exists only while Tasks and Agents are available, and each continuation operation resolves session persistence at the point it needs durability. Disposing that fiber cancels and settles active continuations before releasing their associations. + +`startContinuable` remains distinct from raw `start` because it has a different ownership and timing contract: it allocates the durable child id, creates the Task, and returns both ids synchronously while startup continues inside the Task. Raw `start` instead awaits provider publication and transfers a holder-owned run. Folding the method onto `start` through flags or return unions would broaden the low-level contract and create more change than keeping the existing explicit entry. + +Each `@deepseek-ai/dsh-tool-subagent` instance selects `backgroundMode: 'one-shot' | 'continuable'`, defaulting to `one-shot`. This configuration is policy; `provider.resume` is only the capability check for configured continuable mode. A resumable provider can therefore still run one-shot background work. The `send_message` tool is an independent adapter: loading or omitting it neither enables nor disables `startContinuable`. + +## Alternatives considered + +**Keep the separate service.** This preserves the strongest dependency separation, but every production continuable path composes both services and the extra public key exposes an architectural distinction callers do not need. The internal manager preserves optional Task and persistence dependencies without a second service. + +**Infer continuable mode from `provider.resume`.** Method presence correctly states cold-resume capability but not deployment policy. It forced every resumable provider into continuable background semantics and made missing sibling plugins a runtime error. Explicit tool configuration separates choice from capability. + +**Register continuation access or inspect the follow-up tool.** A registry could tell the delegation tool whether a continuation surface exists, but starting durable work does not require any follow-up adapter. Such a registry would encode UI composition into execution policy and recreate the sibling dependency under another name. + +**Merge raw and continuable starts into one method.** A flag on `start` would return either a ready run or immediate Task and child identities, weakening a simple ownership boundary. Keeping `startContinuable` is the smaller change and preserves both contracts explicitly. + +## Consequences + +- The service topology has one public key and one package fewer while raw provider dispatch remains usable without Tasks or persistence. +- Continuable mode fails at provider mount when the configured provider lacks `resume`; missing Tasks, Agents, or persistence still fail at the earliest operation that requires them. +- Follow-up delivery remains optional. Deployments may start and collect continuable work through Task tools without exposing `send_message`. +- The continuation manager is still Task- and persistence-aware inside the `dsh-subagent` package, so the package declares optional peer dependencies on those services even though ordinary `start` and `resume` callers do not need them. +- Existing continuation races, authorization, durability, cancellation, and settle-then-dispose semantics are unchanged and remain pinned by the migrated `subagent` tests. diff --git a/.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.zh.md b/.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.zh.md new file mode 100644 index 0000000000..cf867ab444 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.zh.md @@ -0,0 +1,37 @@ +# Agent Note: 将 subagent 控制合并到 subagent 服务 + +Status: implemented + +[English](2026-07-26-merge-subagent-control-service.md) | 中文 + +## 问题 + +可继续 child 的编排最初位于原始 `ctx.subagents` 提供方 seam 之上的独立 `ctx.subagentControl` 服务中。该拆分使提供方分发与 Task 和持久化无关,并为模型与人工适配器提供统一的编排契约。实践中,两个服务属于同一组功能,每个可继续调用方都需要二者,而绑定提供方的委派工具必须根据 `provider.resume` 推断策略,并检查控制服务与 `send_message` 工具是否碰巧已加载。如此一来,配套插件是否存在会决定执行语义,并将可继续工作的启动耦合到可选的后续操作接口。 + +## 决策 + +`SubagentService` 是唯一的公开服务。它为自行收集 run 的调用方保留底层 `start(name, request)` 和 `resume(name, request)`,并公开 `startContinuable(spec)` 与 `sendMessage(...)`,用于具备持久性、由 Task 支撑的激活。系统删除独立的 `@deepseek-ai/dsh-subagent-control` 包(package)和 `ctx.subagentControl` 键;可选的 `@deepseek-ai/dsh-tool-subagent-control` 包则直接注入 `ctx.subagents`。 + +继续执行的实现仍是内部管理器,不会扩展提供方注册表的核心状态。`SubagentService` 通过 `ctx.inject(['tasks', 'agents'], ...)` 创建该管理器,因此注入的 Cordis child fiber 拥有自身的 Task 完成监听器和拆卸 effect。加载提供方注册表不要求 Task 或持久化。只有 Task 和 Agent 可用时,该管理器才会存在;每项继续执行操作都在需要持久性时解析会话持久化服务。dispose(资源释放)该 fiber 会先取消并结算活跃的继续执行,再释放其关联。 + +`startContinuable` 与底层 `start` 保持分离,因为二者的所有权与时序契约不同:前者分配持久化 child id、创建 Task,并同步返回两个 id,而启动过程继续在 Task 内运行;底层 `start` 则等待提供方发布,并移交一个由持有方负责的 run。若通过标志或返回值联合类型将该方法并入 `start`,会扩大底层契约,改动反而多于保留现有的显式入口。 + +每个 `@deepseek-ai/dsh-tool-subagent` 实例都会选择 `backgroundMode: 'one-shot' | 'continuable'`,默认值为 `one-shot`。这项配置表示策略;`provider.resume` 只用于检查所配置的可继续模式是否受提供方支持。因此,可恢复的提供方仍可执行一次性后台工作。`send_message` 工具是独立适配器:加载或省略该工具既不会启用也不会禁用 `startContinuable`。 + +## 已考虑的替代方案 + +**保留独立服务。** 这样能保持最严格的依赖分离,但每条生产环境中的可继续路径都要组合两个服务,而额外的公开键会暴露调用方并不需要的架构差异。内部管理器无需第二个服务,也能保留可选的 Task 和持久化依赖。 + +**根据 `provider.resume` 推断可继续模式。** 方法是否存在可以准确表示从持久化存储恢复的功能,却不能表示部署策略。这会迫使每个可恢复的提供方都采用可继续后台语义,并使配套插件缺失成为运行时错误。显式的工具配置将选择与功能分离。 + +**注册继续执行访问入口,或检查后续操作工具。** 注册表可以告诉委派工具继续执行接口是否存在,但启动具备持久性的工作不需要任何后续操作适配器。这样的注册表会把 UI 组合编码进执行策略,并以另一个名称重新建立插件间依赖关系。 + +**将底层启动与可继续启动合并为一个方法。** `start` 上的标志会使该方法返回就绪的 run,或立即返回 Task 和 child 标识,从而削弱简单的所有权边界。保留 `startContinuable` 改动更小,也能明确保留两项契约。 + +## 影响 + +- 服务拓扑少了一个公开键和一个包,同时底层提供方分发仍可在没有 Task 或持久化时使用。 +- 配置的提供方缺少 `resume` 时,可继续模式会在提供方挂载阶段失败;缺少 Task、Agent 或持久化时,仍会在需要它们的最早操作处失败。 +- 后续消息投递仍为可选功能。部署可以通过 Task 工具启动并收集可继续工作,而不公开 `send_message`。 +- `dsh-subagent` 包内的继续执行管理器仍然感知 Task 和持久化,因此该包会将这些服务声明为可选的对等依赖(peer dependency),即使普通的 `start` 和 `resume` 调用方并不需要它们。 +- 现有的继续执行竞态、授权、持久性、取消及先结算再 dispose 的语义均保持不变,并继续由迁移后的 `subagent` 测试固定。 diff --git a/apps/cli/composition.md b/apps/cli/composition.md index 76844279f6..189a8af496 100644 --- a/apps/cli/composition.md +++ b/apps/cli/composition.md @@ -94,8 +94,6 @@ flowchart LR cfg --> plugin_tui_subagent_spawn plugin_tui_subagent_fork["subagent-fork
@deepseek-ai/dsh-subagent-fork"] cfg --> plugin_tui_subagent_fork - plugin_tui_subagent_control["subagent-control
@deepseek-ai/dsh-subagent-control"] - cfg --> plugin_tui_subagent_control plugin_tui_tool_subagent_control["tool-subagent-control
@deepseek-ai/dsh-tool-subagent-control"] cfg --> plugin_tui_tool_subagent_control plugin_tui_tool_subagent["tool-subagent
@deepseek-ai/dsh-tool-subagent"] @@ -189,7 +187,6 @@ flowchart LR | `subagent` | `@deepseek-ai/dsh-subagent` | | `subagent-spawn` | `@deepseek-ai/dsh-subagent-spawn` | | `subagent-fork` | `@deepseek-ai/dsh-subagent-fork` | -| `subagent-control` | `@deepseek-ai/dsh-subagent-control` | | `tool-subagent-control` | `@deepseek-ai/dsh-tool-subagent-control` | | `tool-subagent` | `@deepseek-ai/dsh-tool-subagent` | | `tool-subagent-fork` | `@deepseek-ai/dsh-tool-subagent` | diff --git a/apps/cli/config/base.cordis.yml b/apps/cli/config/base.cordis.yml index 4982a521a7..891e2b359f 100644 --- a/apps/cli/config/base.cordis.yml +++ b/apps/cli/config/base.cordis.yml @@ -257,12 +257,8 @@ config: providerName: fork -# Continuable background children: the control service owns durable child ids -# and Task-backed activations; the control tool registers the one global -# `send_message` shared by both delegation tools. -- id: subagent-control - name: '@deepseek-ai/dsh-subagent-control' - +# Continuable background children are selected per delegation tool. The +# separately loaded follow-up tool registers the one global `send_message`. - id: tool-subagent-control name: '@deepseek-ai/dsh-tool-subagent-control' @@ -271,12 +267,14 @@ config: provider: spawn toolName: subagent + backgroundMode: continuable - id: tool-subagent-fork name: '@deepseek-ai/dsh-tool-subagent' config: provider: fork toolName: subagent_fork + backgroundMode: continuable - id: workflow-workerthread name: '@deepseek-ai/dsh-workflow-workerthread' diff --git a/apps/cli/package.json b/apps/cli/package.json index 4cfdf29725..e9c719dce7 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -101,7 +101,6 @@ "@deepseek-ai/dsh-storage-domain": "workspace:^", "@deepseek-ai/dsh-storage-json": "workspace:^", "@deepseek-ai/dsh-subagent": "workspace:^", - "@deepseek-ai/dsh-subagent-control": "workspace:^", "@deepseek-ai/dsh-subagent-fork": "workspace:^", "@deepseek-ai/dsh-subagent-spawn": "workspace:^", "@deepseek-ai/dsh-subprocess-local": "workspace:^", diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index d7d4618aac..2d14dedd68 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/architecture.md -architecture.md: d7beb60baac3c550eb008d414158d9a05181337a -architecture.zh.md: 200f82df3d936b45f4aeef0cb080c55483af602a +architecture.md: 44be3b55ab5061490a2ceb632175c1bb53a21330 +architecture.zh.md: d803bea1ba39e8fd07a01446dd2d2ae53aca35e1 diff --git a/docs/architecture.md b/docs/architecture.md index d7beb60baa..44be3b55ab 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -38,8 +38,7 @@ Harnesses are [Cordis](cordis-primer.md) contexts; packages contribute services, | `ctx.skills` | [`skill/`](../packages/skill/README.md) | skill provider registry, progressive disclosure | | `ctx.web` | [`web/`](../packages/web/README.md) | search/fetch provider registries | | `ctx.compact`, `ctx.toolResultPrune` | [`compact/`](../packages/compact/README.md)/[`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune/README.md) | summary compaction, optional model-free result pruning | -| `ctx.subagents` | [`subagent/`](../packages/subagent/README.md) | named delegation providers | -| `ctx.subagentControl` | [`subagent/`](../packages/subagent/README.md) | continuable-child Task-backed activation and steer-or-resume routing | +| `ctx.subagents` | [`subagent/`](../packages/subagent/README.md) | named delegation providers plus optional Task-backed continuation and steer-or-resume routing | | `ctx.planMode` | [`plan/`](../packages/plan/README.md) | logged plan collaboration state | | `ctx.tasks` | [`tasks/`](../packages/tasks/README.md) | background task registry, generic `task_*` controls | | `ctx.workflows` | [`workflow/`](../packages/workflow/README.md) | script-driven multi-agent orchestration | diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 200f82df3d..d803bea1ba 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -38,8 +38,7 @@ | `ctx.skills` | [`skill/`](../packages/skill/README.md) | skill(技能)提供方注册表和渐进式披露 | | `ctx.web` | [`web/`](../packages/web/README.md) | 搜索与抓取提供方注册表 | | `ctx.compact`,`ctx.toolResultPrune` | [`compact/`](../packages/compact/README.md)/[`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune/README.md) | 摘要压缩(compaction)和可选的无模型结果裁剪 | -| `ctx.subagents` | [`subagent/`](../packages/subagent/README.md) | 具名委托提供方 | -| `ctx.subagentControl` | [`subagent/`](../packages/subagent/README.md) | 可继续子 agent 的 Task 化 activation,以及 steer 或恢复路由 | +| `ctx.subagents` | [`subagent/`](../packages/subagent/README.md) | 具名委托提供方,以及可选的由 Task 支撑的继续执行与 steer-or-resume 路由 | | `ctx.planMode` | [`plan/`](../packages/plan/README.md) | 落日志的 plan 协作状态 | | `ctx.tasks` | [`tasks/`](../packages/tasks/README.md) | 后台任务注册表和通用 `task_*` 控制 | | `ctx.workflows` | [`workflow/`](../packages/workflow/README.md) | 脚本驱动的多 agent 编排 | diff --git a/docs/capability-seams.md b/docs/capability-seams.md index 946091c2c7..2681bd728a 100644 --- a/docs/capability-seams.md +++ b/docs/capability-seams.md @@ -132,12 +132,11 @@ flowchart LR pkg_compact["compact"] svc_compact["ctx.compact
Compaction seam"] pkg_subagent["subagent"] - svc_subagents["ctx.subagents
Subagent provider registry"] + svc_subagents["ctx.subagents
Subagent provider and continuation service"] pkg_subagent_spawn["subagent-spawn"] pkg_subagent_fork["subagent-fork"] - pkg_tool_ralph["tool-ralph"] - svc_subagentControl["ctx.subagentControl
Continuable-subagent control service"] pkg_tool_subagent_control["tool-subagent-control"] + pkg_tool_ralph["tool-ralph"] pkg_tasks["tasks"] svc_tasks["ctx.tasks
Background task registry"] pkg_tasks_local["tasks-local"] @@ -225,7 +224,6 @@ flowchart LR pkg_storage_domain --> svc_storageDomain pkg_storage_json --> svc_storage pkg_storage_sqlite --> svc_storage - pkg_subagent --> svc_subagentControl pkg_subagent --> svc_subagents pkg_subagent_acp --> svc_subagents pkg_subagent_fork --> svc_subagents @@ -314,10 +312,9 @@ flowchart LR svc_spillStore --> pkg_spill_policy svc_storage --> pkg_storage_domain svc_storageDomain --> pkg_workspace - svc_subagentControl --> pkg_tool_subagent - svc_subagentControl --> pkg_tool_subagent_control svc_subagents --> pkg_tool_ralph svc_subagents --> pkg_tool_subagent + svc_subagents --> pkg_tool_subagent_control svc_subprocess --> pkg_bash_local svc_subprocess --> pkg_bash_sandbox svc_subprocess --> pkg_lsp_local @@ -394,8 +391,7 @@ flowchart LR | `ctx.codeRuntime` | `seam` | [`code-runtime`](../packages/code-runtime/code-runtime) | [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | [`tools`](../packages/core/tools) | - | Runs one model-written program against host-provided async bindings; backends differ by substrate and language (the tool registry consumes it for Code Mode). | | `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-policy`](../packages/fs/fs-policy) | tool-fs executes read/write/edit through ctx.fs; fs-sandbox fences mutations by the shared sandbox mode; fs-policy contributes observed-state checks through the fs/* event gate. | | `ctx.compact` | `seam` | [`compact`](../packages/compact/compact) | [`compact-basic`](../packages/compact/compact-basic) | [`compact-basic`](../packages/compact/compact-basic) | - | The basic backend consumes post-step pressure and request-error recovery events; a model-facing compact tool remains deferred. | -| `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn`](../packages/subagent/subagent-spawn), [`subagent-fork`](../packages/subagent/subagent-fork), [`subagent-acp`](../packages/subagent/subagent-acp) | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-ralph`](../packages/workflow/tool-ralph) | - | Providers implement transports; tool-subagent exposes configured delegation while tool-ralph requires one fresh structured-output route. | -| `ctx.subagentControl` | `core` | [`subagent`](../packages/subagent/subagent) | - | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | - | Binds one durable child session to Task-backed activations over ctx.subagents; tool-subagent starts continuable background children and tool-subagent-control delivers follow-up messages. | +| `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn`](../packages/subagent/subagent-spawn), [`subagent-fork`](../packages/subagent/subagent-fork), [`subagent-acp`](../packages/subagent/subagent-acp) | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/workflow/tool-ralph) | - | Providers implement transports; the service also owns optional Task-backed continuation orchestration, tool-subagent selects one-shot or continuable delegation, tool-subagent-control delivers follow-ups, and tool-ralph requires one fresh structured-output route. | | `ctx.tasks` | `seam` | [`tasks`](../packages/tasks/tasks) | [`tasks-local`](../packages/tasks/tasks-local) | [`tool-bash`](../packages/bash/tool-bash), [`tool-pty`](../packages/pty/tool-pty), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-tasks`](../packages/tasks/tool-tasks) | - | Producers (background bash, PTY sends, and subagent delegations) register running work; tool-tasks is the model-facing control surface that reads, lists, and kills it; tasks-local is the process-local registry. | | `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-local`](../packages/web/web-fetch-local) | [`tool-web`](../packages/web/tool-web) | - | Search and fetch providers register into one ctx.web seam; tool-web owns the stable model-facing names. | | `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | The backend saves oversized tool text and returns a model-facing locator plus retrieval hint; spill-policy is the tools/post-execute consumer that decides when to spill. | diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 7980ef4136..c2158222ee 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1893,6 +1893,12 @@ export interface Config { * parameter and reject forced background calls. */ enableRunInBackground?: boolean + /** + * Background execution policy (default `one-shot`). `continuable` requires + * a provider with persisted resume support and returns both child and Task + * ids; follow-up adapters remain independently optional. + */ + backgroundMode?: 'one-shot' | 'continuable' /** * Agent options applied to every child; omitted fields use child-loop defaults. */ @@ -1928,7 +1934,7 @@ export interface Config { Depends on: [`AgentOptions`](core-data-structures/core.md) -Source: [`packages/subagent/tool-subagent/src/index.ts:27`](../packages/subagent/tool-subagent/src/index.ts) +Source: [`packages/subagent/tool-subagent/src/index.ts:25`](../packages/subagent/tool-subagent/src/index.ts) ## `@deepseek-ai/dsh-tool-tasks` @@ -2343,12 +2349,11 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co - `@deepseek-ai/dsh-session-projection` ([`packages/session-projection/session-projection/src/index.ts`](../packages/session-projection/session-projection/src/index.ts)) - `@deepseek-ai/dsh-storage` ([`packages/storage/storage/src/index.ts`](../packages/storage/storage/src/index.ts)) - `@deepseek-ai/dsh-subagent` ([`packages/subagent/subagent/src/index.ts`](../packages/subagent/subagent/src/index.ts)) -- `@deepseek-ai/dsh-subagent-control` — requires `subagents` · `tasks` · `agents` ([`packages/subagent/subagent-control/src/index.ts`](../packages/subagent/subagent-control/src/index.ts)) - `@deepseek-ai/dsh-subprocess-local` ([`packages/subprocess/subprocess-local/src/index.ts`](../packages/subprocess/subprocess-local/src/index.ts)) - `@deepseek-ai/dsh-tasks-local` ([`packages/tasks/tasks-local/src/index.ts`](../packages/tasks/tasks-local/src/index.ts)) - `@deepseek-ai/dsh-timeout-policy` — requires `tools` ([`packages/timeout/timeout-policy/src/index.ts`](../packages/timeout/timeout-policy/src/index.ts)) - `@deepseek-ai/dsh-tool-ask-user` — requires `tools` · `userInteraction` ([`packages/ui/tool-ask-user/src/index.ts`](../packages/ui/tool-ask-user/src/index.ts)) -- `@deepseek-ai/dsh-tool-subagent-control` — requires `tools` · `subagentControl` ([`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts)) +- `@deepseek-ai/dsh-tool-subagent-control` — requires `tools` · `subagents` ([`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts)) - `@deepseek-ai/dsh-tool-todo` — requires `tools` ([`packages/todo/tool-todo/src/index.ts`](../packages/todo/tool-todo/src/index.ts)) - `@deepseek-ai/dsh-typert-registry` ([`packages/typert/registry/src/index.ts`](../packages/typert/registry/src/index.ts)) - `@deepseek-ai/dsh-user-interaction` ([`packages/ui/user-interaction/src/index.ts`](../packages/ui/user-interaction/src/index.ts)) diff --git a/docs/cordis-catalog/events.md b/docs/cordis-catalog/events.md index 724867f0d8..db6af68970 100644 --- a/docs/cordis-catalog/events.md +++ b/docs/cordis-catalog/events.md @@ -795,7 +795,7 @@ A ready child settled. Scope-filtered dispatch uses the same delegating parent c Types: [Scoped](../core-data-structures/scope.md) · [SubagentService](../core-data-structures/subagent.md) -Source: [`packages/subagent/subagent/src/index.ts:150`](../../packages/subagent/subagent/src/index.ts) +Source: [`packages/subagent/subagent/src/index.ts:165`](../../packages/subagent/subagent/src/index.ts) ### `subagent/provider-added` — emit @@ -812,7 +812,7 @@ A provider became resolvable in the registry. Types: [SubagentProvider](../core-data-structures/subagent.md) -Source: [`packages/subagent/subagent/src/index.ts:124`](../../packages/subagent/subagent/src/index.ts) +Source: [`packages/subagent/subagent/src/index.ts:139`](../../packages/subagent/subagent/src/index.ts) ### `subagent/provider-removed` — emit @@ -827,7 +827,7 @@ A provider left the registry. Accepted runs remain holder-owned. 'subagent/provider-removed'(name: string): void ``` -Source: [`packages/subagent/subagent/src/index.ts:130`](../../packages/subagent/subagent/src/index.ts) +Source: [`packages/subagent/subagent/src/index.ts:145`](../../packages/subagent/subagent/src/index.ts) ### `subagent/start` — emit @@ -849,7 +849,7 @@ A provider established a ready child. For in-process providers, `ctx.agents.get( Types: [Scoped](../core-data-structures/scope.md) · [SubagentService](../core-data-structures/subagent.md) -Source: [`packages/subagent/subagent/src/index.ts:141`](../../packages/subagent/subagent/src/index.ts) +Source: [`packages/subagent/subagent/src/index.ts:156`](../../packages/subagent/subagent/src/index.ts) ## `system-prompt/*` diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index 43965ad9a3..b0d07711f2 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -1946,56 +1946,30 @@ async closeAll(): Promise Source: [`packages/storage/storage-domain/src/index.ts:69`](../../packages/storage/storage-domain/src/index.ts) -## `ctx.subagentControl` — `SubagentControlService` +## `ctx.subagents` — `SubagentService` -The continuable-subagent orchestration service. Tool schema and UI adapters are consumers of this one contract: parent and human messages route through sendMessage and share one activation result and cancellation boundary, while foreground one-shot delegation keeps calling `ctx.subagents.start()` directly. +Named provider registry with raw and Task-backed continuation operations. ```ts cordis-catalog /** - * Start a continuable background child: allocate its stable session id, - * snapshot its durable descriptor, and register the initial activation's - * Task. A synchronous validation failure (a non-JSON descriptor input, - * missing persistence, Task preflight) throws without creating a Task; the - * method otherwise returns both identities immediately, without waiting for - * child publication or descriptor durability. Asynchronous startup failure - * settles the returned Task as `failed` (or `killed` when cancelled) after - * any published run is disposed, which can leave an unmaterialized child id - * that later by-id operations report as unavailable. - * @param spec - provider, Task label, and the delegation request. - * @returns the stable child id and the initial activation's Task id. + * Start one durable continuable child through a Task-backed initial + * activation. + * @param spec - provider, Task label, and delegation request. + * @returns the stable child id and initial activation Task id. */ startContinuable(spec: ContinuableStartSpec): ContinuableStart /** - * Deliver one message to a known continuable child: steer its running - * activation, or cold-resume the durable session into a fresh Task-backed - * activation. The two routes are reported distinctly so timing-dependent - * routing is observable. Rejection means the message was NOT delivered — in - * particular, losing a race with Task settlement does not fall through to - * cold resume within the same call; a later retry after Task terminal may - * start the next activation. The started Task owns descriptor lookup and - * direct-parent authorization (its AbortSignal exists before that lookup), - * so an unknown, foreign, or descriptor-less child settles the started Task - * as `failed` with a detail reporting the id as unavailable. - * @param parent - the live parent agent sending the message (model tool or - * human adapter); Task access is authorized by its session id. - * @param childId - the stable child session id. - * @param message - the user-role content to deliver. - * @param source - caller-supplied attribution retained across either route. - * @returns whether the message `steered` the existing Task or `started` a new one. + * Deliver a message to a continuable child by steering its live activation + * or cold-resuming a fresh Task-backed activation. + * @param parent - live direct parent authorizing the operation. + * @param childId - durable child session id. + * @param message - user-role content to deliver. + * @param source - durable caller attribution. + * @returns the existing steered Task or newly started Task. */ -async sendMessage( parent: Agent, childId: SessionId, message: ContentBlock[], source: MessageSource, ): Promise -``` +sendMessage( parent: Agent, childId: SessionId, message: ContentBlock[], source: MessageSource, ): Promise -Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [ContinuableStart](../core-data-structures/subagent.md) · [ContinuableStartSpec](../core-data-structures/subagent.md) · [MessageSource](../core-data-structures/core.md) · [SendMessageResult](../core-data-structures/subagent.md) · [SessionId](../core-data-structures/core.md) - -Source: [`packages/subagent/subagent-control/src/index.ts:176`](../../packages/subagent/subagent-control/src/index.ts) - -## `ctx.subagents` — `SubagentService` - -Named provider registry and capability-checked start surface. - -```ts cordis-catalog /** * Register a provider under its name. Registration is effect-scoped and HMR * safe; removing a provider blocks new starts but does not revoke runs that @@ -2032,7 +2006,7 @@ async start(name: string, request: SubagentStartRequest): Promise /** * Resume a persisted continuable child through the named provider's * `resume` capability, with the same run lifecycle observation as - * {@link start}. The caller (the control service) has already loaded the + * {@link start}. The internal continuation manager has already loaded the * child, folded its descriptor, and authorized the parent; this method owns * only capability-checked dispatch. * @param name - the provider recorded in the child's descriptor. @@ -2042,9 +2016,9 @@ async start(name: string, request: SubagentStartRequest): Promise async resume(name: string, request: SubagentResumeRequest): Promise ``` -Types: [SubagentProvider](../core-data-structures/subagent.md) · [SubagentResumeRequest](../core-data-structures/subagent.md) · [SubagentRun](../core-data-structures/subagent.md) · [SubagentStartRequest](../core-data-structures/subagent.md) +Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [ContinuableStart](../core-data-structures/subagent.md) · [ContinuableStartSpec](../core-data-structures/subagent.md) · [MessageSource](../core-data-structures/core.md) · [SendMessageResult](../core-data-structures/subagent.md) · [SessionId](../core-data-structures/core.md) · [SubagentProvider](../core-data-structures/subagent.md) · [SubagentResumeRequest](../core-data-structures/subagent.md) · [SubagentRun](../core-data-structures/subagent.md) · [SubagentStartRequest](../core-data-structures/subagent.md) -Source: [`packages/subagent/subagent/src/index.ts:191`](../../packages/subagent/subagent/src/index.ts) +Source: [`packages/subagent/subagent/src/index.ts:206`](../../packages/subagent/subagent/src/index.ts) ## `ctx.subprocess` — `SubprocessService` (abstract seam) diff --git a/docs/core-data-structures/subagent.md b/docs/core-data-structures/subagent.md index acd0dea472..9864df25bd 100644 --- a/docs/core-data-structures/subagent.md +++ b/docs/core-data-structures/subagent.md @@ -4,9 +4,9 @@ English | [中文](subagent.zh.md) The subagent seam — an agent delegating work to a child agent. Like [bash](bash.md) it is **one optional capability**, not part of the agent-loop spine, so its vocabulary lives here rather than in [core.md](core.md). But it differs from every other seam on one axis: **multiple provider implementations coexist** in one context, registered by name (`ctx.subagents`), where bash allows only one executor. The registry shape mirrors the [LLM adapter registry](llm-streaming.md), not the single-service bash executor. -Interface: [dsh-subagent](../../packages/subagent/subagent) (`ctx.subagents` + the vocabulary below). Implementations are sibling packages (`dsh-subagent-spawn`, `-fork`, `-acp`); the model-facing consumers are [dsh-tool-subagent](../../packages/subagent/tool-subagent) (per-provider delegation) and [dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control) (the global `send_message`). Continuable-child orchestration lives on `ctx.subagentControl` in [dsh-subagent-control](../../packages/subagent/subagent-control). The proposals and rationale: [the subagent Agent Note](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md) and [the continuable background subagents Agent Note](../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md). +Interface: [dsh-subagent](../../packages/subagent/subagent) (`ctx.subagents` + the vocabulary below). Implementations are sibling packages (`dsh-subagent-spawn`, `-fork`, `-acp`); the model-facing consumers are [dsh-tool-subagent](../../packages/subagent/tool-subagent) (per-provider delegation) and [dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control) (the optional global `send_message`). The same `ctx.subagents` service owns continuable-child orchestration through an internal Task-backed manager. The rationale lives in [the subagent Agent Note](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md), [the continuable background subagents Agent Note](../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md), and [the merged-service Agent Note](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md). -Sources: [`packages/subagent/subagent/src/types.ts`](../../packages/subagent/subagent/src/types.ts) and [`packages/subagent/subagent-control/src/index.ts`](../../packages/subagent/subagent-control/src/index.ts) +Sources: [`packages/subagent/subagent/src/types.ts`](../../packages/subagent/subagent/src/types.ts), [`packages/subagent/subagent/src/index.ts`](../../packages/subagent/subagent/src/index.ts), and [`packages/subagent/subagent/src/continuation.ts`](../../packages/subagent/subagent/src/continuation.ts) ## Two kinds of capability, discovered two ways @@ -90,7 +90,7 @@ interface SubagentStartRequest { */ readonly persona?: string /** - * Continuable-child intent, resolved by the control service before start. + * Continuable-child intent, resolved by `ctx.subagents` before start. * The provider MUST publish exactly `sessionId` as the child identity * instead of allocating one internally, and MUST append the snapshotted * `descriptor` as the child's turn-enclosed `subagent/descriptor` event @@ -105,7 +105,7 @@ interface SubagentStartRequest { ## Continuable children: `SubagentContinuation` and `SubagentResumeRequest` -A **continuable background subagent** is a durable child session with a series of Task-backed activations. `ctx.subagentControl` (`SubagentControlService` in [dsh-subagent-control](../../packages/subagent/subagent-control)) allocates the stable child id, snapshots the versioned `subagent/descriptor` payload, and passes both through the resolved start request; the provider publishes exactly that id and appends the descriptor inside the child's first turn. On follow-up, the control service loads the persisted child, authorizes the recorded `parentSession` as the direct parent, folds the descriptor, and dispatches a fully resolved resume request through `SubagentService.resume()` to `SubagentProvider.resume()`. The seam stays Task- and persistence-agnostic — descriptor lookup and Task association live only in the control service. `startContinuable()` returns a `ContinuableStart` (both identities), and `sendMessage()` returns a `SendMessageResult` reporting whether the message `steered` the running activation's existing Task or `started` a fresh one. Every sender supplies a `MessageSource`; the model-facing tool uses `CoordinatorMessageSource`, while a human adapter uses `{ kind: 'user' }`. Both project to a user-role model message, but the durable source remains distinct for policy and title consumers. +A **continuable background subagent** is a durable child session with a series of Task-backed activations. `SubagentService.startContinuable()` allocates the stable child id, snapshots the versioned `subagent/descriptor` payload, and passes both through the resolved start request; the provider publishes exactly that id and appends the descriptor inside the child's first turn. `SubagentService.sendMessage()` loads and authorizes a stopped child before dispatching a fully resolved resume request through the raw `resume()` operation, or steers its live activation. An internal manager owns descriptor lookup and Task association only while `ctx.tasks` and `ctx.agents` exist; persistence is required per continuation operation, not to load the provider registry. `startContinuable()` returns both identities, while `sendMessage()` reports whether the message `steered` the existing Task or `started` a fresh one. Every sender supplies a `MessageSource`; the optional model-facing tool uses `CoordinatorMessageSource`, while a human adapter uses `{ kind: 'user' }`. ```ts type-equiv /** Attribution for a model coordinator's follow-up to one of its children. */ @@ -119,10 +119,10 @@ interface CoordinatorMessageSource { ```ts type-equiv /** * The resolved continuable-child identity and durable composition record a - * control-service caller attaches to a start request. + * continuation caller attaches to a start request. */ interface SubagentContinuation { - /** Control-allocated stable child session id, published verbatim. */ + /** Service-allocated stable child session id, published verbatim. */ readonly sessionId: SessionId /** Snapshotted descriptor persisted in the child log for cold resume. */ readonly descriptor: SubagentDescriptorData @@ -132,7 +132,7 @@ interface SubagentContinuation { ```ts type-equiv /** * What a caller asks for when resuming a persisted continuable child. The - * control service loads the child log, folds and authorizes its descriptor, + * continuation manager loads the child log, folds and authorizes its descriptor, * and passes this fully resolved request to * {@link SubagentService.resume}, which dispatches to * {@link SubagentProvider.resume}. The provider reconstructs the declared diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 7fc4f549f2..204bae369d 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -41,10 +41,10 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `settings/document-updated` | `emit` | [`packages/settings/settings/src/index.ts:150`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` | | `settings/updated` | `emit` | [`packages/settings/settings/src/index.ts:137`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) | | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:188`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | [`tui`](../packages/ui/tui) | -| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:150`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`jsonrpc`](../packages/ui/jsonrpc), [`subagent`](../packages/subagent/subagent) | -| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:124`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:130`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:141`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`subagent`](../packages/subagent/subagent) | +| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:165`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`jsonrpc`](../packages/ui/jsonrpc), [`subagent`](../packages/subagent/subagent) | +| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:139`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:145`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:156`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`subagent`](../packages/subagent/subagent) | | `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:29`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`system-prompt`](../packages/core/system-prompt) | | `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:35`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - | | `telemetry/record` | `waterfall` | [`packages/telemetry/session-telemetry/src/index.ts:41`](../packages/telemetry/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/telemetry/session-telemetry) (`waterfall`) | - | diff --git a/docs/module-graph.md b/docs/module-graph.md index d880453727..3e6889c80e 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -65,7 +65,6 @@ flowchart TD subgraph group_subagent["packages/subagent"] pkg_subagent["subagent"] pkg_subagent_acp["subagent-acp"] - pkg_subagent_control["subagent-control"] pkg_subagent_dsh_sdk["subagent-dsh-sdk"] pkg_subagent_fork["subagent-fork"] pkg_subagent_inprocess["subagent-inprocess"] @@ -740,6 +739,8 @@ flowchart TD pkg_subagent --> pkg_llm pkg_subagent --> pkg_scope pkg_subagent --> pkg_session + pkg_subagent --> pkg_session_persistence + pkg_subagent --> pkg_tasks pkg_subagent --> pkg_tools pkg_tool_web --> pkg_invariants pkg_tool_web --> pkg_llm @@ -892,13 +893,6 @@ flowchart TD pkg_subagent_acp --> pkg_session pkg_subagent_acp --> pkg_subagent pkg_subagent_acp --> pkg_subprocess - pkg_subagent_control --> pkg_agent - pkg_subagent_control --> pkg_invariants - pkg_subagent_control --> pkg_llm - pkg_subagent_control --> pkg_session - pkg_subagent_control --> pkg_session_persistence - pkg_subagent_control --> pkg_subagent - pkg_subagent_control --> pkg_tasks pkg_subagent_inprocess --> pkg_agent pkg_subagent_inprocess --> pkg_invariants pkg_subagent_inprocess --> pkg_llm @@ -908,6 +902,17 @@ flowchart TD pkg_subagent_inprocess --> pkg_system_prompt pkg_subagent_inprocess --> pkg_tools pkg_subagent_inprocess --> pkg_user_approval + pkg_tool_subagent --> pkg_agent + pkg_tool_subagent --> pkg_invariants + pkg_tool_subagent --> pkg_llm + pkg_tool_subagent --> pkg_subagent + pkg_tool_subagent --> pkg_tasks + pkg_tool_subagent --> pkg_tools + pkg_tool_subagent_control --> pkg_invariants + pkg_tool_subagent_control --> pkg_llm + pkg_tool_subagent_control --> pkg_session + pkg_tool_subagent_control --> pkg_subagent + pkg_tool_subagent_control --> pkg_tools pkg_repository_plugin --> pkg_invariants pkg_repository_plugin --> pkg_mcp_client pkg_repository_plugin --> pkg_paths @@ -1014,18 +1019,6 @@ flowchart TD pkg_subagent_spawn --> pkg_invariants pkg_subagent_spawn --> pkg_subagent pkg_subagent_spawn --> pkg_subagent_inprocess - pkg_tool_subagent --> pkg_agent - pkg_tool_subagent --> pkg_invariants - pkg_tool_subagent --> pkg_llm - pkg_tool_subagent --> pkg_subagent - pkg_tool_subagent --> pkg_subagent_control - pkg_tool_subagent --> pkg_tasks - pkg_tool_subagent --> pkg_tools - pkg_tool_subagent_control --> pkg_invariants - pkg_tool_subagent_control --> pkg_llm - pkg_tool_subagent_control --> pkg_session - pkg_tool_subagent_control --> pkg_subagent_control - pkg_tool_subagent_control --> pkg_tools pkg_jsonrpc --> pkg_agent pkg_jsonrpc --> pkg_invariants pkg_jsonrpc --> pkg_llm @@ -1199,7 +1192,7 @@ flowchart TD | [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) | | [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) | -| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | +| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) | | [`tool-web`](../packages/web/tool-web) | `web` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) | | [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) | | [`timeout-policy`](../packages/timeout/timeout-policy) | `timeout` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | @@ -1226,8 +1219,9 @@ flowchart TD | [`tool-tasks`](../packages/tasks/tool-tasks) | `tasks` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) | | [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) | -| [`subagent-control`](../packages/subagent/subagent-control) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`subagent`](../packages/subagent/subagent), [`tasks`](../packages/tasks/tasks) | | [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) | +| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) | +| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | | [`repository-plugin`](../packages/cordis/repository-plugin) | `cordis` | [`invariants`](../packages/support/invariants), [`mcp-client`](../packages/mcp/mcp-client), [`paths`](../packages/util/paths), [`skill-local`](../packages/skill/skill-local) | | [`hooks-claude`](../packages/hooks/hooks-claude) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | | [`tui`](../packages/ui/tui) | `ui` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`commands`](../packages/ui/commands), [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-reference`](../packages/context/session-reference), [`session-title`](../packages/session-title/session-title), [`skill`](../packages/skill/skill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) | @@ -1240,8 +1234,6 @@ flowchart TD | [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`subagent-fork`](../packages/subagent/subagent-fork) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | | [`subagent-spawn`](../packages/subagent/subagent-spawn) | `subagent` | [`invariants`](../packages/support/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | -| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`subagent-control`](../packages/subagent/subagent-control), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) | -| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent-control`](../packages/subagent/subagent-control), [`tools`](../packages/core/tools) | | [`jsonrpc`](../packages/ui/jsonrpc) | `ui` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/sdk-protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | | [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/acp/acp), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/ui/app-boot), [`invariants`](../packages/support/invariants), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) | | [`cli-demo`](../packages/examples/cli-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/ui/app-boot), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) | diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index 0a316bc636..87e9a2a526 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -31,7 +31,7 @@ This table connects model-visible tool names to the plugin package and service s | `@deepseek-ai/dsh-tool-skill` | `skill` | `ctx.tools`, `ctx.agents`, `ctx.skills` | `tool/call`, `tool/result`, `user/message replacement catalogs via agent.inject()` | - | - | | `@deepseek-ai/dsh-tool-session-query` | `session_event_read`, `session_event_search`, `session_event_trace`, `session_search`, `session_trace` | `ctx.tools`, `ctx.systemPrompt`, `ctx.sessionQuery`, `a calling Agent for workspace authority` | `tool/call`, `tool/result` | - | The five read-only tools hide provider cursors and authorize every result from the immutable calling agent session. The package is opt-in; compositions that need enforced deadlines or bounded inline output also mount the generic timeout or spill policies. | | `@deepseek-ai/dsh-tool-subagent` | `subagent` | `ctx.tools`, `ctx.subagents` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped example agents load this package once per subagent backend, so the model additionally sees `subagent_fork` (bound to the fork backend) with an identical schema — see `apps/cli/config/base.cordis.yml` and `examples/acp-agent/cordis.yml`. | -| `@deepseek-ai/dsh-tool-subagent-control` | `send_message` | `ctx.tools`, `ctx.subagentControl` | `tool/call`, `tool/result`, `child session events through the control service` | - | The one globally named follow-up tool over continuable background subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` once. | +| `@deepseek-ai/dsh-tool-subagent-control` | `send_message` | `ctx.tools`, `ctx.subagents` | `tool/call`, `tool/result`, `child session events through ctx.subagents` | - | The one globally named follow-up tool over continuable background subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` once. | | `@deepseek-ai/dsh-tool-tasks` | `task_kill`, `task_list`, `task_output` | `ctx.tools`, `ctx.tasks`, `ctx.systemPrompt` | `tool/call`, `tool/result`, `user/message via agent.inject() for background completion notices` | - | The kind-agnostic background-task control surface: background bash commands, PTY sends, and subagents are read, listed, and killed through the same three tools. Loading the plugin attaches the control surface that arms producers' `ctx.tasks.start()`. | | `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`, `owning Agent session` | `tool/call`, `todo/write`, `tool/result` | - | todo_write is session-owned state; UIs render the latest todo/write event as a checklist. | | `@deepseek-ai/dsh-tool-workflow` | `workflow` | `ctx.tools`, `ctx.workflows`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents the script children)` | `tool/call`, `tool/result` | - | - | @@ -1117,7 +1117,7 @@ The five read-only tools hide provider cursors and authorize every result from t ### `subagent` -Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a continuable background subagent: you receive its subagent id and a task id; collect the result with `task_output`, stop it with `task_kill`, and send follow-up messages with `send_message`. +Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`. ```json { @@ -1133,7 +1133,7 @@ Delegate a self-contained task to a subagent (a separate agent that works in its }, "run_in_background": { "type": "boolean", - "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output, stop with task_kill, follow up with send_message." + "description": "Run as a background task and return its id; collect with task_output or stop with task_kill." } }, "required": [ diff --git a/examples/acp-agent/composition.md b/examples/acp-agent/composition.md index 22adf09555..9524184414 100644 --- a/examples/acp-agent/composition.md +++ b/examples/acp-agent/composition.md @@ -39,8 +39,6 @@ flowchart LR cfg --> plugin_acp_subagent_spawn plugin_acp_subagent_fork["subagent-fork
@deepseek-ai/dsh-subagent-fork"] cfg --> plugin_acp_subagent_fork - plugin_acp_subagent_control["subagent-control
@deepseek-ai/dsh-subagent-control"] - cfg --> plugin_acp_subagent_control plugin_acp_tool_subagent_control["tool-subagent-control
@deepseek-ai/dsh-tool-subagent-control"] cfg --> plugin_acp_tool_subagent_control plugin_acp_tool_subagent["tool-subagent
@deepseek-ai/dsh-tool-subagent"] @@ -83,7 +81,6 @@ flowchart LR | `subagent` | `@deepseek-ai/dsh-subagent` | | `subagent-spawn` | `@deepseek-ai/dsh-subagent-spawn` | | `subagent-fork` | `@deepseek-ai/dsh-subagent-fork` | -| `subagent-control` | `@deepseek-ai/dsh-subagent-control` | | `tool-subagent-control` | `@deepseek-ai/dsh-tool-subagent-control` | | `tool-subagent` | `@deepseek-ai/dsh-tool-subagent` | | `tool-subagent-fork` | `@deepseek-ai/dsh-tool-subagent` | diff --git a/examples/acp-agent/cordis.yml b/examples/acp-agent/cordis.yml index e45c56c92d..76cac9cbb8 100644 --- a/examples/acp-agent/cordis.yml +++ b/examples/acp-agent/cordis.yml @@ -96,12 +96,8 @@ config: providerName: fork -# Continuable background children: the control service owns durable child ids -# and Task-backed activations; the separately loaded control tool registers the -# one global `send_message` shared by both delegation tools. -- id: subagent-control - name: '@deepseek-ai/dsh-subagent-control' - +# Continuable background children are selected per delegation tool. The +# separately loaded follow-up tool registers the one global `send_message`. - id: tool-subagent-control name: '@deepseek-ai/dsh-tool-subagent-control' @@ -110,6 +106,7 @@ config: provider: spawn toolName: subagent + backgroundMode: continuable maxDepth: 1 - id: tool-subagent-fork @@ -117,6 +114,7 @@ config: provider: fork toolName: subagent_fork + backgroundMode: continuable maxDepth: 1 diff --git a/examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md index 7df756ee99..e386a25eff 100644 --- a/examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md @@ -122,22 +122,22 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a continuable background subagent: you receive its subagent id and a task id; collect the result with `task_output`, stop it with `task_kill`, and send follow-up messages with `send_message`. */ + /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a continuable background subagent: you receive its stable subagent id and current task id; collect the result with `task_output` and stop it with `task_kill`. */ subagent: { /** A short (3-5 word) description of the delegated task, for display. */ description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** Run as a continuable background subagent and return its subagent and task ids; collect with task_output, stop with task_kill, follow up with send_message. */ + /** Run as a continuable background subagent and return its subagent and task ids; collect with task_output or stop with task_kill. */ run_in_background?: boolean; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a continuable background subagent: you receive its subagent id and a task id; collect the result with `task_output`, stop it with `task_kill`, and send follow-up messages with `send_message`. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a continuable background subagent: you receive its stable subagent id and current task id; collect the result with `task_output` and stop it with `task_kill`. */ subagent_fork: { /** A short (3-5 word) description of the delegated task, for display. */ description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; - /** Run as a continuable background subagent and return its subagent and task ids; collect with task_output, stop with task_kill, follow up with send_message. */ + /** Run as a continuable background subagent and return its subagent and task ids; collect with task_output or stop with task_kill. */ run_in_background?: boolean; } & Record; /** Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops. */ diff --git a/examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json index 5f8e31fe9b..00ae670288 100644 --- a/examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json @@ -276,7 +276,7 @@ }, { "name": "subagent", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a continuable background subagent: you receive its subagent id and a task id; collect the result with `task_output`, stop it with `task_kill`, and send follow-up messages with `send_message`.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a continuable background subagent: you receive its stable subagent id and current task id; collect the result with `task_output` and stop it with `task_kill`.", "parameters": { "type": "object", "properties": { @@ -290,7 +290,7 @@ }, "run_in_background": { "type": "boolean", - "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output, stop with task_kill, follow up with send_message." + "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output or stop with task_kill." } }, "required": [ @@ -301,7 +301,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a continuable background subagent: you receive its subagent id and a task id; collect the result with `task_output`, stop it with `task_kill`, and send follow-up messages with `send_message`.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a continuable background subagent: you receive its stable subagent id and current task id; collect the result with `task_output` and stop it with `task_kill`.", "parameters": { "type": "object", "properties": { @@ -315,7 +315,7 @@ }, "run_in_background": { "type": "boolean", - "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output, stop with task_kill, follow up with send_message." + "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output or stop with task_kill." } }, "required": [ diff --git a/examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json index 1a7e813d7c..6d052ead19 100644 --- a/examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json @@ -219,7 +219,7 @@ }, { "name": "subagent", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a continuable background subagent: you receive its subagent id and a task id; collect the result with `task_output`, stop it with `task_kill`, and send follow-up messages with `send_message`.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a continuable background subagent: you receive its stable subagent id and current task id; collect the result with `task_output` and stop it with `task_kill`.", "parameters": { "type": "object", "properties": { @@ -233,7 +233,7 @@ }, "run_in_background": { "type": "boolean", - "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output, stop with task_kill, follow up with send_message." + "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output or stop with task_kill." } }, "required": [ @@ -244,7 +244,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a continuable background subagent: you receive its subagent id and a task id; collect the result with `task_output`, stop it with `task_kill`, and send follow-up messages with `send_message`.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a continuable background subagent: you receive its stable subagent id and current task id; collect the result with `task_output` and stop it with `task_kill`.", "parameters": { "type": "object", "properties": { @@ -258,7 +258,7 @@ }, "run_in_background": { "type": "boolean", - "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output, stop with task_kill, follow up with send_message." + "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output or stop with task_kill." } }, "required": [ diff --git a/examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md index 31e8cdce23..15b5e8dde6 100644 --- a/examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md @@ -105,22 +105,22 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a continuable background subagent: you receive its subagent id and a task id; collect the result with `task_output`, stop it with `task_kill`, and send follow-up messages with `send_message`. */ + /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a continuable background subagent: you receive its stable subagent id and current task id; collect the result with `task_output` and stop it with `task_kill`. */ subagent: { /** A short (3-5 word) description of the delegated task, for display. */ description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** Run as a continuable background subagent and return its subagent and task ids; collect with task_output, stop with task_kill, follow up with send_message. */ + /** Run as a continuable background subagent and return its subagent and task ids; collect with task_output or stop with task_kill. */ run_in_background?: boolean; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a continuable background subagent: you receive its subagent id and a task id; collect the result with `task_output`, stop it with `task_kill`, and send follow-up messages with `send_message`. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a continuable background subagent: you receive its stable subagent id and current task id; collect the result with `task_output` and stop it with `task_kill`. */ subagent_fork: { /** A short (3-5 word) description of the delegated task, for display. */ description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; - /** Run as a continuable background subagent and return its subagent and task ids; collect with task_output, stop with task_kill, follow up with send_message. */ + /** Run as a continuable background subagent and return its subagent and task ids; collect with task_output or stop with task_kill. */ run_in_background?: boolean; } & Record; /** Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops. */ diff --git a/examples/acp-agent/tests/snapshots/lsp-definition/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/lsp-definition/tool-schemas.expected.json index 517c9b1d71..6124557f08 100644 --- a/examples/acp-agent/tests/snapshots/lsp-definition/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/lsp-definition/tool-schemas.expected.json @@ -235,7 +235,7 @@ }, { "name": "subagent", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a continuable background subagent: you receive its subagent id and a task id; collect the result with `task_output`, stop it with `task_kill`, and send follow-up messages with `send_message`.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a continuable background subagent: you receive its stable subagent id and current task id; collect the result with `task_output` and stop it with `task_kill`.", "parameters": { "type": "object", "properties": { @@ -249,7 +249,7 @@ }, "run_in_background": { "type": "boolean", - "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output, stop with task_kill, follow up with send_message." + "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output or stop with task_kill." } }, "required": [ @@ -260,7 +260,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a continuable background subagent: you receive its subagent id and a task id; collect the result with `task_output`, stop it with `task_kill`, and send follow-up messages with `send_message`.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a continuable background subagent: you receive its stable subagent id and current task id; collect the result with `task_output` and stop it with `task_kill`.", "parameters": { "type": "object", "properties": { @@ -274,7 +274,7 @@ }, "run_in_background": { "type": "boolean", - "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output, stop with task_kill, follow up with send_message." + "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output or stop with task_kill." } }, "required": [ diff --git a/examples/acp-agent/tests/snapshots/pty-tools/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/pty-tools/tool-schemas.expected.json index abc3e13256..d4c004034f 100644 --- a/examples/acp-agent/tests/snapshots/pty-tools/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/pty-tools/tool-schemas.expected.json @@ -198,7 +198,7 @@ }, { "name": "subagent", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a continuable background subagent: you receive its subagent id and a task id; collect the result with `task_output`, stop it with `task_kill`, and send follow-up messages with `send_message`.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a continuable background subagent: you receive its stable subagent id and current task id; collect the result with `task_output` and stop it with `task_kill`.", "parameters": { "type": "object", "properties": { @@ -212,7 +212,7 @@ }, "run_in_background": { "type": "boolean", - "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output, stop with task_kill, follow up with send_message." + "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output or stop with task_kill." } }, "required": [ @@ -223,7 +223,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a continuable background subagent: you receive its subagent id and a task id; collect the result with `task_output`, stop it with `task_kill`, and send follow-up messages with `send_message`.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a continuable background subagent: you receive its stable subagent id and current task id; collect the result with `task_output` and stop it with `task_kill`.", "parameters": { "type": "object", "properties": { @@ -237,7 +237,7 @@ }, "run_in_background": { "type": "boolean", - "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output, stop with task_kill, follow up with send_message." + "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output or stop with task_kill." } }, "required": [ diff --git a/examples/acp-agent/tests/snapshots/session-query-spill/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/session-query-spill/tool-schemas.expected.json index 2ac976d621..72c6b74b72 100644 --- a/examples/acp-agent/tests/snapshots/session-query-spill/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/session-query-spill/tool-schemas.expected.json @@ -402,7 +402,7 @@ }, { "name": "subagent", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a continuable background subagent: you receive its subagent id and a task id; collect the result with `task_output`, stop it with `task_kill`, and send follow-up messages with `send_message`.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a continuable background subagent: you receive its stable subagent id and current task id; collect the result with `task_output` and stop it with `task_kill`.", "parameters": { "type": "object", "properties": { @@ -416,7 +416,7 @@ }, "run_in_background": { "type": "boolean", - "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output, stop with task_kill, follow up with send_message." + "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output or stop with task_kill." } }, "required": [ @@ -427,7 +427,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a continuable background subagent: you receive its subagent id and a task id; collect the result with `task_output`, stop it with `task_kill`, and send follow-up messages with `send_message`.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a continuable background subagent: you receive its stable subagent id and current task id; collect the result with `task_output` and stop it with `task_kill`.", "parameters": { "type": "object", "properties": { @@ -441,7 +441,7 @@ }, "run_in_background": { "type": "boolean", - "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output, stop with task_kill, follow up with send_message." + "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output or stop with task_kill." } }, "required": [ diff --git a/examples/acp-agent/tests/snapshots/text-turn/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/text-turn/tool-schemas.expected.json index 47439bfdb0..bf0103bbeb 100644 --- a/examples/acp-agent/tests/snapshots/text-turn/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/text-turn/tool-schemas.expected.json @@ -198,7 +198,7 @@ }, { "name": "subagent", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a continuable background subagent: you receive its subagent id and a task id; collect the result with `task_output`, stop it with `task_kill`, and send follow-up messages with `send_message`.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a continuable background subagent: you receive its stable subagent id and current task id; collect the result with `task_output` and stop it with `task_kill`.", "parameters": { "type": "object", "properties": { @@ -212,7 +212,7 @@ }, "run_in_background": { "type": "boolean", - "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output, stop with task_kill, follow up with send_message." + "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output or stop with task_kill." } }, "required": [ @@ -223,7 +223,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a continuable background subagent: you receive its subagent id and a task id; collect the result with `task_output`, stop it with `task_kill`, and send follow-up messages with `send_message`.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a continuable background subagent: you receive its stable subagent id and current task id; collect the result with `task_output` and stop it with `task_kill`.", "parameters": { "type": "object", "properties": { @@ -237,7 +237,7 @@ }, "run_in_background": { "type": "boolean", - "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output, stop with task_kill, follow up with send_message." + "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output or stop with task_kill." } }, "required": [ diff --git a/examples/acp-agent/tests/snapshots/web-fetch/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/web-fetch/tool-schemas.expected.json index d1a60f6f92..d143e9d82a 100644 --- a/examples/acp-agent/tests/snapshots/web-fetch/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/web-fetch/tool-schemas.expected.json @@ -198,7 +198,7 @@ }, { "name": "subagent", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a continuable background subagent: you receive its subagent id and a task id; collect the result with `task_output`, stop it with `task_kill`, and send follow-up messages with `send_message`.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to start a continuable background subagent: you receive its stable subagent id and current task id; collect the result with `task_output` and stop it with `task_kill`.", "parameters": { "type": "object", "properties": { @@ -212,7 +212,7 @@ }, "run_in_background": { "type": "boolean", - "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output, stop with task_kill, follow up with send_message." + "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output or stop with task_kill." } }, "required": [ @@ -223,7 +223,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a continuable background subagent: you receive its subagent id and a task id; collect the result with `task_output`, stop it with `task_kill`, and send follow-up messages with `send_message`.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to start a continuable background subagent: you receive its stable subagent id and current task id; collect the result with `task_output` and stop it with `task_kill`.", "parameters": { "type": "object", "properties": { @@ -237,7 +237,7 @@ }, "run_in_background": { "type": "boolean", - "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output, stop with task_kill, follow up with send_message." + "description": "Run as a continuable background subagent and return its subagent and task ids; collect with task_output or stop with task_kill." } }, "required": [ diff --git a/examples/headless-agent/composition.md b/examples/headless-agent/composition.md index ecf343a264..0a3da81e04 100644 --- a/examples/headless-agent/composition.md +++ b/examples/headless-agent/composition.md @@ -37,8 +37,6 @@ flowchart LR cfg --> plugin_headless_subagent_spawn plugin_headless_subagent_fork["subagent-fork
@deepseek-ai/dsh-subagent-fork"] cfg --> plugin_headless_subagent_fork - plugin_headless_subagent_control["subagent-control
@deepseek-ai/dsh-subagent-control"] - cfg --> plugin_headless_subagent_control plugin_headless_tool_subagent_control["tool-subagent-control
@deepseek-ai/dsh-tool-subagent-control"] cfg --> plugin_headless_tool_subagent_control plugin_headless_tool_subagent["tool-subagent
@deepseek-ai/dsh-tool-subagent"] @@ -74,7 +72,6 @@ flowchart LR | `subagent` | `@deepseek-ai/dsh-subagent` | | `subagent-spawn` | `@deepseek-ai/dsh-subagent-spawn` | | `subagent-fork` | `@deepseek-ai/dsh-subagent-fork` | -| `subagent-control` | `@deepseek-ai/dsh-subagent-control` | | `tool-subagent-control` | `@deepseek-ai/dsh-tool-subagent-control` | | `tool-subagent` | `@deepseek-ai/dsh-tool-subagent` | | `tool-subagent-fork` | `@deepseek-ai/dsh-tool-subagent` | diff --git a/examples/headless-agent/cordis.yml b/examples/headless-agent/cordis.yml index 73673aee8d..4caf63672f 100644 --- a/examples/headless-agent/cordis.yml +++ b/examples/headless-agent/cordis.yml @@ -86,12 +86,8 @@ config: providerName: fork -# Continuable background children: the control service owns durable child ids -# and Task-backed activations; the control tool registers the one global -# `send_message` shared by both delegation tools. -- id: subagent-control - name: '@deepseek-ai/dsh-subagent-control' - +# Continuable background children are selected per delegation tool. The +# separately loaded follow-up tool registers the one global `send_message`. - id: tool-subagent-control name: '@deepseek-ai/dsh-tool-subagent-control' @@ -100,6 +96,7 @@ config: provider: spawn toolName: subagent + backgroundMode: continuable maxDepth: 1 - id: tool-subagent-fork @@ -107,6 +104,7 @@ config: provider: fork toolName: subagent_fork + backgroundMode: continuable maxDepth: 1 # The worker-thread workflow engine fans a model-written JavaScript script's diff --git a/examples/package.json b/examples/package.json index fb105d2ad6..bcbb4e21f0 100644 --- a/examples/package.json +++ b/examples/package.json @@ -63,7 +63,6 @@ "@deepseek-ai/dsh-spill-policy": "workspace:*", "@deepseek-ai/dsh-subagent": "workspace:*", "@deepseek-ai/dsh-subagent-acp": "workspace:*", - "@deepseek-ai/dsh-subagent-control": "workspace:*", "@deepseek-ai/dsh-subagent-dsh-sdk": "workspace:*", "@deepseek-ai/dsh-subagent-fork": "workspace:*", "@deepseek-ai/dsh-subagent-spawn": "workspace:*", diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index 43818cd6b7..19f7707262 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -881,23 +881,17 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ ], }, { - key: 'subagentControl', - summary: 'The continuable-subagent orchestration service.', + key: 'subagents', + summary: 'Named provider registry with raw and Task-backed continuation operations.', methods: [ { signature: 'startContinuable(spec: ContinuableStartSpec): ContinuableStart', - jsDoc: '/**\n * Start a continuable background child: allocate its stable session id,\n * snapshot its durable descriptor, and register the initial activation\'s\n * Task. A synchronous validation failure (a non-JSON descriptor input,\n * missing persistence, Task preflight) throws without creating a Task; the\n * method otherwise returns both identities immediately, without waiting for\n * child publication or descriptor durability. Asynchronous startup failure\n * settles the returned Task as `failed` (or `killed` when cancelled) after\n * any published run is disposed, which can leave an unmaterialized child id\n * that later by-id operations report as unavailable.\n * @param spec - provider, Task label, and the delegation request.\n * @returns the stable child id and the initial activation\'s Task id.\n */', + jsDoc: '/**\n * Start one durable continuable child through a Task-backed initial\n * activation.\n * @param spec - provider, Task label, and delegation request.\n * @returns the stable child id and initial activation Task id.\n */', }, { - signature: 'async sendMessage( parent: Agent, childId: SessionId, message: ContentBlock[], source: MessageSource, ): Promise', - jsDoc: '/**\n * Deliver one message to a known continuable child: steer its running\n * activation, or cold-resume the durable session into a fresh Task-backed\n * activation. The two routes are reported distinctly so timing-dependent\n * routing is observable. Rejection means the message was NOT delivered — in\n * particular, losing a race with Task settlement does not fall through to\n * cold resume within the same call; a later retry after Task terminal may\n * start the next activation. The started Task owns descriptor lookup and\n * direct-parent authorization (its AbortSignal exists before that lookup),\n * so an unknown, foreign, or descriptor-less child settles the started Task\n * as `failed` with a detail reporting the id as unavailable.\n * @param parent - the live parent agent sending the message (model tool or\n * human adapter); Task access is authorized by its session id.\n * @param childId - the stable child session id.\n * @param message - the user-role content to deliver.\n * @param source - caller-supplied attribution retained across either route.\n * @returns whether the message `steered` the existing Task or `started` a new one.\n */', + signature: 'sendMessage( parent: Agent, childId: SessionId, message: ContentBlock[], source: MessageSource, ): Promise', + jsDoc: '/**\n * Deliver a message to a continuable child by steering its live activation\n * or cold-resuming a fresh Task-backed activation.\n * @param parent - live direct parent authorizing the operation.\n * @param childId - durable child session id.\n * @param message - user-role content to deliver.\n * @param source - durable caller attribution.\n * @returns the existing steered Task or newly started Task.\n */', }, - ], - }, - { - key: 'subagents', - summary: 'Named provider registry and capability-checked start surface.', - methods: [ { signature: 'registerProvider(provider: SubagentProvider): () => void', jsDoc: '/**\n * Register a provider under its name. Registration is effect-scoped and HMR\n * safe; removing a provider blocks new starts but does not revoke runs that\n * were already returned to their holders.\n * @param provider - the trusted provider implementation.\n * @returns the exact Cordis effect disposer.\n */', @@ -916,7 +910,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ }, { signature: 'async resume(name: string, request: SubagentResumeRequest): Promise', - jsDoc: '/**\n * Resume a persisted continuable child through the named provider\'s\n * `resume` capability, with the same run lifecycle observation as\n * {@link start}. The caller (the control service) has already loaded the\n * child, folded its descriptor, and authorized the parent; this method owns\n * only capability-checked dispatch.\n * @param name - the provider recorded in the child\'s descriptor.\n * @param request - the fully resolved resume request.\n * @returns the fresh holder-owned run for the resumed activation.\n */', + jsDoc: '/**\n * Resume a persisted continuable child through the named provider\'s\n * `resume` capability, with the same run lifecycle observation as\n * {@link start}. The internal continuation manager has already loaded the\n * child, folded its descriptor, and authorized the parent; this method owns\n * only capability-checked dispatch.\n * @param name - the provider recorded in the child\'s descriptor.\n * @param request - the fully resolved resume request.\n * @returns the fresh holder-owned run for the resumed activation.\n */', }, ], }, diff --git a/packages/sdk/helper/src/features/builtin/index.ts b/packages/sdk/helper/src/features/builtin/index.ts index e72bb72c8a..139eae3bde 100644 --- a/packages/sdk/helper/src/features/builtin/index.ts +++ b/packages/sdk/helper/src/features/builtin/index.ts @@ -209,13 +209,12 @@ config: id: 'subagent', summary: 'Delegate work to child agents', mode: 'multiple', - // The control pair rides every resumable in-process option: background - // delegation on spawn/fork is continuable and advertises send_message. + // In-process options select continuable background delegation; the + // follow-up adapter remains an independently loadable global tool. baseResources: [ { kind: 'npm-cordis-config-entry', id: 'tasks', package: '@deepseek-ai/dsh-tasks' }, { kind: 'npm-cordis-config-entry', id: 'tool-tasks', package: '@deepseek-ai/dsh-tool-tasks' }, { kind: 'npm-cordis-config-entry', id: 'subagent', package: '@deepseek-ai/dsh-subagent' }, - { kind: 'npm-cordis-config-entry', id: 'subagent-control', package: '@deepseek-ai/dsh-subagent-control' }, { kind: 'npm-cordis-config-entry', id: 'tool-subagent-control', package: '@deepseek-ai/dsh-tool-subagent-control' }, ], options: [ @@ -229,7 +228,7 @@ config: kind: 'npm-cordis-config-entry', id: 'tool-subagent', package: '@deepseek-ai/dsh-tool-subagent', - config: { provider: 'spawn' } satisfies ToolSubagentConfig, + config: { provider: 'spawn', backgroundMode: 'continuable' } satisfies ToolSubagentConfig, }, ], }, @@ -242,7 +241,11 @@ config: kind: 'npm-cordis-config-entry', id: 'tool-subagent-fork', package: '@deepseek-ai/dsh-tool-subagent', - config: { provider: 'fork', toolName: 'subagent_fork' } satisfies ToolSubagentConfig, + config: { + provider: 'fork', + toolName: 'subagent_fork', + backgroundMode: 'continuable', + } satisfies ToolSubagentConfig, }, ], }, diff --git a/packages/subagent/README.i18n.yaml b/packages/subagent/README.i18n.yaml index 0491b589c6..bead24d34c 100644 --- a/packages/subagent/README.i18n.yaml +++ b/packages/subagent/README.i18n.yaml @@ -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/README.md -README.md: 438907ea7de41842f900b050385f15feac7cc272 -README.zh.md: 87911216bc4e6b5f75e17ca2c58818725f66e7ec +README.md: a195ecbaeb24cb63af8cdd4ac872bb6a2fc97d46 +README.zh.md: b9965030a38b603f7c03d98d6b8021acbeb47fda diff --git a/packages/subagent/README.md b/packages/subagent/README.md index 438907ea7d..a195ecbaeb 100644 --- a/packages/subagent/README.md +++ b/packages/subagent/README.md @@ -6,16 +6,15 @@ The subagent seam: an agent delegating work to a child agent. Like the [bash](.. | Package | Role | ctx key | |---|---|---| -| `subagent/` | Abstract subagent seam: named-provider registry + vocabulary + the durable child descriptor | `ctx.subagents` | +| `subagent/` | Subagent service: named-provider registry, vocabulary, durable descriptor, and optional Task-backed continuation orchestration | `ctx.subagents` | | `subagent-inprocess/` | Shared in-process run driver (no provider; one cleanup effect per run) | — | | `subagent-spawn/` | In-process backend: a fresh child agent, with cold resume | (registers on `ctx.subagents`) | | `subagent-fork/` | In-process backend: a child seeded with the parent's completed-turn prefix, with cold resume | (registers on `ctx.subagents`) | | `subagent-acp/` | Out-of-process backend: a child agent in a spawned subprocess, driven over ACP (one-shot) | (registers on `ctx.subagents`) | | `subagent-dsh-sdk/` | Out-of-process backend: a child harness runtime in a spawned subprocess, driven over stdio JSON-RPC through the TypeScript SDK client | (registers on `ctx.subagents`) | -| `subagent-control/` | Continuable-child orchestration: stable ids, descriptor lookup, Task-backed activation, steer-or-resume routing | `ctx.subagentControl` | | `tool-subagent/` | Model-facing `subagent` delegation tool over `ctx.subagents` | (registers on `ctx.tools`) | -| `tool-subagent-control/` | The one globally named `send_message` follow-up tool over `ctx.subagentControl` | (registers on `ctx.tools`) | +| `tool-subagent-control/` | The optional, globally named `send_message` follow-up tool over `ctx.subagents` | (registers on `ctx.tools`) | -The interface lives at `subagent/subagent/`. The in-process `subagent-spawn` / `subagent-fork` backends share the `subagent-inprocess` driver (a library with no provider of its own — both depend on it, neither on the other), and the out-of-process `subagent-acp` / `subagent-dsh-sdk` backends spawn their children through the [`subprocess/`](../subprocess/README.md) seam (the shared credential scrub, tree-scoped teardown, and dispose ladder). `subagent-control` sits above the seam: it binds one durable child session to a series of disposable Task-backed activations, and both model tools and human-facing adapters route through its one contract. Tests replace only the child boundary with package-local fixtures. +The interface and continuation orchestration live at `subagent/subagent/`. Raw `start` / `resume` dispatch stays independent of Tasks and persistence; an internal manager binds durable child sessions to disposable Task-backed activations only while the Task and Agent services are present, and resolves persistence only when a continuation operation runs. The in-process `subagent-spawn` / `subagent-fork` backends share the `subagent-inprocess` driver (a library with no provider of its own — both depend on it, neither on the other), and the out-of-process `subagent-acp` / `subagent-dsh-sdk` backends spawn their children through the [`subprocess/`](../subprocess/README.md) seam (the shared credential scrub, tree-scoped teardown, and dispose ladder). Tests replace only the child boundary with package-local fixtures. -The proposals and design rationale: [.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md) and [.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md](../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md). +The design rationale: [.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md), [.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md](../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md), and [.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md). diff --git a/packages/subagent/README.zh.md b/packages/subagent/README.zh.md index 87911216bc..b9965030a3 100644 --- a/packages/subagent/README.zh.md +++ b/packages/subagent/README.zh.md @@ -6,16 +6,15 @@ subagent(子 agent)seam 允许 agent(智能体)把工作委派给子 age | 包(package) | 角色 | ctx 键 | |---|---|---| -| `subagent/` | 抽象 subagent seam:具名提供方注册表、词汇与持久化子 agent 描述符 | `ctx.subagents` | +| `subagent/` | Subagent 服务:具名提供方注册表、词汇、持久化描述符与可选的由 Task 支撑的继续执行编排 | `ctx.subagents` | | `subagent-inprocess/` | 共享进程内运行驱动器(不含提供方;每次运行使用一个清理 effect) | 无 | | `subagent-spawn/` | 进程内后端:支持冷恢复的全新子 agent | (注册到 `ctx.subagents`) | | `subagent-fork/` | 进程内后端:以父 agent 已完成轮次的前缀作为初始内容、支持冷恢复的子 agent | (注册到 `ctx.subagents`) | | `subagent-acp/` | 进程外后端:在 spawn 的子进程中运行并通过 ACP(Agent Client Protocol)驱动的一次性子 agent | (注册到 `ctx.subagents`) | | `subagent-dsh-sdk/` | 进程外后端:在 spawn 的子进程中运行的子 harness 运行时,经 TypeScript SDK 客户端走 stdio JSON-RPC 驱动 | (注册到 `ctx.subagents`) | -| `subagent-control/` | 可继续子 agent 编排:稳定 ID、描述符查找、由 Task 支撑的 activation,以及 steer 或恢复路由 | `ctx.subagentControl` | | `tool-subagent/` | 面向模型的 `subagent` 委派工具,基于 `ctx.subagents` | (注册到 `ctx.tools`) | -| `tool-subagent-control/` | 基于 `ctx.subagentControl`、全局名称唯一的 `send_message` 后续消息工具 | (注册到 `ctx.tools`) | +| `tool-subagent-control/` | 基于 `ctx.subagents`、可选且全局名称唯一的 `send_message` 后续消息工具 | (注册到 `ctx.tools`) | -接口位于 `subagent/subagent/`。进程内 `subagent-spawn` / `subagent-fork` 后端共享 `subagent-inprocess` 驱动器(一个自身不含提供方的库:两者都依赖它,彼此不依赖),进程外 `subagent-acp` / `subagent-dsh-sdk` 后端则经由 [`subprocess/`](../subprocess/README.md) seam spawn 其子进程(共享的凭据清除、以进程树为范围的拆卸、dispose(资源释放)阶梯)。`subagent-control` 位于该 seam 之上:它把一个持久化子会话绑定到一系列可 dispose、由 Task 支撑的 activation,模型工具和面向人的适配器都通过这份统一契约进行路由。测试只用包内 fixture(测试前置数据)替换子 agent 边界。 +接口和继续执行编排位于 `subagent/subagent/`。原始 `start` / `resume` 分发仍与 Task 和持久化无关;只有在 Task 与 Agent 服务存在时,内部管理器才会把持久化子会话绑定到可 dispose、由 Task 支撑的 activation,并且只在继续执行操作运行时解析持久化服务。进程内 `subagent-spawn` / `subagent-fork` 后端共享 `subagent-inprocess` 驱动器(一个自身不含提供方的库:两者都依赖它,彼此不依赖),进程外 `subagent-acp` / `subagent-dsh-sdk` 后端则经由 [`subprocess/`](../subprocess/README.md) seam spawn 其子进程(共享的凭据清除、以进程树为范围的拆卸、dispose(资源释放)阶梯)。测试只用包内 fixture(测试前置数据)替换子 agent 边界。 -提案与设计理由见 [.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md) 和 [.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md](../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md)。 +设计理由见 [.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)、[.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md](../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md) 和 [.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md)。 diff --git a/packages/subagent/subagent-control/README.md b/packages/subagent/subagent-control/README.md deleted file mode 100644 index d1afd86687..0000000000 --- a/packages/subagent/subagent-control/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# @deepseek-ai/dsh-subagent-control - -The continuable-subagent control service (`ctx.subagentControl`): the one orchestration path that binds a durable child session to a series of disposable Task-backed activations. Model tools and human-facing adapters call the same contract; the low-level `ctx.subagents` seam stays collection-, Task-, and persistence-agnostic. - -## Activation lifecycle - -A continuable background subagent is a durable child session with a series of Task-backed activations. `startContinuable()` allocates the stable child session id before Task creation, snapshots the descriptor inputs (a non-JSON input throws with no Task), and registers the initial activation's Task; the provider publishes exactly that child id and appends the versioned `subagent/descriptor` event inside the child's first turn. Every activation — initial or resumed — creates a fresh Task whose settlement awaits the provider's durability-confirmed child result, disposes the run, and only then records the `TaskOutcome`: a terminal Task leaves the durable child session but no live child Agent. A provider rejection with `DURABILITY_FAILED` settles the Task as `failed` and copies the error message into `detail`, so `task_output` reports the failed checkpoint and resumability risk without exposing unconfirmed output. - -`sendMessage(parent, childId, message, source)` owns steer-or-resume routing and requires the caller's `MessageSource`. A running activation preserves it through the run's confirmed `steer` capability and returns the existing Task id (`steered`) only after a committed request snapshot admits the message; an absent activation starts a fresh Task that loads the persisted child, authorizes the recorded `parentSession` as the direct parent, folds the descriptor, and dispatches `SubagentService.resume()` with the same source (`started`). Either route projects the content to the model as a user-role message while retaining its source in the child log. Rejection means the message was not delivered: terminal policy or Task settlement winning the admission race never falls through to cold resume within the same call, and a live registry Agent outside the activation association is an ownership conflict rather than an adoption target. - -Cancellation targets the whole activation. `task_kill` or owner disposal aborts the Task-owned signal; before publication the provider rejects only after its creation transaction rolled back to quiescence, afterwards the signal cancels the published run, and settlement records `killed` only once the activation is quiescent. Human input shares this path: an adapter submits child input through `sendMessage()` under the loaded parent, so parent and human messages that joined one turn share its result and cancellation outcome, and `TaskService.start()`'s control-surface requirement applies (load `@deepseek-ai/dsh-tool-tasks` or attach a surface). - -The activation association is process-local routing state, installed before any persistence or provider await and removed after run disposal and Task terminal publication. It is not a durable catalog: restart recovers the child session, not in-flight Tasks or their notifications. - -## Model Experience - -### Task completion and output - -#### What the model sees - -None directly, as this package registers no tool and no prompt text; the model observes continuable children through `@deepseek-ai/dsh-tool-subagent`'s background acknowledgement, `@deepseek-ai/dsh-tool-subagent-control`'s `send_message` results, and the generic task surface, whose outputs this service produces. - -#### Token effect - -None beyond the consuming tools' own results. - -#### KV Cache effect - -None; this service appends nothing to any model-visible sequence. - -## Known Limitations and Deferred Work - -- **Concurrent stopped-child admission is not atomic across awaits** — the synchronous association install admits one activation per child in this process, but a caller bypassing the control service can still race it; the Agent registry's same-id collision is the final backstop, and the losing Task fails with its message not delivered. -- **The association coordinates only one runtime** — concurrent resume from multiple processes needs a persistence-level lease or compare-and-set, which no backend offers yet. -- **Task records are process-local** — restart recovers the durable child session, not an interrupted Task, its result, or its completion notice; durable Task recovery is a separate concern. -- **Human interaction requires the exact live parent Agent** — Task access is fenced by the owner session and owner disposal cancels its Tasks; standalone child conversations belong to the interactive-side-sessions proposal, not this Task-owned lifecycle. -- **ACP children remain one-shot** — `AcpProvider.resume` and per-child continuation advertisement are deferred until the remote-session descriptor contract is resolved. diff --git a/packages/subagent/subagent-control/package.json b/packages/subagent/subagent-control/package.json deleted file mode 100644 index 9522bca29e..0000000000 --- a/packages/subagent/subagent-control/package.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "name": "@deepseek-ai/dsh-subagent-control", - "description": "Continuable-subagent control service: Task-backed activation, durable child descriptors, and steer-or-resume message routing", - "version": "0.0.1", - "private": true, - "type": "module", - "main": "lib/index.js", - "types": "lib/types/index.d.ts", - "exports": { - ".": { - "types": "./lib/types/index.d.ts", - "default": "./lib/index.js" - }, - "./invariant": { - "types": "./lib/types/invariant.d.ts", - "default": "./lib/invariant.js" - }, - "./src/*": "./src/*", - "./package.json": "./package.json" - }, - "files": [ - "lib/index.js", - "lib/invariant.js", - "lib/types/**/*.d.ts", - "lib/types/**/*.d.ts.map", - "src" - ], - "license": "BSD-3-Clause", - "peerDependencies": { - "@deepseek-ai/dsh-agent": "^0.0.1", - "@deepseek-ai/dsh-invariants": "^0.0.1", - "@deepseek-ai/dsh-llm": "^0.0.1", - "@deepseek-ai/dsh-session": "^0.0.1", - "@deepseek-ai/dsh-session-persistence": "^0.0.1", - "@deepseek-ai/dsh-subagent": "^0.0.1", - "@deepseek-ai/dsh-tasks": "^0.0.1", - "cordis": "^4.0.0-rc.7" - }, - "devDependencies": { - "@deepseek-ai/dsh-agent": "workspace:^", - "@deepseek-ai/dsh-agent-loop": "workspace:^", - "@deepseek-ai/dsh-agent-loop-testkit": "workspace:^", - "@deepseek-ai/dsh-invariants": "workspace:^", - "@deepseek-ai/dsh-llm": "workspace:^", - "@deepseek-ai/dsh-session": "workspace:^", - "@deepseek-ai/dsh-session-persistence": "workspace:^", - "@deepseek-ai/dsh-subagent": "workspace:^", - "@deepseek-ai/dsh-subagent-fork": "workspace:^", - "@deepseek-ai/dsh-subagent-spawn": "workspace:^", - "@deepseek-ai/dsh-tasks": "workspace:^", - "@deepseek-ai/dsh-tasks-local": "workspace:^", - "@deepseek-ai/dsh-tool-tasks": "workspace:^", - "cordis": "^4.0.0-rc.7" - } -} diff --git a/packages/subagent/subagent-control/src/invariant.ts b/packages/subagent/subagent-control/src/invariant.ts deleted file mode 100644 index ce40f360ca..0000000000 --- a/packages/subagent/subagent-control/src/invariant.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Package-owned invariant companion for `@deepseek-ai/dsh-subagent-control`. - * @module @deepseek-ai/dsh-subagent-control/invariant - */ - -/* jscpd:ignore-start */ -import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' - -const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-control' - -/** Cordis companion plugin name. */ -export const name = 'subagent-control-invariant' -/** Service required before the companion can reserve package ownership. */ -export const inject = ['invariants'] - -/** - * No runtime invariant: the activation association is deliberately private - * process-local routing state with no event stream of its own; the run - * lifecycle pair it participates in is checked by `@deepseek-ai/dsh-subagent`, - * and Task lifecycle relations belong to `@deepseek-ai/dsh-tasks`. - */ -const install: InvariantInstaller = () => {} - -/** - * Register this package's invariant companion. - * @param ctx - Cordis context carrying the invariant service. - * @returns the installed registration's disposer after setup succeeds. - */ -export const apply = (ctx: Context): Promise<() => void> => - Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) -/* jscpd:ignore-end */ diff --git a/packages/subagent/subagent-control/tsconfig.json b/packages/subagent/subagent-control/tsconfig.json deleted file mode 100644 index d41aacf4fb..0000000000 --- a/packages/subagent/subagent-control/tsconfig.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "lib/types" - }, - "include": [ - "src" - ], - "references": [ - { - "path": "../../../vendor/cosmokit" - }, - { - "path": "../../../vendor/cordis" - }, - { - "path": "../../core/agent" - }, - { - "path": "../../llm/llm" - }, - { - "path": "../../core/session" - }, - { - "path": "../../session-persistence/session-persistence" - }, - { - "path": "../subagent" - }, - { - "path": "../../tasks/tasks" - }, - { - "path": "../../support/invariants" - } - ] -} diff --git a/packages/subagent/subagent/README.md b/packages/subagent/subagent/README.md index d08d18e6e3..af8a2f7b71 100644 --- a/packages/subagent/subagent/README.md +++ b/packages/subagent/subagent/README.md @@ -10,11 +10,10 @@ The family separates the stable interface from implementations and model-facing | Package | Role | |---|---| -| `@deepseek-ai/dsh-subagent` | Provider registry, request/result/descriptor types, and lifecycle events. | +| `@deepseek-ai/dsh-subagent` | Provider registry, request/result/descriptor types, lifecycle events, and continuable-child orchestration. | | `@deepseek-ai/dsh-subagent-spawn` | Fresh in-process child, with cold resume. | | `@deepseek-ai/dsh-subagent-fork` | In-process child seeded with completed parent turns, with cold resume. | | `@deepseek-ai/dsh-subagent-acp` | Fresh out-of-process ACP child (one-shot). | -| `@deepseek-ai/dsh-subagent-control` | Continuable-child orchestration: durable ids, descriptors, Task-backed activation. | | `@deepseek-ai/dsh-tool-subagent` | Model-facing delegation tool over one configured provider. | | `@deepseek-ai/dsh-tool-subagent-control` | The globally named `send_message` follow-up tool. | @@ -22,7 +21,7 @@ Multiple providers may coexist under different names. This lets a deployment exp ## Service API -`SubagentService` has five main operations: +`SubagentService` has seven main operations: | Member | Meaning | |---|---| @@ -30,7 +29,9 @@ Multiple providers may coexist under different names. This lets a deployment exp | `getProvider(name)` | Return the provider, or `undefined` when absent. | | `list()` | Return provider names in insertion order. | | `start(name, request)` | Validate requested capabilities and semantic values, then await the provider until a real child is ready. Fulfillment returns a holder-owned `SubagentRun`; rejection means the provider has already cleaned every partial startup resource. | -| `resume(name, request)` | Capability-checked dispatch to `provider.resume?()` with the same run lifecycle observation as `start`. The caller (the control service) has already loaded the child, folded its descriptor, and authorized the parent; this seam stays collection-, Task-, and persistence-agnostic. | +| `resume(name, request)` | Capability-checked raw dispatch to `provider.resume?()` with the same run lifecycle observation as `start`; the caller owns descriptor lookup, authorization, and collection. | +| `startContinuable(spec)` | Allocate a durable child id and register its initial Task-backed activation. Requires `ctx.tasks`, `ctx.agents`, session persistence, and a resumable provider. | +| `sendMessage(parent, childId, message, source)` | Steer the current activation or start a new Task that cold-resumes the durable child. Requires `ctx.tasks` and `ctx.agents`; cold resume also requires session persistence. | `SubagentStartRequest.signal` is required and is the canonical cancellation channel. An abort before publication makes `start()` reject after rollback; an abort after publication cancels the live child. The request may also select a model, require structured output, cap delegation depth, restrict child tools, set a child persona, or carry a resolved `continuation` (the control-allocated stable child id plus its durable descriptor), which requires the provider's `resume` capability. @@ -63,7 +64,7 @@ The seam owns the depth vocabulary shared by implementations and consumers: the `SubagentRun.result` resolves to `{ output, structured?, stopReason }`. Child-level failures resolve with a non-`completed` reason; only an infrastructure fault that the seam cannot represent may reject. For a continuable activation, a completed result also confirms that the provider made its final state durable; a failed required checkpoint rejects as infrastructure rather than publishing unconfirmed output. `dispose()` is idempotent, cancels remaining work, and waits for the child resources to quiesce. -A local run publishes an ordinary child agent/session before `start()` fulfills, returns that shared session id as `SubagentRun.id`, exposes the exact child as `SubagentRun.localAgent`, and records `request.parent.session.id` in the child's `parentSession` header. A continuable start publishes exactly the control-allocated `continuation.sessionId`. Remote providers instead mint a parent-scoped lifecycle id and return `localAgent: undefined`. +A local run publishes an ordinary child agent/session before `start()` fulfills, returns that shared session id as `SubagentRun.id`, exposes the exact child as `SubagentRun.localAgent`, and records `request.parent.session.id` in the child's `parentSession` header. A continuable start publishes exactly the service-allocated `continuation.sessionId`. Remote providers instead mint a parent-scoped lifecycle id and return `localAgent: undefined`. The service emits `subagent/start` only after `start()` or `resume()` has fulfilled. It attaches the result observer before that synchronous notification, so even an already-settled child still produces `subagent/start` before `subagent/end`. The pair shares a service-minted `runId`; its `local` flag is snapshotted from the provider's exact `localAgent`, so observers never infer run identity or locality from reusable provider/session names. @@ -73,7 +74,7 @@ Provider additions and removals also emit `subagent/provider-added` and `subagen ## Collection model -The model-facing tool collects synchronously by default: it awaits the child result and disposes the run before returning. Background delegation does not change this seam; `@deepseek-ai/dsh-subagent-control` registers each activation with the generic `ctx.tasks` runtime, then collection and cancellation use the shared task tools. See the [background subagent tasks Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md), the [continuable background subagents Agent Note](../../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md), the [capability-seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md), and `src/types.ts` for the complete contracts. +The model-facing tool collects synchronously by default: it awaits the child result and disposes the run before returning. One-shot background delegation registers a plain Task in the tool. Continuable background delegation calls `ctx.subagents.startContinuable()`, whose internal manager exists only while `ctx.tasks` and `ctx.agents` are available; session persistence is resolved per continuation operation. Collection and cancellation use the shared task tools. See the [background subagent tasks Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md), the [continuable background subagents Agent Note](../../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md), the [merged-service Agent Note](../../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md), the [capability-seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md), and `src/types.ts` for the complete contracts. ## Model Experience diff --git a/packages/subagent/subagent/package.json b/packages/subagent/subagent/package.json index 58b51d1888..5f9f617eae 100644 --- a/packages/subagent/subagent/package.json +++ b/packages/subagent/subagent/package.json @@ -33,9 +33,19 @@ "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-scope": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", + "@deepseek-ai/dsh-session-persistence": "^0.0.1", + "@deepseek-ai/dsh-tasks": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", "cordis": "^4.0.0-rc.7" }, + "peerDependenciesMeta": { + "@deepseek-ai/dsh-session-persistence": { + "optional": true + }, + "@deepseek-ai/dsh-tasks": { + "optional": true + } + }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-brand": "workspace:^", @@ -43,6 +53,8 @@ "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-scope": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", + "@deepseek-ai/dsh-session-persistence": "workspace:^", + "@deepseek-ai/dsh-tasks": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", "cordis": "^4.0.0-rc.7" } diff --git a/packages/subagent/subagent-control/src/index.ts b/packages/subagent/subagent/src/continuation.ts similarity index 92% rename from packages/subagent/subagent-control/src/index.ts rename to packages/subagent/subagent/src/continuation.ts index e8a609e2d6..533cf15332 100644 --- a/packages/subagent/subagent-control/src/index.ts +++ b/packages/subagent/subagent/src/continuation.ts @@ -1,10 +1,7 @@ /** - * Continuable-subagent control service (`ctx.subagentControl`): stable child - * ids, descriptor persistence and lookup by known child id, Task-backed - * activation, and steer-or-resume message routing. The low-level - * `ctx.subagents` seam stays collection-, Task-, and persistence-agnostic; - * this service owns the policy that binds one durable child session to a - * series of disposable Task-backed activations. + * Internal continuable-subagent manager: stable child ids, descriptor + * persistence and lookup by known child id, Task-backed activation, and + * steer-or-resume message routing behind `ctx.subagents`. * * Every continuable activation — initial or resumed, parent- or human-started * — has exactly one Task and one result. Task settlement awaits the child @@ -13,26 +10,21 @@ * targets the whole activation: parent and human messages that joined one * turn share its result and its `killed` outcome. * - * @module @deepseek-ai/dsh-subagent-control + * @module @deepseek-ai/dsh-subagent */ import { randomUUID } from 'node:crypto' -import { Context, Service } from 'cordis' +import type { Context } from 'cordis' import type { Agent } from '@deepseek-ai/dsh-agent' import { HarnessError } from '@deepseek-ai/dsh-llm' import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm' import { SessionId } from '@deepseek-ai/dsh-session' import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence' -import { foldSubagentDescriptor, snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent' -import type { SubagentResult, SubagentRun, SubagentStartRequest } from '@deepseek-ai/dsh-subagent' +import { foldSubagentDescriptor, snapshotSubagentDescriptor } from './descriptor.ts' +import type { SubagentResult, SubagentRun, SubagentStartRequest } from './types.ts' +import type { SubagentService } from './index.ts' import type { TaskHooks, TaskId, TaskOutcome } from '@deepseek-ai/dsh-tasks' -declare module 'cordis' { - interface Context { - subagentControl: SubagentControlService - } -} - /** Attribution for a model coordinator's follow-up to one of its children. */ export interface CoordinatorMessageSource { readonly kind: 'coordinator' @@ -46,7 +38,7 @@ declare module '@deepseek-ai/dsh-llm' { } } -/** Typed error for control-service routing, authorization, and delivery failures. */ +/** Typed error for continuation routing, authorization, and delivery failures. */ export class SubagentControlError extends HarnessError { constructor(message: string, code: string, options?: ErrorOptions) { super(message, code, options) @@ -68,7 +60,7 @@ export interface ContinuableStartSpec { readonly request: Omit } -/** Identities returned by {@link SubagentControlService.startContinuable}. */ +/** Identities returned by {@link SubagentContinuationManager.startContinuable}. */ export interface ContinuableStart { /** The durable child session id, stable across activations. */ readonly childId: SessionId @@ -77,7 +69,7 @@ export interface ContinuableStart { } /** - * How {@link SubagentControlService.sendMessage} delivered a message: + * How {@link SubagentContinuationManager.sendMessage} delivered a message: * `steered` joined the running activation's existing Task without creating a * Task of its own; `started` created a fresh Task that cold-resumes the * durable child with the message. Failure is an exception, never a result — @@ -173,14 +165,14 @@ function finalText(blocks: ContentBlock[]): string { * boundary, while foreground one-shot delegation keeps calling * `ctx.subagents.start()` directly. */ -export class SubagentControlService extends Service { - static inject = ['subagents', 'tasks', 'agents'] - +export class SubagentContinuationManager { /** Child session id → its current activation. Process-local, never durable. */ private activations = new Map() - constructor(ctx: Context) { - super(ctx, 'subagentControl') + constructor( + private readonly ctx: Context, + private readonly subagents: SubagentService, + ) { // Terminal publication is one of the two removal conditions. The exact // Task id pins the resolution to this activation, never a later same-child one. ctx.tasks.onTaskDone((snapshot) => { @@ -189,7 +181,7 @@ export class SubagentControlService extends Service { } }) // TaskService deliberately keeps producer Tasks alive across a - // control-surface or producer reload, so this service's disposal must not + // follow-up-tool or producer reload, so this manager's disposal must not // strand the activations it can no longer route to: cancel each one and // await producer settlement (run disposal) before releasing the map. The // effect-scoped onTaskDone listener above is already gone by then, so @@ -198,7 +190,7 @@ export class SubagentControlService extends Service { const active = [...this.activations.values()] this.activations.clear() for (const activation of active) { - activation.controller.abort('subagent control service disposed') + activation.controller.abort('subagent continuation manager disposed') activation.terminal.resolve() } await Promise.allSettled(active.map((activation) => { @@ -207,7 +199,7 @@ export class SubagentControlService extends Service { if (activation.done === undefined) return Promise.resolve() return activation.done })) - }, 'subagentControl.activations()') + }, 'subagents.continuations()') } /** @@ -239,7 +231,7 @@ export class SubagentControlService extends Service { ...request.toolFilter !== undefined ? { toolFilter: request.toolFilter } : {}, }) const taskId = this.startActivation(childId, spec.label, request.parent, signal => - this.ctx.subagents.start(spec.provider, { + this.subagents.start(spec.provider, { ...request, signal, continuation: { sessionId: childId, descriptor }, @@ -294,7 +286,7 @@ export class SubagentControlService extends Service { const activation = this.activations.get(childId) if (activation === undefined) { throw new SubagentControlError( - `subagent "${childId}" has a live agent outside control-service ownership; the message was not delivered`, + `subagent "${childId}" has a live agent outside continuation ownership; the message was not delivered`, 'OWNERSHIP_CONFLICT', ) } @@ -399,7 +391,7 @@ export class SubagentControlService extends Service { 'NOT_RESUMABLE', ) } - return this.ctx.subagents.resume(descriptor.provider, { + return this.subagents.resume(descriptor.provider, { sessionId: childId, prompt: message, source, @@ -501,4 +493,4 @@ function resumeLabel(message: ContentBlock[]): string { return text.length > 80 ? `${text.slice(0, 79)}…` : text } -export default SubagentControlService +export default SubagentContinuationManager diff --git a/packages/subagent/subagent/src/descriptor.ts b/packages/subagent/subagent/src/descriptor.ts index 00942ca448..27b404fed9 100644 --- a/packages/subagent/subagent/src/descriptor.ts +++ b/packages/subagent/subagent/src/descriptor.ts @@ -3,7 +3,7 @@ * `subagent/descriptor` session event that records a child's declared * composition so a known child id can be cold-resumed after its run — and its * process — are gone. Providers append it turn-enclosed in the child's initial - * turn; the control service folds it back on resume. + * turn; the continuation manager folds it back on resume. * * The descriptor deliberately snapshots explicit fields rather than the * merge-extensible `AgentOptions` object: an unrelated extension value cannot diff --git a/packages/subagent/subagent/src/index.ts b/packages/subagent/subagent/src/index.ts index 4f9a013084..f81da156eb 100644 --- a/packages/subagent/subagent/src/index.ts +++ b/packages/subagent/subagent/src/index.ts @@ -13,13 +13,11 @@ * (`@deepseek-ai/dsh-subagent-spawn`, `-fork`, `-acp`) and the model-facing * consumer (`@deepseek-ai/dsh-tool-subagent`) are separate packages. * - * Scope: the seam stays collection-, Task-, and persistence-agnostic — a run - * is started or resumed and its `result` awaited, whether the consumer blocks - * on it (foreground) or registers it as a `ctx.tasks` background task (the - * generic runtime owns ids/polling/stop; this seam gains nothing task-shaped). - * Durable continuable-child ids, descriptor lookup, and Task association - * belong to `@deepseek-ai/dsh-subagent-control`; this service only validates - * and dispatches `start`/`resume` and observes run lifecycle. + * Raw `start` and `resume` remain collection-agnostic provider dispatch. + * When `ctx.tasks` and `ctx.agents` are available, the same service also binds + * an internal continuation manager for durable child ids, descriptor lookup, + * Task-backed activations, and steer-or-resume delivery. Persistence remains + * optional and is required only when a continuation operation is called. * * Same-process providers are trusted typed collaborators. Requests, provider * descriptors, results, and lifecycle payloads are borrowed immutable values; @@ -35,7 +33,7 @@ import { scopeTarget } from '@deepseek-ai/dsh-scope' import type { Scoped } from '@deepseek-ai/dsh-scope' import { assertObjectJsonSchema } from '@deepseek-ai/dsh-tools' import { HarnessError } from '@deepseek-ai/dsh-llm' -import type { ContentBlock } from '@deepseek-ai/dsh-llm' +import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm' import type { Agent } from '@deepseek-ai/dsh-agent' import type { SessionId } from '@deepseek-ai/dsh-session' import type { @@ -47,6 +45,12 @@ import type { SubagentStartRequest, } from './types.ts' import { SubagentRunId } from './types.ts' +import SubagentContinuationManager from './continuation.ts' +import type { + ContinuableStart, + ContinuableStartSpec, + SendMessageResult, +} from './continuation.ts' export * from './out-of-process.ts' export { SubagentRunId } from './types.ts' @@ -67,6 +71,17 @@ export { SUBAGENT_DESCRIPTOR_VERSION, } from './descriptor.ts' export type { SubagentDescriptorData, SubagentDescriptorInput } from './descriptor.ts' +export { + runOutcome, + settleRun, + SubagentControlError, +} from './continuation.ts' +export type { + ContinuableStart, + ContinuableStartSpec, + CoordinatorMessageSource, + SendMessageResult, +} from './continuation.ts' declare module '@deepseek-ai/dsh-agent' { interface AgentOptions { @@ -187,12 +202,49 @@ export class SubagentError extends HarnessError { } } -/** Named provider registry and capability-checked start surface. */ +/** Named provider registry with raw and Task-backed continuation operations. */ export class SubagentService extends Service { private providers = new Map() + private continuations: SubagentContinuationManager | undefined constructor(ctx: Context) { super(ctx, 'subagents') + ctx.inject(['tasks', 'agents'], (childCtx: Context) => { + const manager = new SubagentContinuationManager(childCtx, this) + this.continuations = manager + childCtx.effect(() => () => { + /* v8 ignore else -- one injected binding owns the slot until its fiber disposes. */ + if (this.continuations === manager) this.continuations = undefined + }, 'subagents.continuationBinding()') + }) + } + + /** + * Start one durable continuable child through a Task-backed initial + * activation. + * @param spec - provider, Task label, and delegation request. + * @returns the stable child id and initial activation Task id. + */ + startContinuable(spec: ContinuableStartSpec): ContinuableStart { + return this.requireContinuations().startContinuable(spec) + } + + /** + * Deliver a message to a continuable child by steering its live activation + * or cold-resuming a fresh Task-backed activation. + * @param parent - live direct parent authorizing the operation. + * @param childId - durable child session id. + * @param message - user-role content to deliver. + * @param source - durable caller attribution. + * @returns the existing steered Task or newly started Task. + */ + sendMessage( + parent: Agent, + childId: SessionId, + message: ContentBlock[], + source: MessageSource, + ): Promise { + return this.requireContinuations().sendMessage(parent, childId, message, source) } /** @@ -264,7 +316,7 @@ export class SubagentService extends Service { /** * Resume a persisted continuable child through the named provider's * `resume` capability, with the same run lifecycle observation as - * {@link start}. The caller (the control service) has already loaded the + * {@link start}. The internal continuation manager has already loaded the * child, folded its descriptor, and authorized the parent; this method owns * only capability-checked dispatch. * @param name - the provider recorded in the child's descriptor. @@ -291,6 +343,17 @@ export class SubagentService extends Service { return provider } + /** Resolve the optional Task-backed continuation runtime or fail loud. */ + private requireContinuations(): SubagentContinuationManager { + if (this.continuations === undefined) { + throw new SubagentError( + 'continuable subagents require the tasks and agents services', + 'CONTINUATION_UNAVAILABLE', + ) + } + return this.continuations + } + /** Emit the start/end lifecycle pair for one accepted run and return it. */ private observeRun(name: string, parent: Agent, run: SubagentRun): SubagentRun { const runId = SubagentRunId(randomUUID()) diff --git a/packages/subagent/subagent/src/types.ts b/packages/subagent/subagent/src/types.ts index aa215be03c..50922b183e 100644 --- a/packages/subagent/subagent/src/types.ts +++ b/packages/subagent/subagent/src/types.ts @@ -94,7 +94,7 @@ export interface SubagentStartRequest { */ readonly persona?: string /** - * Continuable-child intent, resolved by the control service before start. + * Continuable-child intent, resolved by `ctx.subagents` before start. * The provider MUST publish exactly `sessionId` as the child identity * instead of allocating one internally, and MUST append the snapshotted * `descriptor` as the child's turn-enclosed `subagent/descriptor` event @@ -106,10 +106,10 @@ export interface SubagentStartRequest { /** * The resolved continuable-child identity and durable composition record a - * control-service caller attaches to a start request. + * continuation caller attaches to a start request. */ export interface SubagentContinuation { - /** Control-allocated stable child session id, published verbatim. */ + /** Service-allocated stable child session id, published verbatim. */ readonly sessionId: SessionId /** Snapshotted descriptor persisted in the child log for cold resume. */ readonly descriptor: SubagentDescriptorData @@ -117,7 +117,7 @@ export interface SubagentContinuation { /** * What a caller asks for when resuming a persisted continuable child. The - * control service loads the child log, folds and authorizes its descriptor, + * continuation manager loads the child log, folds and authorizes its descriptor, * and passes this fully resolved request to * {@link SubagentService.resume}, which dispatches to * {@link SubagentProvider.resume}. The provider reconstructs the declared diff --git a/packages/subagent/subagent-control/tests/subagent-control.spec.ts b/packages/subagent/subagent/tests/continuation.spec.ts similarity index 90% rename from packages/subagent/subagent-control/tests/subagent-control.spec.ts rename to packages/subagent/subagent/tests/continuation.spec.ts index 9e9e4573e8..5a0d20c481 100644 --- a/packages/subagent/subagent-control/tests/subagent-control.spec.ts +++ b/packages/subagent/subagent/tests/continuation.spec.ts @@ -9,7 +9,6 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test import { SessionId } from '@deepseek-ai/dsh-session' import type { SessionEvent } from '@deepseek-ai/dsh-session' import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' -import SubagentService, { SUBAGENT_DESCRIPTOR_VERSION } from '@deepseek-ai/dsh-subagent' import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn' import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork' import { TaskId } from '@deepseek-ai/dsh-tasks' @@ -18,7 +17,12 @@ import * as ToolTasks from '@deepseek-ai/dsh-tool-tasks' import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' import { createUserMessage, HarnessError, LlmAdapter } from '@deepseek-ai/dsh-llm' import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' -import SubagentControlService, { runOutcome, settleRun, SubagentControlError } from '../src/index.ts' +import SubagentService, { + runOutcome, + settleRun, + SubagentControlError, + SUBAGENT_DESCRIPTOR_VERSION, +} from '../src/index.ts' type Script = ConstructorParameters[0] @@ -53,12 +57,12 @@ afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) }) -/** Boot the full continuable stack: loop, persistence, providers, tasks, control. */ +/** Boot the full continuable stack: loop, persistence, providers, tasks, and subagents. */ async function setupWith(adapter: LlmAdapter, options: { persistence?: boolean } = {}) { const ctx = new Context() await mountAgentLoopTestDependencies(ctx) if (options.persistence !== false) { - const root = mkdtempSync(join(tmpdir(), 'dsh-subagent-control-')) + const root = mkdtempSync(join(tmpdir(), 'dsh-subagent-continuation-')) roots.push(root) await ctx.plugin(JsonlSessionPersistence, { root }) } @@ -68,7 +72,6 @@ async function setupWith(adapter: LlmAdapter, options: { persistence?: boolean } await ctx.plugin(SubagentFork, { providerName: 'fork' }) await ctx.plugin(LocalTaskService) await ctx.plugin(ToolTasks, {}) - await ctx.plugin(SubagentControlService) ctx.llm.registerAdapter(['mock'], adapter) const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' }) return { ctx, parent } @@ -93,12 +96,12 @@ async function waitTerminal(ctx: Context, taskId: TaskId, parent: Agent) { } async function waitPublishedRun(ctx: Context, childId: SessionId): Promise { - const control = ctx.subagentControl as unknown as { - activations: Map + const continuations = ctx.subagents as unknown as { + continuations: { activations: Map } } await new Promise((resolve) => { const timer = setInterval(() => { - if (control.activations.get(childId)?.run !== undefined) { + if (continuations.continuations.activations.get(childId)?.run !== undefined) { clearInterval(timer) resolve() } @@ -121,13 +124,13 @@ function sendMessage( childId: SessionId, content: ReturnType, ) { - return ctx.subagentControl.sendMessage(parent, childId, content, { kind: 'user' }) + return ctx.subagents.sendMessage(parent, childId, content, { kind: 'user' }) } -describe('SubagentControlService.startContinuable', () => { +describe('SubagentService.startContinuable', () => { it('returns both identities immediately; the Task settles with the child result after disposal', async () => { const { ctx, parent } = await setup([textResponse('first answer')]) - const started = ctx.subagentControl.startContinuable(startSpec(parent)) + const started = ctx.subagents.startContinuable(startSpec(parent)) expect(started.childId).toMatch(/[0-9a-f-]{36}/) expect(started.taskId).toBe('subagent-1') @@ -138,13 +141,13 @@ describe('SubagentControlService.startContinuable', () => { expect(ctx.agents.get(started.childId)).toBeUndefined() }) - it('publishes the control-allocated child id and appends the turn-enclosed descriptor', async () => { + it('publishes the service-allocated child id and appends the turn-enclosed descriptor', async () => { const { ctx, parent } = await setup([textResponse('answer')]) const seen: SessionEvent[] = [] ctx.on('session/event', (session, event) => { if (session.id !== SessionId('parent')) seen.push(event) }) - const started = ctx.subagentControl.startContinuable(startSpec(parent)) + const started = ctx.subagents.startContinuable(startSpec(parent)) await waitTerminal(ctx, started.taskId, parent) const descriptorIndex = seen.findIndex(event => event.type === 'subagent/descriptor') @@ -162,7 +165,7 @@ describe('SubagentControlService.startContinuable', () => { // Model-hidden: the descriptor never carries surface metadata. expect('surfaceOp' in descriptor).toBe(false) - // The durable log kept the exact control-allocated id. + // The durable log kept the exact service-allocated id. const loaded = await ctx.sessionPersistence.load(started.childId) expect(loaded.meta.id).toBe(started.childId) expect(loaded.meta.parentSession).toBe(SessionId('parent')) @@ -171,7 +174,7 @@ describe('SubagentControlService.startContinuable', () => { it('rejects synchronously with no Task when persistence is not configured', async () => { const { ctx, parent } = await setup([textResponse('unused')], { persistence: false }) - expect(() => ctx.subagentControl.startContinuable(startSpec(parent))) + expect(() => ctx.subagents.startContinuable(startSpec(parent))) .toThrow(/require session persistence/) expect(ctx.tasks.list(parent)).toEqual([]) }) @@ -181,19 +184,21 @@ describe('SubagentControlService.startContinuable', () => { const realStart = ctx.tasks.start.bind(ctx.tasks) ctx.tasks.start = () => { throw new Error('task preflight failed') } try { - expect(() => ctx.subagentControl.startContinuable(startSpec(parent))) + expect(() => ctx.subagents.startContinuable(startSpec(parent))) .toThrow('task preflight failed') } finally { ctx.tasks.start = realStart } - const control = ctx.subagentControl as unknown as { activations: Map } - expect(control.activations.size).toBe(0) + const continuations = ctx.subagents as unknown as { + continuations: { activations: Map } + } + expect(continuations.continuations.activations.size).toBe(0) }) it('rejects a non-JSON descriptor input synchronously with no Task', async () => { const { ctx, parent } = await setup([textResponse('unused')]) const spec = startSpec(parent) - expect(() => ctx.subagentControl.startContinuable({ + expect(() => ctx.subagents.startContinuable({ ...spec, // A symbol survives the static ToolRestriction type only through this // cast — exactly the durable-boundary input the snapshot rejects. @@ -214,7 +219,7 @@ describe('SubagentControlService.startContinuable', () => { maxDepth: 0, }, } - const started = ctx.subagentControl.startContinuable(spec) + const started = ctx.subagents.startContinuable(spec) const snapshot = await waitTerminal(ctx, started.taskId, parent) expect(snapshot.status).toBe('failed') expect(snapshot.detail).toContain('maxDepth') @@ -228,7 +233,7 @@ describe('SubagentControlService.startContinuable', () => { it('task_kill during the run aborts, disposes, and settles killed after quiescence', async () => { const { ctx, parent } = await setup(['hang']) - const started = ctx.subagentControl.startContinuable(startSpec(parent)) + const started = ctx.subagents.startContinuable(startSpec(parent)) // Let the child publish and begin its turn. await new Promise(resolve => setTimeout(resolve, 30)) expect(ctx.agents.get(started.childId)).toBeDefined() @@ -250,7 +255,7 @@ describe('SubagentControlService.startContinuable', () => { checkpointStarted.resolve(undefined) await releaseCheckpoint.promise }) - const started = ctx.subagentControl.startContinuable(startSpec(parent)) + const started = ctx.subagents.startContinuable(startSpec(parent)) await checkpointStarted.promise expect(ctx.tasks.kill(started.taskId, parent, 'no longer needed')).toBe('requested') @@ -262,7 +267,7 @@ describe('SubagentControlService.startContinuable', () => { }) }) -describe('SubagentControlService.sendMessage', () => { +describe('SubagentService.sendMessage', () => { it('omits undeclared model selectors and rejects a provider without live delivery', async () => { const { ctx } = await setup([]) const result = Promise.withResolvers<{ @@ -286,7 +291,7 @@ describe('SubagentControlService.sendMessage', () => { resume: async () => { throw new Error('not used') }, }) const parent = ctx.agentLoop.create(SessionId('bare-parent'), {}) - const started = ctx.subagentControl.startContinuable(startSpec(parent, 'no-steer')) + const started = ctx.subagents.startContinuable(startSpec(parent, 'no-steer')) await waitPublishedRun(ctx, started.childId) expect(descriptor).toEqual({ version: SUBAGENT_DESCRIPTOR_VERSION, provider: 'no-steer' }) @@ -336,7 +341,7 @@ describe('SubagentControlService.sendMessage', () => { }, resume: async () => { throw new Error('not used') }, }) - const started = ctx.subagentControl.startContinuable(startSpec(parent, 'mismatched-local')) + const started = ctx.subagents.startContinuable(startSpec(parent, 'mismatched-local')) await waitPublishedRun(ctx, started.childId) await expect(sendMessage(ctx, parent, started.childId, message('join'))) @@ -357,7 +362,7 @@ describe('SubagentControlService.sendMessage', () => { ]) const { ctx, parent } = await setupWith(adapter) - const started = ctx.subagentControl.startContinuable(startSpec(parent)) + const started = ctx.subagents.startContinuable(startSpec(parent)) // Wait until the first immutable request has crossed the adapter boundary. await new Promise((resolve) => { const timer = setInterval(() => { @@ -368,7 +373,7 @@ describe('SubagentControlService.sendMessage', () => { }, 5) }) - const delivery = ctx.subagentControl.sendMessage( + const delivery = ctx.subagents.sendMessage( parent, started.childId, message('also consider Y'), @@ -406,7 +411,7 @@ describe('SubagentControlService.sendMessage', () => { }) const base = startSpec(parent) - const started = ctx.subagentControl.startContinuable({ + const started = ctx.subagents.startContinuable({ ...base, request: { ...base.request, @@ -419,7 +424,7 @@ describe('SubagentControlService.sendMessage', () => { }) await startedTool.promise - const delivery = ctx.subagentControl.sendMessage( + const delivery = ctx.subagents.sendMessage( parent, started.childId, message('follow-up that terminal policy rejects'), @@ -437,11 +442,11 @@ describe('SubagentControlService.sendMessage', () => { it('cold-resumes a settled child into a fresh Task and reports `started`', async () => { const { ctx, parent } = await setup([textResponse('first answer'), textResponse('second answer')]) - const started = ctx.subagentControl.startContinuable(startSpec(parent)) + const started = ctx.subagents.startContinuable(startSpec(parent)) await waitTerminal(ctx, started.taskId, parent) expect(ctx.agents.get(started.childId)).toBeUndefined() - const followUp = await ctx.subagentControl.sendMessage( + const followUp = await ctx.subagents.sendMessage( parent, started.childId, message('and then?'), @@ -476,7 +481,7 @@ describe('SubagentControlService.sendMessage', () => { toolFilter: { deny: [] as string[] }, }, } - const started = ctx.subagentControl.startContinuable(spec) + const started = ctx.subagents.startContinuable(spec) await waitTerminal(ctx, started.taskId, parent) const loaded = await ctx.sessionPersistence.load(started.childId) @@ -503,7 +508,7 @@ describe('SubagentControlService.sendMessage', () => { parent.followup(createUserMessage({ content: message('parent question one'), source: { kind: 'user' } })) await parent.whenIdle() - const started = ctx.subagentControl.startContinuable(startSpec(parent, 'fork')) + const started = ctx.subagents.startContinuable(startSpec(parent, 'fork')) await waitTerminal(ctx, started.taskId, parent) const firstLoad = await ctx.sessionPersistence.load(started.childId) const seedLength = firstLoad.meta.seedLength ?? 0 @@ -527,7 +532,7 @@ describe('SubagentControlService.sendMessage', () => { it('a resumed child cannot regain a top-level delegation budget (header floor)', async () => { const { ctx, parent } = await setup([textResponse('first'), textResponse('second')]) - const started = ctx.subagentControl.startContinuable(startSpec(parent)) + const started = ctx.subagents.startContinuable(startSpec(parent)) await waitTerminal(ctx, started.taskId, parent) const followUp = await sendMessage(ctx, parent, started.childId, message('go on')) @@ -546,7 +551,7 @@ describe('SubagentControlService.sendMessage', () => { it('rejects a foreign child id: the started Task fails with UNAUTHORIZED and delivers nothing', async () => { const { ctx, parent } = await setup([textResponse('other parent answer'), textResponse('unused')]) const otherParent = ctx.agentLoop.create(SessionId('other-parent'), { provider: 'mock', model: 'mock' }) - const started = ctx.subagentControl.startContinuable(startSpec(otherParent)) + const started = ctx.subagents.startContinuable(startSpec(otherParent)) await waitTerminal(ctx, started.taskId, otherParent) const attempt = await sendMessage(ctx, parent, started.childId, message('mine now')) @@ -590,9 +595,9 @@ describe('SubagentControlService.sendMessage', () => { ]) }) - it('rejects delivery to a live agent outside control-service ownership', async () => { + it('rejects delivery to a live agent outside continuation ownership', async () => { const { ctx, parent } = await setup([textResponse('unused')]) - // A live child created around the control service. + // A live child created outside continuation orchestration. const handle = await ctx.agents.create({ sessionId: SessionId('rogue-child'), meta: { parentSession: parent.id }, @@ -601,7 +606,7 @@ describe('SubagentControlService.sendMessage', () => { await expect(sendMessage(ctx, parent, SessionId('rogue-child'), message('hello'))) .rejects.toThrow(SubagentControlError) await expect(sendMessage(ctx, parent, SessionId('rogue-child'), message('hello'))) - .rejects.toThrow(/outside control-service ownership.*not delivered/) + .rejects.toThrow(/outside continuation ownership.*not delivered/) await handle.dispose() }) @@ -625,7 +630,7 @@ describe('SubagentControlService.sendMessage', () => { } } - const started = ctx.subagentControl.startContinuable(startSpec(parent)) + const started = ctx.subagents.startContinuable(startSpec(parent)) // Wait for the child to finish its turn while the run remains undisposed // and the association therefore still holds. await new Promise((resolve) => { @@ -654,7 +659,7 @@ describe('SubagentControlService.sendMessage', () => { it('each follow-up Task result is fenced to the parent session', async () => { const { ctx, parent } = await setup([textResponse('first'), textResponse('second')]) - const started = ctx.subagentControl.startContinuable(startSpec(parent)) + const started = ctx.subagents.startContinuable(startSpec(parent)) await waitTerminal(ctx, started.taskId, parent) const followUp = await sendMessage(ctx, parent, started.childId, message('more')) const other = ctx.agentLoop.create(SessionId('intruder'), { provider: 'mock', model: 'mock' }) @@ -663,7 +668,7 @@ describe('SubagentControlService.sendMessage', () => { it('kills a cold-resume activation during descriptor lookup without starting child work', async () => { const { ctx, parent } = await setup([textResponse('first'), textResponse('never used')]) - const started = ctx.subagentControl.startContinuable(startSpec(parent)) + const started = ctx.subagents.startContinuable(startSpec(parent)) await waitTerminal(ctx, started.taskId, parent) // Make the persistence load hang until the kill lands. @@ -686,7 +691,7 @@ describe('SubagentControlService.sendMessage', () => { it('admits one process-local activation per child: a second send during resume load steers or fails, never duplicates', async () => { const { ctx, parent } = await setup([textResponse('first'), textResponse('resumed answer')]) - const started = ctx.subagentControl.startContinuable(startSpec(parent)) + const started = ctx.subagents.startContinuable(startSpec(parent)) await waitTerminal(ctx, started.taskId, parent) const realLoad = ctx.sessionPersistence.load.bind(ctx.sessionPersistence) @@ -715,15 +720,15 @@ describe('service disposal with live activations', () => { it('cancels and settles a starting activation on service disposal instead of stranding it', async () => { const ctx = new Context() await mountAgentLoopTestDependencies(ctx) - const root = mkdtempSync(join(tmpdir(), 'dsh-subagent-control-hmr-')) + const root = mkdtempSync(join(tmpdir(), 'dsh-subagent-continuation-hmr-')) roots.push(root) await ctx.plugin(JsonlSessionPersistence, { root }) await ctx.plugin(AgentLoop, { agents: [] }) - await ctx.plugin(SubagentService) + const subagentsFiber = await ctx.plugin(SubagentService) await ctx.plugin(LocalTaskService) await ctx.plugin(ToolTasks, {}) // A provider that stays pending until its signal aborts, so the activation - // is observably mid-start when the control service is disposed. + // is observably mid-start when the subagent service is disposed. let sawAbort = false ctx.subagents.registerProvider({ name: 'pending', @@ -737,19 +742,17 @@ describe('service disposal with live activations', () => { }), resume: () => Promise.reject(new Error('unreachable')), }) - const controlFiber = await ctx.plugin(SubagentControlService) ctx.llm.registerAdapter(['mock'], new MockAdapter([])) const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' }) - const control = ctx.get('subagentControl')! - const started = control.startContinuable({ + const started = ctx.subagents.startContinuable({ provider: 'pending', label: 'will be interrupted', request: { prompt: message('go'), parent }, }) - // LocalTaskService keeps the producer Task; the disposing control service must + // LocalTaskService keeps the producer Task; the disposing subagent service must // cancel its activation and await settlement rather than strand it. - await controlFiber.dispose() + await subagentsFiber.dispose() expect(sawAbort).toBe(true) const snapshot = await waitTerminal(ctx, started.taskId, parent) expect(snapshot.status).toBe('killed') diff --git a/packages/subagent/subagent/tests/service.spec.ts b/packages/subagent/subagent/tests/service.spec.ts index 69dacf70f6..90128302c5 100644 --- a/packages/subagent/subagent/tests/service.spec.ts +++ b/packages/subagent/subagent/tests/service.spec.ts @@ -125,6 +125,17 @@ describe('SubagentService', () => { })).rejects.toMatchObject({ code: 'UNSUPPORTED_CAPABILITY' }) }) + it('rejects Task-backed continuation operations when their runtime services are absent', async () => { + const { subagents } = await service() + expect(() => { + subagents.startContinuable({ + provider: 'unused', + label: 'work', + request: baseRequest(), + }) + }).toThrow(expect.objectContaining({ code: 'CONTINUATION_UNAVAILABLE' })) + }) + it.each([ ['outputSchema', { outputSchema: { type: 'object', properties: {} } }], ['depthLimit', { maxDepth: 1 }], diff --git a/packages/subagent/subagent/tsconfig.json b/packages/subagent/subagent/tsconfig.json index 713e214f04..6684758659 100644 --- a/packages/subagent/subagent/tsconfig.json +++ b/packages/subagent/subagent/tsconfig.json @@ -26,6 +26,12 @@ { "path": "../../core/scope" }, + { + "path": "../../session-persistence/session-persistence" + }, + { + "path": "../../tasks/tasks" + }, { "path": "../../support/invariants" } diff --git a/packages/subagent/tool-subagent-control/README.md b/packages/subagent/tool-subagent-control/README.md index 6a6026e3fd..c308e11d99 100644 --- a/packages/subagent/tool-subagent-control/README.md +++ b/packages/subagent/tool-subagent-control/README.md @@ -1,8 +1,8 @@ # @deepseek-ai/dsh-tool-subagent-control -The globally named `send_message` tool: a thin adapter over `ctx.subagentControl.sendMessage()`. Provider-bound `@deepseek-ai/dsh-tool-subagent` instances register distinct delegation tools per transport; this separately loaded package registers the one shared control tool, so multiple delegation tools never register duplicate global controls. +The optional, globally named `send_message` tool: a thin adapter over `ctx.subagents.sendMessage()`. Provider-bound `@deepseek-ai/dsh-tool-subagent` instances register distinct delegation tools per transport; this separately loaded package registers one shared follow-up tool, so multiple delegation tools never register duplicate global controls. Its presence does not determine whether a delegation tool starts continuable work. -The tool performs no lifecycle routing. It attributes every follow-up as `{ kind: 'coordinator', senderSessionId: parent.id }`; the control service preserves that source while deciding between live delivery to the running activation's existing Task and a fresh Task that cold-resumes the durable child. The tool renders which route was taken and the relevant Task id. A control-service throw becomes an errored tool result stating the message was not delivered. +The tool performs no lifecycle routing. It attributes every follow-up as `{ kind: 'coordinator', senderSessionId: parent.id }`; the subagent service preserves that source while deciding between live delivery to the running activation's existing Task and a fresh Task that cold-resumes the durable child. The tool renders which route was taken and the relevant Task id. A delivery failure becomes an errored tool result stating the message was not delivered. ## Model Experience diff --git a/packages/subagent/tool-subagent-control/package.json b/packages/subagent/tool-subagent-control/package.json index 96c91c4ec6..4fdd483ff7 100644 --- a/packages/subagent/tool-subagent-control/package.json +++ b/packages/subagent/tool-subagent-control/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-tool-subagent-control", - "description": "Globally named send_message tool over the continuable-subagent control service", + "description": "Globally named send_message tool over ctx.subagents continuations", "version": "0.0.1", "private": true, "type": "module", @@ -30,7 +30,7 @@ "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", - "@deepseek-ai/dsh-subagent-control": "^0.0.1", + "@deepseek-ai/dsh-subagent": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", "cordis": "^4.0.0-rc.7" }, @@ -44,7 +44,6 @@ "@deepseek-ai/dsh-session-persistence": "workspace:^", "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^", "@deepseek-ai/dsh-subagent": "workspace:^", - "@deepseek-ai/dsh-subagent-control": "workspace:^", "@deepseek-ai/dsh-subagent-spawn": "workspace:^", "@deepseek-ai/dsh-tasks": "workspace:^", "@deepseek-ai/dsh-tasks-local": "workspace:^", diff --git a/packages/subagent/tool-subagent-control/src/index.ts b/packages/subagent/tool-subagent-control/src/index.ts index 959ff8eb49..d95ecb77a9 100644 --- a/packages/subagent/tool-subagent-control/src/index.ts +++ b/packages/subagent/tool-subagent-control/src/index.ts @@ -1,7 +1,7 @@ /** * The globally named `send_message` tool: a thin model-facing adapter over - * `ctx.subagentControl.sendMessage()`. It performs no lifecycle routing of its - * own — steer-or-resume orchestration belongs to the control service — and it + * `ctx.subagents.sendMessage()`. It performs no lifecycle routing of its + * own — steer-or-resume orchestration belongs to the subagent service — and it * lives apart from the provider-bound `@deepseek-ai/dsh-tool-subagent` * instances so multiple delegation tools share one control tool. * @module @deepseek-ai/dsh-tool-subagent-control @@ -11,14 +11,14 @@ import type { Context } from 'cordis' import { defineTool } from '@deepseek-ai/dsh-tools' import type { ContentBlock } from '@deepseek-ai/dsh-llm' import { SessionId } from '@deepseek-ai/dsh-session' -import type {} from '@deepseek-ai/dsh-subagent-control' +import type {} from '@deepseek-ai/dsh-subagent' export const name = 'tool-subagent-control' -export const inject = ['tools', 'subagentControl'] +export const inject = ['tools', 'subagents'] /** * Register the `send_message` tool. - * @param ctx - context carrying the tool registry and the control service. + * @param ctx - context carrying the tool registry and subagent service. */ export function apply(ctx: Context): void { ctx.tools.register(defineTool({ @@ -67,7 +67,7 @@ export function apply(ctx: Context): void { throw new Error('send_message requires a calling agent (exec.agent was undefined)') } const message: ContentBlock[] = [{ type: 'text', text: args.message }] - const result = ctx.subagentControl.sendMessage( + const result = ctx.subagents.sendMessage( parent, SessionId(args.subagent_id), message, diff --git a/packages/subagent/tool-subagent-control/src/invariant.ts b/packages/subagent/tool-subagent-control/src/invariant.ts index 6fb1c19ea6..c993426a26 100644 --- a/packages/subagent/tool-subagent-control/src/invariant.ts +++ b/packages/subagent/tool-subagent-control/src/invariant.ts @@ -16,7 +16,7 @@ export const inject = ['invariants'] /** * No runtime invariant: this model-facing adapter has no independent lifecycle stream; delivery - * and activation relations are owned by the control service it calls. + * and activation relations are owned by the subagent service it calls. */ const install: InvariantInstaller = () => {} diff --git a/packages/subagent/tool-subagent-control/tests/tool-subagent-control.spec.ts b/packages/subagent/tool-subagent-control/tests/tool-subagent-control.spec.ts index 93c401007e..02c7e6c1a3 100644 --- a/packages/subagent/tool-subagent-control/tests/tool-subagent-control.spec.ts +++ b/packages/subagent/tool-subagent-control/tests/tool-subagent-control.spec.ts @@ -9,7 +9,6 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test import { SessionId } from '@deepseek-ai/dsh-session' import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' import SubagentService from '@deepseek-ai/dsh-subagent' -import SubagentControlService from '@deepseek-ai/dsh-subagent-control' import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn' import LocalTaskService from '@deepseek-ai/dsh-tasks-local' import * as ToolTasks from '@deepseek-ai/dsh-tool-tasks' @@ -34,7 +33,6 @@ async function setup(script: ConstructorParameters[0]) { await ctx.plugin(SubagentSpawn, { providerName: 'spawn' }) await ctx.plugin(LocalTaskService) await ctx.plugin(ToolTasks, {}) - await ctx.plugin(SubagentControlService) await ctx.plugin(tool) ctx.llm.registerAdapter(['mock'], new MockAdapter(script)) const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' }) @@ -68,7 +66,7 @@ describe('dsh-tool-subagent-control', () => { it('cold-resumes a settled child and renders the started route with its task id', async () => { const { ctx, parent } = await setup([textResponse('first answer'), textResponse('second answer')]) - const started = ctx.subagentControl.startContinuable({ + const started = ctx.subagents.startContinuable({ provider: 'spawn', label: 'work', request: { prompt: [{ type: 'text', text: 'child task' }], parent }, @@ -98,10 +96,10 @@ describe('dsh-tool-subagent-control', () => { const { ctx, parent } = await setup([]) let steered: string | undefined let source: unknown - // Reach past the tool into the control service to fake a running route + // Reach past the tool into the subagent service to fake a running route // deterministically: the tool is a thin adapter, so its steered wording is // what this test pins. - ctx.subagentControl.sendMessage = async (agent, _childId, message, messageSource) => { + ctx.subagents.sendMessage = async (agent, _childId, message, messageSource) => { steered = (message[0] as { text: string }).text source = messageSource return { route: 'steered', taskId: ctx.tasks.list(agent)[0]?.id ?? ('subagent-9' as never) } @@ -116,7 +114,7 @@ describe('dsh-tool-subagent-control', () => { expect(text(result)).toBe('message delivered to running task subagent-9') }) - it('reports a control-service failure as an errored, not-delivered result', async () => { + it('reports a delivery failure as an errored, not-delivered result', async () => { const { ctx, parent } = await setup([]) const result = await callTool(ctx, 'send_message', { subagent_id: 'no-such-child', @@ -148,7 +146,6 @@ describe('dsh-tool-subagent-control', () => { await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(SubagentService) await ctx.plugin(LocalTaskService) - await ctx.plugin(SubagentControlService) const fiber = await ctx.plugin(tool) expect(ctx.tools.schemas().some(schema => schema.name === 'send_message')).toBe(true) await fiber.dispose() @@ -158,7 +155,7 @@ describe('dsh-tool-subagent-control', () => { it('has the namespace-plugin export shape (no stray default)', () => { expect('default' in tool).toBe(false) expect(tool.name).toBe('tool-subagent-control') - expect(tool.inject).toEqual(['tools', 'subagentControl']) + expect(tool.inject).toEqual(['tools', 'subagents']) expect(typeof tool.apply).toBe('function') }) }) diff --git a/packages/subagent/tool-subagent-control/tsconfig.json b/packages/subagent/tool-subagent-control/tsconfig.json index 4b2ec045e6..3a57a0437e 100644 --- a/packages/subagent/tool-subagent-control/tsconfig.json +++ b/packages/subagent/tool-subagent-control/tsconfig.json @@ -24,7 +24,7 @@ "path": "../../core/tools" }, { - "path": "../subagent-control" + "path": "../subagent" }, { "path": "../../support/invariants" diff --git a/packages/subagent/tool-subagent/README.i18n.yaml b/packages/subagent/tool-subagent/README.i18n.yaml index c4660b5517..46daae94ea 100644 --- a/packages/subagent/tool-subagent/README.i18n.yaml +++ b/packages/subagent/tool-subagent/README.i18n.yaml @@ -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/README.md -README.md: 7d32da3c974361eb5e58cdb2ee5be756383ad3d1 -README.zh.md: eadc168fd07701b3e3d9600b3fe69bd8b22e235a +README.md: 9d60363602a9825730984700a7fe987d911e1cac +README.zh.md: 5964c38bd847c1c14cac9decdd913ca65c39e8f3 diff --git a/packages/subagent/tool-subagent/README.md b/packages/subagent/tool-subagent/README.md index 7d32da3c97..9d60363602 100644 --- a/packages/subagent/tool-subagent/README.md +++ b/packages/subagent/tool-subagent/README.md @@ -10,7 +10,7 @@ Each plugin instance binds one `provider` to one `toolName`; the model receives A foreground call passes the execution signal through startup and execution, awaits `run.result`, and always awaits `run.dispose()` before returning. Only `completed` returns the canonical `{ kind: 'foreground', runId, output: JsonValue[] }`, rendered as the same final text; abort, refusal, token limit, and other failures become errored tool results without partial output. -With `run_in_background: true`, the route follows the provider's continuation capability and returns canonical `{ kind: 'background', taskId, subagentId? }`. A resumable provider (spawn, fork) delegates to `ctx.subagentControl.startContinuable()`, which owns the durable child id, descriptor snapshot, Task registration, and settle-then-dispose ordering; the result includes `subagentId`, renders as `started subagent as task `, and accepts follow-up messages through the global `send_message` tool. A one-shot provider (ACP) keeps the plain parent-owned task, omits `subagentId`, and renders as `started background subagent task `. Either way a task-owned signal covers pending startup and the child after the starting call returns; `task_kill` and owner disposal abort it, settlement awaits startup rollback or child disposal, and completed final text, abort to `killed`, and other failures to `failed` map identically. The task has no incremental read; generic task tools own later status, collection, cancellation, and notices. See the [background subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md) and the [continuable background subagents Agent Note](../../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md). +With `run_in_background: true`, `backgroundMode` selects the route. `one-shot` registers a plain parent-owned Task and returns canonical `{ kind: 'background', taskId }`, rendered as `started background subagent task `, even when the provider supports resume. `continuable` requires `provider.resume`, calls `ctx.subagents.startContinuable()`, and returns `{ kind: 'background', taskId, subagentId }`, rendered as `started subagent as task `. The optional global `send_message` tool is not required to start continuable work. Either route uses a Task-owned signal, settles only after startup rollback or run disposal, and maps completed final text, abort → `killed`, and other failures → `failed`. Generic task tools own later status, collection, cancellation, and notices. See the [background subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md), the [continuable background subagents Agent Note](../../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md), and the [merged-service Agent Note](../../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md). `toolFilter` changes the child's global tool layer but is not a parent-derived authority ceiling. See the [agent-scope security non-goal](../../../.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md#security-and-authority-are-non-goals). @@ -21,6 +21,7 @@ With `run_in_background: true`, the route follows the provider's continuation ca | `provider` (required) | Provider name (`spawn`, `fork`, `acp`, ...). | | `toolName` | Model-facing name, default `subagent`; distinct for every loaded instance. | | `enableRunInBackground` | Exposes background mode, default `true`; disabling also rejects forced background calls. | +| `backgroundMode` | Background lifecycle policy, default `one-shot`. `continuable` requires provider resume support and returns a durable child id; it does not require the follow-up tool. | | `agentOptions` | Provider-specific child `provider`, `model`, and positive `maxTokens`; the in-process provider treats explicit values as overrides of inherited parent options. | | `persona` | Per-child persona; requires provider `persona` capability. | | `toolFilter` | Per-child global-tool restriction; requires `toolFilter` capability. | @@ -64,7 +65,7 @@ Append-only; newly visible content follows the reusable request prefix and does #### What the model sees -Start returns exactly `started subagent as task ` on a resumable provider, or `started background subagent task ` on a one-shot provider. The generic task surface provides later status, final output, cancellation responses, and notices; `send_message` (from `dsh-tool-subagent-control`) delivers follow-ups to a continuable child. +Start returns exactly `started subagent as task ` in configured continuable mode, or `started background subagent task ` in configured one-shot mode. The generic task surface provides later status, final output, cancellation responses, and notices; an independently loaded `send_message` tool delivers follow-ups to a continuable child. #### Token effect diff --git a/packages/subagent/tool-subagent/README.zh.md b/packages/subagent/tool-subagent/README.zh.md index eadc168fd0..5964c38bd8 100644 --- a/packages/subagent/tool-subagent/README.zh.md +++ b/packages/subagent/tool-subagent/README.zh.md @@ -10,7 +10,7 @@ 前台调用会让执行信号贯穿启动和执行,等待 `run.result`,并且在返回前总会等待 `run.dispose()`。只有 `completed` 会返回规范值 `{ kind: 'foreground', runId, output: JsonValue[] }`,并渲染为相同的最终文本;中止、拒绝、token 上限和其他失败都会变成出错的工具结果,不包含局部输出。 -设置 `run_in_background: true` 后,路由遵循提供方的继续功能,并返回规范值 `{ kind: 'background', taskId, subagentId? }`。可恢复提供方(spawn、fork)会委派给 `ctx.subagentControl.startContinuable()`,由它拥有持久化子 agent ID、描述符快照、Task 注册和先结算后 dispose(资源释放)的顺序;结果包含 `subagentId`,渲染为 `started subagent as task `,并通过全局 `send_message` 工具接收后续消息。一次性提供方 ACP(Agent Client Protocol)保留普通的父级所有任务,省略 `subagentId`,并渲染为 `started background subagent task `。两条路径中,任务拥有的信号都会覆盖待处理的启动阶段和启动调用返回后的子 agent;`task_kill` 和所有者 dispose 会中止它,结算会等待启动回滚或子 agent dispose,然后把完成的最终文本映射为完成、中止映射为 `killed`、其他失败映射为 `failed`。任务不提供增量读取;通用任务工具负责后续状态、收集、取消和通知。见[后台 subagent Agent Note(agent 决策记录)](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md)和[可继续后台 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md)。 +设置 `run_in_background: true` 后,由 `backgroundMode` 选择路由。`one-shot` 会注册普通的父级所有 Task,并返回规范值 `{ kind: 'background', taskId }`;即使提供方支持恢复,也会渲染为 `started background subagent task `。`continuable` 要求 `provider.resume`,调用 `ctx.subagents.startContinuable()`,并返回 `{ kind: 'background', taskId, subagentId }`,渲染为 `started subagent as task `。启动可继续工作不要求加载可选的全局 `send_message` 工具。两条路由都使用 Task 所有的信号,只在启动回滚或 run dispose(资源释放)之后结算,并把完成的最终文本映射为完成、中止映射为 `killed`、其他失败映射为 `failed`。通用任务工具负责后续状态、收集、取消和通知。见[后台 subagent Agent Note(agent 决策记录)](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md)、[可继续后台 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md)和[服务合并 Agent Note](../../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md)。 `toolFilter` 会改变子 agent 的全局工具层,但不是从父级派生的权限上限。见 [agent 作用域的安全非目标](../../../.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md#security-and-authority-are-non-goals)。 @@ -21,6 +21,7 @@ | `provider`(必填) | 提供方名称(`spawn`、`fork`、`acp` 等)。 | | `toolName` | 面向模型的名称,默认 `subagent`;每个已加载实例必须不同。 | | `enableRunInBackground` | 公开后台模式,默认 `true`;禁用时也会拒绝强制后台调用。 | +| `backgroundMode` | 后台生命周期策略,默认 `one-shot`。`continuable` 要求提供方支持恢复并返回持久化子 agent ID;它不要求加载后续消息工具。 | | `agentOptions` | 传给具体提供方的子 agent `provider`、`model` 和正整数 `maxTokens`;进程内提供方会用显式值覆盖继承的父级选项。 | | `persona` | 每个子 agent 独立的 persona;要求提供方具备 `persona` 能力。 | | `toolFilter` | 每个子 agent 独立的全局工具限制;要求提供方具备 `toolFilter` 能力。 | @@ -64,7 +65,7 @@ #### 模型看到的内容 -对于可恢复提供方,启动时精确返回 `started subagent as task `;对于一次性提供方,则返回 `started background subagent task `。通用任务接口提供后续状态、最终输出、取消响应和通知;`send_message`(来自 `dsh-tool-subagent-control`)会把后续消息交付给可继续子 agent。 +在已配置的 continuable 模式下,启动时精确返回 `started subagent as task `;在已配置的 one-shot 模式下,则返回 `started background subagent task `。通用任务接口提供后续状态、最终输出、取消响应和通知;独立加载的 `send_message` 工具会把后续消息交付给可继续子 agent。 #### Token 影响 diff --git a/packages/subagent/tool-subagent/package.json b/packages/subagent/tool-subagent/package.json index d789c9b4f9..353f67ff24 100644 --- a/packages/subagent/tool-subagent/package.json +++ b/packages/subagent/tool-subagent/package.json @@ -31,7 +31,6 @@ "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-subagent": "^0.0.1", - "@deepseek-ai/dsh-subagent-control": "^0.0.1", "@deepseek-ai/dsh-tasks": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", "cordis": "^4.0.0-rc.7" @@ -48,7 +47,6 @@ "@deepseek-ai/dsh-session-persistence": "workspace:^", "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^", "@deepseek-ai/dsh-subagent": "workspace:^", - "@deepseek-ai/dsh-subagent-control": "workspace:^", "@deepseek-ai/dsh-subagent-spawn": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tasks": "workspace:^", diff --git a/packages/subagent/tool-subagent/src/index.ts b/packages/subagent/tool-subagent/src/index.ts index 560e0cb20b..6033bea875 100644 --- a/packages/subagent/tool-subagent/src/index.ts +++ b/packages/subagent/tool-subagent/src/index.ts @@ -1,11 +1,10 @@ /** * Model-facing delegation through one configured `ctx.subagents` provider. * Provider lifecycle controls tool registration and context-sensitive schema - * wording. Foreground calls always dispose the run after collection. A - * background call's route follows the provider's continuation capability: - * a provider with `resume` delegates to `ctx.subagentControl`, which owns the - * durable child id, its descriptor, and the Task-backed activation lifecycle; - * a provider without it (ACP) keeps the one-shot background task. + * wording. Foreground calls always dispose the run after collection. + * Background policy is selected by this plugin's configuration: one-shot + * calls own a plain Task, while continuable calls use + * `ctx.subagents.startContinuable()`. * @module @deepseek-ai/dsh-tool-subagent */ @@ -15,9 +14,8 @@ import { defineTool } from '@deepseek-ai/dsh-tools' import type { AgentOptions } from '@deepseek-ai/dsh-agent' import type { ContentBlock } from '@deepseek-ai/dsh-llm' import type { JsonValue } from '@deepseek-ai/dsh-session' -import { assertSubagentMaxDepth } from '@deepseek-ai/dsh-subagent' +import { assertSubagentMaxDepth, settleRun } from '@deepseek-ai/dsh-subagent' import type { SubagentProvider, SubagentResult, SubagentRun } from '@deepseek-ai/dsh-subagent' -import { settleRun } from '@deepseek-ai/dsh-subagent-control' import type { TaskOutcome } from '@deepseek-ai/dsh-tasks' export const name = 'tool-subagent' @@ -37,6 +35,12 @@ export interface Config { * parameter and reject forced background calls. */ enableRunInBackground?: boolean + /** + * Background execution policy (default `one-shot`). `continuable` requires + * a provider with persisted resume support and returns both child and Task + * ids; follow-up adapters remain independently optional. + */ + backgroundMode?: 'one-shot' | 'continuable' /** * Agent options applied to every child; omitted fields use child-loop defaults. */ @@ -73,6 +77,7 @@ export const Config: z = z.object({ provider: z.string().required(), toolName: z.string().default('subagent'), enableRunInBackground: z.boolean().default(true), + backgroundMode: z.union(['one-shot', 'continuable'] as const).default('one-shot'), // Prevent Schemastery from materializing omitted agentOptions as `{}`. agentOptions: z.object({ provider: z.string(), @@ -191,17 +196,19 @@ export function apply(ctx: Context, config: Config): void { } const wording = providerWording(provider.inheritsParentContext) const backgroundEnabled = config.enableRunInBackground !== false - // The provider's continuation capability decides the background route: a - // resumable provider starts durable, follow-up-able children through the - // control service, while a one-shot provider (ACP) keeps the plain task. - const continuable = provider.resume !== undefined + const continuable = (config.backgroundMode ?? 'one-shot') === 'continuable' + if (continuable && provider.resume === undefined) { + throw new Error( + `tool-subagent: provider "${provider.name}" does not support \`backgroundMode: continuable\``, + ) + } disposeTool = ctx.tools.register(defineTool({ name: config.toolName ?? 'subagent', description: wording.description + (backgroundEnabled ? continuable ? ' Set `run_in_background: true` to start a continuable background subagent: you receive its' - + ' subagent id and a task id; collect the result with `task_output`, stop it with `task_kill`,' - + ' and send follow-up messages with `send_message`.' + + ' stable subagent id and current task id; collect the result with `task_output` and stop it with' + + ' `task_kill`.' : ' Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`.' : ''), parameters: { @@ -220,7 +227,7 @@ export function apply(ctx: Context, config: Config): void { type: 'boolean' as const, description: continuable ? 'Run as a continuable background subagent and return its subagent and task ids; ' - + 'collect with task_output, stop with task_kill, follow up with send_message.' + + 'collect with task_output or stop with task_kill.' : 'Run as a background task and return its id; collect with task_output or stop with task_kill.', }, } : {}, @@ -281,23 +288,7 @@ export function apply(ctx: Context, config: Config): void { throw new Error('run_in_background is disabled for this tool instance (enableRunInBackground: false)') } if (continuable) { - const control = ctx.get('subagentControl') - if (control === undefined) { - throw new Error('continuable background subagents unavailable: load @deepseek-ai/dsh-subagent-control and @deepseek-ai/dsh-tool-tasks') - } - // The schema above tells the model to follow up with - // `send_message`; starting a durable child the model cannot - // continue would make that advertisement false. Sibling load order - // is undetermined at mount, so the check lives at the operation, - // and it resolves in the CALLER's scope so a restriction that - // removes send_message from this agent also blocks the start. - if (ctx.tools.get('send_message', parent) === undefined) { - throw new Error('continuable background subagents unavailable: load @deepseek-ai/dsh-tool-subagent-control (the advertised send_message tool is not registered)') - } - // The control service owns the durable child id, descriptor - // snapshot, Task registration, and settle-then-dispose ordering; a - // synchronous validation failure rejects the call with no Task. - const started = control.startContinuable({ + const started = ctx.subagents.startContinuable({ provider: config.provider, label: args.description, request, diff --git a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts index f631133970..1b45640a94 100644 --- a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts +++ b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts @@ -15,9 +15,7 @@ import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' import SubagentService from '@deepseek-ai/dsh-subagent' import type { SubagentStartRequest } from '@deepseek-ai/dsh-subagent' import LocalTaskService from '@deepseek-ai/dsh-tasks-local' -import SubagentControlService from '@deepseek-ai/dsh-subagent-control' import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn' -import * as ToolSubagentControl from '@deepseek-ai/dsh-tool-subagent-control' import * as ToolTasks from '@deepseek-ai/dsh-tool-tasks' import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' import * as mock from './scripted-provider.ts' @@ -70,6 +68,21 @@ function text(result: { content: { type: string; text?: string }[] }): string { } describe('dsh-tool-subagent', () => { + it('rejects continuable background policy when the configured provider cannot resume', async () => { + let failure: unknown + try { + await setup({ + provider: 'mock', + backgroundMode: 'continuable', + }) + } catch (error: unknown) { + failure = error + } + expect(String(failure)).toContain( + 'provider "mock" does not support `backgroundMode: continuable`', + ) + }) + it('registers a `subagent` tool that delegates to the configured provider and returns its output', async () => { const ctx = await setup({ provider: 'mock' }, { reply: 'child says hi' }) const result = await callSubagent(ctx, { description: 'do a thing', prompt: 'go research X' }) @@ -655,6 +668,47 @@ describe('dsh-tool-subagent background mode', () => { return ctx } + it('keeps a resumable provider one-shot when backgroundMode selects one-shot', async () => { + const ctx = await backgroundSetup({ provider: 'mock' }) + const parent = ownerAgent(ctx, 'sess-parent') + let resumeCalls = 0 + ctx.subagents.registerProvider({ + name: 'resumable', + capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + inheritsParentContext: false, + start: async request => ({ + id: SessionId('one-shot-child'), + localAgent: undefined, + result: Promise.resolve({ + output: [{ type: 'text', text: 'one-shot answer' }], + stopReason: request.signal.aborted ? 'aborted' : 'completed', + }), + dispose: () => Promise.resolve(), + }), + resume: async () => { + resumeCalls += 1 + throw new Error('one-shot policy must not resume') + }, + }) + tool.apply(ctx, { + provider: 'resumable', + toolName: 'subagent_resumable', + backgroundMode: 'one-shot', + maxDepth: 'provider-managed', + }) + + const started = await ctx.tools.execute({ + signal: testToolSignal, + callId: CallId('resumable-one-shot'), + name: 'subagent_resumable', + arguments: { description: 'work', prompt: 'go', run_in_background: true }, + agent: parent, + }) + + expect(text(started)).toBe('started background subagent task subagent-1') + expect(resumeCalls).toBe(0) + }) + it('returns a task id immediately and the answer is collected through task_output', async () => { const ctx = await backgroundSetup({ provider: 'mock', agentOptions: { model: 'child-model' } }, { reply: 'background answer' }) const parent = ownerAgent(ctx, 'sess-parent') @@ -825,8 +879,8 @@ describe('dsh-tool-subagent continuable background mode', () => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) }) - /** Boot the real continuable stack: loop, persistence, spawn, tasks, control. */ - async function continuableSetup(options: { controlTool?: boolean } = {}) { + /** Boot the real continuable stack without any model-facing follow-up adapter. */ + async function continuableSetup() { const ctx = new Context() await mountAgentLoopTestDependencies(ctx) const root = mkdtempSync(path.join(tmpdir(), 'dsh-tool-subagent-continuable-')) @@ -837,9 +891,7 @@ describe('dsh-tool-subagent continuable background mode', () => { await ctx.plugin(SubagentSpawn, { providerName: 'spawn' }) await ctx.plugin(LocalTaskService) await ctx.plugin(ToolTasks, {}) - await ctx.plugin(SubagentControlService) - if (options.controlTool !== false) await ctx.plugin(ToolSubagentControl) - await ctx.plugin(tool, { provider: 'spawn' }) + await ctx.plugin(tool, { provider: 'spawn', backgroundMode: 'continuable' }) ctx.llm.registerAdapter(['mock'], new MockAdapter([ textResponse('continuable answer'), ])) @@ -847,10 +899,10 @@ describe('dsh-tool-subagent continuable background mode', () => { return { ctx, parent } } - it('a resumable provider advertises send_message and returns both ids', async () => { + it('starts a continuable child and returns both ids without send_message', async () => { const { ctx, parent } = await continuableSetup() const schema = ctx.tools.schemas().find(s => s.name === 'subagent')! - expect(schema.description).toContain('send_message') + expect(schema.description).not.toContain('send_message') const started = await callSubagent( ctx, @@ -869,56 +921,6 @@ describe('dsh-tool-subagent continuable background mode', () => { expect(loaded.events.some(event => event.type === 'subagent/descriptor')).toBe(true) }) - it('fails loud when the provider is resumable but the control service is not loaded', async () => { - const ctx = new Context() - await ctx.plugin(SystemPrompt) - await ctx.plugin(ToolRegistry) - await ctx.plugin(SubagentService) - // A resumable provider without ctx.subagentControl. - ctx.subagents.registerProvider({ - name: 'resumable', - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, - inheritsParentContext: false, - start: () => { throw new Error('unreachable') }, - resume: () => { throw new Error('unreachable') }, - }) - await ctx.plugin(tool, { provider: 'resumable', maxDepth: 'provider-managed' }) - - const result = await callSubagent(ctx, { description: 'd', prompt: 'p', run_in_background: true }) - expect(result.isError).toBe(true) - expect(text(result)).toContain('load @deepseek-ai/dsh-subagent-control') - }) - - it('fails loud when the advertised send_message tool is not registered', async () => { - // The schema tells the model to follow up with send_message; starting a - // durable child the model cannot continue would make that false. - const { ctx, parent } = await continuableSetup({ controlTool: false }) - const result = await callSubagent( - ctx, - { description: 'd', prompt: 'p', run_in_background: true }, - { agent: parent }, - ) - expect(result.isError).toBe(true) - expect(text(result)).toContain('load @deepseek-ai/dsh-tool-subagent-control') - // Nothing was started: no Task exists for the parent. - expect(ctx.tasks.list(parent)).toEqual([]) - }) - - it('resolves send_message availability in the CALLER scope, not the global registry', async () => { - // A scoped restriction that keeps this delegation tool but removes - // send_message means this agent cannot execute the promised follow-up; - // the availability check must see the caller's surface. - const { ctx, parent } = await continuableSetup() - parent.ctx.tools.restrict({ deny: ['send_message'] }) - const result = await callSubagent( - ctx, - { description: 'd', prompt: 'p', run_in_background: true }, - { agent: parent }, - ) - expect(result.isError).toBe(true) - expect(text(result)).toContain('load @deepseek-ai/dsh-tool-subagent-control') - expect(ctx.tasks.list(parent)).toEqual([]) - }) }) describe('background preflight failure (no orphaned child, by construction)', () => { diff --git a/packages/subagent/tool-subagent/tsconfig.json b/packages/subagent/tool-subagent/tsconfig.json index a542b520b1..25780c367f 100644 --- a/packages/subagent/tool-subagent/tsconfig.json +++ b/packages/subagent/tool-subagent/tsconfig.json @@ -29,9 +29,6 @@ { "path": "../subagent" }, - { - "path": "../subagent-control" - }, { "path": "../../tasks/tasks" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 80d30aeae9..45790d4591 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -384,9 +384,6 @@ importers: '@deepseek-ai/dsh-subagent': specifier: workspace:^ version: link:../../packages/subagent/subagent - '@deepseek-ai/dsh-subagent-control': - specifier: workspace:^ - version: link:../../packages/subagent/subagent-control '@deepseek-ai/dsh-subagent-fork': specifier: workspace:^ version: link:../../packages/subagent/subagent-fork @@ -737,9 +734,6 @@ importers: '@deepseek-ai/dsh-subagent-acp': specifier: workspace:* version: link:../packages/subagent/subagent-acp - '@deepseek-ai/dsh-subagent-control': - specifier: workspace:* - version: link:../packages/subagent/subagent-control '@deepseek-ai/dsh-subagent-dsh-sdk': specifier: workspace:* version: link:../packages/subagent/subagent-dsh-sdk @@ -4885,6 +4879,12 @@ importers: '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session + '@deepseek-ai/dsh-session-persistence': + specifier: workspace:^ + version: link:../../session-persistence/session-persistence + '@deepseek-ai/dsh-tasks': + specifier: workspace:^ + version: link:../../tasks/tasks '@deepseek-ai/dsh-tools': specifier: workspace:^ version: link:../../core/tools @@ -4932,51 +4932,6 @@ importers: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis - packages/subagent/subagent-control: - devDependencies: - '@deepseek-ai/dsh-agent': - specifier: workspace:^ - version: link:../../core/agent - '@deepseek-ai/dsh-agent-loop': - specifier: workspace:^ - version: link:../../core/agent-loop - '@deepseek-ai/dsh-agent-loop-testkit': - specifier: workspace:^ - version: link:../../support/agent-loop-testkit - '@deepseek-ai/dsh-invariants': - specifier: workspace:^ - version: link:../../support/invariants - '@deepseek-ai/dsh-llm': - specifier: workspace:^ - version: link:../../llm/llm - '@deepseek-ai/dsh-session': - specifier: workspace:^ - version: link:../../core/session - '@deepseek-ai/dsh-session-persistence': - specifier: workspace:^ - version: link:../../session-persistence/session-persistence - '@deepseek-ai/dsh-subagent': - specifier: workspace:^ - version: link:../subagent - '@deepseek-ai/dsh-subagent-fork': - specifier: workspace:^ - version: link:../subagent-fork - '@deepseek-ai/dsh-subagent-spawn': - specifier: workspace:^ - version: link:../subagent-spawn - '@deepseek-ai/dsh-tasks': - specifier: workspace:^ - version: link:../../tasks/tasks - '@deepseek-ai/dsh-tasks-local': - specifier: workspace:^ - version: link:../../tasks/tasks-local - '@deepseek-ai/dsh-tool-tasks': - specifier: workspace:^ - version: link:../../tasks/tool-tasks - cordis: - specifier: ^4.0.0-rc.7 - version: link:../../../vendor/cordis - packages/subagent/subagent-dsh-sdk: dependencies: schemastery: @@ -5184,9 +5139,6 @@ importers: '@deepseek-ai/dsh-subagent': specifier: workspace:^ version: link:../subagent - '@deepseek-ai/dsh-subagent-control': - specifier: workspace:^ - version: link:../subagent-control '@deepseek-ai/dsh-subagent-spawn': specifier: workspace:^ version: link:../subagent-spawn @@ -5238,9 +5190,6 @@ importers: '@deepseek-ai/dsh-subagent': specifier: workspace:^ version: link:../subagent - '@deepseek-ai/dsh-subagent-control': - specifier: workspace:^ - version: link:../subagent-control '@deepseek-ai/dsh-subagent-spawn': specifier: workspace:^ version: link:../subagent-spawn @@ -6510,9 +6459,6 @@ importers: '@deepseek-ai/dsh-subagent-acp': specifier: workspace:^ version: link:../../packages/subagent/subagent-acp - '@deepseek-ai/dsh-subagent-control': - specifier: workspace:^ - version: link:../../packages/subagent/subagent-control '@deepseek-ai/dsh-subagent-fork': specifier: workspace:^ version: link:../../packages/subagent/subagent-fork diff --git a/python/sdk-runtime/package.json b/python/sdk-runtime/package.json index df5d302915..a4d555055d 100644 --- a/python/sdk-runtime/package.json +++ b/python/sdk-runtime/package.json @@ -66,7 +66,6 @@ "@deepseek-ai/dsh-skill-local": "workspace:^", "@deepseek-ai/dsh-subagent": "workspace:^", "@deepseek-ai/dsh-subagent-acp": "workspace:^", - "@deepseek-ai/dsh-subagent-control": "workspace:^", "@deepseek-ai/dsh-subagent-fork": "workspace:^", "@deepseek-ai/dsh-subagent-inprocess": "workspace:^", "@deepseek-ai/dsh-subagent-spawn": "workspace:^", diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index 02215862db..ed46fd687d 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -424,19 +424,11 @@ const SERVICE_ROLES: ServiceRole[] = [ { key: 'subagents', pkg: 'subagent', - title: 'Subagent provider registry', + title: 'Subagent provider and continuation service', mode: 'seam', implementations: ['subagent-spawn', 'subagent-fork', 'subagent-acp'], - consumers: ['tool-subagent', 'tool-ralph'], - note: 'Providers implement transports; tool-subagent exposes configured delegation while tool-ralph requires one fresh structured-output route.', - }, - { - key: 'subagentControl', - pkg: 'subagent', - title: 'Continuable-subagent control service', - mode: 'core', - consumers: ['tool-subagent', 'tool-subagent-control'], - note: 'Binds one durable child session to Task-backed activations over ctx.subagents; tool-subagent starts continuable background children and tool-subagent-control delivers follow-up messages.', + consumers: ['tool-subagent', 'tool-subagent-control', 'tool-ralph'], + note: 'Providers implement transports; the service also owns optional Task-backed continuation orchestration, tool-subagent selects one-shot or continuable delegation, tool-subagent-control delivers follow-ups, and tool-ralph requires one fresh structured-output route.', }, { key: 'tasks', diff --git a/scripts/gen-tool-catalog.ts b/scripts/gen-tool-catalog.ts index 9d3907821a..96d6a97457 100644 --- a/scripts/gen-tool-catalog.ts +++ b/scripts/gen-tool-catalog.ts @@ -28,7 +28,6 @@ import * as WebSearchExa from '@deepseek-ai/dsh-web-search-exa' import * as WebFetchLocal from '@deepseek-ai/dsh-web-fetch-local' import SubagentService from '@deepseek-ai/dsh-subagent' import type { SubagentProvider } from '@deepseek-ai/dsh-subagent' -import SubagentControlService from '@deepseek-ai/dsh-subagent-control' import * as ToolSubagentControl from '@deepseek-ai/dsh-tool-subagent-control' import SkillService from '@deepseek-ai/dsh-skill' import * as SkillLocal from '@deepseek-ai/dsh-skill-local' @@ -108,8 +107,6 @@ function registerCatalogSubagentProvider(ctx: Context, name: string): void { capabilities: { outputSchema: true, depthLimit: true, toolFilter: true, persona: true }, inheritsParentContext: false, start: () => Promise.reject(new Error('tool-catalog provider cannot start a child')), - // Presence marks the continuation capability, so tool-subagent harvests - // its shipped continuable background wording (spawn/fork are resumable). resume: () => Promise.reject(new Error('tool-catalog provider cannot resume a child')), } ctx.subagents.registerProvider(provider) @@ -388,13 +385,12 @@ const TOOL_PACKAGES: ToolPackage[] = [ pkg: '@deepseek-ai/dsh-tool-subagent-control', dir: 'tool-subagent-control', source: 'packages/subagent/tool-subagent-control/src/index.ts', - requires: ['ctx.tools', 'ctx.subagentControl'], - writes: ['tool/call', 'tool/result', 'child session events through the control service'], + requires: ['ctx.tools', 'ctx.subagents'], + writes: ['tool/call', 'tool/result', 'child session events through ctx.subagents'], async mount(ctx) { await ctx.plugin(SubagentService) await ctx.plugin(LocalTaskService) await ctx.plugin(AgentRegistry) - await ctx.plugin(SubagentControlService) await ctx.plugin(ToolSubagentControl) }, note: diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index 9e8f68177d..10c652f704 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -1102,7 +1102,7 @@ { "doc": "docs/core-data-structures/subagent.md", "symbol": "CoordinatorMessageSource", - "source": "packages/subagent/subagent-control/src/index.ts" + "source": "packages/subagent/subagent/src/continuation.ts" }, { "doc": "docs/core-data-structures/subagent.md", diff --git a/tsconfig.host.json b/tsconfig.host.json index 3f46bf5ee6..2e5c6ea08c 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -178,7 +178,6 @@ { "path": "./packages/support/loader-smoke" }, { "path": "./packages/support/llm-mock-server" }, { "path": "./packages/subagent/subagent" }, - { "path": "./packages/subagent/subagent-control" }, { "path": "./packages/subagent/tool-subagent" }, { "path": "./packages/subagent/tool-subagent-control" }, { "path": "./packages/subagent/subagent-inprocess" },