Continuable subagents
可继续的 subagent
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 qu
English
Problem
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 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 Jobs 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.
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
A continuable subagent has one durable Session and at most one process-local Activation:
persisted Session
-> optional live Activation
-> one retained AgentHandle
-> Agent inbox as the only turn FIFO
-> zero or more owned child Activations
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. No continuable subagent has a Task, an Activation FIFO, or queued Activation state.
Materialization and public operations
The named subagent provider participates only in preparing the initial creation spec, where spawn and fork differ. Its optional prepareContinuable(request): Promise<ContinuableCreateSpec> method is the continuable-creation capability. The returned spec contains only detached provider-specific creation inputs such as the optional parent-history seed; it contains no Agent, AgentHandle, prompt delivery, result, disposal, or resume operation. The manager reserves the child identity, resolves the durable descriptor and common Agent setup, calls ctx.agents.create() through a private activation-owner scope, installs the returned AgentHandle into the Activation, establishes any continuable-parent ownership, and then calls Agent.followup(initialPrompt). Inbox acceptance yields a MessageId; at that boundary ctx.subagents.startContinuable() returns { childId, messageId } without waiting for the turn to start or for the message to enter the Session log.
Any failure before inbox acceptance rejects without returning either id. Agent creation provides rollback before handle transfer; after transfer, the manager keeps one closing transaction visible to concurrent delivery and drain, disposes the created handle, removes the Activation, and rolls back any parent ownedChildren membership before rejecting. Failure before the residency start edge publishes no terminal edge, while failure after a published start closes the lifecycle pair through normal disposal.
backgroundMode: 'one-shot' | 'continuable' remains deployment policy. Configured continuable mode requires prepareContinuable; method presence replaces SubagentProvider.resume?() as the capability check, while a capable provider may still run one-shot work.
Cold resume does not dispatch through a subagent provider. The continuation manager folds the generic in-process descriptor, calls ctx.agents.resume() through the same activation-owner scope, installs the returned AgentHandle, and submits the waiting next-turn. SubagentProvider.resume?() and SubagentProviderResumeRequest are absent. The descriptor retains the initial provider name after that provider unregisters; the name does not grant a recovery capability or require the provider for later residency. Remote providers require a separate design.
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(parent, childId, content, { source, signal }) remains the sole parent-to-child continuation-message operation. The exact live parent Agent authorizes delivery; cold resume checks that authority before reconstruction and every path checks it again in the final no-await inbox-admission span, so a parent unregistered or replaced during materialization cannot authorize delivery. source records who supplied the admitted message 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.
Durable Session and live Activation
The Session owns the stable child identity, transcript, direct-parent lineage, delegation depth, and versioned continuation descriptor. SessionHeader.parentSession records the direct parent and is an authorization input; it is not a live routing capability and does not imply that the recorded 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. 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 internal residency lifecycle has three conditions and no separate queued state:
running
| Agent quiescent with live children
v
waiting
| next-turn
+--------------------------> running
running or waiting
| Agent quiescent and no live children
v
settled
| AgentHandle.dispose completes
v
no Activation
running means the Agent has an active admission or turn, or its inbox contains waking work. waiting means the Agent is quiescent but the Activation still owns at least one child Activation that has not completed disposal. settled means the Agent is quiescent and every owned child is disposed; the manager then disposes the AgentHandle and removes the Activation.
The manager derives these states from Agent quiescence and the owned-child set rather than maintaining a second execution state machine. A next-turn delivered while running joins the Agent inbox. A next-turn delivered while waiting wakes the same Agent and returns the Activation to running. Delivery after disposal cold-resumes a new Activation.
The manager linearizes delivery, child release, and disposal for each durable child. If a delivery races with final disposal, exactly one side wins the admission cutoff: delivery either enters the still-live Agent inbox, or waits for disposal and cold-resumes a new Activation. No caller can send to a handle after its disposal transaction begins.
One inbox and follow-up delivery
The Agent inbox is the only queue. Every continuation message uses Agent.followup() and becomes one FIFO turn; neither the continuation manager nor the host maintains another message queue. Every accepted waking item keeps the current Activation live until Agent.whenIdle() observes the complete waking suffix.
Routing depends only on Activation residency:
| 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.
Child ownership
Every Activation owns its AgentHandle and an ownedChildren: Set<SessionId>. Because one Session has at most one live Activation, the child Session id identifies the live child without another runtime-incarnation reference. SessionHeader.parentSession records the durable direct-parent identity, while membership in ownedChildren records the process-local ownership relationship.
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 best-effort final session flush settles, and the child's AgentHandle completes disposal. The manager awaits ctx.sessions.flush(child.session) but does not interpret its participation boolean: an arbitrary listener cannot prove that the selected persistence backend stored the state. A rejection is logged without preventing handle disposal or ownership release, because retaining a 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 design deliberately does not add.
Top-level teardown is host-owned rather than represented as another Activation. Manager unload invokes its internal manager-wide drain to close admission synchronously, await every admitted materialization through publication or rollback, stop the stable live forest, and release it child-first. A host that owns selected top-level Agents uses drainContinuableDescendants(parents): exact Agent identities close admission only below those roots until each leaves the registry, while unrelated forests and manager-wide admission remain live; the manager stops their visible descendants before its first await, waits only materializations admitted below those roots, and releases only the selected branches. Every materialized start and live delivery rechecks caller cancellation, the applicable draining scope, Activation disposal, and exact parent authority in the same synchronous span as inbox submission, so teardown or parent replacement that wins before acceptance prevents delivery to the closing handle. Only after the applicable drain settles may the host dispose its top-level Agents; only manager-wide drain precedes manager-scope disposal.
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. Each materialization registers its barrier participant and snapshots its exact live ancestry before starting the inner transaction, then remains tracked until it installs an Activation or fully rolls back. The Activation retains weak membership of that ancestry, so an intermediate Agent may leave the registry without hiding a still-live descendant from its host root. Each Activation installs one memoized disposal promise before cancellation or recursive callbacks, allowing scoped host shutdown, global manager unload, child release, and normal settlement to converge without double release. Cancellation propagates top-down before slow descendant cleanup; handle release remains child-first. Sibling branches drain independently; one disposal failure is recorded but does not prevent the manager from attempting the remaining selected handles, and the aggregate drain reports failure after all selected branches settle. Durable child Sessions survive this process-local teardown.
Report delivery extension
The optional child-scoped report(output) tool was added later without changing Activation residency or adding another queue. It can be called zero or multiple times per turn, derives the live direct parent rather than accepting a recipient, and selects quiet injection or a waking parent follow-up through deployment config. The report-tool Agent Note owns its authority, acknowledgement, setup-contribution, and delivery contracts.
Deferred steering
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 contract.
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 recorded sender identity
Authority is supplied by an exact live Agent tool context. After admission, MessageSource and senderSessionId record who supplied the message; callers cannot use those fields as authority.
This version authorizes only the durable child's direct parent. The manager checks SessionHeader.parentSession against the exact live parent Agent at the final no-await inbox-admission boundary before registering the child in that parent's ownedChildren; cold resume also performs an earlier check before reconstruction for fail-fast rejection. Other Agents, ancestors, hosts, teams, and workflows remain rejected until a concrete consumer justifies another authority protocol.
Parent-originated delivery requires the parent to be live when admitted and keeps it live through the ownership relationship.
Durability, disposal, and recovery
Without Jobs there is no job_output, job_kill, Task status, or per-message result promise. The caller signal can abort start or follow-up only before inbox acceptance. After acceptance, the parent cannot cancel the accepted message or dispose the Activation through ctx.subagents; the only public stop is the later current-turn interrupt, which cancels the live target's current turn with keepInbox and leaves residency, pending work, and descendants intact.
Host and manager teardown remains the lifecycle stop path. Manager unload applies it globally; a host applies it only below the exact top-level Agents it owns. Each form closes the applicable admission scope, stops the selected visible Activations, awaits admitted materializations in that scope, releases child-first, and preserves the durable Sessions.
Each turn requests the Session durability checkpoint, while final Activation settlement additionally awaits ctx.sessions.flush() as a best-effort barrier. The manager deliberately ignores the boolean result because listener participation cannot identify a persistence backend. A rejection is logged without changing the lifecycle result or host-drain outcome; the manager still disposes the handle and releases ownership, and the persisted child state may be missing or stale on a later resume.
Only messages written to the child Session log are reconstructable with the source that supplied them; inbox acceptance alone provides no restart guarantee.
Session and descriptor persistence survive restart. Activation state, Agent inbox contents, and the ownership graph are process-local. A process crash may lose an accepted initial prompt or follow-up that remained in the inbox without reaching the Session log. The Session and descriptor may survive so a later authorized message can cold-resume the child, but the lost message is not replayed automatically. Recovering accepted unfinished or unlogged messages requires a durable inbox protocol and is not implied here.
Scope
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 host-user continuation, subagent steering operation, durable mailbox, cross-process lease, automatic replay of interrupted inbox work, team authority, workflow authority, public residency query, new live-Activation or descendant limit, or runtime cache; the later current-turn interrupt added the one public stop operation on top of this lifecycle. Existing delegation-depth policy remains unchanged. Optional child-to-parent reporting is a later consumer of this lifecycle rather than part of the base continuable capability.
Alternatives considered
Keep Task-backed Activations. Jobs provide generic status, result collection, and cancellation, but using them for conversation delivery creates a second queue and duplicates turn ownership. This design gives up those generic Task controls so the Agent inbox remains the only execution order.
Create one Activation per next-turn. This restores independent result and cancellation boundaries, but it requires a manager FIFO beside the Agent inbox and makes a retained Agent cross artificial Activation boundaries. One Activation per residency epoch is smaller and follows the AgentHandle lifetime directly.
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 retain provider ownership with no shipped behavior to justify it.
Make report delivery part of the base lifecycle. Repeatable child-to-parent reporting is compatible with this lifecycle, but quiet versus next-step delivery, acknowledgement, durability, and retry behavior are independent product choices. The later report package remains optional and consumes an explicit child-setup hook, so continuable residency does not silently grant a return channel.
Treat SessionHeader.parentSession as live ownership. Durable lineage does not prove that the recorded parent currently owns the child. Membership in the live parent's ownedChildren records the process-local relationship without changing the durable parent id.
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 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. 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 continuation API 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.
Use a child reference count. A count cannot identify which child still owns teardown work and permits duplicate decrement errors. An identity set retains cancellation and disposal obligations explicitly.
Consequences
The implementation pins these behaviors:
- A continuable child has at most one live Activation and one Agent inbox; the continuation manager has no Activation FIFO or queued Activation state.
SubagentProvider.prepareContinuable?()returns only a detachedContinuableCreateSpec; configured continuable mode requires that capability, whilebackgroundModeremains an independent policy choice.- The manager calls
ctx.agents.create()through its private activation-owner scope, installs the returnedAgentHandleand parent ownership, callsAgent.followup(initialPrompt), and returns{ childId, messageId }when inbox acceptance yields theMessageId, without waiting for turn start or a Session-log write. - Every failure before initial-prompt inbox acceptance rejects without ids and rolls back any created handle, Activation, and parent
ownedChildrenmembership through a closing transaction visible to concurrent delivery and drain; lifecycle publication failure emits no unmatched terminal edge. - Cold resume calls
ctx.agents.resume()from the continuation manager and never dispatches through or requires the initial subagent provider; the descriptor retains the initial provider name after provider removal, whileSubagentProvider.resume?()andSubagentProviderResumeRequestare absent. - A continuable Activation directly owns
AgentHandleand never creates, wraps, or retainsSubagentRun;SubagentProvider.start()andSubagentRunremain one-shot-only, withoutSubagentRun.steer?(). followup()accepts only the exact live direct parent and rechecks that identity at the final no-await inbox-admission boundary after any materialization; durable message source fields 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 itssend_messageadapter return only the acceptedMessageId; the continuation layer accepts no delivery target and defines no subagent-specific route result.- Caller signals stop start and follow-up only before inbox acceptance, while host-scoped and manager-global teardown retain child-first cleanup; the current-turn interrupt is the one public stop and does not enter teardown.
- This version exposes no subagent steering operation or current-turn controller state.
- An idle Agent with live owned children yields a
waitingActivation whoseAgentHandleremains retained. - A
next-turndelivered towaitingwakes 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
AgentHandledisposal; top-level Agents do not join the waiting graph. - Final Activation settlement awaits
ctx.sessions.flush(child.session)as a best-effort barrier, logs rejection without interpreting listener participation as durability proof, then disposes the child handle and releases parent ownership so a flush failure cannot leak awaitingActivation. - Manager teardown closes admission globally; a host owning selected top-level Agents instead closes admission only below their exact identities until those roots leave the registry. Both track admitted materializations by exact ancestry, install one memoized disposal cutoff per selected visible Activation, propagate cancellation top-down, release handles child-first, await every selected branch despite individual failures, and only then dispose the corresponding top-level Agents or manager scope.
- The base lifecycle has no implicit report behavior; the optional report package contributes an explicit child-scoped tool through the setup hook.
- Session logs reconstruct only messages that were actually written, with the source that supplied each message; inbox-accepted but unlogged messages have no restart guarantee.
- No continuable-subagent path creates or depends on a Task,
JobId, 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 and lifecycle-publication failure, global and parent-scoped drain quiescence for materialization caught between Agent publication and Activation registration, sibling-forest isolation, exact ancestry after an intermediate Agent leaves the registry, provider-independent cold resume, final exact-parent reauthorization after cold-resume materialization, caller-signal and teardown 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,
MessageIdcorrelation through inbox events, follow-up during an open turn, waiting wakeup, cold resume, ownership registration and release, child-first disposal, send-versus-dispose races, best-effort final flush with absent and failing listeners, and the absence of public subagent cancellation and steering. - Report-package unit coverage separately pins child-only visibility, setup revocation, authority, delivery modes, stable message identity, and lifecycle races.
- A keyless assembled-app snapshot covers parent delegation and follow-up queueing, the absence of subagent steering and implicit report delivery, retained waiting
AgentHandle, and child-first disposal. A separate report snapshot covers the optional explicit return channel.
Accepted costs
Removing Jobs 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 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 the optional report package, completing a child turn neither sends its content to nor wakes the historical parent. With the package, only an explicit report call sends selected content; quiet delivery does not wake the parent, while next-step delivery wakes it and joins its nearest step boundary. In every case the detailed child output remains in its durable Session.
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 best-effort final flush is logged while the runtime ownership graph continues draining; the persisted child state may be missing or stale. Retry and repair require a separate recovery design.
中文
问题
以前的继续执行管理器让一个 Task、一次提供方执行和一个结果边界共享同一生命周期。Task 结算会 dispose(资源释放)child Agent,Task 完成会注入完成通知,后续输入则重建另一个 Agent。这曾使通用后台工作抽象与会话投递耦合,而可继续 subagent 已经具备会话和 Agent inbox。
如果继续执行管理器为继续执行请求排队,而 Agent 保留自己的 inbox,系统就会出现两个 FIFO,且没有唯一的顺序权威。而把所有消息都交给 Task,则重复了 agent loop(智能体循环)已有的准入、取消和完全停稳机制。Agent.whenIdle() 无法恢复单项请求的 Task 结果,因为一个运行区间可能清空多个排队轮次;宽泛的 Agent.cancel() 也不能精确移除一项排队请求。
运行时生命周期也比单个轮次更长。subagent 可能已经结束自身轮次,但它创建的 child 仍在运行。此时 dispose parent 运行时,会移除仍负责后代拆卸的 Agent。反之,如果让所有历史 subagent 始终驻留,内存使用就会失去上界。
parent Agent 还需要在不改变当前轮次的前提下,向同一个在线 child 发送后续工作。将每条继续执行消息作为 follow-up 排队,可以保留唯一的排序规则。
决策
一个可继续 subagent 拥有一个持久化会话,并且至多拥有一个进程内激活:
persisted Session
-> optional live Activation
-> one retained AgentHandle
-> Agent inbox as the only turn FIFO
-> zero or more owned child Activations
激活是重建 child Agent 的一次驻留周期。它可以执行多个 FIFO 轮次,并在等待后代时保持驻留。它不是请求、结果、取消或 Task 边界。
继续执行管理器负责激活准入、权限检查、在线所有权图、冷恢复和 child-first dispose。Agent loop 负责全部轮次排序与执行。没有任何可继续 subagent 拥有 Task、激活 FIFO 或 queued 激活状态。
物化与公开操作
具名 subagent 提供方只参与准备初始创建规格,此时 spawn 与 fork 有所区别。其可选的 prepareContinuable(request): Promise<ContinuableCreateSpec> 方法就是可继续创建能力。返回的规格只包含与 Agent 实例分离且由提供方决定的创建输入,例如可选的 parent 历史种子;它不包含 Agent、AgentHandle、提示词投递、结果、dispose 或恢复操作。管理器会预留 child 身份,解析持久化描述符和通用 Agent 配置,通过私有 activation-owner 作用域调用 ctx.agents.create(),将返回的 AgentHandle 安装到激活中,建立适用的可继续 parent 所有权,然后调用 Agent.followup(initialPrompt)。inbox 接受消息后会产生一个 MessageId;ctx.subagents.startContinuable() 在此边界返回 { childId, messageId },不等待轮次开始,也不等待消息写入会话日志。
inbox 接受消息前发生任何失败,操作都会在不返回任何 id 的情况下被拒绝。Agent 创建流程负责 handle 移交前的回滚;移交后,管理器会保留一个对并发投递和 drain 可见的关闭事务,dispose 已创建的 handle、移除激活并回滚 parent ownedChildren 中的任何成员关系,再拒绝操作。在驻留 start 事件发布前失败不会发布终止事件,start 发布后失败则通过正常 dispose 闭合生命周期配对。
backgroundMode: 'one-shot' | 'continuable' 仍是部署策略。配置为 continuable 时要求存在 prepareContinuable;该方法是否存在会取代 SubagentProvider.resume?() 成为能力检查,而具备该能力的提供方仍可运行 one-shot 工作。
冷恢复不会通过 subagent 提供方分发。继续执行管理器会归并通用的进程内描述符,通过同一个 activation-owner 作用域调用 ctx.agents.resume(),安装返回的 AgentHandle,并提交等待中的 next-turn。SubagentProvider.resume?() 和 SubagentProviderResumeRequest 均不存在。初始提供方注销后,描述符仍保留其名称;该名称不赋予恢复能力,也不要求后续驻留时该提供方存在。远程提供方需要单独设计。
SubagentProvider.start() 和 SubagentRun 只保留在不变的 one-shot 路径上。可继续激活直接持有自身的 AgentHandle,绝不创建、包装或保留 SubagentRun;因此,SubagentRun.steer?() 不存在。
ctx.subagents.followup(parent, childId, content, { source, signal }) 仍是唯一的从 parent 到 child 的继续执行消息操作。确切的在线 parent Agent 授权投递;冷恢复会在重建前检查该权限,每条路径还会在最终无 await 的 inbox 准入区间再次检查,因此在物化期间被注销或替换的 parent 无法授权投递。source 记录谁提供了获准消息,不赋予任何权限。面向模型的 send_message 工具只保留稳定的 subagent_id 和 message 字段,并始终提交一个 follow-up 轮次。start 和 follow-up 都返回已接受的 MessageId,两者都不报告管理器如何物化激活。
对于 start 和 follow-up,调用方 signal 只在 inbox 接受消息前持有查找、物化和准入。操作返回 MessageId 后,管理器会独立持有该激活;调用方之后的取消不会取消已接受的轮次,也不会 dispose child。
持久化会话与在线激活
会话持有稳定的 child 身份、transcript(文本记录)、直接 parent 谱系、委派深度和带版本的继续执行描述符。SessionHeader.parentSession 记录直接 parent,并作为鉴权输入;它不是在线路由能力,也不表示记录的 parent 仍然驻留。
空闲的历史会话没有 AgentHandle。第一条通过鉴权的 next-turn 投递会根据持久化会话恢复激活,并将消息提交到其 inbox。冷恢复使用经过身份认证的确切在线 parent Agent 执行鉴权;当该 parent 有激活时,还使用它建立所有权,但绝不使用 parent 执行重建。
激活会直接持有已发布的 AgentHandle 直至结算,而管理器的私有 activation-owner 作用域则是其 Cordis 结构化所有者。可继续 subagent 路径不创建任何中间的带结果执行包装层,包括 SubagentRun;一次性委派保持不变,且不属于该生命周期。远程提供方不在此处的范围内,引入时需要单独的激活所有权约定。激活 dispose 后,历史会话不消耗运行时内存。
激活生命周期
内部驻留生命周期有三个条件,没有单独的 queued 状态:
running
| Agent quiescent with live children
v
waiting
| next-turn
+--------------------------> running
running or waiting
| Agent quiescent and no live children
v
settled
| AgentHandle.dispose completes
v
no Activation
running 表示 Agent 正在执行准入或轮次,或者 inbox 中存在会唤醒 Agent 的工作。waiting 表示 Agent 已经完全停稳,但激活仍持有至少一个尚未完成 dispose 的 child 激活。settled 表示 Agent 已经完全停稳且所有持有的 child 都已 dispose;随后管理器会 dispose AgentHandle 并移除激活。
管理器根据 Agent 是否完全停稳以及所持 child 集合派生这些状态,而不是维护第二套执行状态机。在 running 时投递的 next-turn 会进入 Agent inbox。在 waiting 时投递的 next-turn 会唤醒同一个 Agent,并使激活回到 running。在 dispose 完成后投递消息则会冷恢复新激活。
管理器会针对每个持久化 child,将投递、child 释放和 dispose 线性化。如果投递与最终 dispose 发生竞争,只有一方能越过准入截止点:投递要么进入仍在线的 Agent inbox,要么等待 dispose 完成后冷恢复新激活。任何调用方都不能向已经开始 dispose 事务的 handle 发送消息。
一个 inbox 与 follow-up 投递
Agent inbox 是唯一队列。每条继续执行消息都使用 Agent.followup(),并成为一个 FIFO 轮次;继续执行管理器和宿主都不维护另一条消息队列。每个已接受且会唤醒 Agent 的条目都会让当前激活保持在线,直至 Agent.whenIdle() 观察到完整的唤醒工作后缀已经结束。
路由只取决于激活的驻留状态:
| 激活状态 | followup |
|---|---|
running | 在同一激活中排队 |
waiting | 唤醒同一激活 |
| 无激活 | 冷恢复新激活 |
继续执行层不定义单独的投递路由结果。成功投递 ctx.subagents.followup() 或 send_message 时会返回已接受的 MessageId,投递失败则会抛出异常。现有的 agent/inbox/enqueue、agent/inbox/dequeue 和 agent/inbox/discard 事件仍用于观测消息生命周期;适配器可以呈现通用的接受确认,但不暴露 started、queued、resumed 或其他 subagent 专属路由词汇。
child 所有权
每次激活都持有自身的 AgentHandle 和一个 ownedChildren: Set<SessionId>。由于一个会话至多有一次在线激活,child 会话 id 足以标识在线 child,无需另一个运行时 incarnation 引用。SessionHeader.parentSession 记录持久化的直接 parent 身份,ownedChildren 中的成员关系则记录进程内所有权关系。
当经过身份认证的 parent 自身是由继续执行管理器管理的激活时,启动 child 或提交由 parent 发起的工作,会在 child 可以运行或消息可以进入其 inbox 前,将 child 会话 id 加入该 parent 的 ownedChildren。该集合非空时,这个 parent 不能结算或 dispose。顶层 Agent 或其他非继续执行 Agent 没有激活,也不会加入该等待图。
只有在 child Agent 完全停稳、该 child 持有的每个 child 都已 dispose、best-effort 的最终会话 flush 结算且 child 的 AgentHandle 完成 dispose 后,系统才释放 child。管理器会等待 ctx.sessions.flush(child.session),但不解释其参与布尔值:任意 listener 都无法证明所选持久化后端已存储该状态。rejection 会被记录,但不会阻止 handle dispose 或释放所有权,因为保留 child 会让其祖先永久固定在 waiting。如果 child 归 parent 所有,管理器随后会通过 SessionHeader.parentSession 解析在线 parent,并从其 ownedChildren 中移除 child 会话 id。管理器拆卸使用相同的 child-first 顺序。
系统会一直保留所有权,直至 child 激活完成 dispose。后续改进可以更早释放限定到请求的 lease,但这需要精确关联轮次完成,而本 Task-free 设计特意不增加该机制。
顶层拆卸由宿主负责,而不表示为另一次激活。管理器卸载会调用其内部的管理器全局 drain,同步关闭准入,等待每个已获准的物化过程完成发布或回滚,停止稳定的在线森林,并按 child-first 顺序释放。拥有选定顶层 Agent 的宿主使用 drainContinuableDescendants(parents):确切的 Agent 身份只关闭这些根之下的准入,直到每个身份离开注册表,而无关森林和管理器全局准入保持在线;管理器会在第一次 await 之前停止其可见后代,只等待这些根之下已获准的物化过程,并且只释放选定分支。每个已物化的 start 和在线投递都会在与 inbox 提交相同的同步区间内重新检查调用方取消、适用的 draining 作用域、Activation dispose 和确切的 parent 权限,因此只要拆卸或 parent 替换先于接受发生,就会阻止向正在关闭的 handle 投递。只有适用的 drain 结算后,宿主才能 dispose 自己的顶层 Agent;只有管理器全局 drain 会先于管理器作用域 dispose。
activation-owner 作用域之所以存在,是因为普通 Cordis owner effect 按注册逆序撤销,无法表达动态 child 图。管理器初始化时先注册私有作用域的结构化 disposer,再注册自身的 drain disposer,使逆序撤销先执行 drain、再释放该作用域;如果只在与后续 Agent handle 相同的作用域上注册 cleanup effect,结构化 handle dispose 就可能绕过 child-first 顺序。每个物化过程都会在启动内部事务前注册其屏障参与项,并对其确切的在线祖先建立快照,然后保持跟踪,直到安装 Activation 或完全回滚。Activation 会保留其在这组祖先中的弱成员关系,因此中间 Agent 即使离开注册表,也不会让仍在线的后代脱离宿主根节点的可见范围。每个 Activation 都会在取消或递归回调前安装一个记忆化的 dispose promise,使限定作用域的宿主关闭、全局管理器卸载、child 释放和正常结算能够汇合,而不会重复释放。取消会在等待缓慢的后代清理之前自顶向下传播;handle 释放仍是 child-first。同级分支独立 drain;系统会记录单次 dispose 失败,但仍会尝试其余选中 handle,聚合 drain 则在所有选中分支结算后报告失败。这次进程内拆卸不会销毁持久化 child 会话。
报告投递扩展
后来添加的可选 child 作用域 report(output) 工具不会改变 Activation 驻留状态,也不会增加另一条队列。它每轮可调用零次或多次,不允许指定接收方,而是推导在线的直接 parent;投递采用静默注入还是唤醒 parent follow-up,由部署配置选择。report 工具 Agent Note规定其权限、确认、设置贡献和投递约定。
延后的 steering(中途引导)
本版本不暴露 subagent steering 操作。parent 的继续执行消息始终开启后续 FIFO 轮次,因此继续执行层不存储当前轮次控制方,也不新增能够感知控制方的 Agent 准入约定。
后续宿主 UI 可以分别暴露 Steer 和 Follow up 操作。宿主 steering 必须严格且仅限在线使用:只有当激活接受下一步骤时,它才能调用现有的 Agent steering 路径;其他情况必须拒绝,而且绝不能转为排队或冷恢复。是否通过面向模型的工具暴露 parent steering 仍需单独设计。
权限与已记录的发送方身份
权限来自确切的在线 Agent 工具上下文。准入后,MessageSource 和 senderSessionId 记录谁提供了消息;调用方不能用这些字段取得权限。
本版本只授权持久化 child 的直接 parent。管理器会在将 child 注册到该 parent 的 ownedChildren 之前,于最终无 await 的 inbox 准入边界根据确切的在线 parent Agent 检查 SessionHeader.parentSession;冷恢复还会在重建前执行一次更早的检查,以便快速失败。其他 Agent、祖先、宿主、团队和工作流仍被拒绝,直至有具体消费方证明另一种权限协议合理。
由 parent 发起的投递要求 parent 在准入时在线,并通过所有权关系使其继续在线。
持久性、dispose 与恢复
没有 Task 后,系统不再提供 job_output、job_kill、Task 状态或逐消息结果 promise。调用方 signal 只能在 inbox 接受消息前中止 start 或 follow-up。消息被接受后,parent 不能通过 ctx.subagents 取消已接受的消息或 dispose 激活;唯一的公开停止操作是后来的当前轮次中断,它以 keepInbox 取消在线目标的当前轮次,驻留、待处理工作与后代均保持不变。
宿主和管理器拆卸仍是生命周期停止路径。管理器卸载会全局应用它;宿主只会在自己确切拥有的顶层 Agent 之下应用它。两种形式都会关闭适用的准入作用域,停止选中的可见 Activation,等待该作用域中已获准的物化过程,按 child-first 顺序释放,并保留持久化 Session。
每个轮次都会请求执行会话持久性检查点,而 Activation 最终结算还会等待 ctx.sessions.flush(),将其作为 best-effort 屏障。管理器特意忽略布尔结果,因为 listener 是否参与无法标识持久化后端。rejection 会被记录,但不会改变生命周期结果或宿主 drain 的结果;管理器仍会 dispose handle 并释放所有权,后续恢复时持久化 child 状态可能缺失或陈旧。
只有实际写入 child 会话日志的消息,才能在重建时保留提供它的来源;仅被 inbox 接受并不提供重启保证。
会话和描述符的持久化状态可在重启后保留。激活状态、Agent inbox 内容和所有权图都是进程内状态。进程崩溃可能丢失已被接受但仍留在 inbox、尚未写入会话日志的初始提示词或 follow-up。会话和描述符可能保留,因此后续获得授权的消息仍可冷恢复 child,但丢失的消息不会自动回放。恢复已接受但未完成或未写入日志的消息需要持久化 inbox 协议,本提案不隐含该能力。
范围
本版本覆盖可继续的进程内 child,一次性委派保持不变。远程提供方必须具备单独的激活 handle,以及等价的认证控制与 child-first 完全停稳约定,才能支持同样的行为。
它不新增 host-user 继续执行、subagent steering 操作、持久化邮箱、跨进程 lease、中断 inbox 工作的自动回放、团队权限、工作流权限、公开驻留查询、新的在线激活数量或后代总数限制,以及运行时缓存;后来的当前轮次中断在此生命周期之上补充了唯一的公开停止操作。现有委派深度策略保持不变。可选的 child 到 parent 报告是后续消费该生命周期的功能,不属于基础可继续能力。
曾考虑的替代方案
保留由 Task 支撑的激活。 Task 可以提供通用状态、结果收集和取消,但使用 Task 投递会话会产生第二条队列,并重复轮次所有权。本设计放弃这些通用 Task 控制,让 Agent inbox 成为唯一执行顺序。
每个 next-turn 创建一次激活。 这会恢复独立的结果与取消边界,但需要在 Agent inbox 旁维护管理器 FIFO,还会使所保留的 Agent 跨越人为划分的激活边界。每个驻留周期对应一次激活更小,也直接跟随 AgentHandle 生命周期。
等待期间 dispose Agent。 child 仍属于上一个进程内所有权图时重建 parent,需要持久化所有权与拆卸协议。只为尚未完成的所有权图保留 AgentHandle,可以在不让已结算历史驻留的前提下,保留 child-first 拆卸。
让提供方通过 Agent handle 创建、恢复 child 或投递消息。 初始提供方只持有 prepareContinuable() 及其分离式创建规格这一项差异:child 是全新启动,还是带有 parent 前缀。管理器必须通过私有 activation-owner 作用域自行调用 ctx.agents.create(),使该作用域成为每个 handle 的结构化所有者。持久化的进程内会话已经包含初始前缀及通用重建描述符,消息投递则属于 Agent inbox。让提供方持有任何后续 handle、SubagentRun 或消息所有权,会让提供方保留所有权,却没有已发布行为需要它。
将报告投递纳入基础生命周期。 可重复的 child 到 parent 报告与该生命周期兼容,但静默投递还是 next-step 投递、确认、持久性和重试行为都是独立的产品决策。后续的 report 包保持可选,并消费一个显式的 child 设置钩子,因此可继续驻留不会默认授予返回通道。
将 SessionHeader.parentSession 视为在线所有权。 持久化谱系不能证明已记录的 parent 当前持有 child。在线 parent 的 ownedChildren 成员关系会记录进程内关系,而不改变持久化 parent id。
在单独的 link 中保留确切的 parent Agent。 parent 激活已经持有自身 AgentHandle,而且 ownedChildren 会在 child 仍然在线时阻止该激活 dispose。因此,通过会话 id 解析 parent 已经足够,也可以避免冗余的运行时引用。
为继续执行消息维护单独队列。 第二个 FIFO 会让它和 Agent 已接受消息之间顺序不明确。单个 Agent inbox 为每个已接受轮次提供唯一且可观察的顺序。
现在就暴露 subagent steering。 parent steering 需要当前轮次控制方状态,以及不同于 follow-up 投递的单独准入策略。首个版本将每条继续执行消息都排队,可以避免引入该状态及其准入竞争。
在没有 host 消费方的情况下暴露 host-user follow-up。 公开的权限铸造方法和用户分支可以在没有历史 parent 的情况下实现冷恢复,但没有生产 host 适配器调用该操作。在具体的经认证宿主交互能够收到私有能力之前,继续执行 API 只接受确切的在线 parent。
返回 subagent 专属的投递路由。 started、queued 和 resumed 等标签重复了激活与 inbox 状态,却没有给调用方提供独立结果。复用 MessageId 和现有 inbox 事件,可以让投递关联继续由其所属的 Agent 约定承载。
使用 child 引用计数。 计数无法识别哪个 child 仍持有拆卸工作,也允许重复递减错误。身份集合会显式保留取消和 dispose 义务。
影响
本实现固定了以下行为:
- 可继续 child 至多拥有一个在线激活和一个 Agent inbox;继续执行管理器没有激活 FIFO 或 queued 激活状态。
SubagentProvider.prepareContinuable?()只返回分离式ContinuableCreateSpec;配置为 continuable 时要求具备该能力,而backgroundMode仍是独立的策略选择。- 管理器通过私有 activation-owner 作用域调用
ctx.agents.create(),安装返回的AgentHandle并建立 parent 所有权,调用Agent.followup(initialPrompt),然后在 inbox 接受消息并产生MessageId时返回{ childId, messageId },而不等待轮次开始或消息写入会话日志。 - 初始提示词被 inbox 接受前的每条失败路径都会导致操作被拒绝且不返回 id,并通过一个对并发投递和 drain 可见的关闭事务回滚已创建的任何 handle、激活和 parent
ownedChildren成员关系;生命周期发布失败不会产生无配对的终止事件。 - 冷恢复由继续执行管理器调用
ctx.agents.resume(),绝不通过或依赖初始 subagent 提供方;提供方移除后,描述符仍保留初始提供方名称,且SubagentProvider.resume?()和SubagentProviderResumeRequest均不存在。 - 可继续激活直接持有
AgentHandle,绝不创建、包装或保留SubagentRun;SubagentProvider.start()和SubagentRun只用于 one-shot,且没有SubagentRun.steer?()。 followup()只接受确切的在线直接 parent,并在任何物化之后的最终无 await 的 inbox 准入边界再次检查该身份;持久化消息来源信息不能授权投递。- 继续执行消息始终使用
Agent.followup()并共享其 inbox FIFO,包括 child 已有开放轮次的情况。 ctx.subagents.followup()及其send_message适配器只返回已接受的MessageId;继续执行层不接受投递 target,也不定义 subagent 专属路由结果。- 调用方 signal 只能在 inbox 接受消息前停止 start 和 follow-up,限定到宿主的拆卸与管理器全局拆卸则保留 child-first 清理;当前轮次中断是唯一的公开停止操作,且不进入拆卸流程。
- 本版本不暴露 subagent steering 操作或当前轮次控制方状态。
- 带有在线所持 child 的空闲 Agent 会产生
waiting激活,其AgentHandle继续保留。 - 向
waiting投递next-turn会唤醒同一个激活;完成 dispose 后投递消息会冷恢复新激活。 - 每个由继续执行管理器管理的 parent 激活只会在直接持有的所有 child 激活完成
AgentHandledispose 后进行 dispose;顶层 Agent 不加入等待图。 - Activation 最终结算会等待
ctx.sessions.flush(child.session),将其作为 best-effort 屏障;它会记录 rejection,但不会把 listener 参与解释为持久性证明,然后 dispose child handle 并释放 parent 所有权,使 flush 失败不会泄漏waitingActivation。 - 管理器拆卸会全局关闭准入;拥有选定顶层 Agent 的宿主则只关闭这些确切身份之下的准入,直到这些根离开注册表。两者都会按确切祖先关系跟踪已获准的物化过程,为每个选中的可见 Activation 安装一个记忆化 dispose 截止点,自顶向下传播取消,按 child-first 顺序释放 handle,即使个别分支失败也会等待所有选中分支,之后才 dispose 对应的顶层 Agent 或管理器作用域。
- 基础生命周期不暴露隐式报告行为;可选的 report 包通过 setup 钩子贡献一个显式的 child 作用域工具。
- 会话日志只会重建实际写入的消息,并保留每条消息的提供来源;已被 inbox 接受但未写入日志的消息没有重启保证。
- 可继续 subagent 路径不创建或依赖 Task、
JobId、Task 完成通知、Task 取消或中间的带结果执行包装层。 - 单元覆盖固定
startContinuable()在 inbox 接受消息时的返回边界、每条接受前和生命周期发布失败路径的完整回滚、全局和限定到 parent 作用域的 drain 都会等待夹在 Agent 发布与 Activation 注册之间的物化过程完全停稳、同级森林隔离、中间 Agent 离开注册表后的确切祖先关系、不依赖提供方的冷恢复、冷恢复物化后的最终确切 parent 再授权、接受前后两个阶段的调用方 signal 与拆卸所有权,以及已接受但未写入日志的消息不会自动回放。 - 单元覆盖固定仅由驻留状态决定的路由表、单 inbox 顺序、通过 inbox 事件关联
MessageId、在开放轮次期间 follow-up、等待唤醒、冷恢复、所有权注册与释放、child-first dispose、发送与 dispose 的竞争、没有 listener 和 listener 失败时的 best-effort 最终 flush,以及不存在公开 subagent 取消和 steering。 - report 包的单元覆盖会分别固定仅 child 可见性、setup 撤销、权限、投递模式、稳定消息身份和生命周期竞争。
- 一项无密钥整套应用快照覆盖 parent 委派和 follow-up 排队、不存在 subagent steering 和隐式 report 投递、保留 waiting 中的
AgentHandle以及 child-first dispose。另一项 report 快照覆盖可选的显式返回通道。
已接受的代价
移除 Task 会放弃通用后台工作检查、结果收集和精确 Task 取消。如果这些产品功能成为需求,就需要不会重新引入第二条执行队列的请求 ticket 或 inbox 能力。
在后代运行期间保留激活,会按尚未完成所有权图的规模消耗 Agent 资源。现有委派深度策略仍会限制嵌套层级,但本版本不新增在线激活数量或后代总数限制;已结算的历史会话不保留 AgentHandle。
进程内 inbox 和所有权图无法协调两个 harness 进程。允许多个进程并发访问同一持久化存储的部署,仍需要持久化 lease 和邮箱协议。
未安装可选 report 包时,完成 child 轮次既不会把内容发送给历史 parent,也不会唤醒它。安装后,只有显式调用 report 才会发送选中内容;静默投递不唤醒 parent,next-step 投递则会唤醒它并加入最近的 step 边界。无论如何,child 的详细输出都会保留在其持久化会话中。
将每条继续执行消息排队,意味着 parent 无法立即纠正正在进行的 child 轮次;纠正操作会在下一个轮次执行。后续 UI steering 操作可以缩短该延迟,而不改变 follow-up 排序。
best-effort 最终 flush 失败时会记录日志,同时运行时所有权图继续 drain;持久化 child 状态可能缺失或陈旧。重试与修复需要单独的恢复设计。