fix(subagent): keep one-shot labels optional
This commit is contained in:
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md
|
||||
2026-07-22-durable-subagent-catalog-and-list-agents.md: 16d81e4fa5cdbf63fde5e8c42fbe50ec18b18718
|
||||
2026-07-22-durable-subagent-catalog-and-list-agents.zh.md: 5cdce5059b85c17a4ca3b6b22e57a0bc070f3e26
|
||||
2026-07-22-durable-subagent-catalog-and-list-agents.md: b288c113d38e067d323513a3c73afb15163ef76e
|
||||
2026-07-22-durable-subagent-catalog-and-list-agents.zh.md: b6050239b45d375f554b6b0f5e488fbe1a5c045b
|
||||
|
||||
+6
-4
@@ -21,7 +21,7 @@ Parent-to-child enumeration is a service capability with consumer-specific proje
|
||||
- report corpus activity separately as `running` or `inactive`, without implying completion or resumability;
|
||||
- return every resulting child in stable `createdAt` ascending, child-id ascending order.
|
||||
|
||||
Every ordinary local start receives a `one-shot` descriptor, while the continuation manager persists a `continuable` descriptor containing its additional reconstruction fields. The model-facing `list_agents` adapter filters the service result to continuable children and maps `inactive` to its existing `complete` presentation; a UI can consume both modes and decide how to render inactive one-shot history. Descriptor persistence, by-id lookup, direct-parent authorization, and provider-independent cold resume remain owned by the implemented Activation contract. Listing consumes those facts but cannot weaken them or invent a second descriptor representation.
|
||||
Every ordinary local start receives a `one-shot` descriptor with an optional caller-owned display label, while the continuation manager persists a labeled `continuable` descriptor containing its additional reconstruction fields. The model-facing delegation tool already owns a short `description` and supplies it for one-shot display; lower-level callers such as workflows need not invent presentation metadata. The model-facing `list_agents` adapter filters the service result to continuable children and maps `inactive` to its existing `complete` presentation; a UI can consume both modes and choose an id-based fallback for unlabeled one-shot history. Descriptor persistence, by-id lookup, direct-parent authorization, and provider-independent cold resume remain owned by the implemented Activation contract. Listing consumes those facts but cannot weaken them or invent a second descriptor representation.
|
||||
|
||||
### Enumeration decision
|
||||
|
||||
@@ -29,7 +29,7 @@ The first implementation consumes `ctx.sessionQuery.traceSession(parentSessionId
|
||||
|
||||
Corpus construction precedes per-child descriptor inspection. A failure while building the initial trace, including persistence listing failure, a live/persisted header conflict anywhere in the observed corpus, or invalid target lineage, fails the whole `list_agents` call because no trustworthy candidate set exists. Only failures after a successful trace are isolated to one candidate; "corrupt child" in that per-child contract therefore means corrupt loaded event surface or descriptor data, not a corpus-level header conflict.
|
||||
|
||||
Session lineage is broader than subagent identity: an ordinary `ctx.sessions.fork()` also creates a direct child. The session header gains no `kind` discriminator; each candidate must instead contain exactly one valid `subagent/descriptor` event in its own suffix. `SubagentService.start()` resolves `{ mode: 'one-shot', provider, label }` before ordinary provider dispatch, while the continuation manager snapshots and seeds `{ mode: 'continuable', ...composition }` during initial child creation. The local in-process one-shot driver appends its resolved descriptor only during initial creation, and cold resume appends no further descriptor; a second event is corruption rather than evidence of another Activation. The event is the sole evidence that a traced child is a session-backed subagent. A candidate without it is an ordinary fork, a remote child without a local session record, or another non-subagent session and is omitted without a diagnostic.
|
||||
Session lineage is broader than subagent identity: an ordinary `ctx.sessions.fork()` also creates a direct child. The session header gains no `kind` discriminator; each candidate must instead contain exactly one valid `subagent/descriptor` event in its own suffix. `SubagentService.start()` resolves `{ mode: 'one-shot', provider, label? }` before ordinary provider dispatch, while the continuation manager snapshots and seeds `{ mode: 'continuable', ...composition }` during initial child creation. The local in-process one-shot driver appends its resolved descriptor only during initial creation, and cold resume appends no further descriptor; a second event is corruption rather than evidence of another Activation. The event is the sole evidence that a traced child is a session-backed subagent. A candidate without it is an ordinary fork, a remote child without a local session record, or another non-subagent session and is omitted without a diagnostic.
|
||||
|
||||
The published logical record is also the activity source: `SessionRecord.live` means `running`, while `live: false, persisted: true` means `inactive`. Activity comes directly from the trace and causes no additional child-log load. `inactive` encodes neither successful completion nor resumability: it may describe settled one-shot history or a continuable child for which `send_message` can materialize another Activation. Conversely, `running` says only that the session is live: a live continuable Agent outside the continuation manager's matching Activation still appears as `running`, but `send_message` rejects it as an ownership conflict. A child is not visible before its session is published, and no process-local Activation entry is added as a second candidate or activity source. Listing is a snapshot that may race publication, disposal, or a later message; `send_message` remains the authoritative delivery-time operation.
|
||||
|
||||
@@ -45,7 +45,7 @@ If measured scale later requires an index, that index is derived state: session
|
||||
|
||||
`SubagentService.listChildren(parentSessionId: SessionId)` returns `Promise<SubagentListEntry[]>`, one array preserving the trace's candidate order rather than separate child and diagnostic arrays. `SubagentListEntry` is a closed union discriminated by its readonly `kind`:
|
||||
|
||||
- `kind: 'child'` carries readonly `id: SessionId`, durable `label: string`, `mode: 'one-shot' | 'continuable'`, and `activity: 'running' | 'inactive'`;
|
||||
- `kind: 'child'` carries readonly `id: SessionId`, `mode: 'one-shot' | 'continuable'`, and `activity: 'running' | 'inactive'`; a continuable child carries `label: string`, while a one-shot child carries `label?: string`;
|
||||
- `kind: 'diagnostic'` carries readonly `id: SessionId` and `reason: 'corrupt' | 'unsupported' | 'unavailable'`.
|
||||
|
||||
A valid descriptor produces one child entry, a per-child inspection failure produces one diagnostic entry, and a candidate without a descriptor produces no entry. `mode` is durable creation policy; `activity` is a process-local corpus snapshot. Activity is neither `AgentStatus`, the manager's internal Activation state, nor a durable outcome, and the result does not expose the internal `createdAt` sorting key. Exact Activation states and durable outcomes such as successful completion, failure, cancellation, and stop reason require a separate durable activation record and are outside this feature.
|
||||
@@ -78,6 +78,8 @@ The first version has no child deletion operation. If later product behavior del
|
||||
|
||||
**Persist a parent-session catalog event.** Direct-child headers already provide the durable enumeration seed, and the child descriptor is the reconstruction authority. A second parent log duplicates state and creates cross-session ordering and stale-entry behavior without helping by-id resume.
|
||||
|
||||
**Require a display label on every raw start.** This guarantees uniform UI text but makes a presentation concern part of workflow, transport, test, and programmatic start contracts. The raw request keeps the label optional; high-level delegation and continuation APIs supply one where they already own the concept, and UI consumers choose a fallback for unlabeled one-shot children.
|
||||
|
||||
**Fail the whole listing when one child cannot be loaded.** This makes corruption impossible to overlook, but one damaged sibling removes visibility into every healthy child. Per-child diagnostics preserve discovery while keeping each omission explicit.
|
||||
|
||||
**Return separate child and diagnostic arrays.** Separate arrays introduce two ordering domains or require exposing another sort key to reconstruct candidate order. One discriminated entry array preserves the trace order while keeping child and diagnostic fields type-safe.
|
||||
@@ -88,7 +90,7 @@ The first version has no child deletion operation. If later product behavior del
|
||||
|
||||
## Testing
|
||||
|
||||
- `packages/subagent/subagent/tests/service.spec.ts` pins descriptor v2 parsing for both modes and proves raw start resolves a one-shot descriptor before provider dispatch. `packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts` proves the local driver appends that descriptor inside the initial turn. Workflow and delegation-tool tests pin the display label across their respective start boundaries.
|
||||
- `packages/subagent/subagent/tests/service.spec.ts` pins descriptor v2 parsing for both modes and proves an unlabeled raw start resolves a one-shot descriptor before provider dispatch. `packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts` proves the local driver appends that descriptor inside the initial turn. Delegation-tool tests pin propagation of their existing display description.
|
||||
- `packages/subagent/subagent/tests/list-children.spec.ts` pins a query-only composition with sessions, `subagents`, and `sessionQuery` but no `agents`, then drives the full real stack (agent loop, JSONL persistence, spawn/fork providers, the subagent service, and a concrete session-query service) keylessly: one-shot and continuable children from one real trace; a persisted (restart-shaped) parent target; `createdAt`-then-id ordering with authored ties; ordinary-fork and fork-seed ancestor-descriptor exclusion without diagnostics; live `running` vs persisted `inactive`; duplicate-descriptor, malformed-payload, invalid-surface, mismatched-header, and changed-read-target corruption diagnostics that leave healthy siblings visible; unsupported-version and per-child unavailable diagnostics; provider absence without child omission; compacted/uncompacted twins listing identically; grandchild exclusion; trace-phase failure failing the whole call while candidate-phase failures isolate to one child; configuration/window and unrecognized failures propagating as operation failures; forwarded trace/exact-read cancellation with stable `CANCELLED` normalization; and the `SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE` no-service contract. `packages/subagent/subagent/tests/optional-session-query.spec.ts` rejects eager evaluation of the optional runtime while importing the ordinary subagent surface.
|
||||
- `packages/subagent/tool-subagent-control/tests/list-agents.spec.ts` pins the `list_agents` schema (no parameters), the continuable-only projection that omits a healthy one-shot sibling while preserving diagnostics, the fixed child/diagnostic/empty text forms, an end-to-end settled-child listing with its durable label, the no-agent rejection, load-time `sessionQuery` injection, and HMR disposal.
|
||||
- The keyless ACP snapshot scenario `subagent-list-agents` (examples/acp-agent) pins the model-visible transcript: a background delegation settles, and `list_agents` executes for real against the subagent service, session query, and JSONL persistence, rendering `<id> [complete] — <label>`.
|
||||
|
||||
+6
-4
@@ -21,7 +21,7 @@ parent 到 child 的枚举是一项带消费方专用投影的服务功能。`Su
|
||||
- 将语料活动状态单独报告为 `running` 或 `inactive`,但不暗示已完成或可恢复;
|
||||
- 按 `createdAt` 升序、再按 child id 升序稳定返回所有结果 child。
|
||||
|
||||
每次普通的本地启动都会收到 `one-shot` 描述符,而继续执行管理器会持久化包含附加重建字段的 `continuable` 描述符。面向模型的 `list_agents` 适配器会将服务结果过滤为可继续 child,并将 `inactive` 映射为其现有的 `complete` 表示;UI 可以消费两种模式,并自行决定如何渲染非活跃的一次性历史。描述符持久化、按 id 查找、直接 parent 鉴权和不依赖提供方的冷恢复仍归已实现的 Activation 契约负责。列表查询消费这些事实,但不能削弱它们,也不能另行发明第二种描述符表示。
|
||||
每次普通的本地启动都会收到带可选、由调用方拥有之显示标签的 `one-shot` 描述符,而继续执行管理器会持久化带标签、包含附加重建字段的 `continuable` 描述符。面向模型的委派工具已经拥有简短 `description`,会将其用于一次性显示;workflow 等底层调用方无需凭空构造展示元数据。面向模型的 `list_agents` 适配器会将服务结果过滤为可继续 child,并将 `inactive` 映射为其现有的 `complete` 表示;UI 可以消费两种模式,并为无标签的一次性历史选择基于 id 的回退展示。描述符持久化、按 id 查找、直接 parent 鉴权和不依赖提供方的冷恢复仍归已实现的 Activation 契约负责。列表查询消费这些事实,但不能削弱它们,也不能另行发明第二种描述符表示。
|
||||
|
||||
### 枚举决策
|
||||
|
||||
@@ -29,7 +29,7 @@ parent 到 child 的枚举是一项带消费方专用投影的服务功能。`Su
|
||||
|
||||
语料构建先于逐 child 描述符检查。构建初始追踪时如果发生持久化列表查询失败、所观测语料中任意位置的存活/持久化 header 冲突或目标谱系无效,整个 `list_agents` 调用都会失败,因为此时不存在可信的候选集。只有初始追踪成功后的失败才会被隔离到单个候选;因此,这项逐 child 契约中的“损坏 child”是指已加载的事件 surface 或描述符数据损坏,而不是语料级 header 冲突。
|
||||
|
||||
会话谱系涵盖的范围比 subagent 身份更广:普通 `ctx.sessions.fork()` 也会创建直接 child。会话 header 不新增 `kind` 判别字段;每个候选必须改为在自身后缀中恰好包含一个有效的 `subagent/descriptor` 事件。`SubagentService.start()` 会在普通提供方分发前解析 `{ mode: 'one-shot', provider, label }`,而继续执行管理器会在初始创建 child 时为 `{ mode: 'continuable', ...composition }` 建立快照并将其作为 seed。本地进程内的一次性驱动只在初始创建期间追加已解析的描述符,从持久化存储冷恢复时不会追加其他描述符;第二个事件属于损坏,而不是另一次 Activation 的证据。该事件是已追踪 child 属于由会话支撑的 subagent 的唯一证据。缺少该事件的候选属于普通 fork、没有本地会话记录的远端 child 或其他非 subagent 会话,系统会将其排除且不产生 diagnostic。
|
||||
会话谱系涵盖的范围比 subagent 身份更广:普通 `ctx.sessions.fork()` 也会创建直接 child。会话 header 不新增 `kind` 判别字段;每个候选必须改为在自身后缀中恰好包含一个有效的 `subagent/descriptor` 事件。`SubagentService.start()` 会在普通提供方分发前解析 `{ mode: 'one-shot', provider, label? }`,而继续执行管理器会在初始创建 child 时为 `{ mode: 'continuable', ...composition }` 建立快照并将其作为 seed。本地进程内的一次性驱动只在初始创建期间追加已解析的描述符,从持久化存储冷恢复时不会追加其他描述符;第二个事件属于损坏,而不是另一次 Activation 的证据。该事件是已追踪 child 属于由会话支撑的 subagent 的唯一证据。缺少该事件的候选属于普通 fork、没有本地会话记录的远端 child 或其他非 subagent 会话,系统会将其排除且不产生 diagnostic。
|
||||
|
||||
已发布的逻辑记录同时也是活动状态来源:`SessionRecord.live` 表示 `running`,而 `live: false, persisted: true` 表示 `inactive`。活动状态直接来自追踪结果,不会导致额外加载 child 日志。`inactive` 既不表示执行成功,也不表示可恢复:它可能表示已结算的一次性历史,也可能表示 `send_message` 可以为其物化另一次 Activation 的可继续 child。反过来,`running` 只表示会话存活:位于继续执行管理器对应 Activation 之外的存活可继续 Agent 仍会显示为 `running`,但 `send_message` 会将其作为所有权冲突拒绝。child 会话发布前不可见,也不会添加进程内 Activation 条目作为第二个候选来源或活动状态来源。列表查询是一份快照,可能与发布、dispose 或后续消息发生竞态;`send_message` 仍是消息送达时的权威操作。
|
||||
|
||||
@@ -45,7 +45,7 @@ subagent 服务将 `sessionQuery` 保持为可选依赖,因此没有该服务
|
||||
|
||||
`SubagentService.listChildren(parentSessionId: SessionId)` 返回 `Promise<SubagentListEntry[]>`,其中的单个数组不会将 child 与 diagnostic 分开,而是保留追踪结果中的候选顺序。`SubagentListEntry` 是一个由只读 `kind` 判别的封闭联合类型:
|
||||
|
||||
- `kind: 'child'` 携带只读的 `id: SessionId`、持久化 `label: string`、`mode: 'one-shot' | 'continuable'` 和 `activity: 'running' | 'inactive'`;
|
||||
- `kind: 'child'` 携带只读的 `id: SessionId`、`mode: 'one-shot' | 'continuable'` 和 `activity: 'running' | 'inactive'`;可继续 child 携带 `label: string`,一次性 child 则携带 `label?: string`;
|
||||
- `kind: 'diagnostic'` 携带只读的 `id: SessionId` 和 `reason: 'corrupt' | 'unsupported' | 'unavailable'`。
|
||||
|
||||
有效描述符产生一个 child 条目,逐 child 检查失败产生一个 diagnostic 条目,缺少描述符的候选不产生条目。`mode` 是持久化创建策略;`activity` 是进程本地语料快照。活动状态既不是 `AgentStatus`、管理器内部的 Activation 状态,也不是持久化结果,结果不公开内部 `createdAt` 排序键。成功完成、失败、取消和停止原因等精确 Activation 状态与持久化结果需要单独的持久化激活记录,不在本功能范围内。
|
||||
@@ -78,6 +78,8 @@ diagnostic 是瞬时查询结果,不属于会话事件或目录状态。推导
|
||||
|
||||
**持久化 parent 会话目录事件。** 直接 child header 已经提供持久化枚举种子,child 描述符则是重建的权威信息。第二份 parent 日志会重复状态,并造成跨会话顺序和陈旧条目行为,却无助于按 id 恢复。
|
||||
|
||||
**要求每次底层启动都提供显示标签。** 这会保证 UI 文本一致,却会把展示关注点引入 workflow、传输、测试和程序化启动契约。底层请求保持标签可选;高层委派与继续执行 API 在本就拥有该概念时提供标签,UI 消费方则为无标签的一次性 child 选择回退展示。
|
||||
|
||||
**某个 child 无法加载时让整次列表查询失败。** 这种做法不会让损坏问题被忽略,但一个损坏的 sibling 会让每个健康 child 都不再可见。逐 child diagnostic 在保持每次排除明确可见的同时,也保留了发现能力。
|
||||
|
||||
**分别返回 child 和 diagnostic 数组。** 分离的数组会引入两个排序域,或者要求公开另一个排序键才能重建候选顺序。一个带判别字段的条目数组既能保留追踪顺序,也能保证 child 与 diagnostic 字段的类型安全。
|
||||
@@ -88,7 +90,7 @@ diagnostic 是瞬时查询结果,不属于会话事件或目录状态。推导
|
||||
|
||||
## 测试
|
||||
|
||||
- `packages/subagent/subagent/tests/service.spec.ts` 固定两种模式下的描述符 v2 解析,并证明底层启动会在分发给提供方之前解析出一次性描述符。`packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts` 证明本地驱动会在初始轮次内追加该描述符。workflow 与委派工具测试固定显示标签在各自启动边界上的传递。
|
||||
- `packages/subagent/subagent/tests/service.spec.ts` 固定两种模式下的描述符 v2 解析,并证明无标签的底层启动会在分发给提供方之前解析出一次性描述符。`packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts` 证明本地驱动会在初始轮次内追加该描述符。委派工具测试固定其现有显示说明的传递。
|
||||
- `packages/subagent/subagent/tests/list-children.spec.ts` 先固定一个只有会话、`subagents` 和 `sessionQuery` 而没有 `agents` 的纯查询组合,再以无密钥方式驱动完整真实栈(agent loop、JSONL 持久化、spawn/fork 提供方、subagent 服务,以及一个具体的会话查询服务):来自同一真实追踪的一次性与可继续 child;只存在于持久化存储中(重启形态)的 parent 目标;带有人工构造并列项的按 `createdAt` 再按 id 排序;排除普通 fork 和 fork seed 中祖先描述符且不产生 diagnostic;存活 `running` 与持久化 `inactive` 的对比;重复描述符、载荷格式错误、无效 surface、header 不匹配和读取目标已变化的损坏 diagnostic 均不隐藏健康的 sibling;不受支持版本与逐 child unavailable diagnostic;提供方缺失时不排除 child;压缩与未压缩的孪生 child 列表结果一致;排除孙代会话;追踪阶段失败导致整次调用失败而候选阶段失败只隔离到单个 child;配置/窗口错误和无法识别的失败作为操作失败向上传播;转发 trace/精确读取取消并稳定归一化为 `CANCELLED`;以及 `SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE` 缺服务契约。`packages/subagent/subagent/tests/optional-session-query.spec.ts` 会在导入普通 subagent surface 时拒绝对可选运行时的 eager 求值。
|
||||
- `packages/subagent/tool-subagent-control/tests/list-agents.spec.ts` 固定 `list_agents` 的 schema(无参数)、只保留可继续 child 且排除健康的一次性 sibling、同时保留 diagnostic 的投影、child/diagnostic/空结果的固定文本形式、带持久化 label 的已结束 child 端到端列表、无调用 agent 时的拒绝、加载时的 `sessionQuery` 注入,以及 HMR dispose。
|
||||
- 无密钥 ACP 快照场景 `subagent-list-agents`(examples/acp-agent)固定模型可见的转写:一次后台委派结束后,`list_agents` 针对 subagent 服务、会话查询和 JSONL 持久化真实执行,渲染 `<id> [complete] — <label>`。
|
||||
|
||||
@@ -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: 7af4e119fa07d360f03242b2db85c60a5301cd6b
|
||||
subagent.zh.md: 046135ea378e07d5e5d96b8e7fc9663c4a9329c6
|
||||
subagent.md: fec0cc0c54d7d895cde8708bb8b12d81a3b21dd1
|
||||
subagent.zh.md: c36ae26b791cbd3dc8972e6f185ed4ff9594b70f
|
||||
|
||||
@@ -45,8 +45,8 @@ The tool layer builds this request from the model input and its own config; the
|
||||
* {@link SubagentProvider.start}.
|
||||
*/
|
||||
interface SubagentStartRequest {
|
||||
/** Short display label persisted with a session-backed child. */
|
||||
readonly label: string
|
||||
/** Optional short display label persisted with a session-backed child. */
|
||||
readonly label?: string
|
||||
/** Content delivered as the child's user message. */
|
||||
readonly prompt: ContentBlock[]
|
||||
/**
|
||||
@@ -214,13 +214,13 @@ interface ContinuableCreateSpec {
|
||||
}
|
||||
```
|
||||
|
||||
The descriptor (`SubagentDescriptorData` in [descriptor.ts](../../packages/subagent/subagent/src/descriptor.ts)) is a mode-discriminated durable identity for every session-backed subagent. Both modes carry the provider name and delegation `description` as the durable creation `label`. `one-shot` stops there; `continuable` additionally snapshots resolved child `agentOptions.provider`/`model` and optional `persona`/`toolFilter` for cold resume. It never snapshots the merge-extensible `AgentOptions` object, so an unrelated extension value cannot break continuation and a later composition input is a deliberate version change. It omits `subagentDepth` (cold resume trusts the persisted header's `delegationDepth` as the monotone floor) and `outputSchema` (one run or Activation's result contract, not durable identity).
|
||||
The descriptor (`SubagentDescriptorData` in [descriptor.ts](../../packages/subagent/subagent/src/descriptor.ts)) is a mode-discriminated durable identity for every session-backed subagent. Both modes carry the provider name. A `one-shot` descriptor optionally carries a caller-owned display `label`; a `continuable` descriptor requires the delegation `description` as its durable creation label and additionally snapshots resolved child `agentOptions.provider`/`model` and optional `persona`/`toolFilter` for cold resume. It never snapshots the merge-extensible `AgentOptions` object, so an unrelated extension value cannot break continuation and a later composition input is a deliberate version change. It omits `subagentDepth` (cold resume trusts the persisted header's `delegationDepth` as the monotone floor) and `outputSchema` (one run or Activation's result contract, not durable identity).
|
||||
|
||||
A local one-shot provider appends the descriptor inside the child's initial turn before its first request. The continuation manager appends the descriptor after any provider-supplied lineage and before the initial prompt is admitted; `header.seedLength` remains the fork-lineage boundary, so descriptor lookup reads the child's own suffix. The event is log-only: no `surfaceOp`, never in model history, and retained across compaction by the append-only log. Malformed current-version descriptors are corrupt; unsupported versions cannot be classified by this runtime.
|
||||
|
||||
## Durable enumeration: `listChildren()` and `SubagentListEntry`
|
||||
|
||||
`SubagentService.listChildren(parentSessionId)` enumerates the parent's direct session-backed subagents from one `ctx.sessionQuery.traceSession()` observation, without loading or resuming any Agent. Session lineage is broader than subagent identity — ordinary forks share `parentSession` — so exactly one supported `subagent/descriptor` event in the child's own suffix (after `seedLength`, so a fork seed cannot leak an ancestor's descriptor) is the sole subagent discriminator. The result is one `SubagentListEntry[]` in the trace's `createdAt`-then-id candidate order: a valid descriptor yields a `child` entry with `mode: 'one-shot' | 'continuable'` and `activity: 'running' | 'inactive'`; a per-child inspection failure yields a `diagnostic` entry (`corrupt`, `unsupported`, or `unavailable`) so one damaged sibling cannot hide healthy children; a missing descriptor yields no entry. Activity snapshots only whether the logical record is live in `ctx.sessions`, not outcome or resumability. A service consumer such as a UI can display both modes, while the model-facing `list_agents` adapter (the separately loadable `/list-agents` plugin of [dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control)) keeps only continuable entries and maps activity to its existing `running`/`complete` vocabulary. A failure while building the initial trace fails the whole call — per-child isolation begins only after a trustworthy candidate set exists. The service keeps `sessionQuery` optional for by-id continuation: `listChildren()` throws `SubagentError` with code `SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE` when it is absent, while the list tool requires `ctx.subagents` and `ctx.sessionQuery` at plugin load. Listing does not consult the continuation manager's Activation map, Agent registry, or provider availability; `send_message` remains the authoritative delivery-time operation, and a listed running continuable child may still reject delivery as an ownership conflict.
|
||||
`SubagentService.listChildren(parentSessionId)` enumerates the parent's direct session-backed subagents from one `ctx.sessionQuery.traceSession()` observation, without loading or resuming any Agent. Session lineage is broader than subagent identity — ordinary forks share `parentSession` — so exactly one supported `subagent/descriptor` event in the child's own suffix (after `seedLength`, so a fork seed cannot leak an ancestor's descriptor) is the sole subagent discriminator. The result is one `SubagentListEntry[]` in the trace's `createdAt`-then-id candidate order: a valid descriptor yields a `child` entry with `mode: 'one-shot' | 'continuable'` and `activity: 'running' | 'inactive'`; continuable entries always carry `label`, while one-shot entries carry it only when the start caller supplied presentation metadata. A per-child inspection failure yields a `diagnostic` entry (`corrupt`, `unsupported`, or `unavailable`) so one damaged sibling cannot hide healthy children; a missing descriptor yields no entry. Activity snapshots only whether the logical record is live in `ctx.sessions`, not outcome or resumability. A service consumer such as a UI can display both modes and choose an unlabeled one-shot fallback, while the model-facing `list_agents` adapter (the separately loadable `/list-agents` plugin of [dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control)) keeps only continuable entries and maps activity to its existing `running`/`complete` vocabulary. A failure while building the initial trace fails the whole call — per-child isolation begins only after a trustworthy candidate set exists. The service keeps `sessionQuery` optional for by-id continuation: `listChildren()` throws `SubagentError` with code `SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE` when it is absent, while the list tool requires `ctx.subagents` and `ctx.sessionQuery` at plugin load. Listing does not consult the continuation manager's Activation map, Agent registry, or provider availability; `send_message` remains the authoritative delivery-time operation, and a listed running continuable child may still reject delivery as an ownership conflict.
|
||||
|
||||
## The terminal result: `SubagentResult`
|
||||
|
||||
|
||||
@@ -45,8 +45,8 @@ interface SubagentCapabilities {
|
||||
* {@link SubagentProvider.start}.
|
||||
*/
|
||||
interface SubagentStartRequest {
|
||||
/** Short display label persisted with a session-backed child. */
|
||||
readonly label: string
|
||||
/** Optional short display label persisted with a session-backed child. */
|
||||
readonly label?: string
|
||||
/** Content delivered as the child's user message. */
|
||||
readonly prompt: ContentBlock[]
|
||||
/**
|
||||
@@ -214,13 +214,13 @@ interface ContinuableCreateSpec {
|
||||
}
|
||||
```
|
||||
|
||||
描述符([descriptor.ts](../../packages/subagent/subagent/src/descriptor.ts) 中的 `SubagentDescriptorData`)是每个由会话支撑的 subagent 所使用、按模式判别的持久化身份。两种模式都携带提供方名称和作为持久化创建 `label` 的委派 `description`。`one-shot` 到此为止;`continuable` 还会对已解析的子 agent `agentOptions.provider`/`model` 与可选的 `persona`/`toolFilter` 建立快照,用于冷恢复。它绝不会对可合并扩展的 `AgentOptions` 对象建立快照,因此无关的扩展值不会破坏继续执行,后续新增组合配置输入则是一次有意的版本更改。描述符省略 `subagentDepth`(冷恢复以持久化 header 中的 `delegationDepth` 作为单调下界)和 `outputSchema`(单次运行或 Activation 的结果契约,而非持久化身份)。
|
||||
描述符([descriptor.ts](../../packages/subagent/subagent/src/descriptor.ts) 中的 `SubagentDescriptorData`)是每个由会话支撑的 subagent 所使用、按模式判别的持久化身份。两种模式都携带提供方名称。`one-shot` 描述符可以携带调用方拥有的可选显示 `label`;`continuable` 描述符要求以委派 `description` 作为持久化创建标签,并另外对已解析的子 agent `agentOptions.provider`/`model` 与可选的 `persona`/`toolFilter` 建立快照,用于冷恢复。它绝不会对可合并扩展的 `AgentOptions` 对象建立快照,因此无关的扩展值不会破坏继续执行,后续新增组合配置输入则是一次有意的版本更改。描述符省略 `subagentDepth`(冷恢复以持久化 header 中的 `delegationDepth` 作为单调下界)和 `outputSchema`(单次运行或 Activation 的结果契约,而非持久化身份)。
|
||||
|
||||
本地一次性提供方会在子 agent 的初始轮次内、首次请求前追加描述符。继续执行管理器会在任何提供方提供的谱系之后、初始 prompt 获准之前追加描述符;`header.seedLength` 仍是 fork 谱系边界,因此描述符查找会读取子 agent 自身的后缀。该事件只进入日志:不含 `surfaceOp`,绝不进入模型历史,并由仅追加日志跨压缩保留。格式错误的当前版本描述符属于损坏;本运行时无法对不受支持的版本进行分类。
|
||||
|
||||
## 持久化枚举:`listChildren()` 与 `SubagentListEntry`
|
||||
|
||||
`SubagentService.listChildren(parentSessionId)` 从一次 `ctx.sessionQuery.traceSession()` 观测中枚举 parent 直接且由会话支撑的 subagent,而不会加载或恢复任何 Agent。会话谱系的范围比 subagent 身份更广——普通 fork 也会共享 `parentSession`——因此,child 自身后缀中恰好一个受支持的 `subagent/descriptor` 事件(位于 `seedLength` 之后,避免 fork seed 泄漏祖先描述符)是唯一的 subagent 判别信息。结果是一个按追踪结果中 `createdAt`、再按 id 排列候选顺序的 `SubagentListEntry[]`:有效描述符生成带有 `mode: 'one-shot' | 'continuable'` 和 `activity: 'running' | 'inactive'` 的 `child` 条目;逐 child 检查失败生成 `diagnostic` 条目(`corrupt`、`unsupported` 或 `unavailable`),因此一个损坏的 sibling 不会隐藏健康 child;缺少描述符则不生成条目。活动状态只表示逻辑记录是否在 `ctx.sessions` 中存活,而不表示结果或可恢复性。UI 等服务消费方可以展示两种模式;面向模型的 `list_agents` 适配器([dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control) 中可单独加载的 `/list-agents` 插件)则只保留可继续条目,并将活动状态映射到现有的 `running`/`complete` 词汇。构建初始追踪时的失败会让整个调用失败——只有得到可信候选集后才开始逐 child 隔离。服务将 `sessionQuery` 保持为按 id 继续执行时的可选依赖:缺少该服务时,`listChildren()` 抛出 `SubagentError`,并携带错误码 `SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE`;列表工具则在插件加载时要求 `ctx.subagents` 与 `ctx.sessionQuery`。枚举不会查询继续执行管理器的 Activation map、Agent 注册表或提供方可用性;`send_message` 仍是消息送达时的权威操作,列表中的运行中可继续 child 仍可能因所有权冲突而拒绝投递。
|
||||
`SubagentService.listChildren(parentSessionId)` 从一次 `ctx.sessionQuery.traceSession()` 观测中枚举 parent 直接且由会话支撑的 subagent,而不会加载或恢复任何 Agent。会话谱系的范围比 subagent 身份更广——普通 fork 也会共享 `parentSession`——因此,child 自身后缀中恰好一个受支持的 `subagent/descriptor` 事件(位于 `seedLength` 之后,避免 fork seed 泄漏祖先描述符)是唯一的 subagent 判别信息。结果是一个按追踪结果中 `createdAt`、再按 id 排列候选顺序的 `SubagentListEntry[]`:有效描述符生成带有 `mode: 'one-shot' | 'continuable'` 和 `activity: 'running' | 'inactive'` 的 `child` 条目;可继续条目始终携带 `label`,一次性条目则只在启动调用方提供展示元数据时携带该字段。逐 child 检查失败生成 `diagnostic` 条目(`corrupt`、`unsupported` 或 `unavailable`),因此一个损坏的 sibling 不会隐藏健康 child;缺少描述符则不生成条目。活动状态只表示逻辑记录是否在 `ctx.sessions` 中存活,而不表示结果或可恢复性。UI 等服务消费方可以展示两种模式,并为无标签的一次性 child 选择回退展示;面向模型的 `list_agents` 适配器([dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control) 中可单独加载的 `/list-agents` 插件)则只保留可继续条目,并将活动状态映射到现有的 `running`/`complete` 词汇。构建初始追踪时的失败会让整个调用失败——只有得到可信候选集后才开始逐 child 隔离。服务将 `sessionQuery` 保持为按 id 继续执行时的可选依赖:缺少该服务时,`listChildren()` 抛出 `SubagentError`,并携带错误码 `SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE`;列表工具则在插件加载时要求 `ctx.subagents` 与 `ctx.sessionQuery`。枚举不会查询继续执行管理器的 Activation map、Agent 注册表或提供方可用性;`send_message` 仍是消息送达时的权威操作,列表中的运行中可继续 child 仍可能因所有权冲突而拒绝投递。
|
||||
|
||||
## 终态结果:`SubagentResult`
|
||||
|
||||
|
||||
@@ -1817,7 +1817,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'ContinuableSubagentDescriptorData',
|
||||
declaration: 'export interface ContinuableSubagentDescriptorData extends SubagentDescriptorBase {\n readonly mode: \'continuable\';\n readonly agentProvider?: string;\n readonly agentModel?: string;\n readonly persona?: string;\n readonly toolFilter?: ToolRestriction;\n}',
|
||||
declaration: 'export interface ContinuableSubagentDescriptorData extends SubagentDescriptorBase {\n readonly mode: \'continuable\';\n readonly label: string;\n readonly agentProvider?: string;\n readonly agentModel?: string;\n readonly persona?: string;\n readonly toolFilter?: ToolRestriction;\n}',
|
||||
},
|
||||
{
|
||||
name: 'CreateAgentOptions',
|
||||
@@ -2133,7 +2133,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'OneShotSubagentDescriptorData',
|
||||
declaration: 'export interface OneShotSubagentDescriptorData extends SubagentDescriptorBase {\n readonly mode: \'one-shot\';\n}',
|
||||
declaration: 'export interface OneShotSubagentDescriptorData extends SubagentDescriptorBase {\n readonly mode: \'one-shot\';\n readonly label?: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'PermissionSelect',
|
||||
@@ -2713,7 +2713,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'SubagentListEntry',
|
||||
declaration: 'export type SubagentListEntry = {\n readonly kind: \'child\';\n readonly id: SessionId;\n readonly label: string;\n readonly mode: \'one-shot\' | \'continuable\';\n readonly activity: \'running\' | \'inactive\';\n} | {\n readonly kind: \'diagnostic\';\n readonly id: SessionId;\n readonly reason: \'corrupt\' | \'unsupported\' | \'unavailable\';\n};',
|
||||
declaration: 'export type SubagentListEntry = {\n readonly kind: \'child\';\n readonly id: SessionId;\n readonly activity: \'running\' | \'inactive\';\n} & ({\n readonly mode: \'one-shot\';\n readonly label?: string;\n} | {\n readonly mode: \'continuable\';\n readonly label: string;\n}) | {\n readonly kind: \'diagnostic\';\n readonly id: SessionId;\n readonly reason: \'corrupt\' | \'unsupported\' | \'unavailable\';\n};',
|
||||
},
|
||||
{
|
||||
name: 'SubagentProvider',
|
||||
@@ -2729,7 +2729,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'SubagentStartRequest',
|
||||
declaration: 'export interface SubagentStartRequest {\n readonly label: string;\n readonly prompt: ContentBlock[];\n readonly parent: Agent;\n readonly signal: AbortSignal;\n readonly agentOptions?: AgentOptions;\n readonly outputSchema?: ObjectJsonSchema;\n readonly maxDepth?: number;\n readonly toolFilter?: ToolRestriction;\n readonly persona?: string;\n}',
|
||||
declaration: 'export interface SubagentStartRequest {\n readonly label?: string;\n readonly prompt: ContentBlock[];\n readonly parent: Agent;\n readonly signal: AbortSignal;\n readonly agentOptions?: AgentOptions;\n readonly outputSchema?: ObjectJsonSchema;\n readonly maxDepth?: number;\n readonly toolFilter?: ToolRestriction;\n readonly persona?: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SubagentStopReason',
|
||||
|
||||
@@ -64,7 +64,6 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('ACP backend with-key e2e (drive
|
||||
})
|
||||
|
||||
const run = await ctx.subagents.start('acp', {
|
||||
label: 'reply pong',
|
||||
prompt: [{ type: 'text', text: 'Reply with exactly the word PONG and nothing else. Do not use any tools.' }],
|
||||
parent: fakeParent,
|
||||
signal: new AbortController().signal,
|
||||
@@ -96,7 +95,6 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('ACP backend with-key e2e (drive
|
||||
})
|
||||
|
||||
const run = await ctx.subagents.start('acp', {
|
||||
label: 'write proof file',
|
||||
prompt: [{ type: 'text', text:
|
||||
'Use the bash tool to write the text ACP_CHILD_WAS_HERE into a file named proof.txt '
|
||||
+ 'in the current directory. Then reply DONE.' }],
|
||||
|
||||
@@ -27,7 +27,7 @@ const mockServer = fileURLToPath(new URL('./mock-acp-server.ts', import.meta.url
|
||||
const fakeParent = { id: 'parent', session: { header: { cwd: process.cwd() } } } as unknown as Agent
|
||||
|
||||
function request(text = 'p', signal = new AbortController().signal) {
|
||||
return { label: text, prompt: [{ type: 'text' as const, text }], parent: fakeParent, signal }
|
||||
return { prompt: [{ type: 'text' as const, text }], parent: fakeParent, signal }
|
||||
}
|
||||
|
||||
interface SetupEnv {
|
||||
@@ -210,9 +210,7 @@ describe('cwd resolution', () => {
|
||||
try {
|
||||
const ctx = await setup({ MOCK_ECHO_CWD: '1' })
|
||||
const parent = { id: 'parent', session: { header: { cwd: workdir } } } as unknown as Agent
|
||||
const run = await ctx.subagents.start('acp', {
|
||||
label: 'p', prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal,
|
||||
})
|
||||
const run = await ctx.subagents.start('acp', { prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal })
|
||||
const result = await run.result
|
||||
await run.dispose()
|
||||
// Line 1: where the child process actually ran; line 2: the workspace the
|
||||
@@ -233,9 +231,7 @@ describe('cwd resolution', () => {
|
||||
// A command that would create the sentinel if the child were ever spawned.
|
||||
await ctx.plugin(acp, { providerName: 'acp', command: 'touch', args: [sentinel], permission: 'reject', env: {} })
|
||||
const parent = { id: 'parent', session: { header: {} } } as unknown as Agent
|
||||
await expect(ctx.subagents.start('acp', {
|
||||
label: 'p', prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal,
|
||||
}))
|
||||
await expect(ctx.subagents.start('acp', { prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal }))
|
||||
.rejects.toThrow('no working directory')
|
||||
// Resolution failed BEFORE the process boundary — nothing was launched.
|
||||
expect(existsSync(sentinel)).toBe(false)
|
||||
@@ -260,9 +256,7 @@ describe('cwd resolution', () => {
|
||||
env: { MOCK_ECHO_CWD: '1' },
|
||||
})
|
||||
const parent = { id: 'parent', session: { header: { cwd: parentDir } } } as unknown as Agent
|
||||
const run = await ctx.subagents.start('acp', {
|
||||
label: 'p', prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal,
|
||||
})
|
||||
const run = await ctx.subagents.start('acp', { prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal })
|
||||
const result = await run.result
|
||||
await run.dispose()
|
||||
expect(text(result.output)).toBe(`${configured}\n${configured}`)
|
||||
@@ -358,9 +352,7 @@ describe('cwd resolution', () => {
|
||||
// re-introduce the launch-directory dependency this resolution removes.
|
||||
const ctx = await setup({})
|
||||
const parent = { id: 'parent', session: { header: { cwd: 'relative/workspace' } } } as unknown as Agent
|
||||
await expect(ctx.subagents.start('acp', {
|
||||
label: 'p', prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal,
|
||||
}))
|
||||
await expect(ctx.subagents.start('acp', { prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal }))
|
||||
.rejects.toThrow('must be an absolute path')
|
||||
})
|
||||
|
||||
@@ -371,9 +363,7 @@ describe('cwd resolution', () => {
|
||||
try {
|
||||
const ctx = await setup({})
|
||||
const parent = { id: 'parent', session: { header: { cwd: file } } } as unknown as Agent
|
||||
await expect(ctx.subagents.start('acp', {
|
||||
label: 'p', prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal,
|
||||
}))
|
||||
await expect(ctx.subagents.start('acp', { prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal }))
|
||||
.rejects.toThrow('not an accessible directory')
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true })
|
||||
@@ -389,9 +379,7 @@ describe('cwd resolution', () => {
|
||||
await ctx.plugin(LocalSubprocessService)
|
||||
await ctx.plugin(acp, { providerName: 'acp', command: 'touch', args: [sentinel], permission: 'reject', env: {} })
|
||||
const parent = { id: 'parent', session: { header: { cwd: join(tmp, 'vanished') } } } as unknown as Agent
|
||||
await expect(ctx.subagents.start('acp', {
|
||||
label: 'p', prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal,
|
||||
}))
|
||||
await expect(ctx.subagents.start('acp', { prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal }))
|
||||
.rejects.toThrow('not an accessible directory')
|
||||
expect(existsSync(sentinel)).toBe(false)
|
||||
} finally {
|
||||
|
||||
@@ -22,16 +22,8 @@ async function mountInvariants(ctx: Context): Promise<void> {
|
||||
await ctx.plugin(AgentLoopInvariant)
|
||||
}
|
||||
|
||||
function start(
|
||||
ctx: Context,
|
||||
provider: string,
|
||||
request: Omit<SubagentStartRequest, 'label' | 'signal'> & { label?: string; signal?: AbortSignal },
|
||||
) {
|
||||
return ctx.subagents.start(provider, {
|
||||
label: request.label ?? 'child task',
|
||||
signal: request.signal ?? new AbortController().signal,
|
||||
...request,
|
||||
})
|
||||
function start(ctx: Context, provider: string, request: Omit<SubagentStartRequest, 'signal'> & { signal?: AbortSignal }) {
|
||||
return ctx.subagents.start(provider, { signal: request.signal ?? new AbortController().signal, ...request })
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,16 +25,8 @@ async function mountInvariants(ctx: Context): Promise<void> {
|
||||
await ctx.plugin(AgentLoopInvariant)
|
||||
}
|
||||
|
||||
function start(
|
||||
ctx: Context,
|
||||
provider: string,
|
||||
request: Omit<SubagentStartRequest, 'label' | 'signal'> & { label?: string; signal?: AbortSignal },
|
||||
) {
|
||||
return ctx.subagents.start(provider, {
|
||||
label: request.label ?? 'child task',
|
||||
signal: request.signal ?? new AbortController().signal,
|
||||
...request,
|
||||
})
|
||||
function start(ctx: Context, provider: string, request: Omit<SubagentStartRequest, 'signal'> & { signal?: AbortSignal }) {
|
||||
return ctx.subagents.start(provider, { signal: request.signal ?? new AbortController().signal, ...request })
|
||||
}
|
||||
|
||||
/** A bare `stop` finish that streams no content → the turn ends `completed`
|
||||
|
||||
@@ -49,16 +49,8 @@ function text(blocks: { type: string; text?: string }[]): string {
|
||||
return blocks.filter(b => b.type === 'text').map(b => b.text).join('')
|
||||
}
|
||||
|
||||
function start(
|
||||
ctx: Context,
|
||||
provider: string,
|
||||
request: Omit<SubagentStartRequest, 'label' | 'signal'> & { label?: string; signal?: AbortSignal },
|
||||
) {
|
||||
return ctx.subagents.start(provider, {
|
||||
label: request.label ?? 'child task',
|
||||
signal: request.signal ?? new AbortController().signal,
|
||||
...request,
|
||||
})
|
||||
function start(ctx: Context, provider: string, request: Omit<SubagentStartRequest, 'signal'> & { signal?: AbortSignal }) {
|
||||
return ctx.subagents.start(provider, { signal: request.signal ?? new AbortController().signal, ...request })
|
||||
}
|
||||
|
||||
/** Invoke the child lifecycle effect while its parent-owned setup is still unpublished. */
|
||||
|
||||
@@ -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: df2582518e97bb38d124f8c33259c30b9d11d759
|
||||
README.zh.md: d788f30f31d92c3fd919b62891e9b03624f455f7
|
||||
README.md: da03249d2957a67a0a0a1b3b935e90a9398125c8
|
||||
README.zh.md: 74aebb4efbae05375b016f9a39ab8cd134eb030a
|
||||
|
||||
@@ -34,7 +34,7 @@ Multiple providers may coexist under different names. This lets a deployment exp
|
||||
| `drainContinuableDescendants(parents)` | Close admission below exact live host-owned parent Agents, stop only their visible continuable descendants, await materializations admitted below those roots through publication or rollback, then release the selected forests child-first. The cutoff lasts until each exact parent leaves the registry; unrelated parent forests and manager-wide admission remain live. |
|
||||
| `listChildren(parentSessionId, signal?)` | List direct session-backed subagents with their `one-shot`/`continuable` mode and `running`/`inactive` activity, plus per-child diagnostics, in stable trace order without loading or resuming them. Requires session query; it does not require `ctx.agents` or the continuation manager. |
|
||||
|
||||
`SubagentStartRequest.label` is the short durable display label for a session-backed child. `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.
|
||||
`SubagentStartRequest.label` is an optional short durable display label for a session-backed one-shot child. Model-facing delegation supplies its existing `description`; lower-level callers need not invent presentation metadata. Continuable starts always carry their own required label. `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.
|
||||
|
||||
Follow-up authority comes from the exact live direct parent recorded in the child's durable header. Cold resume checks that authority before reconstruction and again in the final no-await inbox-admission span, so a parent unregistered or replaced during materialization cannot authorize delivery. The `source` on a follow-up is durable provenance retained on the delivered message and grants no authority.
|
||||
|
||||
@@ -53,7 +53,7 @@ Continuable creation is the optional `SubagentProvider.prepareContinuable?()` me
|
||||
|
||||
## The durable descriptor
|
||||
|
||||
The seam owns the versioned `subagent/descriptor` session event vocabulary (`src/descriptor.ts`): `snapshotSubagentDescriptor()` validates and detaches the record before provider work, and `foldSubagentDescriptor()` validates the complete current-version payload before recovering it from a loaded child log. Every local session-backed start appends one descriptor with the durable creation label, provider name, and lifecycle `mode`. A `one-shot` descriptor stops there; a `continuable` descriptor additionally records resolved child `agentOptions.provider`/`model` and optional `persona`/`toolFilter` for cold resume. These are explicit fields, never the merge-extensible `AgentOptions` object, so an unrelated extension value cannot break continuation. The descriptor omits `subagentDepth` (the persisted header's `delegationDepth` is the monotone floor) and `outputSchema` (an activation's result contract). The event is log-only: no `surfaceOp`, absent from model history, and retained by the append-only log across compaction. Malformed current-version payloads are corrupt; unsupported versions cannot be classified by this runtime.
|
||||
The seam owns the versioned `subagent/descriptor` session event vocabulary (`src/descriptor.ts`): `snapshotSubagentDescriptor()` validates and detaches the record before provider work, and `foldSubagentDescriptor()` validates the complete current-version payload before recovering it from a loaded child log. Every local session-backed start appends one descriptor with the provider name and lifecycle `mode`. A `one-shot` descriptor optionally carries the caller-owned durable display `label`; a `continuable` descriptor requires its durable creation label and additionally records resolved child `agentOptions.provider`/`model` and optional `persona`/`toolFilter` for cold resume. These are explicit fields, never the merge-extensible `AgentOptions` object, so an unrelated extension value cannot break continuation. The descriptor omits `subagentDepth` (the persisted header's `delegationDepth` is the monotone floor) and `outputSchema` (an Activation's result contract). The event is log-only: no `surfaceOp`, absent from model history, and retained by the append-only log across compaction. Malformed current-version payloads are corrupt; unsupported versions cannot be classified by this runtime.
|
||||
|
||||
## Delegation depth
|
||||
|
||||
@@ -89,7 +89,7 @@ Provider additions and removals also emit `subagent/provider-added` and `subagen
|
||||
|
||||
## Collection model
|
||||
|
||||
The model-facing tool collects synchronously by default: it awaits the child result and disposes the run before returning. One-shot background delegation registers a plain Task in the tool, whose generic status, collection, and cancellation tools own later interaction. Continuable background delegation calls `ctx.subagents.startContinuable()` and returns only the durable child id; the child owns its own turns from inbox acceptance, so there is no Task, no result promise, and no public subagent cancellation — a caller sends later work with the `send_message` follow-up tool, and the durable child Session remains the source of the child's detailed output. The continuation manager exists only while `ctx.agents` is available, and session persistence is resolved per continuation operation. Independently, `listChildren()` resolves session query and dynamically imports its optional runtime only when called, then interprets a read-only live-preferred scan of all descriptor-bearing direct children without consulting the continuation manager, Agent registrations, Activations, or providers. Service consumers such as a UI can retain both modes; the model-facing `list_agents` tool projects only `continuable` entries and maps service activity to its existing `running`/`complete` vocabulary. The scan forwards the caller's signal to cancellable trace and exact-read operations, checks cancellation around the remaining event-list read, and reports every observed abort as `SubagentError` code `CANCELLED`. See the [background subagent tasks Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md), the [continuable background subagents Agent Note](../../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md), the [durable catalog Agent Note](../../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md), the [merged-service Agent Note](../../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md), the [capability-seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md), and `src/types.ts` for the complete contracts.
|
||||
The model-facing tool collects synchronously by default: it awaits the child result and disposes the run before returning. One-shot background delegation registers a plain Task in the tool, whose generic status, collection, and cancellation tools own later interaction, and persists its model-supplied `description` as the optional display label. Continuable background delegation calls `ctx.subagents.startContinuable()` and returns only the durable child id; the child owns its own turns from inbox acceptance, so there is no Task, no result promise, and no public subagent cancellation — a caller sends later work with the `send_message` follow-up tool, and the durable child Session remains the source of the child's detailed output. The continuation manager exists only while `ctx.agents` is available, and session persistence is resolved per continuation operation. Independently, `listChildren()` resolves session query and dynamically imports its optional runtime only when called, then interprets a read-only live-preferred scan of all descriptor-bearing direct children without consulting the continuation manager, Agent registrations, Activations, or providers. Service consumers such as a UI can retain both modes and choose a fallback for an unlabeled one-shot child; the model-facing `list_agents` tool projects only `continuable` entries and maps service activity to its existing `running`/`complete` vocabulary. The scan forwards the caller's signal to cancellable trace and exact-read operations, checks cancellation around the remaining event-list read, and reports every observed abort as `SubagentError` code `CANCELLED`. See the [background subagent tasks Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md), the [continuable background subagents Agent Note](../../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md), the [durable catalog Agent Note](../../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md), the [merged-service Agent Note](../../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md), the [capability-seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md), and `src/types.ts` for the complete contracts.
|
||||
|
||||
Continuable Activations require final durability confirmation. One-shot runs retain best-effort session checkpointing, so a completed one-shot child is discoverable after disposal only when its session actually reached persistence; the service does not invent a catalog entry from Task history when that checkpoint is absent.
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
|
||||
| `drainContinuableDescendants(parents)` | 在由 host 确切拥有的在线 parent Agent 之下关闭准入,只停止其可见的可继续后代,等待在这些根之下已获准的物化过程完成发布或回滚,再按 child-first 顺序释放所选森林。该截止状态会持续到每个确切 parent 离开注册表;无关的 parent 森林和管理器全局准入保持在线。 |
|
||||
| `listChildren(parentSessionId, signal?)` | 按稳定的追踪顺序列出由会话支撑的直接 subagent,包括其 `one-shot`/`continuable` 模式和 `running`/`inactive` 活动状态,以及逐 child diagnostic,且不会加载或恢复它们。要求会话查询;不要求 `ctx.agents` 或继续执行管理器。 |
|
||||
|
||||
`SubagentStartRequest.label` 是由会话支撑的 child 所使用的简短持久化显示标签。`signal` 是必填项,也是一次性 `start` 的规范取消通道。发布前中止会使 `start()` 在回滚后拒绝;发布后中止会取消实时子 agent。请求还可以选择模型、要求结构化输出、限制委派深度、约束子 agent 工具或设置子 agent persona。对于可继续启动或后续操作,调用方信号只在 inbox 接受之前掌管查找、物化和准入;此后由管理器独立拥有 Activation,因此调用方后续取消既不会取消已接受的轮次,也不会 dispose 子 agent。
|
||||
`SubagentStartRequest.label` 是由会话支撑的一次性 child 所使用的可选简短持久化显示标签。面向模型的委派会提供其已有的 `description`;底层调用方无需凭空构造展示元数据。可继续启动始终携带自身的必填标签。`signal` 是必填项,也是一次性 `start` 的规范取消通道。发布前中止会使 `start()` 在回滚后拒绝;发布后中止会取消实时子 agent。请求还可以选择模型、要求结构化输出、限制委派深度、约束子 agent 工具或设置子 agent persona。对于可继续启动或后续操作,调用方信号只在 inbox 接受之前掌管查找、物化和准入;此后由管理器独立拥有 Activation,因此调用方后续取消既不会取消已接受的轮次,也不会 dispose 子 agent。
|
||||
|
||||
后续操作的权限来自子 agent 持久化 header 中记录的确切在线直接父级。冷恢复会在重建前检查该权限,并在最终无 await 的 inbox 准入区间再次检查,因此在物化期间被注销或替换的 parent 无法授权投递。后续操作上的 `source` 是保留在所投递消息上的持久化来源,不授予任何权限。
|
||||
|
||||
@@ -53,7 +53,7 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
|
||||
|
||||
## 持久化描述符
|
||||
|
||||
该 seam 拥有版本化的 `subagent/descriptor` 会话事件词汇(`src/descriptor.ts`):`snapshotSubagentDescriptor()` 会在提供方工作之前校验并分离记录,`foldSubagentDescriptor()` 则会在从已加载子 agent 日志中恢复描述符之前,校验当前版本的完整 payload。每次由本地会话支撑的启动都会追加一个描述符,其中包含持久化创建标签、提供方名称与生命周期 `mode`。`one-shot` 描述符到此为止;`continuable` 描述符还会记录已解析的子 agent `agentOptions.provider`/`model`,以及用于从持久化存储恢复的可选 `persona`/`toolFilter`。这些是显式字段,绝不是可通过合并扩展的 `AgentOptions` 对象,因此无关的扩展值不会破坏继续执行。描述符省略 `subagentDepth`(持久化 header 的 `delegationDepth` 是单调下界)和 `outputSchema`(单次激活的结果契约)。该事件只进入日志:不含 `surfaceOp`,不进入模型历史,并由仅追加日志跨压缩保留。格式错误的当前版本 payload 属于损坏;本运行时无法对不受支持的版本进行分类。
|
||||
该 seam 拥有版本化的 `subagent/descriptor` 会话事件词汇(`src/descriptor.ts`):`snapshotSubagentDescriptor()` 会在提供方工作之前校验并分离记录,`foldSubagentDescriptor()` 则会在从已加载子 agent 日志中恢复描述符之前,校验当前版本的完整 payload。每次由本地会话支撑的启动都会追加一个带有提供方名称与生命周期 `mode` 的描述符。`one-shot` 描述符可以携带调用方拥有的可选持久化显示 `label`;`continuable` 描述符要求其持久化创建标签,并另外记录已解析的子 agent `agentOptions.provider`/`model`,以及用于从持久化存储恢复的可选 `persona`/`toolFilter`。这些是显式字段,绝不是可通过合并扩展的 `AgentOptions` 对象,因此无关的扩展值不会破坏继续执行。描述符省略 `subagentDepth`(持久化 header 的 `delegationDepth` 是单调下界)和 `outputSchema`(单次 Activation 的结果契约)。该事件只进入日志:不含 `surfaceOp`,不进入模型历史,并由仅追加日志跨压缩保留。格式错误的当前版本 payload 属于损坏;本运行时无法对不受支持的版本进行分类。
|
||||
|
||||
## 委派深度
|
||||
|
||||
@@ -89,7 +89,7 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
|
||||
|
||||
## 收集模型
|
||||
|
||||
面向模型的工具默认同步收集:先等待子 agent 结果,再 dispose 运行,然后才返回。一次性后台委派会在工具中注册普通 Task,其通用状态、收集和取消工具负责后续交互。可继续后台委派会调用 `ctx.subagents.startContinuable()`,只返回持久化子 agent id;子 agent 自 inbox 接受起就拥有自己的轮次,因此没有 Task、没有结果 promise,也没有公开的子 agent 取消操作——调用方通过 `send_message` 后续操作工具发送后续工作,而持久化子 agent Session 仍是子 agent 详细输出的来源。只有 `ctx.agents` 可用时,继续执行管理器才会存在,而会话持久化按每项继续执行操作解析。与此独立,`listChildren()` 只在被调用时解析会话查询并动态导入其可选运行时,然后解释对所有带描述符的直接 child 所作的只读、实时优先扫描,且不查询继续执行管理器、Agent 注册信息、Activation 或提供方。UI 等服务消费方可以保留两种模式;面向模型的 `list_agents` 工具只投影 `continuable` 条目,并将服务活动状态映射到现有的 `running`/`complete` 词汇。扫描会把调用方的取消信号转发到可取消的追踪与精确读取操作,在其余事件列表读取的前后检查取消,并将每次检测到的中止报告为 `SubagentError` 错误码 `CANCELLED`。完整契约见[后台 subagent 任务 Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md)、[可继续后台 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md)、[持久化目录 Agent Note](../../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md)、[服务合并 Agent Note](../../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md)、[能力 seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)和 `src/types.ts`。
|
||||
面向模型的工具默认同步收集:先等待子 agent 结果,再 dispose 运行,然后才返回。一次性后台委派会在工具中注册普通 Task,其通用状态、收集和取消工具负责后续交互,并将模型提供的 `description` 持久化为可选显示标签。可继续后台委派会调用 `ctx.subagents.startContinuable()`,只返回持久化子 agent id;子 agent 自 inbox 接受起就拥有自己的轮次,因此没有 Task、没有结果 promise,也没有公开的子 agent 取消操作——调用方通过 `send_message` 后续操作工具发送后续工作,而持久化子 agent Session 仍是子 agent 详细输出的来源。只有 `ctx.agents` 可用时,继续执行管理器才会存在,而会话持久化按每项继续执行操作解析。与此独立,`listChildren()` 只在被调用时解析会话查询并动态导入其可选运行时,然后解释对所有带描述符的直接 child 所作的只读、实时优先扫描,且不查询继续执行管理器、Agent 注册信息、Activation 或提供方。UI 等服务消费方可以保留两种模式,并为无标签的一次性 child 选择回退展示;面向模型的 `list_agents` 工具只投影 `continuable` 条目,并将服务活动状态映射到现有的 `running`/`complete` 词汇。扫描会把调用方的取消信号转发到可取消的追踪与精确读取操作,在其余事件列表读取的前后检查取消,并将每次检测到的中止报告为 `SubagentError` 错误码 `CANCELLED`。完整契约见[后台 subagent 任务 Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md)、[可继续后台 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md)、[持久化目录 Agent Note](../../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md)、[服务合并 Agent Note](../../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md)、[能力 seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)和 `src/types.ts`。
|
||||
|
||||
可继续 Activation 要求最终持久性确认。一次性运行保留尽力执行的会话检查点,因此已完成的一次性 child 只有在其会话确实进入持久化存储时,才可在 dispose 后继续被发现;如果该检查点缺失,服务不会根据 Task 历史虚构目录条目。
|
||||
|
||||
|
||||
@@ -49,22 +49,24 @@ interface SubagentDescriptorBase {
|
||||
readonly mode: 'one-shot' | 'continuable'
|
||||
/** The `ctx.subagents` provider name that established the child. */
|
||||
readonly provider: string
|
||||
/**
|
||||
* The initial delegation's short `description`, kept as the child's durable
|
||||
* creation label so enumeration can identify the conversation without
|
||||
* replaying parent tool results or exposing the child prompt.
|
||||
*/
|
||||
readonly label: string
|
||||
}
|
||||
|
||||
/** A session-backed subagent that cannot be cold-resumed after its run. */
|
||||
export interface OneShotSubagentDescriptorData extends SubagentDescriptorBase {
|
||||
readonly mode: 'one-shot'
|
||||
/**
|
||||
* The initial delegation's short `description`, kept as the child's durable
|
||||
* creation label so enumeration can identify the conversation without
|
||||
* replaying parent tool results or exposing the child prompt.
|
||||
*/
|
||||
readonly label?: string
|
||||
}
|
||||
|
||||
/** A session-backed subagent whose declared composition supports cold resume. */
|
||||
export interface ContinuableSubagentDescriptorData extends SubagentDescriptorBase {
|
||||
readonly mode: 'continuable'
|
||||
/** The initial delegation's short `description`, used for durable enumeration. */
|
||||
readonly label: string
|
||||
/** Resolved child `agentOptions.provider`, when one was declared. */
|
||||
readonly agentProvider?: string
|
||||
/** Resolved child `agentOptions.model`, when one was declared. */
|
||||
@@ -86,18 +88,20 @@ interface SubagentDescriptorInputBase {
|
||||
readonly mode: 'one-shot' | 'continuable'
|
||||
/** The `ctx.subagents` provider name that will establish the child. */
|
||||
readonly provider: string
|
||||
/** The initial delegation's short `description`, the durable creation label. */
|
||||
readonly label: string
|
||||
}
|
||||
|
||||
/** Input for a one-shot child's durable identity. */
|
||||
export interface OneShotSubagentDescriptorInput extends SubagentDescriptorInputBase {
|
||||
readonly mode: 'one-shot'
|
||||
/** Optional initial delegation `description` used as the durable creation label. */
|
||||
readonly label?: string
|
||||
}
|
||||
|
||||
/** Input for a continuable child's durable identity and resumable composition. */
|
||||
export interface ContinuableSubagentDescriptorInput extends SubagentDescriptorInputBase {
|
||||
readonly mode: 'continuable'
|
||||
/** Initial delegation `description` used for durable enumeration. */
|
||||
readonly label: string
|
||||
/** Requested child `agentOptions.provider`. */
|
||||
readonly agentProvider?: string
|
||||
/** Requested child `agentOptions.model`. */
|
||||
@@ -207,18 +211,19 @@ function parseSubagentDescriptor(value: unknown): SubagentDescriptorData | undef
|
||||
if (typeof provider !== 'string') {
|
||||
throw new Error('persisted subagent descriptor provider must be a string')
|
||||
}
|
||||
const label = value['label']
|
||||
if (typeof label !== 'string') {
|
||||
throw new Error('persisted subagent descriptor label must be a string')
|
||||
}
|
||||
if (mode === 'one-shot') {
|
||||
const label = optionalString(value, 'label')
|
||||
return {
|
||||
version: SUBAGENT_DESCRIPTOR_VERSION,
|
||||
mode,
|
||||
provider,
|
||||
label,
|
||||
...label !== undefined ? { label } : {},
|
||||
}
|
||||
}
|
||||
const label = value['label']
|
||||
if (typeof label !== 'string') {
|
||||
throw new Error('persisted subagent descriptor label must be a string')
|
||||
}
|
||||
const agentProvider = optionalString(value, 'agentProvider')
|
||||
const agentModel = optionalString(value, 'agentModel')
|
||||
const persona = optionalString(value, 'persona')
|
||||
@@ -259,20 +264,23 @@ export function snapshotSubagentDescriptor(
|
||||
input: ContinuableSubagentDescriptorInput,
|
||||
): ContinuableSubagentDescriptorData
|
||||
export function snapshotSubagentDescriptor(input: SubagentDescriptorInput): SubagentDescriptorData {
|
||||
const candidate: SubagentDescriptorData = {
|
||||
version: SUBAGENT_DESCRIPTOR_VERSION,
|
||||
mode: input.mode,
|
||||
provider: input.provider,
|
||||
label: input.label,
|
||||
...input.mode === 'continuable'
|
||||
? {
|
||||
...input.agentProvider !== undefined ? { agentProvider: input.agentProvider } : {},
|
||||
...input.agentModel !== undefined ? { agentModel: input.agentModel } : {},
|
||||
...input.persona !== undefined ? { persona: input.persona } : {},
|
||||
...input.toolFilter !== undefined ? { toolFilter: input.toolFilter } : {},
|
||||
}
|
||||
: {},
|
||||
}
|
||||
const candidate: SubagentDescriptorData = input.mode === 'one-shot'
|
||||
? {
|
||||
version: SUBAGENT_DESCRIPTOR_VERSION,
|
||||
mode: input.mode,
|
||||
provider: input.provider,
|
||||
...input.label !== undefined ? { label: input.label } : {},
|
||||
}
|
||||
: {
|
||||
version: SUBAGENT_DESCRIPTOR_VERSION,
|
||||
mode: input.mode,
|
||||
provider: input.provider,
|
||||
label: input.label,
|
||||
...input.agentProvider !== undefined ? { agentProvider: input.agentProvider } : {},
|
||||
...input.agentModel !== undefined ? { agentModel: input.agentModel } : {},
|
||||
...input.persona !== undefined ? { persona: input.persona } : {},
|
||||
...input.toolFilter !== undefined ? { toolFilter: input.toolFilter } : {},
|
||||
}
|
||||
const snapshot = snapshotJsonValue(candidate)
|
||||
if (snapshot === undefined) {
|
||||
throw new Error('subagent descriptor is not losslessly JSON-serializable')
|
||||
|
||||
@@ -314,7 +314,7 @@ export class SubagentService extends Service {
|
||||
const descriptor = snapshotSubagentDescriptor({
|
||||
mode: 'one-shot',
|
||||
provider: name,
|
||||
label: request.label,
|
||||
...request.label !== undefined ? { label: request.label } : {},
|
||||
})
|
||||
const resolved: ResolvedSubagentStartRequest = { ...request, descriptor }
|
||||
return observeRun(this.emitLifecycle, name, request.parent, await provider.start(resolved))
|
||||
|
||||
@@ -30,10 +30,6 @@ export type SubagentListEntry =
|
||||
readonly kind: 'child'
|
||||
/** The durable child session id, stable across Activations. */
|
||||
readonly id: SessionId
|
||||
/** The durable creation label from the child's descriptor. */
|
||||
readonly label: string
|
||||
/** Lifecycle policy declared when the child was created. */
|
||||
readonly mode: 'one-shot' | 'continuable'
|
||||
/**
|
||||
* Corpus snapshot activity: `running` means the logical record is live in
|
||||
* `ctx.sessions`; `inactive` means it exists only in persistence. Neither
|
||||
@@ -41,7 +37,20 @@ export type SubagentListEntry =
|
||||
* delivery as an ownership conflict.
|
||||
*/
|
||||
readonly activity: 'running' | 'inactive'
|
||||
}
|
||||
} & (
|
||||
| {
|
||||
/** A terminal one-shot child. */
|
||||
readonly mode: 'one-shot'
|
||||
/** Optional durable creation label from the child's descriptor. */
|
||||
readonly label?: string
|
||||
}
|
||||
| {
|
||||
/** A resumable conversation. */
|
||||
readonly mode: 'continuable'
|
||||
/** Durable creation label from the child's descriptor. */
|
||||
readonly label: string
|
||||
}
|
||||
)
|
||||
| {
|
||||
readonly kind: 'diagnostic'
|
||||
/** The traced candidate's session id. */
|
||||
@@ -139,13 +148,17 @@ async function inspectChild(
|
||||
if (descriptor === undefined) {
|
||||
return { kind: 'diagnostic', id: childId, reason: 'unsupported' }
|
||||
}
|
||||
return {
|
||||
kind: 'child',
|
||||
id: childId,
|
||||
label: descriptor.label,
|
||||
mode: descriptor.mode,
|
||||
activity: candidate.live ? 'running' : 'inactive',
|
||||
const activity = candidate.live ? 'running' : 'inactive'
|
||||
if (descriptor.mode === 'one-shot') {
|
||||
return {
|
||||
kind: 'child',
|
||||
id: childId,
|
||||
mode: descriptor.mode,
|
||||
...descriptor.label !== undefined ? { label: descriptor.label } : {},
|
||||
activity,
|
||||
}
|
||||
}
|
||||
return { kind: 'child', id: childId, mode: descriptor.mode, label: descriptor.label, activity }
|
||||
} catch (error: unknown) {
|
||||
const reason = perChildDiagnosticReason(error, queryRuntime.SessionQueryError)
|
||||
if (reason === undefined) throw error
|
||||
|
||||
@@ -93,8 +93,8 @@ export interface SubagentCapabilities {
|
||||
* {@link SubagentProvider.start}.
|
||||
*/
|
||||
export interface SubagentStartRequest {
|
||||
/** Short display label persisted with a session-backed child. */
|
||||
readonly label: string
|
||||
/** Optional short display label persisted with a session-backed child. */
|
||||
readonly label?: string
|
||||
/** Content delivered as the child's user message. */
|
||||
readonly prompt: ContentBlock[]
|
||||
/**
|
||||
|
||||
@@ -144,7 +144,6 @@ describe('SubagentService.listChildren', () => {
|
||||
it('lists one-shot and continuable children from the same trace', async () => {
|
||||
const { ctx, parent } = await setup([textResponse('once'), textResponse('again')])
|
||||
const oneShot = await ctx.subagents.start('spawn', {
|
||||
label: 'one-shot child',
|
||||
prompt: [{ type: 'text', text: 'finish once' }],
|
||||
parent,
|
||||
signal: new AbortController().signal,
|
||||
@@ -159,7 +158,6 @@ describe('SubagentService.listChildren', () => {
|
||||
expect(entries).toContainEqual({
|
||||
kind: 'child',
|
||||
id: oneShotId,
|
||||
label: 'one-shot child',
|
||||
mode: 'one-shot',
|
||||
activity: 'inactive',
|
||||
})
|
||||
|
||||
@@ -28,7 +28,6 @@ const NO_CAPS: SubagentCapabilities = { outputSchema: false, depthLimit: false,
|
||||
|
||||
function baseRequest(overrides: Partial<SubagentStartRequest> = {}): SubagentStartRequest {
|
||||
return {
|
||||
label: 'do a thing',
|
||||
prompt: [{ type: 'text', text: 'do a thing' }],
|
||||
parent: fakeParent(),
|
||||
signal: new AbortController().signal,
|
||||
@@ -119,7 +118,6 @@ describe('SubagentService', () => {
|
||||
version: SUBAGENT_DESCRIPTOR_VERSION,
|
||||
mode: 'one-shot',
|
||||
provider: 'one-shot',
|
||||
label: 'do a thing',
|
||||
},
|
||||
})
|
||||
expect(provider.lastRequest).not.toBe(request)
|
||||
@@ -308,14 +306,18 @@ describe('subagent descriptors', () => {
|
||||
|
||||
it('omits absent fields, recovers a complete payload, and rejects unsupported versions', () => {
|
||||
expect(foldSubagentDescriptor([])).toBeUndefined()
|
||||
const minimal = snapshotSubagentDescriptor({ mode: 'one-shot', provider: 'spawn', label: 'child work' })
|
||||
const minimal = snapshotSubagentDescriptor({ mode: 'one-shot', provider: 'spawn' })
|
||||
expect(minimal).toEqual({
|
||||
version: SUBAGENT_DESCRIPTOR_VERSION,
|
||||
mode: 'one-shot',
|
||||
provider: 'spawn',
|
||||
label: 'child work',
|
||||
})
|
||||
expect(foldSubagentDescriptor([event(minimal)])).toEqual(minimal)
|
||||
expect(snapshotSubagentDescriptor({
|
||||
mode: 'one-shot',
|
||||
provider: 'spawn',
|
||||
label: 'child work',
|
||||
})).toEqual({ ...minimal, label: 'child work' })
|
||||
const complete = {
|
||||
version: SUBAGENT_DESCRIPTOR_VERSION,
|
||||
mode: 'continuable' as const,
|
||||
@@ -380,6 +382,12 @@ describe('subagent descriptors', () => {
|
||||
label: 'l',
|
||||
persona: 'reviewer',
|
||||
}, 'payload has unknown field "persona"'],
|
||||
['invalid one-shot label', {
|
||||
version: SUBAGENT_DESCRIPTOR_VERSION,
|
||||
mode: 'one-shot',
|
||||
provider: 'spawn',
|
||||
label: 7,
|
||||
}, 'label must be a string'],
|
||||
['unknown payload field', {
|
||||
version: SUBAGENT_DESCRIPTOR_VERSION,
|
||||
mode: 'continuable',
|
||||
|
||||
@@ -12,7 +12,6 @@ function fakeParent(id = 'parent-1'): Agent {
|
||||
|
||||
function baseRequest(over: Partial<SubagentStartRequest> = {}): SubagentStartRequest {
|
||||
return {
|
||||
label: 'task',
|
||||
prompt: [{ type: 'text', text: 'task' }],
|
||||
parent: fakeParent(),
|
||||
signal: new AbortController().signal,
|
||||
|
||||
@@ -91,7 +91,6 @@ async function settleSubagent(
|
||||
})
|
||||
try {
|
||||
const run = await ctx.subagents.start(info.provider, {
|
||||
label: 'synthetic child',
|
||||
parent,
|
||||
prompt: [],
|
||||
signal: new AbortController().signal,
|
||||
@@ -557,13 +556,11 @@ describe('HarnessSdkServer', () => {
|
||||
})
|
||||
|
||||
const firstRun = await ctx.subagents.start('reused', {
|
||||
label: 'first reused child',
|
||||
parent: oldParent.agent,
|
||||
prompt: [],
|
||||
signal: new AbortController().signal,
|
||||
})
|
||||
const sameLifetimeRun = await ctx.subagents.start('reused', {
|
||||
label: 'same lifetime child',
|
||||
parent: oldParent.agent,
|
||||
prompt: [],
|
||||
signal: new AbortController().signal,
|
||||
@@ -583,7 +580,6 @@ describe('HarnessSdkServer', () => {
|
||||
})
|
||||
currentLocalAgent = newChild.agent
|
||||
const secondRun = await ctx.subagents.start('reused', {
|
||||
label: 'second reused child',
|
||||
parent: newParent.agent,
|
||||
prompt: [],
|
||||
signal: new AbortController().signal,
|
||||
@@ -654,7 +650,6 @@ describe('HarnessSdkServer', () => {
|
||||
}),
|
||||
})
|
||||
const localRun = await ctx.subagents.start('reused-provider', {
|
||||
label: 'local reused child',
|
||||
parent: parent.agent,
|
||||
prompt: [],
|
||||
signal: new AbortController().signal,
|
||||
@@ -673,7 +668,6 @@ describe('HarnessSdkServer', () => {
|
||||
}),
|
||||
})
|
||||
const remoteRun = await ctx.subagents.start('reused-provider', {
|
||||
label: 'remote reused child',
|
||||
parent: parent.agent,
|
||||
prompt: [],
|
||||
signal: new AbortController().signal,
|
||||
@@ -756,7 +750,6 @@ describe('HarnessSdkServer', () => {
|
||||
// Start before the server subscribes. The terminal payload still carries
|
||||
// this run's exact local child without reconstructing it from ids.
|
||||
const missedStartRun = await ctx.subagents.start('fork', {
|
||||
label: 'missed start child',
|
||||
parent: parentHandle.agent,
|
||||
prompt: [],
|
||||
signal: new AbortController().signal,
|
||||
|
||||
@@ -322,7 +322,6 @@ export class WorkerRun implements WorkflowRun {
|
||||
let run: SubagentRun
|
||||
try {
|
||||
run = await this.subagents.start(this.provider, {
|
||||
label: request.label,
|
||||
prompt: [{ type: 'text', text: request.prompt }],
|
||||
parent: this.parent,
|
||||
signal: this.controller.signal,
|
||||
|
||||
@@ -275,7 +275,6 @@ export class WorkflowExecution {
|
||||
let run: ChildHandle
|
||||
try {
|
||||
run = await this.children.startAgent({
|
||||
label,
|
||||
prompt: rawPrompt,
|
||||
...opts.schema !== undefined ? { schema: opts.schema } : {},
|
||||
...opts.provider !== undefined ? { provider: opts.provider } : {},
|
||||
|
||||
@@ -38,8 +38,6 @@ export interface WorkerInit {
|
||||
|
||||
/** What the worker asks the host to start for one `agent()` call (options already validated worker-side). */
|
||||
export interface ChildStartRequest {
|
||||
/** Short display label resolved by the worker runtime. */
|
||||
label: string
|
||||
/** The child's prompt text. */
|
||||
prompt: string
|
||||
/** The structured-output schema, if the call passed one (already subset-checked). */
|
||||
|
||||
@@ -420,13 +420,10 @@ describe('runWorkerSession over an in-process MessageChannel', () => {
|
||||
`))
|
||||
await host.result()
|
||||
const starts = host.ofType(WorkerToHostType.AgentStart).map(m => m.info)
|
||||
const requests = host.ofType(WorkerToHostType.ChildStart).map(m => m.request)
|
||||
expect(starts[0]).toMatchObject({ seq: 1, phase: 'Find' })
|
||||
expect(starts[0]!.label.length).toBeLessThanOrEqual(48)
|
||||
expect(starts[0]!.label).not.toContain('second line')
|
||||
expect(starts[1]).toMatchObject({ seq: 2, label: 'named', phase: 'Custom' })
|
||||
expect(requests[0]!.label).toBe(starts[0]!.label)
|
||||
expect(requests[1]!.label).toBe('named')
|
||||
host.close()
|
||||
})
|
||||
|
||||
|
||||
@@ -211,7 +211,7 @@ describe('dsh-workflow-workerthread', () => {
|
||||
reply: () => ({ output: [], structured: { files: ['x.ts', 'y.ts'] }, stopReason: 'completed' }),
|
||||
})
|
||||
const result = await run(ctx, parent, scripted(`
|
||||
const found = await agent('list files', { label: 'inventory', model: 'deepseek-v4-pro', schema: { type: 'object', properties: { files: { type: 'array', items: { type: 'string' } } }, required: ['files'] } })
|
||||
const found = await agent('list files', { model: 'deepseek-v4-pro', schema: { type: 'object', properties: { files: { type: 'array', items: { type: 'string' } } }, required: ['files'] } })
|
||||
return { first: found.files[0], count: found.files.length }
|
||||
`))
|
||||
expect(result.value).toEqual({ first: 'x.ts', count: 2 })
|
||||
@@ -221,7 +221,6 @@ describe('dsh-workflow-workerthread', () => {
|
||||
required: ['files'],
|
||||
})
|
||||
expect(provider.runs[0]!.request.agentOptions).toEqual({ model: 'deepseek-v4-pro' })
|
||||
expect(provider.runs[0]!.request.label).toBe('inventory')
|
||||
expect(provider.runs[0]!.request.parent).toBeDefined()
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user