refactor(subagent): drop host-user authority and split lifecycle publication
Remove the host-user continuation capability and the public residency query, then separate the seam's public event payloads from its internal lifecycle control interfaces. `followup()` now takes the exact live direct parent `Agent` instead of a `SubagentAuthority` union. No production adapter ever supplied user authority, so the `UserAuthorityGrant` brand token existed only to stop a forged discriminant from bypassing the direct-parent check — deleting the branch retires the token, its mint method, and that attack surface together. Narrowing `parent` from `Agent | undefined` to `Agent` removes three special cases, including the path where a parentless epoch dispatched its lifecycle events unscoped. Scoped-versus-global dispatch is now decided by the event, not by whether a caller happened to have a parent. `activationState()` had no caller; `ActivationState`, `ActivationObserver`, and `ContinuationHost` are package-private. New `src/lifecycle.ts` owns the contained emitter, the one-shot run observer, and the Activation observer, while `SubagentRunInfo`/`SubagentRunEndInfo` move to `src/types.ts` beside the other consumer-facing contracts. Those payloads are public API — dsh-jsonrpc, hooks-claude, and the package invariant all consume them — whereas the observer is a contract between two in-package collaborators, so they no longer share a home merely for both being lifecycle-shaped. The service keeps ownership of the scope carrier: `scopeTarget()` composes the service's own context filter, so a narrowed stand-in would silently change scope filtering. Also drops now-unused dsh-tasks-local and dsh-tool-tasks dev dependencies, and corrects the README claim that a pre-residency failure emits a terminal edge — that path only ever rethrew.
This commit is contained in:
@@ -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: a56da8ad389964dcc873a722a66e335062811f37
|
||||
2026-07-28-continuable-subagent-conversations.zh.md: 71089cd71ae6fda7712ffcc614852a483e13e3ba
|
||||
2026-07-28-continuable-subagent-conversations.md: df2aaa71dde4980bf2dd533c11254d0db8fe61b3
|
||||
2026-07-28-continuable-subagent-conversations.zh.md: 4437e73a3fa2f4d2043d2cfffe71259754fddeef
|
||||
|
||||
@@ -10,11 +10,11 @@ This record replaces the Task-backed continuation manager from [Continuable back
|
||||
|
||||
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 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.
|
||||
Giving queued continuation requests to the manager while the Agent retained its own inbox would create two FIFOs with no single ordering authority. Giving all messages 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.
|
||||
|
||||
Users and parent Agents also need to send later work to the same live child without changing its current turn. Queueing every continuation message as a follow-up preserves one ordering rule for both senders.
|
||||
Parent Agents need to send later work to the same live child without changing its current turn. Queueing every continuation message as a follow-up preserves one ordering rule.
|
||||
|
||||
## Decision
|
||||
|
||||
@@ -44,7 +44,7 @@ Cold resume does not dispatch through a subagent provider. The continuation mana
|
||||
|
||||
`SubagentProvider.start()` and `SubagentRun` remain exclusively on the unchanged one-shot path. A continuable Activation directly owns its `AgentHandle` and never creates, wraps, or retains a `SubagentRun`; `SubagentRun.steer?()` is therefore absent.
|
||||
|
||||
`ctx.subagents.followup(authority, childId, content, { source, signal })` remains the sole continuation-message operation. `authority` is either `{ kind: 'parent', agent }` or `{ kind: 'user' }`; the parent variant is admitted only from an exact live Agent tool context, while only a trusted host adapter can supply user authority. `source` remains durable provenance and grants no authority. The model-facing `send_message` tool keeps only its stable `subagent_id` and `message` fields and always submits a follow-up turn. Both start and follow-up return the accepted `MessageId`, and neither reports how the manager materialized the Activation.
|
||||
`ctx.subagents.followup(parent, childId, content, { source, signal })` remains the sole continuation-message operation. The exact live parent Agent authorizes delivery; `source` remains durable provenance and grants no authority. The model-facing `send_message` tool keeps only its stable `subagent_id` and `message` fields and always submits a follow-up turn. Both start and follow-up return the accepted `MessageId`, and neither reports how the manager materialized the Activation.
|
||||
|
||||
For start and follow-up, the caller signal owns lookup, materialization, and admission only until inbox acceptance. After the operation returns its `MessageId`, the manager owns the Activation independently; later caller cancellation does not cancel the accepted turn or dispose the child.
|
||||
|
||||
@@ -52,13 +52,13 @@ For start and follow-up, the caller signal owns lookup, materialization, and adm
|
||||
|
||||
The Session owns the stable child identity, transcript, direct-parent lineage, delegation depth, and versioned continuation descriptor. `SessionHeader.parentSession` is durable provenance and an authorization input; it is not a live routing capability and does not imply that the historical parent is resident.
|
||||
|
||||
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.
|
||||
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. Cold 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 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
|
||||
|
||||
The public lifecycle has three states and no `queued` state:
|
||||
The internal residency lifecycle has three conditions and no separate `queued` state:
|
||||
|
||||
```text
|
||||
running
|
||||
@@ -89,11 +89,11 @@ The Agent inbox is the only queue. Every continuation message uses `Agent.follow
|
||||
|
||||
Routing depends only on Activation residency:
|
||||
|
||||
| Activation state | Sender | `followup` |
|
||||
|---|---|---|
|
||||
| `running` | parent or user | enqueue in the same Activation |
|
||||
| `waiting` | parent or user | wake the same Activation |
|
||||
| no Activation | parent or user | cold-resume a new Activation |
|
||||
| Activation state | `followup` |
|
||||
|---|---|
|
||||
| `running` | enqueue in the same Activation |
|
||||
| `waiting` | wake the same Activation |
|
||||
| no Activation | cold-resume a new Activation |
|
||||
|
||||
The continuation layer defines no separate delivery-route result. Successful `ctx.subagents.followup()` and `send_message` delivery returns the accepted `MessageId`, while delivery failure throws. Existing `agent/inbox/enqueue`, `agent/inbox/dequeue`, and `agent/inbox/discard` events remain the message-lifecycle observations; adapters may render a generic acceptance but do not expose `started`, `queued`, `resumed`, or another subagent-specific route vocabulary.
|
||||
|
||||
@@ -103,13 +103,11 @@ Every Activation owns its `AgentHandle` and an `ownedChildren: Set<SessionId>`.
|
||||
|
||||
When the authenticated parent is itself a continuation-managed Activation, starting a child or submitting parent-originated work adds the child Session id to that parent's `ownedChildren` before the child can run or the message can enter its inbox. That parent cannot settle or dispose while this set is non-empty. A top-level or other non-continuation Agent has no Activation and does not join this waiting graph.
|
||||
|
||||
Child release occurs only after the child Agent is quiescent, every child of that child is disposed, the final durability checkpoint settles, and the child's `AgentHandle` completes disposal. The manager calls `ctx.sessions.flush(child.session)`: `true` confirms durability, while `false` or rejection is normalized to `DURABILITY_FAILED`. A failed checkpoint is reported but does not prevent handle disposal or ownership release, because retaining a failed child would permanently pin its ancestors in `waiting`. If the child is owned, the manager then resolves the live parent through `SessionHeader.parentSession` and removes the child Session id from its `ownedChildren`; a user-resumed child with no live owner has nothing to release. Manager teardown uses the same child-first order.
|
||||
|
||||
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.
|
||||
Child release occurs only after the child Agent is quiescent, every child of that child is disposed, the final durability checkpoint settles, and the child's `AgentHandle` completes disposal. The manager calls `ctx.sessions.flush(child.session)`: `true` confirms durability, while `false` or rejection is normalized to `DURABILITY_FAILED`. A failed checkpoint is reported but does not prevent handle disposal or ownership release, because retaining a failed child would permanently pin its ancestors in `waiting`. If the child is owned, the manager then resolves the live parent through `SessionHeader.parentSession` and removes the child Session id from its `ownedChildren`. Manager teardown uses the same child-first order.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
The activation-owner scope exists because ordinary Cordis owner effects unwind in reverse registration order, which cannot express the dynamic child graph. Manager initialization registers the private scope's structural disposer first and its drain disposer afterward, so reverse unwind invokes the drain before releasing that scope; merely registering a cleanup effect on the same scope as later Agent handles would allow structural handle disposal to bypass child-first ordering. The manager snapshots the live roots after closing admission, stops its outward lifecycle notifications before cancellation, and retains its internal ownership bookkeeping until every handle settles. Each Activation has one memoized disposal promise so host shutdown, manager unload, child release, and normal settlement can converge without double release. Sibling branches drain independently; one disposal failure is recorded but does not prevent the manager from attempting the remaining handles, and the aggregate drain reports failure after all branches settle. Durable child Sessions survive this process-local teardown.
|
||||
|
||||
@@ -121,21 +119,21 @@ A later proposal may add an ordinary model-facing `report(output)` tool that can
|
||||
|
||||
### Deferred steering
|
||||
|
||||
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.
|
||||
This version exposes no subagent steering operation. Parent 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.
|
||||
A later host UI may expose separate **Steer** and **Follow up** actions. Host 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.
|
||||
|
||||
### Authority and provenance
|
||||
|
||||
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.
|
||||
Authority is supplied by an exact live Agent tool context. `MessageSource` and `senderSessionId` are durable provenance after admission, not caller-controlled authority.
|
||||
|
||||
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.
|
||||
This version authorizes only the durable child's direct parent. The manager checks `SessionHeader.parentSession` against the exact live parent Agent before registering the child in that parent's `ownedChildren`. Other Agents, ancestors, hosts, teams, and workflows remain rejected until a concrete consumer justifies another authority protocol.
|
||||
|
||||
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.
|
||||
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 version 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, the parent cannot 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.
|
||||
|
||||
@@ -149,7 +147,7 @@ Session and descriptor persistence survive restart. Activation state, Agent inbo
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
It adds no host-user continuation, 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, public residency query, new live-Activation or descendant limit, or runtime cache. Existing delegation-depth policy remains unchanged.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -159,7 +157,7 @@ It adds no subagent steering operation, report tool, child-to-parent content del
|
||||
|
||||
**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 shipped 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.
|
||||
|
||||
**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.
|
||||
|
||||
@@ -167,9 +165,11 @@ It adds no subagent steering operation, report tool, child-to-parent content del
|
||||
|
||||
**Retain the exact parent Agent in a separate link.** The parent Activation already owns its `AgentHandle`, and `ownedChildren` prevents that Activation from disposing while the child remains live. Resolving the parent by Session id is therefore sufficient and avoids a redundant runtime reference.
|
||||
|
||||
**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.
|
||||
**Maintain a separate queue for continuation messages.** A second FIFO creates ambiguous ordering against messages already accepted by the Agent. A single Agent inbox gives every accepted turn one observable order.
|
||||
|
||||
**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.
|
||||
**Expose subagent steering now.** Parent steering needs current-turn controller state and a separate admission policy from follow-up delivery. Queueing every first-version continuation avoids that state and its admission race.
|
||||
|
||||
**Expose host-user follow-up without a host consumer.** A public authority-minting method and user branch would make cold resume possible without the historical parent, but no production host adapter calls that operation. The seam accepts only the exact live parent until a concrete authenticated host interaction can receive a private capability.
|
||||
|
||||
**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.
|
||||
|
||||
@@ -185,9 +185,8 @@ The implementation pins these behaviors:
|
||||
- Every failure before initial-prompt inbox acceptance rejects without ids and rolls back any created handle, Activation, and parent `ownedChildren` membership.
|
||||
- Cold resume calls `ctx.agents.resume()` from the continuation manager and never dispatches through the initial subagent provider; `SubagentProvider.resume?()` and `SubagentProviderResumeRequest` are absent.
|
||||
- A continuable Activation directly owns `AgentHandle` and never creates, wraps, or retains `SubagentRun`; `SubagentProvider.start()` and `SubagentRun` remain one-shot-only, without `SubagentRun.steer?()`.
|
||||
- A user can cold-resume a persisted child without loading its historical parent.
|
||||
- `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.
|
||||
- `followup()` accepts only the exact live direct parent; durable message provenance cannot authorize delivery.
|
||||
- Continuation messages always use `Agent.followup()` and share its inbox FIFO, including when 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.
|
||||
- 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.
|
||||
@@ -201,7 +200,7 @@ The implementation pins these behaviors:
|
||||
- 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.
|
||||
- Unit coverage pins the residency-only routing table, single-inbox ordering, `MessageId` correlation through inbox events, follow-up during an open turn, waiting wakeup, cold resume, ownership registration and release, child-first disposal, send-versus-dispose races, both `false` and rejection from the final durability checkpoint without ownership leaks, and the absence of public subagent cancellation, steering, and report tools.
|
||||
- A keyless assembled-app snapshot covers parent delegation, mixed parent/user follow-up queueing, the absence of subagent steering, report delivery, and automatic parent wakeup, retained waiting `AgentHandle`, and child-first disposal.
|
||||
- A keyless assembled-app snapshot covers parent delegation and follow-up queueing, the absence of subagent steering, report delivery, and automatic parent wakeup, retained waiting `AgentHandle`, and child-first disposal.
|
||||
|
||||
### Accepted costs
|
||||
|
||||
@@ -213,6 +212,6 @@ The process-local inbox and ownership graph do not coordinate two harness proces
|
||||
|
||||
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 here.
|
||||
Queueing every continuation message means a parent cannot correct an in-progress child turn immediately; the correction runs as the next turn. A later UI steering action may reduce that latency without changing follow-up ordering.
|
||||
|
||||
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.
|
||||
|
||||
@@ -10,11 +10,11 @@ Status: implemented
|
||||
|
||||
以前的继续执行管理器让一个 Task、一次提供方执行和一个结果边界共享同一生命周期。Task 结算会 dispose(资源释放)child Agent,Task 完成会注入完成通知,后续输入则重建另一个 Agent。这曾使通用后台工作抽象与会话投递耦合,而可继续 subagent 已经具备会话和 Agent inbox。
|
||||
|
||||
如果继续执行管理器为 parent 请求排队,而 Agent 接收用户消息,系统就会出现两个 FIFO,且没有唯一的顺序权威。而把两种消息都交给 Task,则重复了 agent loop(智能体循环)已有的准入、取消和完全停稳机制。`Agent.whenIdle()` 无法恢复单项请求的 Task 结果,因为一个运行区间可能清空多个排队轮次;宽泛的 `Agent.cancel()` 也不能精确移除一项排队请求。
|
||||
如果继续执行管理器为继续执行请求排队,而 Agent 保留自己的 inbox,系统就会出现两个 FIFO,且没有唯一的顺序权威。而把所有消息都交给 Task,则重复了 agent loop(智能体循环)已有的准入、取消和完全停稳机制。`Agent.whenIdle()` 无法恢复单项请求的 Task 结果,因为一个运行区间可能清空多个排队轮次;宽泛的 `Agent.cancel()` 也不能精确移除一项排队请求。
|
||||
|
||||
运行时生命周期也比单个轮次更长。subagent 可能已经结束自身轮次,但它创建的 child 仍在运行。此时 dispose parent 运行时,会移除仍负责后代拆卸的 Agent。反之,如果让所有历史 subagent 始终驻留,内存使用就会失去上界。
|
||||
|
||||
用户和 parent Agent 还需要在不改变当前轮次的前提下,向同一个在线 child 发送后续工作。将每条继续执行消息作为 follow-up 排队,可以让两类发送方遵循同一项排序规则。
|
||||
parent Agent 还需要在不改变当前轮次的前提下,向同一个在线 child 发送后续工作。将每条继续执行消息作为 follow-up 排队,可以保留唯一的排序规则。
|
||||
|
||||
## 决策
|
||||
|
||||
@@ -44,7 +44,7 @@ inbox 接受消息前发生任何失败,操作都会在不返回任何 id 的
|
||||
|
||||
`SubagentProvider.start()` 和 `SubagentRun` 只保留在不变的 one-shot 路径上。可继续激活直接持有自身的 `AgentHandle`,绝不创建、包装或保留 `SubagentRun`;因此,`SubagentRun.steer?()` 不存在。
|
||||
|
||||
`ctx.subagents.followup(authority, childId, content, { source, signal })` 仍是唯一的继续执行消息操作。`authority` 可以是 `{ kind: 'parent', agent }` 或 `{ kind: 'user' }`;parent 变体仅能从确切的在线 Agent 工具上下文通过准入,只有可信宿主适配器才能提供用户权限。`source` 仍是持久化来源信息,不赋予任何权限。面向模型的 `send_message` 工具只保留稳定的 `subagent_id` 和 `message` 字段,并始终提交一个 follow-up 轮次。start 和 follow-up 都返回已接受的 `MessageId`,两者都不报告管理器如何物化激活。
|
||||
`ctx.subagents.followup(parent, childId, content, { source, signal })` 仍是唯一的继续执行消息操作。确切的在线 parent Agent 授权投递;`source` 仍是持久化来源信息,不赋予任何权限。面向模型的 `send_message` 工具只保留稳定的 `subagent_id` 和 `message` 字段,并始终提交一个 follow-up 轮次。start 和 follow-up 都返回已接受的 `MessageId`,两者都不报告管理器如何物化激活。
|
||||
|
||||
对于 start 和 follow-up,调用方 signal 只在 inbox 接受消息前持有查找、物化和准入。操作返回 `MessageId` 后,管理器会独立持有该激活;调用方之后的取消不会取消已接受的轮次,也不会 dispose child。
|
||||
|
||||
@@ -52,13 +52,13 @@ inbox 接受消息前发生任何失败,操作都会在不返回任何 id 的
|
||||
|
||||
会话持有稳定的 child 身份、transcript(文本记录)、直接 parent 谱系、委派深度和带版本的继续执行描述符。`SessionHeader.parentSession` 是持久化来源信息和鉴权输入;它不是在线路由能力,也不表示历史 parent 仍然驻留。
|
||||
|
||||
空闲的历史会话没有 `AgentHandle`。第一条通过鉴权的 `next-turn` 投递会根据持久化会话恢复激活,并将消息提交到其 inbox。经用户授权的冷恢复不会加载历史 parent Agent。parent 发起的恢复使用经过身份认证的确切在线 parent Agent 执行鉴权;当该 parent 有激活时,还使用它建立所有权,但绝不使用 parent 执行重建。
|
||||
空闲的历史会话没有 `AgentHandle`。第一条通过鉴权的 `next-turn` 投递会根据持久化会话恢复激活,并将消息提交到其 inbox。冷恢复使用经过身份认证的确切在线 parent Agent 执行鉴权;当该 parent 有激活时,还使用它建立所有权,但绝不使用 parent 执行重建。
|
||||
|
||||
激活作为消费方会直接持有已发布的 `AgentHandle` 直至结算,而管理器的私有 activation-owner 作用域则是其 Cordis 结构化所有者。可继续 subagent 路径不创建任何中间的带结果执行包装层,包括 `SubagentRun`;一次性委派保持不变,且不属于该生命周期。远程提供方不在此处的范围内,引入时需要单独的激活所有权契约。激活 dispose 后,历史会话不消耗运行时内存。
|
||||
|
||||
### 激活生命周期
|
||||
|
||||
公开生命周期只有 3 个状态,没有 `queued` 状态:
|
||||
内部驻留生命周期有三个条件,没有单独的 `queued` 状态:
|
||||
|
||||
```text
|
||||
running
|
||||
@@ -89,11 +89,11 @@ Agent inbox 是唯一队列。每条继续执行消息都使用 `Agent.followup(
|
||||
|
||||
路由只取决于激活的驻留状态:
|
||||
|
||||
| 激活状态 | 发送方 | `followup` |
|
||||
|---|---|---|
|
||||
| `running` | parent 或 user | 在同一激活中排队 |
|
||||
| `waiting` | parent 或 user | 唤醒同一激活 |
|
||||
| 无激活 | parent 或 user | 冷恢复新激活 |
|
||||
| 激活状态 | `followup` |
|
||||
|---|---|
|
||||
| `running` | 在同一激活中排队 |
|
||||
| `waiting` | 唤醒同一激活 |
|
||||
| 无激活 | 冷恢复新激活 |
|
||||
|
||||
继续执行层不定义单独的投递路由结果。成功投递 `ctx.subagents.followup()` 或 `send_message` 时会返回已接受的 `MessageId`,投递失败则会抛出异常。现有的 `agent/inbox/enqueue`、`agent/inbox/dequeue` 和 `agent/inbox/discard` 事件仍用于观测消息生命周期;适配器可以呈现通用的接受确认,但不暴露 `started`、`queued`、`resumed` 或其他 subagent 专属路由词汇。
|
||||
|
||||
@@ -103,13 +103,11 @@ Agent inbox 是唯一队列。每条继续执行消息都使用 `Agent.followup(
|
||||
|
||||
当经过身份认证的 parent 自身是由继续执行管理器管理的激活时,启动 child 或提交由 parent 发起的工作,会在 child 可以运行或消息可以进入其 inbox 前,将 child 会话 id 加入该 parent 的 `ownedChildren`。该集合非空时,这个 parent 不能结算或 dispose。顶层 Agent 或其他非继续执行 Agent 没有激活,也不会加入该等待图。
|
||||
|
||||
只有在 child Agent 完全停稳、该 child 持有的每个 child 都已 dispose、最终持久性检查点结算且 child 的 `AgentHandle` 完成 dispose 后,系统才释放 child。管理器会调用 `ctx.sessions.flush(child.session)`:只有 `true` 确认持久性,`false` 或 rejection 则统一报告为 `DURABILITY_FAILED`。检查点失败会被报告,但不会阻止 handle dispose 或释放所有权,因为保留失败的 child 会让其祖先永久固定在 `waiting`。如果 child 归 parent 所有,管理器随后会通过 `SessionHeader.parentSession` 解析在线 parent,并从其 `ownedChildren` 中移除 child 会话 id;由用户恢复且没有在线 owner 的 child 则没有需要释放的所有权记录。管理器拆卸使用相同的 child-first 顺序。
|
||||
|
||||
用户冷恢复会创建一次激活,但不会将其加入历史 parent 的 `ownedChildren`。如果直接 parent 随后向这个在线激活提交工作,且该 parent 自身由继续执行管理器管理,准入过程会在消息入队前建立所有权;非继续执行 parent 仍位于等待图之外。
|
||||
只有在 child Agent 完全停稳、该 child 持有的每个 child 都已 dispose、最终持久性检查点结算且 child 的 `AgentHandle` 完成 dispose 后,系统才释放 child。管理器会调用 `ctx.sessions.flush(child.session)`:只有 `true` 确认持久性,`false` 或 rejection 则统一报告为 `DURABILITY_FAILED`。检查点失败会被报告,但不会阻止 handle dispose 或释放所有权,因为保留失败的 child 会让其祖先永久固定在 `waiting`。如果 child 归 parent 所有,管理器随后会通过 `SessionHeader.parentSession` 解析在线 parent,并从其 `ownedChildren` 中移除 child 会话 id。管理器拆卸使用相同的 child-first 顺序。
|
||||
|
||||
系统会一直保留所有权,直至 child 激活完成 dispose。后续改进可以更早释放限定到请求的 lease,但这需要精确关联轮次完成,而本 Task-free 提案特意不增加该机制。
|
||||
|
||||
顶层拆卸由宿主负责,而不表示为另一次激活。宿主首先要求管理器同步进入 draining,拒绝新的创建、恢复和投递准入;然后按 child-first 顺序 dispose 整个在线激活森林,并等待全部 `AgentHandle.dispose()` 调用。只有该 drain 结算后,宿主才能 dispose 顶层 Agent 和管理器作用域。管理器卸载使用相同的 drain,并涵盖由用户恢复且没有在线 owner 的激活。
|
||||
顶层拆卸由宿主负责,而不表示为另一次激活。宿主首先要求管理器同步进入 draining,拒绝新的创建、恢复和投递准入;然后按 child-first 顺序 dispose 整个在线激活森林,并等待全部 `AgentHandle.dispose()` 调用。只有该 drain 结算后,宿主才能 dispose 顶层 Agent 和管理器作用域。管理器卸载使用相同的 drain。
|
||||
|
||||
activation-owner 作用域之所以存在,是因为普通 Cordis owner effect 按注册逆序撤销,无法表达动态 child 图。管理器初始化时先注册私有作用域的结构化 disposer,再注册自身的 drain disposer,使逆序撤销先执行 drain、再释放该作用域;如果只在与后续 Agent handle 相同的作用域上注册 cleanup effect,结构化 handle dispose 就可能绕过 child-first 顺序。管理器在关闭准入后对在线根节点创建快照,在取消前停止自身的对外生命周期通知,并保留内部所有权簿记,直至每个 handle 都结算。每次激活有一个记忆化的 dispose promise,使宿主关闭、管理器卸载、child 释放和正常结算能够汇合,而不会重复释放。同级分支独立 drain;系统会记录单次 dispose 失败,但仍会尝试其余 handle,聚合 drain 则在所有分支结算后报告失败。这次进程内拆卸不会销毁持久化 child 会话。
|
||||
|
||||
@@ -121,21 +119,21 @@ activation-owner 作用域之所以存在,是因为普通 Cordis owner effect
|
||||
|
||||
### 延后的 steering(中途引导)
|
||||
|
||||
本版本不暴露 subagent steering 操作。parent 和用户的继续执行消息始终开启后续 FIFO 轮次,因此继续执行层不存储当前轮次控制方,也不新增能够感知控制方的 Agent 准入 seam。
|
||||
本版本不暴露 subagent steering 操作。parent 的继续执行消息始终开启后续 FIFO 轮次,因此继续执行层不存储当前轮次控制方,也不新增能够感知控制方的 Agent 准入 seam。
|
||||
|
||||
后续宿主 UI 可以分别暴露 **Steer** 和 **Follow up** 操作。用户 steering 必须严格且仅限在线使用:只有当激活接受下一步骤时,它才能调用现有的 Agent steering 路径;其他情况必须拒绝,而且绝不能转为排队或冷恢复。是否通过面向模型的工具暴露 parent steering 仍需单独设计,因为不同的工具名称可以表达意图,却不能确定 parent 是否可以修改由用户控制的轮次。
|
||||
后续宿主 UI 可以分别暴露 **Steer** 和 **Follow up** 操作。宿主 steering 必须严格且仅限在线使用:只有当激活接受下一步骤时,它才能调用现有的 Agent steering 路径;其他情况必须拒绝,而且绝不能转为排队或冷恢复。是否通过面向模型的工具暴露 parent steering 仍需单独设计。
|
||||
|
||||
### 权限与来源
|
||||
|
||||
权限来自可信宿主交互或确切的在线 Agent 工具上下文。`MessageSource` 和 `senderSessionId` 是准入后的持久化来源信息,不是由调用方控制的权限。
|
||||
权限来自确切的在线 Agent 工具上下文。`MessageSource` 和 `senderSessionId` 是准入后的持久化来源信息,不是由调用方控制的权限。
|
||||
|
||||
本版本授权宿主用户和持久化 child 的直接 parent。系统会根据经过身份认证的 parent Agent 检查 `SessionHeader.parentSession`,然后才将 child 注册到该 parent 的 `ownedChildren`。其他 Agent、祖先、团队和工作流仍被拒绝,直至系统具备显式权限协议。
|
||||
本版本只授权持久化 child 的直接 parent。管理器会根据确切的在线 parent Agent 检查 `SessionHeader.parentSession`,然后才将 child 注册到该 parent 的 `ownedChildren`。其他 Agent、祖先、宿主、团队和工作流仍被拒绝,直至有具体消费方证明另一种权限协议合理。
|
||||
|
||||
用户权限可以在 parent 不在线时冷恢复 child。由 parent 发起的投递要求 parent 在准入时在线,并通过所有权关系使其继续在线。
|
||||
由 parent 发起的投递要求 parent 在准入时在线,并通过所有权关系使其继续在线。
|
||||
|
||||
### 持久性、dispose 与恢复
|
||||
|
||||
没有 Task 后,系统不再提供 `task_output`、`task_kill`、Task 状态、逐消息结果 promise 或公开 subagent 取消操作。调用方 signal 只能在 inbox 接受消息前中止 start 或 follow-up。消息被接受后,parent 和用户都不能通过 `ctx.subagents` 取消该消息、轮次或激活;`Agent.cancel()` 仍是底层 Agent 能力,但本版本不通过 subagent 服务暴露它。
|
||||
没有 Task 后,系统不再提供 `task_output`、`task_kill`、Task 状态、逐消息结果 promise 或公开 subagent 取消操作。调用方 signal 只能在 inbox 接受消息前中止 start 或 follow-up。消息被接受后,parent 不能通过 `ctx.subagents` 取消该消息、轮次或激活;`Agent.cancel()` 仍是底层 Agent 能力,但本版本不通过 subagent 服务暴露它。
|
||||
|
||||
宿主和管理器拆卸仍是覆盖整个生命周期的停止路径。它会关闭准入,按 child-first 顺序 dispose 每个在线激活森林,并保留持久化会话。
|
||||
|
||||
@@ -149,7 +147,7 @@ activation-owner 作用域之所以存在,是因为普通 Cordis owner effect
|
||||
|
||||
本版本覆盖可继续的进程内 child,一次性委派保持不变。远程提供方必须具备单独的激活 handle,以及等价的认证控制与 child-first 完全停稳契约,才能支持同样的行为。
|
||||
|
||||
它不新增 subagent steering 操作、报告工具、从 child 到 parent 的内容投递、自动唤醒 parent、持久化邮箱、跨进程 lease、中断 inbox 工作的自动回放、团队权限、工作流权限、公开 subagent 取消操作、新的在线激活数量或后代总数限制,以及运行时缓存。现有委派深度策略保持不变。
|
||||
它不新增 host-user 继续执行、subagent steering 操作、报告工具、从 child 到 parent 的内容投递、自动唤醒 parent、持久化邮箱、跨进程 lease、中断 inbox 工作的自动回放、团队权限、工作流权限、公开 subagent 取消操作、公开驻留查询、新的在线激活数量或后代总数限制,以及运行时缓存。现有委派深度策略保持不变。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
@@ -159,7 +157,7 @@ activation-owner 作用域之所以存在,是因为普通 Cordis owner effect
|
||||
|
||||
**等待期间 dispose Agent。** child 仍属于上一个进程内所有权图时重建 parent,需要持久化所有权与拆卸协议。只为尚未完成的所有权图保留 `AgentHandle`,可以在不让已结算历史驻留的前提下,保留 child-first 拆卸。
|
||||
|
||||
**让提供方通过 Agent handle 创建、恢复 child 或投递消息。** 初始提供方只持有 `prepareContinuable()` 及其分离式创建规格这一项差异:child 是全新启动,还是带有 parent 前缀。管理器必须通过私有 activation-owner 作用域自行调用 `ctx.agents.create()`,使该作用域成为每个 handle 的结构化所有者。持久化的进程内会话已经包含初始前缀及通用重建描述符,消息投递则属于 Agent inbox。让提供方持有任何后续 handle、`SubagentRun` 或消息所有权,会保留一条没有已发布行为可承载的 seam,还会因不必要的在线 parent 输入使用户冷恢复更加复杂。
|
||||
**让提供方通过 Agent handle 创建、恢复 child 或投递消息。** 初始提供方只持有 `prepareContinuable()` 及其分离式创建规格这一项差异:child 是全新启动,还是带有 parent 前缀。管理器必须通过私有 activation-owner 作用域自行调用 `ctx.agents.create()`,使该作用域成为每个 handle 的结构化所有者。持久化的进程内会话已经包含初始前缀及通用重建描述符,消息投递则属于 Agent inbox。让提供方持有任何后续 handle、`SubagentRun` 或消息所有权,会保留一条没有已发布行为可承载的 seam。
|
||||
|
||||
**现在就增加报告投递。** 可重复调用的面向模型工具与该生命周期兼容,但静默投递还是唤醒投递、接收方选择、确认、持久性和重试行为都是独立的产品决策。延后该工具,可以让首个版本专注于会话准入与驻留,又不限制后续策略。
|
||||
|
||||
@@ -167,9 +165,11 @@ activation-owner 作用域之所以存在,是因为普通 Cordis owner effect
|
||||
|
||||
**在单独的 link 中保留确切的 parent Agent。** parent 激活已经持有自身 `AgentHandle`,而且 `ownedChildren` 会在 child 仍然在线时阻止该激活 dispose。因此,通过会话 id 解析 parent 已经足够,也可以避免冗余的运行时引用。
|
||||
|
||||
**为 parent 消息维护单独队列。** 第二个 FIFO 会让它和 Agent 已接受的用户消息之间顺序不明确。单个 Agent inbox 为两种来源提供唯一且可观察的顺序。
|
||||
**为继续执行消息维护单独队列。** 第二个 FIFO 会让它和 Agent 已接受消息之间顺序不明确。单个 Agent inbox 为每个已接受轮次提供唯一且可观察的顺序。
|
||||
|
||||
**现在就暴露 subagent steering。** 用户 steering 可以是严格且仅限在线使用的宿主操作,但 parent steering 需要当前轮次控制方状态,以保护由用户控制的轮次。首个版本将每条继续执行消息都排队,可以避免引入该状态及其准入竞争。后续 UI 可以新增一项仅限用户的独立操作,而不改变 follow-up 排序。
|
||||
**现在就暴露 subagent steering。** parent steering 需要当前轮次控制方状态,以及不同于 follow-up 投递的单独准入策略。首个版本将每条继续执行消息都排队,可以避免引入该状态及其准入竞争。
|
||||
|
||||
**在没有 host 消费方的情况下暴露 host-user follow-up。** 公开的权限铸造方法和用户分支可以在没有历史 parent 的情况下实现冷恢复,但没有生产 host 适配器调用该操作。在具体的经认证宿主交互能够收到私有能力之前,该 seam 只接受确切的在线 parent。
|
||||
|
||||
**返回 subagent 专属的投递路由。** `started`、`queued` 和 `resumed` 等标签重复了激活与 inbox 状态,却没有给调用方提供独立结果。复用 `MessageId` 和现有 inbox 事件,可以让投递关联继续由其所属的 Agent 契约承载。
|
||||
|
||||
@@ -185,9 +185,8 @@ activation-owner 作用域之所以存在,是因为普通 Cordis owner effect
|
||||
- 初始提示词被 inbox 接受前的每条失败路径都会导致操作被拒绝且不返回 id,并回滚已创建的任何 handle、激活和 parent `ownedChildren` 成员关系。
|
||||
- 冷恢复由继续执行管理器调用 `ctx.agents.resume()`,绝不通过初始 subagent 提供方分发;`SubagentProvider.resume?()` 和 `SubagentProviderResumeRequest` 均不存在。
|
||||
- 可继续激活直接持有 `AgentHandle`,绝不创建、包装或保留 `SubagentRun`;`SubagentProvider.start()` 和 `SubagentRun` 只用于 one-shot,且没有 `SubagentRun.steer?()`。
|
||||
- 用户可以在不加载历史 parent 的前提下冷恢复持久化 child。
|
||||
- `followup()` 只接受可信 parent 或用户权限;持久化消息来源信息不能授权投递。
|
||||
- Parent 和用户的继续执行消息始终使用 `Agent.followup()` 并共享其 inbox FIFO,包括一种来源排在另一种来源之后,以及 child 已有开放轮次的情况。
|
||||
- `followup()` 只接受确切的在线直接 parent;持久化消息来源信息不能授权投递。
|
||||
- 继续执行消息始终使用 `Agent.followup()` 并共享其 inbox FIFO,包括 child 已有开放轮次的情况。
|
||||
- `ctx.subagents.followup()` 及其 `send_message` 适配器只返回已接受的 `MessageId`;继续执行层不接受投递 target,也不定义 subagent 专属路由结果。
|
||||
- 本版本不暴露公开 subagent 取消操作;调用方 signal 只能在 inbox 接受消息前停止 start 和 follow-up,宿主和管理器拆卸则保留 child-first 全局清理。
|
||||
- 本版本不暴露 subagent steering 操作或当前轮次控制方状态。
|
||||
@@ -201,7 +200,7 @@ activation-owner 作用域之所以存在,是因为普通 Cordis owner effect
|
||||
- 可继续 subagent 路径不创建或依赖 Task、`TaskId`、Task 完成通知、Task 取消或中间的带结果执行包装层。
|
||||
- 单元覆盖固定 `startContinuable()` 在 inbox 接受消息时的返回边界、每条接受前失败路径的完整回滚、接受前后两个阶段的调用方 signal 所有权,以及已接受但未写入日志的消息不会自动回放。
|
||||
- 单元覆盖固定仅由驻留状态决定的路由表、单 inbox 顺序、通过 inbox 事件关联 `MessageId`、在开放轮次期间 follow-up、等待唤醒、冷恢复、所有权注册与释放、child-first dispose、发送与 dispose 的竞争、最终持久性检查点返回 `false` 和 rejection 时都不泄漏所有权,以及不存在公开 subagent 取消、steering 和报告工具这一事实。
|
||||
- 一项无密钥整套应用快照覆盖 parent 委派、parent 与用户混合的 follow-up 排队、不存在 subagent steering、报告投递和自动唤醒 parent、保留等待中的 `AgentHandle` 以及 child-first dispose。
|
||||
- 一项无密钥整套应用快照覆盖 parent 委派和 follow-up 排队、不存在 subagent steering、报告投递和自动唤醒 parent、保留等待中的 `AgentHandle` 以及 child-first dispose。
|
||||
|
||||
### 已接受的代价
|
||||
|
||||
@@ -213,6 +212,6 @@ activation-owner 作用域之所以存在,是因为普通 Cordis owner effect
|
||||
|
||||
没有报告投递时,完成 child 轮次既不会把内容发送给历史 parent,也不会唤醒它。输出会保留在持久化 child 会话中,直至调用方检查该 transcript 或提交另一个经过授权的轮次。后续报告工具可以增加静默投递或唤醒投递,而无需改变激活生命周期。
|
||||
|
||||
将每条继续执行消息排队,意味着 parent 无法立即纠正正在进行的 child 轮次;纠正操作会在下一个轮次执行。后续仅限用户的 UI steering 操作可以缩短该延迟,而无需在此引入 parent 与用户之间的控制方策略。
|
||||
将每条继续执行消息排队,意味着 parent 无法立即纠正正在进行的 child 轮次;纠正操作会在下一个轮次执行。后续 UI steering 操作可以缩短该延迟,而不改变 follow-up 排序。
|
||||
|
||||
最终持久性检查点失败时,运行时所有权图仍可完成 drain,但持久化 child 状态会缺失或陈旧。该失败会以 `DURABILITY_FAILED` 的形式被观测到;重试与修复需要单独的恢复设计。
|
||||
|
||||
Reference in New Issue
Block a user