fix(subagent): address codex review round 3

- Make host-user authority unforgeable. `{ kind: 'user' }` was a bare
  discriminant, so any plugin holding `ctx.subagents` — including
  model-generated cordis_mount code, which the advanced ACP composition ships
  alongside continuable subagents — could construct it and skip the
  direct-parent check for any known child id. It now carries an opaque grant
  that only SubagentService.userAuthority() mints, which composition hands to
  trusted host adapters; a model-facing tool uses parent authority from its own
  execution context.
- Reconcile a delivery discarded inside its own admission window. An enqueue
  listener that cancels fires the discard before followup() returns, so the
  discard listener could not clear an id it had not seen; submit() retained it
  and residency stayed `running` until an explicit drain.
- Recheck the caller signal after materialization. An abort landing between
  publication and inbox acceptance still submitted the prompt and returned both
  ids; it now rolls the child back.
- Stop promising the model transcript access that no shipped continuable config
  mounts. The tools now state only that a background child does not report back.
- Restate the implemented note as shipped state rather than a proposal, so it
  works as current authority.
This commit is contained in:
Dudu-0223
2026-07-30 16:48:42 +08:00
committed by Tianyi Cui
parent cbaceb73a9
commit 7428cdf41e
29 changed files with 297 additions and 141 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md
2026-07-28-continuable-subagent-conversations.md: 5ab17ea13d15d66afab4fee6766b082dd207b8a3
2026-07-28-continuable-subagent-conversations.zh.md: eb14ebcec9682432682f6b5b4d8399f35b6882a2
2026-07-28-continuable-subagent-conversations.md: a56da8ad389964dcc873a722a66e335062811f37
2026-07-28-continuable-subagent-conversations.zh.md: 71089cd71ae6fda7712ffcc614852a483e13e3ba

View File

@@ -4,13 +4,13 @@ Status: implemented
English | [中文](2026-07-28-continuable-subagent-conversations.zh.md)
This proposal would replace the Task-backed continuation manager from [Continuable background subagents](../../implemented/feature/2026-07-21-continuable-background-subagents.md). It retains the single `ctx.subagents` service from [Merge subagent control into the subagent service](../../implemented/simplification/2026-07-26-merge-subagent-control-service.md) and the intent-named `followup` operation from [Intent-named subagent continuation operations](../../implemented/simplification/2026-07-27-intent-named-subagent-continuation-operations.md).
This record replaces the Task-backed continuation manager from [Continuable background subagents](../../implemented/feature/2026-07-21-continuable-background-subagents.md). It retains the single `ctx.subagents` service from [Merge subagent control into the subagent service](../../implemented/simplification/2026-07-26-merge-subagent-control-service.md) and the intent-named `followup` operation from [Intent-named subagent continuation operations](../../implemented/simplification/2026-07-27-intent-named-subagent-continuation-operations.md).
## Problem
The continuation manager currently makes one Task, one provider execution, and one result boundary the same object lifetime. Task settlement disposes the child Agent, Task completion injects the completion notice, and later input reconstructs another Agent. This couples a generic background-work abstraction to conversation delivery even though a continuable subagent already has a Session and an Agent inbox.
The previous continuation manager made one Task, one provider execution, and one result boundary the same object lifetime. Task settlement disposed the child Agent, Task completion injected the completion notice, and later input reconstructed another Agent. That coupled a generic background-work abstraction to conversation delivery even though a continuable subagent already has a Session and an Agent inbox.
Giving queued parent requests to the continuation manager and user messages to the Agent creates two FIFOs with no single ordering authority. Giving both to Tasks instead duplicates the Agent loop's admission, cancellation, and quiescence machinery. `Agent.whenIdle()` cannot recover a per-request Task result because one running interval may drain multiple queued turns, and broad `Agent.cancel()` cannot remove one queued request exactly.
Giving queued parent requests to the continuation manager and user messages to the Agent would create two FIFOs with no single ordering authority. Giving both to Tasks instead duplicated the Agent loop's admission, cancellation, and quiescence machinery. `Agent.whenIdle()` cannot recover a per-request Task result because one running interval may drain multiple queued turns, and broad `Agent.cancel()` cannot remove one queued request exactly.
The runtime lifetime is also wider than one turn. A subagent can finish its own turn while a child it created is still running. Disposing the parent runtime at that point removes the Agent that still owns descendant teardown. Keeping every historical subagent resident instead would make memory use unbounded.
@@ -30,7 +30,7 @@ persisted Session
An Activation is one residency epoch for a reconstructed child Agent. It may execute multiple FIFO turns and remain resident while waiting for descendants. It is not a request, result, cancellation, or Task boundary.
The continuation manager owns activation admission, authority checks, the live ownership graph, cold resume, and child-first disposal. The Agent loop owns all turn ordering and execution. The proposal creates no Task for a continuable subagent, no Activation FIFO, and no queued Activation state.
The continuation manager owns activation admission, authority checks, the live ownership graph, cold resume, and child-first disposal. The Agent loop owns all turn ordering and execution. No continuable subagent has a Task, an Activation FIFO, or queued Activation state.
### Materialization and public operations
@@ -54,7 +54,7 @@ The Session owns the stable child identity, transcript, direct-parent lineage, d
An idle historical Session has no `AgentHandle`. The first authorized `next-turn` delivery resumes an Activation from the persisted Session and submits the message to its inbox. A user-authorized cold resume does not load the historical parent Agent. A parent-originated resume uses the exact live parent Agent for authorization and, when that parent has an Activation, ownership; it never uses the parent for reconstruction.
The Activation directly owns the published `AgentHandle` until it settles, while the manager's private activation-owner scope is its structural Cordis owner. The continuable path creates no intermediate result-bearing execution wrapper, including `SubagentRun`; one-shot delegation remains unchanged and outside this lifecycle. Remote providers are outside the MVP and require a separate Activation ownership contract when introduced. Historical Sessions consume no runtime memory after their Activation is disposed.
The Activation directly owns the published `AgentHandle` until it settles, while the manager's private activation-owner scope is its structural Cordis owner. The continuable path creates no intermediate result-bearing execution wrapper, including `SubagentRun`; one-shot delegation remains unchanged and outside this lifecycle. Remote providers are out of scope here and require a separate Activation ownership contract when introduced. Historical Sessions consume no runtime memory after their Activation is disposed.
### Activation lifecycle
@@ -107,7 +107,7 @@ Child release occurs only after the child Agent is quiescent, every child of tha
A user cold-resume creates an Activation without adding it to the historical parent's `ownedChildren`. If the direct parent later submits work to that live Activation and is itself continuation-managed, admission establishes ownership before enqueueing the message; a non-continuation parent remains outside the waiting graph.
The MVP retains ownership until the child Activation is disposed. A later refinement may release a request-scoped lease earlier, but it would require an exact turn-completion correlation that this Task-free proposal deliberately does not add.
Ownership is retained until the child Activation is disposed. A later refinement may release a request-scoped lease earlier, but it would require an exact turn-completion correlation that this Task-free proposal deliberately does not add.
Top-level teardown is host-owned rather than represented as another Activation. The host first asks the manager to enter draining synchronously, which rejects new creation, resume, and delivery admission, then disposes every live Activation forest in child-first order and awaits all `AgentHandle.dispose()` calls. Only after that drain settles may the host dispose top-level Agents and the manager scope. Manager unload uses the same drain and includes user-resumed Activations without live owners.
@@ -115,13 +115,13 @@ The activation-owner scope exists because ordinary Cordis owner effects unwind i
### Deferred report delivery
The MVP exposes no `report` tool and provides no child-to-parent content delivery or automatic parent wakeup. The durable child Session remains the source of the child's detailed output.
This version exposes no `report` tool and provides no child-to-parent content delivery or automatic parent wakeup. The durable child Session remains the source of the child's detailed output.
A later proposal may add an ordinary model-facing `report(output)` tool that can be called zero or multiple times in one turn. Its delivery policy may distinguish quiet parent injection from waking the parent; recipient selection, acknowledgement, durability, and retry semantics are deferred with that tool. Adding report delivery does not require another Activation state or execution queue.
### Deferred steering
The MVP exposes no subagent steering operation. Parent and user continuation messages always open later FIFO turns, so the continuation layer stores no current-turn controller and adds no controller-aware Agent admission seam.
This version exposes no subagent steering operation. Parent and user continuation messages always open later FIFO turns, so the continuation layer stores no current-turn controller and adds no controller-aware Agent admission seam.
A later host UI may expose separate **Steer** and **Follow up** actions. User steering would be strict and live-only: it may call the existing Agent steering path only while the Activation accepts a next step, must reject otherwise, and must never fall back to queueing or cold resume. Exposing parent steering to a model-facing tool remains a separate design because distinct tool names express intent but do not establish whether the parent may modify a user-controlled turn.
@@ -129,13 +129,13 @@ A later host UI may expose separate **Steer** and **Follow up** actions. User st
Authority is supplied by a trusted host interaction or an exact live Agent tool context. `MessageSource` and `senderSessionId` are durable provenance after admission, not caller-controlled authority.
The MVP authorizes the host user and the durable child's direct parent. Parent authorization checks `SessionHeader.parentSession` against the authenticated parent Agent before registering the child in that parent's `ownedChildren`. Other Agents, ancestors, teams, and workflows remain rejected until an explicit authority protocol exists.
This version authorizes the host user and the durable child's direct parent. Parent authorization checks `SessionHeader.parentSession` against the authenticated parent Agent before registering the child in that parent's `ownedChildren`. Other Agents, ancestors, teams, and workflows remain rejected until an explicit authority protocol exists.
User authority may cold-resume a child without its parent. Parent-originated delivery requires the parent to be live when admitted and keeps it live through the ownership relationship.
### Durability, disposal, and recovery
Without Tasks there is no `task_output`, `task_kill`, Task status, per-message result promise, or public subagent cancellation operation. The caller signal can abort start or follow-up only before inbox acceptance. After acceptance, neither parent nor user can cancel the message, turn, or Activation through `ctx.subagents`; `Agent.cancel()` remains a lower-level Agent capability that this MVP does not expose through the subagent service.
Without Tasks there is no `task_output`, `task_kill`, Task status, per-message result promise, or public subagent cancellation operation. The caller signal can abort start or follow-up only before inbox acceptance. After acceptance, neither parent nor user can cancel the message, turn, or Activation through `ctx.subagents`; `Agent.cancel()` remains a lower-level Agent capability that this version does not expose through the subagent service.
Host and manager teardown remains the lifecycle-wide stop path. It closes admission, disposes every live Activation forest child-first, and preserves the durable Sessions.
@@ -147,9 +147,9 @@ Session and descriptor persistence survive restart. Activation state, Agent inbo
### Scope
The MVP covers continuable in-process children and leaves one-shot delegation unchanged. Remote providers require a separate Activation handle with equivalent authenticated control and child-first quiescence contracts before they can support the same behavior.
This version covers continuable in-process children and leaves one-shot delegation unchanged. Remote providers require a separate Activation handle with equivalent authenticated control and child-first quiescence contracts before they can support the same behavior.
The MVP adds no subagent steering operation, report tool, child-to-parent content delivery, automatic parent wakeup, durable mailbox, cross-process lease, automatic replay of interrupted inbox work, team authority, workflow authority, public subagent cancellation operation, new live-Activation or descendant limit, or runtime cache. Existing delegation-depth policy remains unchanged.
It adds no subagent steering operation, report tool, child-to-parent content delivery, automatic parent wakeup, durable mailbox, cross-process lease, automatic replay of interrupted inbox work, team authority, workflow authority, public subagent cancellation operation, new live-Activation or descendant limit, or runtime cache. Existing delegation-depth policy remains unchanged.
## Alternatives considered
@@ -159,9 +159,9 @@ The MVP adds no subagent steering operation, report tool, child-to-parent conten
**Dispose the Agent while waiting.** Reconstructing a parent while its child still belongs to the previous process-local ownership graph would require a durable ownership and teardown protocol. Retaining the `AgentHandle` only for the unfinished graph preserves child-first teardown without keeping settled history resident.
**Let the provider create, resume, or deliver through an Agent handle.** Initial providers own only `prepareContinuable()` and its detached creation-spec distinction: whether a child begins fresh or with a parent prefix. The manager must call `ctx.agents.create()` through its private activation-owner scope so that scope is a structural owner of every handle. A persisted in-process Session already contains the initial prefix and generic reconstruction descriptor, while delivery belongs to the Agent inbox. Giving providers any later handle, `SubagentRun`, or message ownership would preserve a seam with no MVP behavior to own and would complicate user cold resume with an unnecessary live-parent input.
**Let the provider create, resume, or deliver through an Agent handle.** Initial providers own only `prepareContinuable()` and its detached creation-spec distinction: whether a child begins fresh or with a parent prefix. The manager must call `ctx.agents.create()` through its private activation-owner scope so that scope is a structural owner of every handle. A persisted in-process Session already contains the initial prefix and generic reconstruction descriptor, while delivery belongs to the Agent inbox. Giving providers any later handle, `SubagentRun`, or message ownership would preserve a seam with no shipped behavior to own and would complicate user cold resume with an unnecessary live-parent input.
**Add report delivery to the MVP.** A repeatable model-facing tool is compatible with this lifecycle, but quiet versus waking delivery, recipient selection, acknowledgement, durability, and retry behavior are independent product choices. Deferring the tool keeps the first version focused on conversation admission and residency without constraining that later policy.
**Add report delivery now.** A repeatable model-facing tool is compatible with this lifecycle, but quiet versus waking delivery, recipient selection, acknowledgement, durability, and retry behavior are independent product choices. Deferring the tool keeps the first version focused on conversation admission and residency without constraining that later policy.
**Treat `SessionHeader.parentSession` as live ownership.** Durable lineage does not prove that the historical parent currently owns the child. Membership in the live parent's `ownedChildren` records the process-local relationship without changing durable provenance.
@@ -169,7 +169,7 @@ The MVP adds no subagent steering operation, report tool, child-to-parent conten
**Maintain a separate queue for parent messages.** A second FIFO creates ambiguous ordering against user messages already accepted by the Agent. A single Agent inbox gives both origins one observable order.
**Expose subagent steering in the MVP.** User steering can be a strict live-only host action, but parent steering needs current-turn controller state to protect a user-controlled turn. Queueing every first-version continuation avoids that state and its admission race. A later UI can add a distinct user-only action without changing follow-up ordering.
**Expose subagent steering now.** User steering can be a strict live-only host action, but parent steering needs current-turn controller state to protect a user-controlled turn. Queueing every first-version continuation avoids that state and its admission race. A later UI can add a distinct user-only action without changing follow-up ordering.
**Return a subagent-specific delivery route.** Labels such as `started`, `queued`, and `resumed` duplicate Activation and inbox state without giving the caller an independent result. Reusing `MessageId` and the existing inbox events keeps delivery correlation on the Agent contract that owns it.
@@ -189,14 +189,14 @@ The implementation pins these behaviors:
- `followup()` accepts only trusted parent or user authority; durable message provenance cannot authorize delivery.
- Parent and user continuation messages always use `Agent.followup()` and share its inbox FIFO, including when one origin queues behind the other or the child already has an open turn.
- `ctx.subagents.followup()` and its `send_message` adapter return only the accepted `MessageId`; the continuation layer accepts no delivery target and defines no subagent-specific route result.
- The MVP exposes no public subagent cancellation operation; caller signals stop start and follow-up only before inbox acceptance, while host and manager teardown retains child-first global cleanup.
- The MVP exposes no subagent steering operation or current-turn controller state.
- This version exposes no public subagent cancellation operation; caller signals stop start and follow-up only before inbox acceptance, while host and manager teardown retains child-first global cleanup.
- This version exposes no subagent steering operation or current-turn controller state.
- An idle Agent with live owned children yields a `waiting` Activation whose `AgentHandle` remains retained.
- A `next-turn` delivered to `waiting` wakes the same Activation; delivery after completed disposal cold-resumes a new Activation.
- Every continuation-managed parent Activation disposes only after all directly owned child Activations complete `AgentHandle` disposal; top-level Agents do not join the waiting graph.
- Final Activation settlement treats only `ctx.sessions.flush(child.session) === true` as durability confirmation; `false` and rejection report `DURABILITY_FAILED`, still dispose the child handle, and still release parent ownership so durability failure cannot leak a `waiting` Activation.
- Host and manager teardown synchronously enter draining, reject new materialization and delivery, stop manager-owned outward notifications, dispose every snapshotted live Activation forest child-first, await every branch despite individual failures, and only then dispose top-level Agents and the manager scope; a private activation-owner scope preserves this order against Cordis effect unwinding, and one memoized disposal promise per Activation makes concurrent normal settlement idempotent.
- The MVP exposes no `report` tool, child-to-parent content delivery, or automatic parent wakeup.
- This version exposes no `report` tool, child-to-parent content delivery, or automatic parent wakeup.
- Session logs reconstruct only messages that were actually written, with their admitted provenance; inbox-accepted but unlogged messages have no restart guarantee.
- No continuable-subagent path creates or depends on a Task, `TaskId`, Task completion notice, Task cancellation, or intermediate result-bearing execution wrapper.
- Unit coverage pins the `startContinuable()` inbox-acceptance return boundary, complete rollback for each pre-acceptance failure, caller-signal ownership on both sides of acceptance, and the absence of automatic replay for accepted-but-unlogged messages.
@@ -207,12 +207,12 @@ The implementation pins these behaviors:
Removing Tasks gives up generic background-work inspection, result collection, and exact Task cancellation. If those product features become requirements, they need a request ticket or inbox capability that does not reintroduce a second execution queue.
Retaining an Activation while descendants run consumes Agent resources proportional to the unfinished ownership graph. The existing delegation-depth policy still bounds nesting, but the MVP adds no live-Activation or total-descendant limit; settled historical Sessions retain no `AgentHandle`.
Retaining an Activation while descendants run consumes Agent resources proportional to the unfinished ownership graph. The existing delegation-depth policy still bounds nesting, but this version adds no live-Activation or total-descendant limit; settled historical Sessions retain no `AgentHandle`.
The process-local inbox and ownership graph do not coordinate two harness processes. Deployments allowing concurrent access to one persistence store still require a durable lease and mailbox protocol.
Without report delivery, completing a child turn neither sends its content to nor wakes the historical parent. The output remains in the durable child Session until a caller inspects that transcript or submits another authorized turn. A later report tool may add quiet or waking delivery without changing the Activation lifecycle.
Queueing every continuation message means a parent cannot correct an in-progress child turn immediately; the correction runs as the next turn. A later user-only UI steering action may reduce that latency without introducing parent-versus-user controller policy into the MVP.
Queueing every continuation message means a parent cannot correct an in-progress child turn immediately; the correction runs as the next turn. A later user-only UI steering action may reduce that latency without introducing parent-versus-user controller policy here.
A failed final durability checkpoint allows the runtime ownership graph to drain but leaves the persisted child state missing or stale. The failure is observable as `DURABILITY_FAILED`; retry and repair require a separate recovery design.

View File

@@ -4,13 +4,13 @@ Status: implemented
[English](2026-07-28-continuable-subagent-conversations.md) | 中文
提案将取代[可继续的后台 subagent](../../implemented/feature/2026-07-21-continuable-background-subagents.md)中由 Task 支撑的继续执行管理器。提案保留[将 subagent 控制合并到 subagent 服务](../../implemented/simplification/2026-07-26-merge-subagent-control-service.md)确立的单一 `ctx.subagents` 服务,以及[以意图命名的 subagent 继续执行操作](../../implemented/simplification/2026-07-27-intent-named-subagent-continuation-operations.md)确立的 `followup` 操作。
记录取代[可继续的后台 subagent](../../implemented/feature/2026-07-21-continuable-background-subagents.md)中由 Task 支撑的继续执行管理器。保留[将 subagent 控制合并到 subagent 服务](../../implemented/simplification/2026-07-26-merge-subagent-control-service.md)确立的单一 `ctx.subagents` 服务,以及[以意图命名的 subagent 继续执行操作](../../implemented/simplification/2026-07-27-intent-named-subagent-continuation-operations.md)确立的 `followup` 操作。
## 问题
继续执行管理器目前让一个 Task、一次提供方执行和一个结果边界共享同一生命周期。Task 结算会 dispose资源释放child AgentTask 完成会注入完成通知,后续输入则重建另一个 Agent。这使通用后台工作抽象与会话投递耦合而可继续 subagent 已经具备会话和 Agent inbox。
以前的继续执行管理器让一个 Task、一次提供方执行和一个结果边界共享同一生命周期。Task 结算会 dispose资源释放child AgentTask 完成会注入完成通知,后续输入则重建另一个 Agent。这使通用后台工作抽象与会话投递耦合,而可继续 subagent 已经具备会话和 Agent inbox。
如果继续执行管理器为 parent 请求排队,而 Agent 接收用户消息,系统就会出现两个 FIFO且没有唯一的顺序权威。如果两种消息都交给 Task系统又会重复 agent loop智能体循环已有的准入、取消和完全停稳机制。`Agent.whenIdle()` 无法恢复单项请求的 Task 结果,因为一个运行区间可能清空多个排队轮次;宽泛的 `Agent.cancel()` 也不能精确移除一项排队请求。
如果继续执行管理器为 parent 请求排队,而 Agent 接收用户消息,系统就会出现两个 FIFO且没有唯一的顺序权威。而把两种消息都交给 Task重复 agent loop智能体循环已有的准入、取消和完全停稳机制。`Agent.whenIdle()` 无法恢复单项请求的 Task 结果,因为一个运行区间可能清空多个排队轮次;宽泛的 `Agent.cancel()` 也不能精确移除一项排队请求。
运行时生命周期也比单个轮次更长。subagent 可能已经结束自身轮次,但它创建的 child 仍在运行。此时 dispose parent 运行时,会移除仍负责后代拆卸的 Agent。反之如果让所有历史 subagent 始终驻留,内存使用就会失去上界。
@@ -30,7 +30,7 @@ persisted Session
激活是重建 child Agent 的一次驻留周期。它可以执行多个 FIFO 轮次,并在等待后代时保持驻留。它不是请求、结果、取消或 Task 边界。
继续执行管理器负责激活准入、权限检查、在线所有权图、冷恢复和 child-first dispose。Agent loop 负责全部轮次排序与执行。本提案不会为可继续 subagent 创建 Task、激活 FIFO 或 queued 激活状态。
继续执行管理器负责激活准入、权限检查、在线所有权图、冷恢复和 child-first dispose。Agent loop 负责全部轮次排序与执行。没有任何可继续 subagent 拥有 Task、激活 FIFO 或 queued 激活状态。
### 物化与公开操作
@@ -54,7 +54,7 @@ inbox 接受消息前发生任何失败,操作都会在不返回任何 id 的
空闲的历史会话没有 `AgentHandle`。第一条通过鉴权的 `next-turn` 投递会根据持久化会话恢复激活,并将消息提交到其 inbox。经用户授权的冷恢复不会加载历史 parent Agent。parent 发起的恢复使用经过身份认证的确切在线 parent Agent 执行鉴权;当该 parent 有激活时,还使用它建立所有权,但绝不使用 parent 执行重建。
激活作为消费方会直接持有已发布的 `AgentHandle` 直至结算,而管理器的私有 activation-owner 作用域则是其 Cordis 结构化所有者。可继续 subagent 路径不创建任何中间的带结果执行包装层,包括 `SubagentRun`;一次性委派保持不变,且不属于该生命周期。远程提供方不在 MVP 范围内,引入时需要单独的激活所有权契约。激活 dispose 后,历史会话不消耗运行时内存。
激活作为消费方会直接持有已发布的 `AgentHandle` 直至结算,而管理器的私有 activation-owner 作用域则是其 Cordis 结构化所有者。可继续 subagent 路径不创建任何中间的带结果执行包装层,包括 `SubagentRun`;一次性委派保持不变,且不属于该生命周期。远程提供方不在此处的范围内,引入时需要单独的激活所有权契约。激活 dispose 后,历史会话不消耗运行时内存。
### 激活生命周期
@@ -107,7 +107,7 @@ Agent inbox 是唯一队列。每条继续执行消息都使用 `Agent.followup(
用户冷恢复会创建一次激活,但不会将其加入历史 parent 的 `ownedChildren`。如果直接 parent 随后向这个在线激活提交工作,且该 parent 自身由继续执行管理器管理,准入过程会在消息入队前建立所有权;非继续执行 parent 仍位于等待图之外。
MVP 会一直保留所有权,直至 child 激活完成 dispose。后续改进可以更早释放限定到请求的 lease但这需要精确关联轮次完成而本 Task-free 提案特意不增加该机制。
系统会一直保留所有权,直至 child 激活完成 dispose。后续改进可以更早释放限定到请求的 lease但这需要精确关联轮次完成而本 Task-free 提案特意不增加该机制。
顶层拆卸由宿主负责,而不表示为另一次激活。宿主首先要求管理器同步进入 draining拒绝新的创建、恢复和投递准入然后按 child-first 顺序 dispose 整个在线激活森林,并等待全部 `AgentHandle.dispose()` 调用。只有该 drain 结算后,宿主才能 dispose 顶层 Agent 和管理器作用域。管理器卸载使用相同的 drain并涵盖由用户恢复且没有在线 owner 的激活。
@@ -115,13 +115,13 @@ activation-owner 作用域之所以存在,是因为普通 Cordis owner effect
### 延后的报告投递
MVP 不暴露 `report` 工具,也不提供从 child 到 parent 的内容投递或自动唤醒 parent。持久化 child 会话仍是 child 详细输出的来源。
本版本不暴露 `report` 工具,也不提供从 child 到 parent 的内容投递或自动唤醒 parent。持久化 child 会话仍是 child 详细输出的来源。
后续提案可以增加一个普通的面向模型 `report(output)` 工具;模型在一个轮次中可以调用它零次或多次。其投递策略可以区分静默注入 parent 与唤醒 parent接收方选择、确认、持久性和重试语义均与该工具一并延后决定。增加报告投递无需引入另一个激活状态或执行队列。
### 延后的 steering中途引导
MVP 不暴露 subagent steering 操作。parent 和用户的继续执行消息始终开启后续 FIFO 轮次,因此继续执行层不存储当前轮次控制方,也不新增能够感知控制方的 Agent 准入 seam。
本版本不暴露 subagent steering 操作。parent 和用户的继续执行消息始终开启后续 FIFO 轮次,因此继续执行层不存储当前轮次控制方,也不新增能够感知控制方的 Agent 准入 seam。
后续宿主 UI 可以分别暴露 **Steer****Follow up** 操作。用户 steering 必须严格且仅限在线使用:只有当激活接受下一步骤时,它才能调用现有的 Agent steering 路径;其他情况必须拒绝,而且绝不能转为排队或冷恢复。是否通过面向模型的工具暴露 parent steering 仍需单独设计,因为不同的工具名称可以表达意图,却不能确定 parent 是否可以修改由用户控制的轮次。
@@ -129,13 +129,13 @@ MVP 不暴露 subagent steering 操作。parent 和用户的继续执行消息
权限来自可信宿主交互或确切的在线 Agent 工具上下文。`MessageSource``senderSessionId` 是准入后的持久化来源信息,不是由调用方控制的权限。
MVP 授权宿主用户和持久化 child 的直接 parent。系统会根据经过身份认证的 parent Agent 检查 `SessionHeader.parentSession`,然后才将 child 注册到该 parent 的 `ownedChildren`。其他 Agent、祖先、团队和工作流仍被拒绝直至系统具备显式权限协议。
本版本授权宿主用户和持久化 child 的直接 parent。系统会根据经过身份认证的 parent Agent 检查 `SessionHeader.parentSession`,然后才将 child 注册到该 parent 的 `ownedChildren`。其他 Agent、祖先、团队和工作流仍被拒绝直至系统具备显式权限协议。
用户权限可以在 parent 不在线时冷恢复 child。由 parent 发起的投递要求 parent 在准入时在线,并通过所有权关系使其继续在线。
### 持久性、dispose 与恢复
没有 Task 后,系统不再提供 `task_output``task_kill`、Task 状态、逐消息结果 promise 或公开 subagent 取消操作。调用方 signal 只能在 inbox 接受消息前中止 start 或 follow-up。消息被接受后parent 和用户都不能通过 `ctx.subagents` 取消该消息、轮次或激活;`Agent.cancel()` 仍是底层 Agent 能力,但本 MVP 不通过 subagent 服务暴露它。
没有 Task 后,系统不再提供 `task_output``task_kill`、Task 状态、逐消息结果 promise 或公开 subagent 取消操作。调用方 signal 只能在 inbox 接受消息前中止 start 或 follow-up。消息被接受后parent 和用户都不能通过 `ctx.subagents` 取消该消息、轮次或激活;`Agent.cancel()` 仍是底层 Agent 能力,但本版本不通过 subagent 服务暴露它。
宿主和管理器拆卸仍是覆盖整个生命周期的停止路径。它会关闭准入,按 child-first 顺序 dispose 每个在线激活森林,并保留持久化会话。
@@ -147,9 +147,9 @@ MVP 授权宿主用户和持久化 child 的直接 parent。系统会根据经
### 范围
MVP 覆盖可继续的进程内 child一次性委派保持不变。远程提供方必须具备单独的激活 handle以及等价的认证控制与 child-first 完全停稳契约,才能支持同样的行为。
本版本覆盖可继续的进程内 child一次性委派保持不变。远程提供方必须具备单独的激活 handle以及等价的认证控制与 child-first 完全停稳契约,才能支持同样的行为。
MVP 不新增 subagent steering 操作、报告工具、从 child 到 parent 的内容投递、自动唤醒 parent、持久化邮箱、跨进程 lease、中断 inbox 工作的自动回放、团队权限、工作流权限、公开 subagent 取消操作、新的在线激活数量或后代总数限制,以及运行时缓存。现有委派深度策略保持不变。
不新增 subagent steering 操作、报告工具、从 child 到 parent 的内容投递、自动唤醒 parent、持久化邮箱、跨进程 lease、中断 inbox 工作的自动回放、团队权限、工作流权限、公开 subagent 取消操作、新的在线激活数量或后代总数限制,以及运行时缓存。现有委派深度策略保持不变。
## 曾考虑的替代方案
@@ -159,9 +159,9 @@ MVP 不新增 subagent steering 操作、报告工具、从 child 到 parent 的
**等待期间 dispose Agent。** child 仍属于上一个进程内所有权图时重建 parent需要持久化所有权与拆卸协议。只为尚未完成的所有权图保留 `AgentHandle`,可以在不让已结算历史驻留的前提下,保留 child-first 拆卸。
**让提供方通过 Agent handle 创建、恢复 child 或投递消息。** 初始提供方只持有 `prepareContinuable()` 及其分离式创建规格这一项差异child 是全新启动,还是带有 parent 前缀。管理器必须通过私有 activation-owner 作用域自行调用 `ctx.agents.create()`,使该作用域成为每个 handle 的结构化所有者。持久化的进程内会话已经包含初始前缀及通用重建描述符,消息投递则属于 Agent inbox。让提供方持有任何后续 handle、`SubagentRun` 或消息所有权,会保留一条没有 MVP 行为可承载的 seam还会因不必要的在线 parent 输入使用户冷恢复更加复杂。
**让提供方通过 Agent handle 创建、恢复 child 或投递消息。** 初始提供方只持有 `prepareContinuable()` 及其分离式创建规格这一项差异child 是全新启动,还是带有 parent 前缀。管理器必须通过私有 activation-owner 作用域自行调用 `ctx.agents.create()`,使该作用域成为每个 handle 的结构化所有者。持久化的进程内会话已经包含初始前缀及通用重建描述符,消息投递则属于 Agent inbox。让提供方持有任何后续 handle、`SubagentRun` 或消息所有权,会保留一条没有已发布行为可承载的 seam还会因不必要的在线 parent 输入使用户冷恢复更加复杂。
**在 MVP 中增加报告投递。** 可重复调用的面向模型工具与该生命周期兼容,但静默投递还是唤醒投递、接收方选择、确认、持久性和重试行为都是独立的产品决策。延后该工具,可以让首个版本专注于会话准入与驻留,又不限制后续策略。
**现在就增加报告投递。** 可重复调用的面向模型工具与该生命周期兼容,但静默投递还是唤醒投递、接收方选择、确认、持久性和重试行为都是独立的产品决策。延后该工具,可以让首个版本专注于会话准入与驻留,又不限制后续策略。
**将 `SessionHeader.parentSession` 视为在线所有权。** 持久化谱系不能证明历史 parent 当前持有 child。在线 parent 的 `ownedChildren` 成员关系会记录进程内关系,而不改变持久化来源。
@@ -169,7 +169,7 @@ MVP 不新增 subagent steering 操作、报告工具、从 child 到 parent 的
**为 parent 消息维护单独队列。** 第二个 FIFO 会让它和 Agent 已接受的用户消息之间顺序不明确。单个 Agent inbox 为两种来源提供唯一且可观察的顺序。
**在 MVP 中暴露 subagent steering。** 用户 steering 可以是严格且仅限在线使用的宿主操作,但 parent steering 需要当前轮次控制方状态,以保护由用户控制的轮次。首个版本将每条继续执行消息都排队,可以避免引入该状态及其准入竞争。后续 UI 可以新增一项仅限用户的独立操作,而不改变 follow-up 排序。
**现在就暴露 subagent steering。** 用户 steering 可以是严格且仅限在线使用的宿主操作,但 parent steering 需要当前轮次控制方状态,以保护由用户控制的轮次。首个版本将每条继续执行消息都排队,可以避免引入该状态及其准入竞争。后续 UI 可以新增一项仅限用户的独立操作,而不改变 follow-up 排序。
**返回 subagent 专属的投递路由。** `started``queued``resumed` 等标签重复了激活与 inbox 状态,却没有给调用方提供独立结果。复用 `MessageId` 和现有 inbox 事件,可以让投递关联继续由其所属的 Agent 契约承载。
@@ -189,14 +189,14 @@ MVP 不新增 subagent steering 操作、报告工具、从 child 到 parent 的
- `followup()` 只接受可信 parent 或用户权限;持久化消息来源信息不能授权投递。
- Parent 和用户的继续执行消息始终使用 `Agent.followup()` 并共享其 inbox FIFO包括一种来源排在另一种来源之后以及 child 已有开放轮次的情况。
- `ctx.subagents.followup()` 及其 `send_message` 适配器只返回已接受的 `MessageId`;继续执行层不接受投递 target也不定义 subagent 专属路由结果。
- MVP 不暴露公开 subagent 取消操作;调用方 signal 只能在 inbox 接受消息前停止 start 和 follow-up宿主和管理器拆卸则保留 child-first 全局清理。
- MVP 不暴露 subagent steering 操作或当前轮次控制方状态。
- 本版本不暴露公开 subagent 取消操作;调用方 signal 只能在 inbox 接受消息前停止 start 和 follow-up宿主和管理器拆卸则保留 child-first 全局清理。
- 本版本不暴露 subagent steering 操作或当前轮次控制方状态。
- 带有在线所持 child 的空闲 Agent 会产生 `waiting` 激活,其 `AgentHandle` 继续保留。
-`waiting` 投递 `next-turn` 会唤醒同一个激活;完成 dispose 后投递消息会冷恢复新激活。
- 每个由继续执行管理器管理的 parent 激活只会在直接持有的所有 child 激活完成 `AgentHandle` dispose 后进行 dispose顶层 Agent 不加入等待图。
- 激活最终结算时,只有 `ctx.sessions.flush(child.session) === true` 才确认持久性;`false` 和 rejection 会报告 `DURABILITY_FAILED`,但仍会 dispose child handle 并释放 parent 所有权,使持久性失败不会泄漏 `waiting` 激活。
- 宿主和管理器拆卸会同步进入 draining拒绝新的物化和投递停止由管理器负责的对外通知按 child-first 顺序 dispose 处于快照中的整个在线激活森林,即使个别分支失败也会等待所有分支,之后才 dispose 顶层 Agent 和管理器作用域;私有 activation-owner 作用域会确保 Cordis effect 的逆序撤销不破坏该顺序,每次激活使用一个记忆化的 dispose promise使并发的正常结算保持幂等。
- MVP 不暴露 `report` 工具,不提供从 child 到 parent 的内容投递,也不自动唤醒 parent。
- 本版本不暴露 `report` 工具,不提供从 child 到 parent 的内容投递,也不自动唤醒 parent。
- 会话日志只能根据准入来源重建实际写入的消息;已被 inbox 接受但未写入日志的消息没有重启保证。
- 可继续 subagent 路径不创建或依赖 Task、`TaskId`、Task 完成通知、Task 取消或中间的带结果执行包装层。
- 单元覆盖固定 `startContinuable()` 在 inbox 接受消息时的返回边界、每条接受前失败路径的完整回滚、接受前后两个阶段的调用方 signal 所有权,以及已接受但未写入日志的消息不会自动回放。
@@ -207,12 +207,12 @@ MVP 不新增 subagent steering 操作、报告工具、从 child 到 parent 的
移除 Task 会放弃通用后台工作检查、结果收集和精确 Task 取消。如果这些产品功能成为需求,就需要不会重新引入第二条执行队列的请求 ticket 或 inbox 能力。
在后代运行期间保留激活,会按尚未完成所有权图的规模消耗 Agent 资源。现有委派深度策略仍会限制嵌套层级,但 MVP 不新增在线激活数量或后代总数限制;已结算的历史会话不保留 `AgentHandle`
在后代运行期间保留激活,会按尚未完成所有权图的规模消耗 Agent 资源。现有委派深度策略仍会限制嵌套层级,但本版本不新增在线激活数量或后代总数限制;已结算的历史会话不保留 `AgentHandle`
进程内 inbox 和所有权图无法协调两个 harness 进程。允许多个进程并发访问同一持久化存储的部署,仍需要持久化 lease 和邮箱协议。
没有报告投递时,完成 child 轮次既不会把内容发送给历史 parent也不会唤醒它。输出会保留在持久化 child 会话中,直至调用方检查该 transcript 或提交另一个经过授权的轮次。后续报告工具可以增加静默投递或唤醒投递,而无需改变激活生命周期。
将每条继续执行消息排队,意味着 parent 无法立即纠正正在进行的 child 轮次;纠正操作会在下一个轮次执行。后续仅限用户的 UI steering 操作可以缩短该延迟,而无需在 MVP 中引入 parent 与用户之间的控制方策略。
将每条继续执行消息排队,意味着 parent 无法立即纠正正在进行的 child 轮次;纠正操作会在下一个轮次执行。后续仅限用户的 UI steering 操作可以缩短该延迟,而无需在引入 parent 与用户之间的控制方策略。
最终持久性检查点失败时,运行时所有权图仍可完成 drain但持久化 child 状态会缺失或陈旧。该失败会以 `DURABILITY_FAILED` 的形式被观测到;重试与修复需要单独的恢复设计。

View File

@@ -794,7 +794,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:141`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:143`](../../packages/subagent/subagent/src/index.ts)
### `subagent/provider-added` — emit
@@ -811,7 +811,7 @@ A provider became resolvable in the registry.
Types: [SubagentProvider](../core-data-structures/subagent.md)
Source: [`packages/subagent/subagent/src/index.ts:115`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:117`](../../packages/subagent/subagent/src/index.ts)
### `subagent/provider-removed` — emit
@@ -826,7 +826,7 @@ A provider left the registry. Accepted runs remain holder-owned.
'subagent/provider-removed'(name: string): void
```
Source: [`packages/subagent/subagent/src/index.ts:121`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:123`](../../packages/subagent/subagent/src/index.ts)
### `subagent/start` — emit
@@ -848,7 +848,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:132`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:134`](../../packages/subagent/subagent/src/index.ts)
## `system-prompt/*`

View File

@@ -1980,6 +1980,15 @@ async startContinuable(spec: ContinuableStartSpec): Promise<ContinuableStart>
*/
async followup( authority: SubagentAuthority, childId: SessionId, content: ContentBlock[], options: SubagentFollowupOptions, ): Promise<MessageId>
/**
* Host-user authority for continuable operations, which may continue any
* durable child without its parent. A composition passes this only to a
* trusted host adapter carrying real human interaction; a model-facing tool
* uses `{ kind: 'parent', agent }` from its own execution context instead.
* @returns the authority a host adapter supplies to {@link followup}.
*/
userAuthority(): SubagentAuthority
/**
* Read one durable child's live residency state.
* @param childId - durable child session id.
@@ -2033,7 +2042,7 @@ async start(name: string, request: SubagentStartRequest): Promise<SubagentRun>
Types: [ActivationState](../core-data-structures/subagent.md) · [ContentBlock](../core-data-structures/core.md) · [ContinuableStart](../core-data-structures/subagent.md) · [ContinuableStartSpec](../core-data-structures/subagent.md) · [MessageId](../core-data-structures/core.md) · [SessionId](../core-data-structures/core.md) · [SubagentAuthority](../core-data-structures/subagent.md) · [SubagentFollowupOptions](../core-data-structures/subagent.md) · [SubagentProvider](../core-data-structures/subagent.md) · [SubagentRun](../core-data-structures/subagent.md) · [SubagentStartRequest](../core-data-structures/subagent.md)
Source: [`packages/subagent/subagent/src/index.ts:174`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:176`](../../packages/subagent/subagent/src/index.ts)
## `ctx.subprocess` — `SubprocessService` (abstract seam)

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/core-data-structures/subagent.md
subagent.md: a58ecf13ba1f5df0e8e35c793eaf9aefc1e8a900
subagent.zh.md: 541eace7fc6c8ae10ee22639680918e12d7762b3
subagent.md: ceff3586bf6724bd6f47b71e9fb737361a2830f8
subagent.zh.md: aa39ea382fe1e2a52b6ee794cfa71d8abc945da3

View File

@@ -123,7 +123,7 @@ persisted Session
The Agent inbox is the only queue. Every continuation message becomes one `Agent.followup()` FIFO turn, so parent and user messages share one observable order and a follow-up cannot redirect a turn already underway. Successful delivery returns the accepted `MessageId`; the existing `agent/inbox/enqueue`, `agent/inbox/dequeue`, and `agent/inbox/discard` events remain the message-lifecycle observations, and the continuation layer defines no subagent-specific delivery route.
Authority is supplied by a trusted host interaction or an exact live Agent tool context. The parent variant is admitted only when the authenticated Agent is the durable child's direct parent recorded in `SessionHeader.parentSession`; only a trusted host adapter can supply user authority. `MessageSource` and `senderSessionId` are durable provenance after admission and grant no authority — the optional model-facing tool uses `CoordinatorMessageSource`, while a host adapter uses `{ kind: 'user' }`. User authority may cold-resume a child without loading its historical parent.
Authority is supplied by a trusted host interaction or an exact live Agent tool context. The parent variant is admitted only when the authenticated Agent is the durable child's direct parent recorded in `SessionHeader.parentSession`. User authority carries an opaque grant that only `SubagentService.userAuthority()` mints, so a caller cannot claim it by writing the discriminant — a plugin holding `ctx.subagents`, including model-generated mount code, would otherwise bypass the direct-parent check for any known child id. `MessageSource` and `senderSessionId` are durable provenance after admission and grant no authority — the optional model-facing tool uses `CoordinatorMessageSource`, while a host adapter uses `{ kind: 'user' }`. User authority may cold-resume a child without loading its historical parent.
For both operations the caller signal owns lookup, materialization, and admission only until inbox acceptance. Afterwards the manager owns the Activation independently: later caller cancellation neither cancels the accepted turn nor disposes the child, and the seam exposes no public subagent cancellation or steering operation.
@@ -149,8 +149,14 @@ interface CoordinatorMessageSource {
type SubagentAuthority =
/** The exact live parent Agent whose tool context is making the call. */
| { readonly kind: 'parent'; readonly agent: Agent }
/** A trusted host adapter acting for the human user. */
| { readonly kind: 'user' }
/**
* A trusted host adapter acting for the human user. The `grant` must be the
* exact token {@link SubagentService.userAuthority} minted, so a discriminant
* alone cannot claim this authority — any plugin holding `ctx.subagents`,
* including model-generated mount code, could otherwise forge it and bypass
* the direct-parent check.
*/
| { readonly kind: 'user'; readonly grant: UserAuthorityGrant }
```
```ts type-equiv

View File

@@ -149,8 +149,14 @@ interface CoordinatorMessageSource {
type SubagentAuthority =
/** The exact live parent Agent whose tool context is making the call. */
| { readonly kind: 'parent'; readonly agent: Agent }
/** A trusted host adapter acting for the human user. */
| { readonly kind: 'user' }
/**
* A trusted host adapter acting for the human user. The `grant` must be the
* exact token {@link SubagentService.userAuthority} minted, so a discriminant
* alone cannot claim this authority — any plugin holding `ctx.subagents`,
* including model-generated mount code, could otherwise forge it and bypass
* the direct-parent check.
*/
| { readonly kind: 'user'; readonly grant: UserAuthorityGrant }
```
```ts type-equiv

View File

@@ -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:141`](../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:115`](../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:121`](../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:132`](../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:143`](../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:117`](../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:123`](../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:134`](../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`) | - |

View File

@@ -1151,7 +1151,7 @@ The registered tool name is the load-time `toolName` config (default `subagent`)
### `send_message`
Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. The subagent does not reply to you — read its transcript by its id to see what it did. A failure means the message was NOT delivered.
Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. The subagent does not reply to you, so use this only to give it more work. A failure means the message was NOT delivered.
```json
{

View File

@@ -110,7 +110,7 @@ interface ToolArgsMap {
/** Maximum number of lines to return. Defaults to 2000. */
limit?: number;
} & Record<string, JsonValue>;
/** Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. The subagent does not reply to you — read its transcript by its id to see what it did. A failure means the message was NOT delivered. */
/** Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. The subagent does not reply to you, so use this only to give it more work. A failure means the message was NOT delivered. */
send_message: {
/** The subagent id returned when the background subagent was started. */
subagent_id: string;
@@ -122,22 +122,22 @@ interface ToolArgsMap {
/** The exact skill name from the available skills list. */
name: string;
} & Record<string, JsonValue>;
/** 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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back to you, so read its transcript by that id, or send it more work 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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back, so use this only for work whose result you do not need returned; `send_message` sends it more work. */
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 background subagent that keeps its conversation and return its subagent id; send it more work with send_message. */
/** Run as a background subagent that keeps its conversation and return its subagent id. It does not report its result back; send it more work with send_message. */
run_in_background?: boolean;
} & Record<string, JsonValue>;
/** 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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back to you, so read its transcript by that id, or send it more work 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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back, so use this only for work whose result you do not need returned; `send_message` sends it more work. */
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 background subagent that keeps its conversation and return its subagent id; send it more work with send_message. */
/** Run as a background subagent that keeps its conversation and return its subagent id. It does not report its result back; send it more work with send_message. */
run_in_background?: boolean;
} & Record<string, JsonValue>;
/** Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops. */

View File

@@ -239,7 +239,7 @@
},
{
"name": "send_message",
"description": "Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. The subagent does not reply to you — read its transcript by its id to see what it did. A failure means the message was NOT delivered.",
"description": "Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. The subagent does not reply to you, so use this only to give it more work. A failure means the message was NOT delivered.",
"parameters": {
"type": "object",
"properties": {
@@ -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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back to you, so read its transcript by that id, or send it more work 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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back, so use this only for work whose result you do not need returned; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -290,7 +290,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return its subagent id; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return its subagent id. It does not report its result back; send it more work with send_message."
}
},
"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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back to you, so read its transcript by that id, or send it more work 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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back, so use this only for work whose result you do not need returned; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -315,7 +315,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return its subagent id; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return its subagent id. It does not report its result back; send it more work with send_message."
}
},
"required": [

View File

@@ -182,7 +182,7 @@
},
{
"name": "send_message",
"description": "Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. The subagent does not reply to you — read its transcript by its id to see what it did. A failure means the message was NOT delivered.",
"description": "Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. The subagent does not reply to you, so use this only to give it more work. A failure means the message was NOT delivered.",
"parameters": {
"type": "object",
"properties": {
@@ -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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back to you, so read its transcript by that id, or send it more work 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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back, so use this only for work whose result you do not need returned; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -233,7 +233,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return its subagent id; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return its subagent id. It does not report its result back; send it more work with send_message."
}
},
"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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back to you, so read its transcript by that id, or send it more work 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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back, so use this only for work whose result you do not need returned; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -258,7 +258,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return its subagent id; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return its subagent id. It does not report its result back; send it more work with send_message."
}
},
"required": [

View File

@@ -93,7 +93,7 @@ interface ToolArgsMap {
/** Maximum number of lines to return. Defaults to 2000. */
limit?: number;
} & Record<string, JsonValue>;
/** Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. The subagent does not reply to you — read its transcript by its id to see what it did. A failure means the message was NOT delivered. */
/** Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. The subagent does not reply to you, so use this only to give it more work. A failure means the message was NOT delivered. */
send_message: {
/** The subagent id returned when the background subagent was started. */
subagent_id: string;
@@ -105,22 +105,22 @@ interface ToolArgsMap {
/** The exact skill name from the available skills list. */
name: string;
} & Record<string, JsonValue>;
/** 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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back to you, so read its transcript by that id, or send it more work 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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back, so use this only for work whose result you do not need returned; `send_message` sends it more work. */
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 background subagent that keeps its conversation and return its subagent id; send it more work with send_message. */
/** Run as a background subagent that keeps its conversation and return its subagent id. It does not report its result back; send it more work with send_message. */
run_in_background?: boolean;
} & Record<string, JsonValue>;
/** 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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back to you, so read its transcript by that id, or send it more work 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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back, so use this only for work whose result you do not need returned; `send_message` sends it more work. */
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 background subagent that keeps its conversation and return its subagent id; send it more work with send_message. */
/** Run as a background subagent that keeps its conversation and return its subagent id. It does not report its result back; send it more work with send_message. */
run_in_background?: boolean;
} & Record<string, JsonValue>;
/** Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops. */

View File

@@ -198,7 +198,7 @@
},
{
"name": "send_message",
"description": "Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. The subagent does not reply to you — read its transcript by its id to see what it did. A failure means the message was NOT delivered.",
"description": "Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. The subagent does not reply to you, so use this only to give it more work. A failure means the message was NOT delivered.",
"parameters": {
"type": "object",
"properties": {
@@ -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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back to you, so read its transcript by that id, or send it more work 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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back, so use this only for work whose result you do not need returned; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -249,7 +249,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return its subagent id; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return its subagent id. It does not report its result back; send it more work with send_message."
}
},
"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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back to you, so read its transcript by that id, or send it more work 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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back, so use this only for work whose result you do not need returned; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -274,7 +274,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return its subagent id; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return its subagent id. It does not report its result back; send it more work with send_message."
}
},
"required": [

View File

@@ -161,7 +161,7 @@
},
{
"name": "send_message",
"description": "Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. The subagent does not reply to you — read its transcript by its id to see what it did. A failure means the message was NOT delivered.",
"description": "Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. The subagent does not reply to you, so use this only to give it more work. A failure means the message was NOT delivered.",
"parameters": {
"type": "object",
"properties": {
@@ -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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back to you, so read its transcript by that id, or send it more work 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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back, so use this only for work whose result you do not need returned; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -212,7 +212,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return its subagent id; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return its subagent id. It does not report its result back; send it more work with send_message."
}
},
"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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back to you, so read its transcript by that id, or send it more work 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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back, so use this only for work whose result you do not need returned; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -237,7 +237,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return its subagent id; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return its subagent id. It does not report its result back; send it more work with send_message."
}
},
"required": [

View File

@@ -161,7 +161,7 @@
},
{
"name": "send_message",
"description": "Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. The subagent does not reply to you — read its transcript by its id to see what it did. A failure means the message was NOT delivered.",
"description": "Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. The subagent does not reply to you, so use this only to give it more work. A failure means the message was NOT delivered.",
"parameters": {
"type": "object",
"properties": {
@@ -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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back to you, so read its transcript by that id, or send it more work 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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back, so use this only for work whose result you do not need returned; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -416,7 +416,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return its subagent id; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return its subagent id. It does not report its result back; send it more work with send_message."
}
},
"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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back to you, so read its transcript by that id, or send it more work 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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back, so use this only for work whose result you do not need returned; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -441,7 +441,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return its subagent id; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return its subagent id. It does not report its result back; send it more work with send_message."
}
},
"required": [

View File

@@ -161,7 +161,7 @@
},
{
"name": "send_message",
"description": "Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. The subagent does not reply to you — read its transcript by its id to see what it did. A failure means the message was NOT delivered.",
"description": "Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. The subagent does not reply to you, so use this only to give it more work. A failure means the message was NOT delivered.",
"parameters": {
"type": "object",
"properties": {
@@ -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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back to you, so read its transcript by that id, or send it more work 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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back, so use this only for work whose result you do not need returned; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -212,7 +212,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return its subagent id; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return its subagent id. It does not report its result back; send it more work with send_message."
}
},
"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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back to you, so read its transcript by that id, or send it more work 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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back, so use this only for work whose result you do not need returned; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -237,7 +237,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return its subagent id; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return its subagent id. It does not report its result back; send it more work with send_message."
}
},
"required": [

View File

@@ -161,7 +161,7 @@
},
{
"name": "send_message",
"description": "Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. The subagent does not reply to you — read its transcript by its id to see what it did. A failure means the message was NOT delivered.",
"description": "Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. The subagent does not reply to you, so use this only to give it more work. A failure means the message was NOT delivered.",
"parameters": {
"type": "object",
"properties": {
@@ -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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back to you, so read its transcript by that id, or send it more work 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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back, so use this only for work whose result you do not need returned; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -212,7 +212,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return its subagent id; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return its subagent id. It does not report its result back; send it more work with send_message."
}
},
"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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back to you, so read its transcript by that id, or send it more work 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 background subagent that keeps its conversation: you receive its subagent id and it works on its own. It does not report back, so use this only for work whose result you do not need returned; `send_message` sends it more work.",
"parameters": {
"type": "object",
"properties": {
@@ -237,7 +237,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background subagent that keeps its conversation and return its subagent id; send it more work with send_message."
"description": "Run as a background subagent that keeps its conversation and return its subagent id. It does not report its result back; send it more work with send_message."
}
},
"required": [

View File

@@ -892,6 +892,10 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
signature: 'async followup( authority: SubagentAuthority, childId: SessionId, content: ContentBlock[], options: SubagentFollowupOptions, ): Promise<MessageId>',
jsDoc: '/**\n * Deliver one later message to a continuable child as its next FIFO turn. A\n * resident child\'s Agent inbox accepts it directly (waking a `waiting`\n * Activation), while an absent one is cold-resumed from its persisted\n * Session. The Agent inbox is the only queue, so parent and user messages\n * share one observable order.\n * @param authority - trusted parent or user authority for this delivery.\n * @param childId - durable child session id.\n * @param content - user-role content to deliver.\n * @param options - durable provenance and caller cancellation, which stops the\n * operation only before inbox acceptance.\n * @returns the accepted message\'s inbox id.\n * @throws when continuation services are unavailable, authority is rejected,\n * or the message was not admitted.\n */',
},
{
signature: 'userAuthority(): SubagentAuthority',
jsDoc: '/**\n * Host-user authority for continuable operations, which may continue any\n * durable child without its parent. A composition passes this only to a\n * trusted host adapter carrying real human interaction; a model-facing tool\n * uses `{ kind: \'parent\', agent }` from its own execution context instead.\n * @returns the authority a host adapter supplies to {@link followup}.\n */',
},
{
signature: 'activationState(childId: SessionId): ActivationState | undefined',
jsDoc: '/**\n * Read one durable child\'s live residency state.\n * @param childId - durable child session id.\n * @returns its Activation state, or `undefined` when no Activation is live.\n * @throws when continuation services are unavailable.\n */',
@@ -2693,7 +2697,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'SubagentAuthority',
declaration: 'export type SubagentAuthority = {\n readonly kind: \'parent\';\n readonly agent: Agent;\n} | {\n readonly kind: \'user\';\n};',
declaration: 'export type SubagentAuthority = {\n readonly kind: \'parent\';\n readonly agent: Agent;\n} | {\n readonly kind: \'user\';\n readonly grant: UserAuthorityGrant;\n};',
},
{
name: 'SubagentCapabilities',
@@ -3035,6 +3039,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
name: 'TypertTypeModel',
declaration: 'export interface TypertTypeModel {\n readonly name: string;\n readonly declaration: string;\n}',
},
{
name: 'UserAuthorityGrant',
declaration: 'export type UserAuthorityGrant = {\n readonly __brand: \'SubagentUserAuthority\';\n};',
},
{
name: 'UserInteractionProvider',
declaration: 'export interface UserInteractionProvider {\n ask(request: AskUserQuestionRequest): Promise<AskUserQuestionAnswer>;\n}',

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent/README.md
README.md: fc1eecb7d22c45377d5525ef0247bcf369a441a8
README.zh.md: 762a027324bc40f159129c3cd4a438d2265fa32b
README.md: 6a8016dc71d928c1770cc0769f99d2cb53c6b035
README.zh.md: 53f553bd2747bebac0f2d42ac80ad8b6eb660c45

View File

@@ -31,12 +31,13 @@ Multiple providers may coexist under different names. This lets a deployment exp
| `start(name, request)` | Validate an ordinary caller request, then await the provider until a real one-shot child is ready. Fulfillment returns a holder-owned `SubagentRun`; rejection means the provider has already cleaned every partial startup resource. Continuable children never enter through this operation. |
| `startContinuable(spec)` | Establish one durable continuable child and deliver its initial prompt. Resolves with `{ childId, messageId }` when the child's inbox accepts that prompt, without waiting for the turn to start or for the message to reach the Session log; any earlier failure rejects with no ids and rolls the child back entirely. Requires `ctx.agents`, session persistence, and a provider with the `prepareContinuable` capability. |
| `followup(authority, childId, content, { source, signal })` | Deliver one later message to a continuable child as its next FIFO turn, matching `Agent.followup()` terminology, and return the accepted `AgentMessageId`. A resident child's inbox accepts it directly (waking a `waiting` Activation); an absent one cold-resumes from its persisted Session. Requires `ctx.agents`; cold resume also requires session persistence. |
| `userAuthority()` | Mint the host-user authority a trusted adapter passes to `followup()`. Composition hands this only to a host carrying real human interaction; a model-facing tool uses its own `{ kind: 'parent', agent }` instead. |
| `activationState(childId)` | Read one durable child's live residency state (`running`, `waiting`, or `settled`), or `undefined` when no Activation is live. |
| `drainContinuable()` | Close continuable admission synchronously, then dispose every live Activation forest child-first. A host calls this before disposing top-level agents so no descendant outlives the runtime that owns its teardown. An aggregate error surfaces after every branch settles when any failed. |
`SubagentStartRequest.signal` is required and is the canonical cancellation channel for a one-shot `start`. 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, or set a child persona. For a continuable start or follow-up, the caller signal owns lookup, materialization, and admission only until inbox acceptance; afterward the manager owns the Activation independently, so later caller cancellation neither cancels the accepted turn nor disposes the child.
Authority for continuable operations comes from a trusted host interaction or an exact live Agent tool context: `SubagentAuthority` is `{ kind: 'parent', agent }` or `{ kind: 'user' }`. The `source` on a follow-up is durable provenance retained on the delivered message and grants no authority. Parent authority requires the exact live direct parent recorded in the child's durable header; user authority may continue any child, and may cold-resume it without loading its historical parent.
Authority for continuable operations comes from a trusted host interaction or an exact live Agent tool context: `SubagentAuthority` is `{ kind: 'parent', agent }` or `{ kind: 'user', grant }`, whose grant only `userAuthority()` mints so the discriminant alone cannot claim it. The `source` on a follow-up is durable provenance retained on the delivered message and grants no authority. Parent authority requires the exact live direct parent recorded in the child's durable header; user authority may continue any child, and may cold-resume it without loading its historical parent.
Same-process requests, descriptors, results, and event payloads are trusted typed values borrowed as immutable. The service does not clone or freeze them; serialization and hostile-input validation belong at actual process, worker, persistence, and model boundaries.

View File

@@ -31,12 +31,13 @@ subagent seam 允许一个 agent智能体通过具名提供方把工作委
| `start(name, request)` | 校验普通调用方请求,然后等待提供方,直到真实的一次性子 agent 就绪。兑现时返回由持有方拥有的 `SubagentRun`;拒绝表示提供方已清理所有局部启动资源。可继续子 agent 绝不通过此操作进入。 |
| `startContinuable(spec)` | 建立一个持久化可继续子 agent并投递其初始提示词。子 agent 的 inbox 接受该提示词时,兑现为 `{ childId, messageId }`,无需等待轮次开始或消息写入 Session 日志;此前任何失败都会以无 id 拒绝,并完全回滚该子 agent。要求 `ctx.agents`、会话持久化以及具备 `prepareContinuable` 能力的提供方。 |
| `followup(authority, childId, content, { source, signal })` | 将一条后续消息作为可继续子 agent 的下一个 FIFO 轮次投递,术语与 `Agent.followup()` 一致,并返回被接受的 `AgentMessageId`。驻留中的子 agent 由其 inbox 直接接受(唤醒处于 `waiting` 的 Activation不驻留的则从其持久化 Session 冷恢复。要求 `ctx.agents`;冷恢复还要求会话持久化。 |
| `userAuthority()` | 铸造可信 host 适配器传给 `followup()` 的 host 用户权限。组合装配仅将其交给承载真实人类交互的 host面向模型的工具改用自身执行上下文的 `{ kind: 'parent', agent }`。 |
| `activationState(childId)` | 读取某个持久化子 agent 的实时驻留状态(`running``waiting``settled`);无实时 Activation 时返回 `undefined`。 |
| `drainContinuable()` | 同步关闭可继续准入,然后以子先于父的顺序 dispose 每一个实时 Activation 森林。host 会在 dispose 顶层 agent 之前调用它,使任何后代都不会比拥有其拆卸职责的运行时存活更久。任一分支失败时,会在所有分支结算后抛出聚合错误。 |
`SubagentStartRequest.signal` 是必填项,也是一次性 `start` 的规范取消通道。发布前中止会使 `start()` 在回滚后拒绝;发布后中止会取消实时子 agent。请求还可以选择模型、要求结构化输出、限制委派深度、约束子 agent 工具或设置子 agent persona。对于可继续启动或后续操作调用方信号只在 inbox 接受之前掌管查找、物化和准入;此后由管理器独立拥有 Activation因此调用方后续取消既不会取消已接受的轮次也不会 dispose 子 agent。
可继续操作的权限来自可信的 host 交互或准确的实时 Agent 工具上下文:`SubagentAuthority``{ kind: 'parent', agent }``{ kind: 'user' }`。后续操作上的 `source` 是保留在所投递消息上的持久化来源,不授予任何权限。父级权限要求准确匹配子 agent 持久化 header 中记录的实时直接父级;用户权限可以继续任何子 agent并且可以在不加载其历史父级的情况下将其冷恢复。
可继续操作的权限来自可信的 host 交互或准确的实时 Agent 工具上下文:`SubagentAuthority``{ kind: 'parent', agent }``{ kind: 'user', grant }`——其 grant 仅由 `userAuthority()` 铸造,因此仅凭判别式无法声明该权限。后续操作上的 `source` 是保留在所投递消息上的持久化来源,不授予任何权限。父级权限要求准确匹配子 agent 持久化 header 中记录的实时直接父级;用户权限可以继续任何子 agent并且可以在不加载其历史父级的情况下将其冷恢复。
同进程请求、描述符、结果和事件 payload 都是以不可变方式借用的可信类型值。服务不会克隆或冻结它们序列化和不可信输入校验属于真实的进程、worker、持久化和模型边界。

View File

@@ -61,8 +61,20 @@ declare module '@deepseek-ai/dsh-llm' {
export type SubagentAuthority =
/** The exact live parent Agent whose tool context is making the call. */
| { readonly kind: 'parent'; readonly agent: Agent }
/** A trusted host adapter acting for the human user. */
| { readonly kind: 'user' }
/**
* A trusted host adapter acting for the human user. The `grant` must be the
* exact token {@link SubagentService.userAuthority} minted, so a discriminant
* alone cannot claim this authority — any plugin holding `ctx.subagents`,
* including model-generated mount code, could otherwise forge it and bypass
* the direct-parent check.
*/
| { readonly kind: 'user'; readonly grant: UserAuthorityGrant }
/**
* Opaque proof that a caller obtained user authority from the service rather
* than constructing it. Only {@link SubagentService.userAuthority} mints one.
*/
export type UserAuthorityGrant = { readonly __brand: 'SubagentUserAuthority' }
/** What a caller asks for when starting a continuable background child. */
export interface ContinuableStartSpec {
@@ -245,6 +257,8 @@ export class SubagentContinuationManager {
constructor(
private readonly ctx: Context,
private readonly host: ContinuationHost,
/** The single token that proves host-user authority for this manager. */
private readonly userGrant: UserAuthorityGrant,
) {
// Ordinary Cordis owner effects unwind in reverse registration order, which
// cannot express the dynamic child graph. Register the private scope's
@@ -325,6 +339,10 @@ export class SubagentContinuationManager {
composition: { persona: request.persona, toolFilter: request.toolFilter },
signal: spec.signal,
})
// Materialization published the Activation; an abort landing in that
// window — a `subagent/start` listener can cancel synchronously — must
// roll the child back instead of opening its first turn.
await this.rollbackIfAborted(activation, spec.signal)
return this.submit(activation, request.prompt, { kind: 'user' }, { kind: 'parent', agent: parent })
})
return { childId, messageId }
@@ -494,9 +512,25 @@ export class SubagentContinuationManager {
composition: { persona: descriptor.persona, toolFilter: descriptor.toolFilter },
signal: options.signal,
})
await this.rollbackIfAborted(activation, options.signal)
return this.submit(activation, content, options.source, authority)
}
/**
* Dispose a freshly materialized Activation when the caller signal won the
* handoff between publication and inbox acceptance, so an aborted operation
* never leaves a resident child.
* @param activation - the just-published Activation.
* @param signal - the caller signal owning admission until acceptance.
*/
private async rollbackIfAborted(activation: Activation, signal: AbortSignal): Promise<void> {
if (!signal.aborted) return
/* v8 ignore next -- the swallow only covers a disposal fault during rollback, which
* must not mask the caller's abort as the operation's failure. */
await this.dispose(activation).catch(() => undefined)
signal.throwIfAborted()
}
/**
* Create or resume the child Agent through the private activation-owner
* scope, install the handle in a fresh Activation, and register ownership on
@@ -686,7 +720,17 @@ export class SubagentContinuationManager {
childId: SessionId,
parentSession: SessionId | undefined,
): void {
if (authority.kind === 'user') return
if (authority.kind === 'user') {
// Identity, not shape: a forged discriminant must not skip the
// direct-parent check for an arbitrary known child id.
if (authority.grant !== this.userGrant) {
throw new SubagentError(
`subagent "${childId}" delivery presented an invalid user-authority grant`,
'UNAUTHORIZED',
)
}
return
}
const parent = authority.agent
if (this.ctx.agents.get(parent.id) !== parent) {
throw new SubagentError(

View File

@@ -54,6 +54,7 @@ import SubagentContinuationManager from './continuation.ts'
import type {
ActivationObserver,
ActivationState,
UserAuthorityGrant,
ContinuableStart,
ContinuableStartSpec,
SubagentAuthority,
@@ -94,6 +95,7 @@ export type { ChildComposition } from './child-agent.ts'
export type {
ActivationObserver,
ActivationState,
UserAuthorityGrant,
ContinuableStart,
ContinuableStartSpec,
CoordinatorMessageSource,
@@ -174,6 +176,15 @@ export interface SubagentRunEndInfo {
export class SubagentService extends Service {
private providers = new Map<string, SubagentProvider>()
private continuations: SubagentContinuationManager | undefined
/**
* The process-local proof of host-user authority. Minted here so the value is
* unguessable and unforgeable: a caller must obtain it from
* {@link userAuthority}, which composition hands only to trusted host
* adapters.
*/
private readonly userGrant = Object.freeze({
__brand: 'SubagentUserAuthority',
}) as UserAuthorityGrant
constructor(ctx: Context) {
super(ctx, 'subagents')
@@ -181,7 +192,7 @@ export class SubagentService extends Service {
const manager = new SubagentContinuationManager(childCtx, {
prepareContinuable: (name, request) => this.prepareContinuable(name, request),
observeActivation: (provider, childId, parent) => this.observeActivation(provider, childId, parent),
})
}, this.userGrant)
this.continuations = manager
childCtx.effect(() => () => {
/* v8 ignore else -- one injected binding owns the slot until its fiber disposes. */
@@ -227,6 +238,17 @@ export class SubagentService extends Service {
return this.requireContinuations().followup(authority, childId, content, options)
}
/**
* Host-user authority for continuable operations, which may continue any
* durable child without its parent. A composition passes this only to a
* trusted host adapter carrying real human interaction; a model-facing tool
* uses `{ kind: 'parent', agent }` from its own execution context instead.
* @returns the authority a host adapter supplies to {@link followup}.
*/
userAuthority(): SubagentAuthority {
return { kind: 'user', grant: this.userGrant }
}
/**
* Read one durable child's live residency state.
* @param childId - durable child session id.

View File

@@ -213,6 +213,22 @@ describe('SubagentService.startContinuable', () => {
})
})
it('rolls the child back when the signal aborts between publication and acceptance', async () => {
const { ctx, parent } = await setup([textResponse('unused')])
const controller = new AbortController()
// `subagent/start` fires once the epoch is resident, before the prompt is
// submitted, so cancelling here lands squarely in the handoff window.
ctx.on('subagent/start', () => { controller.abort('caller gave up') })
await expect(ctx.subagents.startContinuable(startSpec(parent, 'spawn', controller.signal)))
.rejects.toThrow()
// No resident child and no queued turn survive the abort.
await vi.waitFor(() => {
expect(ctx.agents.list().map(agent => agent.id)).toEqual([SessionId('parent')])
})
})
it('rejects a continuable child that would exceed the configured depth cap', async () => {
const { ctx, parent } = await setup([])
await expect(ctx.subagents.startContinuable({
@@ -287,7 +303,7 @@ describe('SubagentService.startContinuable', () => {
await fresh.plugin(AgentLoop, { agents: [] })
await fresh.plugin(SubagentService)
await fresh.plugin(SubagentSpawn, { providerName: 'spawn' })
await followup(fresh, { kind: 'user' }, started.childId, message('resume routeless'))
await followup(fresh, fresh.subagents.userAuthority(), started.childId, message('resume routeless'))
const resumed = await vi.waitFor(() => {
const found = fresh.agents.get(started.childId)
@@ -337,7 +353,7 @@ describe('SubagentService.startContinuable', () => {
expect(descriptor?.data).toMatchObject({ persona: 'You are scoped.' })
// Cold resume reconstructs the declared composition from that descriptor.
await followup(ctx, { kind: 'user' }, started.childId, message('resume it'))
await followup(ctx, ctx.subagents.userAuthority(), started.childId, message('resume it'))
await waitNoActivation(ctx, started.childId)
const resumed = await ctx.sessionPersistence.load(started.childId)
expect(hasUserText(resumed.events, 'resume it')).toBe(true)
@@ -360,7 +376,7 @@ describe('SubagentService.followup residency routing', () => {
// Both origins queue behind the open turn, in call order.
const parentMessage = await followup(ctx, { kind: 'parent', agent: parent }, started.childId, message('from parent'))
const userMessage = await followup(ctx, { kind: 'user' }, started.childId, message('from user'))
const userMessage = await followup(ctx, ctx.subagents.userAuthority(), started.childId, message('from user'))
expect(parentMessage).not.toBe(userMessage)
// Still the same Activation: no second child Agent was created.
expect(ctx.agents.get(started.childId)).toBe(child)
@@ -376,7 +392,7 @@ describe('SubagentService.followup residency routing', () => {
const started = await ctx.subagents.startContinuable(startSpec(parent))
await waitNoActivation(ctx, started.childId)
const messageId = await followup(ctx, { kind: 'user' }, started.childId, message('continue please'))
const messageId = await followup(ctx, ctx.subagents.userAuthority(), started.childId, message('continue please'))
expect(messageId).toBeTypeOf('string')
await waitNoActivation(ctx, started.childId)
@@ -410,7 +426,7 @@ describe('SubagentService.followup residency routing', () => {
// Waiting retains the handle: the same Agent is still live.
expect(ctx.agents.get(started.childId)).toBe(child)
await followup(ctx, { kind: 'user' }, started.childId, message('while waiting'))
await followup(ctx, ctx.subagents.userAuthority(), started.childId, message('while waiting'))
// Woken back to running on the SAME Activation.
expect(ctx.agents.get(started.childId)).toBe(child)
@@ -421,6 +437,23 @@ describe('SubagentService.followup residency routing', () => {
expect(userTexts(loaded.events)).toEqual(['child task', 'while waiting'])
})
it('rejects a forged user-authority grant', async () => {
const { ctx, parent } = await setup([textResponse('first')])
const started = await ctx.subagents.startContinuable(startSpec(parent))
await waitNoActivation(ctx, started.childId)
// Any plugin holding `ctx.subagents` can write this shape, so shape alone
// must not skip the direct-parent check for an arbitrary known child id.
const forged = { kind: 'user', grant: { __brand: 'SubagentUserAuthority' } } as unknown as SubagentAuthority
await expect(followup(ctx, forged, started.childId, message('not really the user')))
.rejects.toMatchObject({ code: 'UNAUTHORIZED' })
// The service-minted grant is accepted.
await expect(followup(ctx, ctx.subagents.userAuthority(), started.childId, message('really the user')))
.resolves.toBeTypeOf('string')
await waitNoActivation(ctx, started.childId)
})
it('rejects a parent that is not the durable direct parent', async () => {
const { ctx, parent } = await setup([textResponse('first')])
const started = await ctx.subagents.startContinuable(startSpec(parent))
@@ -447,7 +480,7 @@ describe('SubagentService.followup residency routing', () => {
fresh.llm.registerAdapter(['mock'], new MockAdapter([textResponse('resumed cold')]))
expect(fresh.agents.get(SessionId('parent'))).toBeUndefined()
await followup(fresh, { kind: 'user' }, started.childId, message('user continues'))
await followup(fresh, fresh.subagents.userAuthority(), started.childId, message('user continues'))
await waitNoActivation(fresh, started.childId)
const loaded = await fresh.sessionPersistence.load(started.childId)
@@ -469,13 +502,13 @@ describe('SubagentService.followup residency routing', () => {
const oneShotId = run.id
await run.dispose()
await expect(followup(ctx, { kind: 'user' }, oneShotId, message('continue')))
await expect(followup(ctx, ctx.subagents.userAuthority(), oneShotId, message('continue')))
.rejects.toThrow(/no supported continuation state/)
})
it('reports an unknown child id as unavailable', async () => {
const { ctx } = await setup([])
await expect(followup(ctx, { kind: 'user' }, SessionId('missing'), message('hello')))
await expect(followup(ctx, ctx.subagents.userAuthority(), SessionId('missing'), message('hello')))
.rejects.toMatchObject({ code: 'NOT_RESUMABLE' })
})
@@ -491,7 +524,7 @@ describe('SubagentService.followup residency routing', () => {
// exactly one side wins the cutoff. A delivery that loses awaits release and
// cold-resumes rather than reaching a handle being torn down.
const delivery = child.whenIdle().then(() =>
followup(ctx, { kind: 'user' }, started.childId, message('raced')))
followup(ctx, ctx.subagents.userAuthority(), started.childId, message('raced')))
await expect(delivery).resolves.toBeTypeOf('string')
await waitNoActivation(ctx, started.childId)
@@ -619,7 +652,7 @@ describe('continuable durability and teardown', () => {
await expect(ctx.subagents.startContinuable(startSpec(parent)))
.rejects.toMatchObject({ code: 'DRAINING' })
await expect(followup(ctx, { kind: 'user' }, started.childId, message('too late')))
await expect(followup(ctx, ctx.subagents.userAuthority(), started.childId, message('too late')))
.rejects.toMatchObject({ code: 'DRAINING' })
})
@@ -630,7 +663,7 @@ describe('continuable durability and teardown', () => {
const started = await ctx.subagents.startContinuable(startSpec(parent))
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
// Accepted into the inbox, but this queued turn never opens.
await followup(ctx, { kind: 'user' }, started.childId, message('never logged'))
await followup(ctx, ctx.subagents.userAuthority(), started.childId, message('never logged'))
const drained = ctx.subagents.drainContinuable()
hold.resolve(undefined)
@@ -674,7 +707,7 @@ describe('continuable review regressions', () => {
const controller = new AbortController()
controller.abort('caller gave up')
await expect(followup(ctx, { kind: 'user' }, started.childId, message('cancelled'), controller.signal))
await expect(followup(ctx, ctx.subagents.userAuthority(), started.childId, message('cancelled'), controller.signal))
.rejects.toThrow()
// Nothing was enqueued, so no later turn can carry it.
@@ -699,7 +732,7 @@ describe('continuable review regressions', () => {
// A cold resume is a new epoch: it must report its OWN answer, never the
// previous epoch's, which the replayed transcript still contains.
await followup(ctx, { kind: 'user' }, started.childId, message('again'))
await followup(ctx, ctx.subagents.userAuthority(), started.childId, message('again'))
await waitNoActivation(ctx, started.childId)
await vi.waitFor(() => { expect(ends).toHaveLength(2) })
expect(ends[1]!.lastAssistantMessage).toEqual([{ type: 'text', text: 'second answer' }])
@@ -717,7 +750,7 @@ describe('continuable review regressions', () => {
if (subject === parent) return next()
return { kind: 'block', reason: 'blocked by policy' }
})
await followup(ctx, { kind: 'user' }, started.childId, message('again'))
await followup(ctx, ctx.subagents.userAuthority(), started.childId, message('again'))
await waitNoActivation(ctx, started.childId)
await vi.waitFor(() => { expect(ends).toHaveLength(1) })
@@ -786,7 +819,7 @@ describe('continuable review regressions', () => {
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
// Queue a turn, then cancel so it is discarded rather than dequeued. The
// Activation must still reach settlement instead of waiting on that id.
await followup(ctx, { kind: 'user' }, started.childId, message('discarded'))
await followup(ctx, ctx.subagents.userAuthority(), started.childId, message('discarded'))
const drained = ctx.subagents.drainContinuable()
hold.resolve(undefined)
@@ -797,6 +830,32 @@ describe('continuable review regressions', () => {
expect(hasUserText(loaded.events, 'discarded')).toBe(false)
})
it('settles after a delivery discarded inside its own admission window', async () => {
const releaseFirst = Promise.withResolvers<undefined>()
const adapter = new GatedAdapter([{ chunks: textResponse('working'), gate: releaseFirst.promise }])
const { ctx, parent } = await setupWith(adapter)
const started = await ctx.subagents.startContinuable(startSpec(parent))
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
const child = ctx.agents.get(started.childId)!
// Cancel from the synchronous enqueue observer: the discard fires before
// `followup()` returns, so the id is discarded before it can be recorded.
const off = child.ctx.on('agent/inbox/enqueue', (_agent, accepted) => {
if (accepted.message.content.some(block => block.type === 'text' && block.text === 'doomed')) {
child.cancel({ kind: 'user' })
}
})
await followup(ctx, ctx.subagents.userAuthority(), started.childId, message('doomed'))
off()
releaseFirst.resolve(undefined)
// Retaining the discarded id would pin residency at `running` forever, so
// reaching no-Activation without an explicit drain is the assertion.
await waitNoActivation(ctx, started.childId)
const loaded = await ctx.sessionPersistence.load(started.childId)
expect(hasUserText(loaded.events, 'doomed')).toBe(false)
})
it('reports completed when no ordinary turn closed', async () => {
const { ctx, parent } = await setup([])
const ends: SubagentRunEndInfo[] = []
@@ -832,7 +891,7 @@ describe('continuable review regressions', () => {
const started = await ctx.subagents.startContinuable(startSpec(parent))
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
await followup(ctx, { kind: 'user' }, started.childId, message('queued'))
await followup(ctx, ctx.subagents.userAuthority(), started.childId, message('queued'))
expect(states.length).toBeGreaterThan(0)
expect(states).not.toContain('settled')
@@ -854,7 +913,7 @@ describe('continuable lifecycle observation', () => {
await vi.waitFor(() => { expect(ends).toHaveLength(1) })
// A cold resume is a NEW epoch with its own pair.
await followup(ctx, { kind: 'user' }, started.childId, message('again'))
await followup(ctx, ctx.subagents.userAuthority(), started.childId, message('again'))
await waitNoActivation(ctx, started.childId)
await vi.waitFor(() => { expect(ends).toHaveLength(2) })
@@ -898,7 +957,7 @@ describe('continuable public surface', () => {
const controller = new AbortController()
controller.abort('caller gave up')
await expect(followup(ctx, { kind: 'user' }, started.childId, message('aborted'), controller.signal))
await expect(followup(ctx, ctx.subagents.userAuthority(), started.childId, message('aborted'), controller.signal))
.rejects.toThrow()
const loaded = await ctx.sessionPersistence.load(started.childId)
@@ -916,7 +975,7 @@ describe('continuable public surface', () => {
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
const controller = new AbortController()
await followup(ctx, { kind: 'user' }, started.childId, message('survives'), controller.signal)
await followup(ctx, ctx.subagents.userAuthority(), started.childId, message('survives'), controller.signal)
// After acceptance the manager owns the Activation independently.
controller.abort('caller gave up')
@@ -945,7 +1004,7 @@ describe('continuable errors', () => {
}).continuations
manager.activations.delete(started.childId)
await expect(followup(ctx, { kind: 'user' }, started.childId, message('hello')))
await expect(followup(ctx, ctx.subagents.userAuthority(), started.childId, message('hello')))
.rejects.toThrow(SubagentError)
expect(ctx.agents.get(started.childId)).toBe(child)
hold.resolve(undefined)
@@ -1068,7 +1127,7 @@ describe('continuable errors', () => {
.toMatchObject({ agentProvider: 'mock', agentModel: 'child-model' })
// The resumed Activation runs on the declared route, not the parent's.
await followup(ctx, { kind: 'user' }, started.childId, message('again'))
await followup(ctx, ctx.subagents.userAuthority(), started.childId, message('again'))
await vi.waitFor(() => {
expect(ctx.agents.get(started.childId)?.options.model).toBe('child-model')
})

View File

@@ -133,7 +133,7 @@ describe('SubagentService', () => {
signal: new AbortController().signal,
})).rejects.toMatchObject({ code: 'CONTINUATION_UNAVAILABLE' })
await expect(subagents.followup(
{ kind: 'user' },
subagents.userAuthority(),
SessionId('child'),
[{ type: 'text', text: 'hello' }],
{ source: { kind: 'user' }, signal: new AbortController().signal },

View File

@@ -26,8 +26,8 @@ export function apply(ctx: Context): void {
description:
'Send a message to a background subagent by its subagent id, continuing the same conversation. It '
+ 'becomes the subagent\'s next turn: if it is still working, the message waits until its current turn '
+ 'finishes, so it cannot redirect work already underway. The subagent does not reply to you — read its '
+ 'transcript by its id to see what it did. A failure means the message was NOT delivered.',
+ 'finishes, so it cannot redirect work already underway. The subagent does not reply to you, so use '
+ 'this only to give it more work. A failure means the message was NOT delivered.',
parameters: {
subagent_id: {
type: 'string',

View File

@@ -207,8 +207,8 @@ export function apply(ctx: Context, config: Config): void {
description: wording.description + (backgroundEnabled
? continuable
? ' Set `run_in_background: true` to start a background subagent that keeps its conversation:'
+ ' you receive its subagent id and it works on its own. It does not report back to you, so read'
+ ' its transcript by that id, or send it more work with `send_message`.'
+ ' you receive its subagent id and it works on its own. It does not report back, so use this'
+ ' only for work whose result you do not need returned; `send_message` sends it more work.'
: ' Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`.'
: ''),
parameters: {
@@ -226,8 +226,8 @@ export function apply(ctx: Context, config: Config): void {
run_in_background: {
type: 'boolean' as const,
description: continuable
? 'Run as a background subagent that keeps its conversation and return its subagent id; '
+ 'send it more work with send_message.'
? 'Run as a background subagent that keeps its conversation and return its subagent id. '
+ 'It does not report its result back; send it more work with send_message.'
: 'Run as a background task and return its id; collect with task_output or stop with task_kill.',
},
} : {},