Merge remote-tracking branch 'origin/worktree/ci-native-windows-20260808' into worktree/ci-native-windows-coverage-20260808
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md
|
||||
2026-07-22-durable-subagent-catalog-and-list-agents.md: 8337a926238bf7fc4395896fcd4ca180c9c1ac1c
|
||||
2026-07-22-durable-subagent-catalog-and-list-agents.zh.md: bcf2895a9a69c8cff949788c78158bfccd198c5c
|
||||
2026-07-22-durable-subagent-catalog-and-list-agents.md: 61bda2a52f2ea5db0a582fe668643a13b967f091
|
||||
2026-07-22-durable-subagent-catalog-and-list-agents.zh.md: a2bc36200d8c67b8da9dd3d55ea4179baa72e691
|
||||
|
||||
@@ -52,9 +52,9 @@ If measured scale later requires an index, that index is derived state: session
|
||||
|
||||
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.
|
||||
|
||||
The model-facing `list_agents` tool takes one optional `scope: 'children' | 'descendants'` argument, derives the root id from the current execution Agent, and is a thin adapter in `@deepseek-ai/dsh-tool-subagent-control`. It keeps diagnostics, drops `one-shot` child entries, derives status from the live Agent registry — `running` for an active driver, `idle` for a resident Agent between turns, and `complete` when no live Agent remains — then renders `<id> [<status>] — <label>` or `<id> [diagnostic: <reason>]` in stable catalog order. The `descendants` scope reads `SubagentService.listDescendants(rootSessionId)`, which flattens the complete tree from one live-preferred corpus in stable pre-order, traverses ordinary and one-shot intermediates so deeper continuable agents are discovered, revalidates each cold candidate against its enumerated lifecycle, and adds `parentId`/`depth` to every entry. The tool inserts ` parent=<id> depth=<n>` before the label; `parent` is the durable direct-parent session id and may name an omitted ordinary session. For the current caller, only depth-1 child rows are `send_message` candidates, while deeper child rows may be selected for `interrupt_agent` ([interrupt contract](2026-08-06-continuable-subagent-interrupt.md)). Discovery is a hint only — follow-up authority stays exact-direct-parent, and interrupt authority stays with the service's live-lineage check. An empty projection renders `(no subagents)`.
|
||||
The model-facing `list_agents` tool takes one optional `scope: 'children' | 'descendants'` argument, derives the root id from the current execution Agent, and resolves the request through an explicit request-to-spec step (`undefined` → `children`) before either execution or rendering. The resolved `children` scope calls `SubagentService.listChildren(rootSessionId)`, while `descendants` calls `SubagentService.listDescendants(rootSessionId)`. Its internal output projection keeps `id` and `parent` as branded `SessionId` values until the tool JSON boundary. It keeps diagnostics, drops `one-shot` child entries, derives status from the live Agent registry — `running` for an active driver, `idle` for a resident Agent between turns, and `complete` when no live Agent remains — then renders `<id> [<status>] — <label>` or `<id> [diagnostic: <reason>]` in stable catalog order. The `descendants` scope flattens the complete tree from one live-preferred corpus in stable pre-order, traverses ordinary and one-shot intermediates so deeper continuable agents are discovered, revalidates each cold candidate against its enumerated lifecycle, and adds `parentId`/`depth` to every entry. The tool inserts ` parent=<id> depth=<n>` before the label; `parent` is the durable direct-parent session id and may name an omitted ordinary session. For the current caller, only depth-1 child rows are `send_message` candidates, while deeper child rows may be selected for `interrupt_agent` ([interrupt contract](2026-08-06-continuable-subagent-interrupt.md)). Discovery is a hint only — follow-up authority stays exact-direct-parent, and interrupt authority stays with the service's live-lineage check. An empty projection renders `(no subagents)`.
|
||||
|
||||
Diagnostics use three fixed reasons. Malformed event surfaces, conflicting headers discovered during an exact child load, a read result whose immutable header differs from the traced candidate or no longer names the requested direct parent, a target that is no longer the located descriptor event, malformed descriptor content, and multiple descriptor events map to `corrupt`. An unknown descriptor version maps to `unsupported`. `SESSION_QUERY_SESSION_NOT_FOUND`, `SESSION_QUERY_EVENT_NOT_FOUND`, and `SESSION_QUERY_PERSISTENCE_FAILED` from a per-child read map to `unavailable`. This phase boundary is intentional: a persistence outage during the initial trace fails the operation, while the same outage beginning during candidate reads may produce one identical `unavailable` diagnostic per affected child; the first version neither coalesces those diagnostics nor promotes them to a global failure. A missing descriptor is instead a non-subagent exclusion without a diagnostic. Configuration/window errors and unrecognized failures are not child diagnostics and propagate as operation failures. Each diagnostic identifies the child id and reason without exposing model-hidden descriptor content; the candidate is omitted while healthy siblings remain visible. Sessions outside the trace's direct descendants are never read and produce no diagnostic.
|
||||
In the superseded trace-based path, diagnostics used three fixed reasons. Malformed event surfaces, conflicting headers discovered during an exact child load, a read result whose immutable header differs from the traced candidate or no longer names the requested direct parent, a target that is no longer the located descriptor event, malformed descriptor content, and multiple descriptor events mapped to `corrupt`. An unknown descriptor version mapped to `unsupported`. `SESSION_QUERY_SESSION_NOT_FOUND`, `SESSION_QUERY_EVENT_NOT_FOUND`, and `SESSION_QUERY_PERSISTENCE_FAILED` from a per-child read mapped to `unavailable`. This phase boundary was intentional: a persistence outage during the initial trace failed the operation, while the same outage beginning during candidate reads could produce one identical `unavailable` diagnostic per affected child; the first version neither coalesced those diagnostics nor promoted them to a global failure. A missing descriptor was instead a non-subagent exclusion without a diagnostic. Configuration/window errors and unrecognized failures were not child diagnostics and propagated as operation failures. Each diagnostic identified the child id and reason without exposing model-hidden descriptor content; the candidate was omitted while healthy siblings remained visible. Sessions outside the trace's direct descendants were never read and produced no diagnostic.
|
||||
|
||||
Diagnostics are transient query results, not session events or catalog state. Deriving a diagnostic performs no additional load beyond the `listEvents()` or conditional `readEvent()` operation whose result produced it.
|
||||
|
||||
|
||||
@@ -52,9 +52,9 @@ subagent 服务将 `sessionQuery` 保持为可选依赖,因此没有该服务
|
||||
|
||||
有效描述符产生一个 child 条目,逐 child 检查失败产生一个 diagnostic 条目,缺少描述符的候选不产生条目。`mode` 是持久化创建策略;`activity` 是进程本地语料快照。活动状态既不是 `AgentStatus`、管理器内部的 Activation 状态,也不是持久化结果,结果不公开内部 `createdAt` 排序键。成功完成、失败、取消和停止原因等精确 Activation 状态与持久化结果需要单独的持久化激活记录,不在本功能范围内。
|
||||
|
||||
面向模型的 `list_agents` 工具接受一个可选的 `scope: 'children' | 'descendants'` 参数,从当前执行 Agent 推导根 id,并作为 `@deepseek-ai/dsh-tool-subagent-control` 中的轻量适配器。它保留 diagnostic,丢弃 `one-shot` child 条目,状态取自在线 Agent 注册表——driver 活跃为 `running`,驻留但处于轮次之间为 `idle`,没有在线 Agent 时为 `complete`——然后按稳定目录顺序渲染 `<id> [<status>] — <label>` 或 `<id> [diagnostic: <reason>]`。`descendants` scope 读取 `SubagentService.listDescendants(rootSessionId)`:它从一份实时优先语料按稳定 pre-order 展平完整树,遍历普通与一次性中间节点以发现更深的可继续 agent,依据枚举生命周期重新校验每个冷候选,并为每个条目附加 `parentId`/`depth`。工具会在 label 之前插入 ` parent=<id> depth=<n>`;`parent` 是持久化直接 parent 会话 id,可能指向被省略的普通会话。对于当前调用方,只有 depth-1 child 条目可作为 `send_message` 候选,更深的 child 条目则可供 `interrupt_agent` 选择([中断契约](2026-08-06-continuable-subagent-interrupt.md))。发现结果只是提示——follow-up 权限仍仅属于确切直接 parent,中断权限仍由服务的在线 lineage 检查决定。空投影渲染为 `(no subagents)`。
|
||||
面向模型的 `list_agents` 工具接受一个可选的 `scope: 'children' | 'descendants'` 参数,从当前执行 Agent 推导根 id,并在执行或渲染前通过显式的 request-to-spec 步骤解析请求(`undefined` → `children`)。解析后的 `children` scope 调用 `SubagentService.listChildren(rootSessionId)`,`descendants` scope 则调用 `SubagentService.listDescendants(rootSessionId)`。其内部输出投影中的 `id` 与 `parent` 会一直保持为品牌化的 `SessionId` 值,直到工具 JSON 边界。它保留 diagnostic,丢弃 `one-shot` child 条目,状态取自在线 Agent 注册表——driver 活跃为 `running`,驻留但处于轮次之间为 `idle`,没有在线 Agent 时为 `complete`——然后按稳定目录顺序渲染 `<id> [<status>] — <label>` 或 `<id> [diagnostic: <reason>]`。`descendants` scope 从一份实时优先语料按稳定 pre-order 展平完整树,遍历普通与一次性中间节点以发现更深的可继续 agent,依据枚举生命周期重新校验每个冷候选,并为每个条目附加 `parentId`/`depth`。工具会在 label 之前插入 ` parent=<id> depth=<n>`;`parent` 是持久化直接 parent 会话 id,可能指向被省略的普通会话。对于当前调用方,只有 depth-1 child 条目可作为 `send_message` 候选,更深的 child 条目则可供 `interrupt_agent` 选择([中断契约](2026-08-06-continuable-subagent-interrupt.md))。发现结果只是提示——follow-up 权限仍仅属于确切直接 parent,中断权限仍由服务的在线 lineage 检查决定。空投影渲染为 `(no subagents)`。
|
||||
|
||||
diagnostic 使用三种固定原因。格式错误的事件 surface、精确加载 child 时发现的 header 冲突、读取结果中的不可变 header 与追踪到的候选不一致或不再指向请求的直接 parent、读取目标不再是先前定位的描述符事件、格式错误的描述符内容和多个描述符事件映射为 `corrupt`。未知描述符版本映射为 `unsupported`。逐 child 读取产生的 `SESSION_QUERY_SESSION_NOT_FOUND`、`SESSION_QUERY_EVENT_NOT_FOUND` 和 `SESSION_QUERY_PERSISTENCE_FAILED` 映射为 `unavailable`。这项阶段边界是有意为之:初始追踪期间发生持久化故障会让操作失败,而同一故障如果始于候选读取期间,可能会让每个受影响的 child 分别产生一条相同的 `unavailable` diagnostic;第一版既不合并这些 diagnostic,也不会把它们提升为全局失败。缺少描述符则作为非 subagent 排除,且不产生 diagnostic。配置错误、窗口错误和未识别的失败不属于 child diagnostic,会作为操作失败继续向上传播。每条 diagnostic 都标识 child id 及原因,不暴露对模型隐藏的描述符内容;系统会排除该候选,而其他健康的 sibling 仍然可见。系统绝不会读取不属于追踪结果直接后代的会话,也不会为它们产生 diagnostic。
|
||||
在已被取代的追踪读路径中,diagnostic 使用三种固定原因。格式错误的事件 surface、精确加载 child 时发现的 header 冲突、读取结果中的不可变 header 与追踪到的候选不一致或不再指向请求的直接 parent、读取目标不再是先前定位的描述符事件、格式错误的描述符内容和多个描述符事件映射为 `corrupt`。未知描述符版本映射为 `unsupported`。逐 child 读取产生的 `SESSION_QUERY_SESSION_NOT_FOUND`、`SESSION_QUERY_EVENT_NOT_FOUND` 和 `SESSION_QUERY_PERSISTENCE_FAILED` 映射为 `unavailable`。这项阶段边界是有意为之:初始追踪期间发生持久化故障会让操作失败,而同一故障如果始于候选读取期间,可能会让每个受影响的 child 分别产生一条相同的 `unavailable` diagnostic;第一版既不合并这些 diagnostic,也不会把它们提升为全局失败。缺少描述符则作为非 subagent 排除,且不产生 diagnostic。配置错误、窗口错误和未识别的失败不属于 child diagnostic,会作为操作失败继续向上传播。每条 diagnostic 都标识 child id 及原因,不暴露对模型隐藏的描述符内容;系统会排除该候选,而其他健康的 sibling 仍然可见。系统绝不会读取不属于追踪结果直接后代的会话,也不会为它们产生 diagnostic。
|
||||
|
||||
diagnostic 是瞬时查询结果,不属于会话事件或目录状态。推导 diagnostic 时,除了产生该结果的 `listEvents()` 或条件性 `readEvent()` 操作外,不会执行额外加载。
|
||||
|
||||
|
||||
@@ -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-27-web-subagent-conversations.md
|
||||
2026-07-27-web-subagent-conversations.md: da4de7d4bcd23f61459eba3fee8108b0af1c95f9
|
||||
2026-07-27-web-subagent-conversations.zh.md: 43d9710ec2b63a09bd3db029f172746d63d91cfe
|
||||
2026-07-27-web-subagent-conversations.md: 46c4949c56ebffe78e5ffda126a5784a1c648421
|
||||
2026-07-27-web-subagent-conversations.zh.md: 6b2d7bbec1133cdeb2d0cd31a6f9a43b068d21f5
|
||||
|
||||
@@ -45,7 +45,7 @@ Healthy rows reuse the standard session projections retained in the list mirror.
|
||||
|
||||
Selecting a row records its exact address before opening the resident client `Session`. History pagination, event folding, tool render intents, titles, and live mux reconciliation reuse the ordinary conversation machinery. Breadcrumbs use catalog labels, follow parent links only through `origin: 'subagent'` rows, include the first ordinary owner, and keep ordinary forks single-level. Forking an addressed subagent creates an ordinary fork with direct source lineage and attaches it to the nearest workspace-owning ancestor. The catalog is an ARIA tree with lazy ArrowRight/ArrowLeft disclosure, linear ArrowUp/ArrowDown navigation, Home/End, Escape, and focus restoration.
|
||||
|
||||
A one-shot row always replaces the composer with copy explaining that the execution record is read-only. A continuable row does so only while `parentAvailable` is false and the child is not running; a running parent-offline child keeps the ordinary composer with its input disabled so the same primary Stop stays reachable, and the read-only takeover returns once it stops. With a live parent, Enter admits another FIFO turn even while the child runs, and the single primary action follows the ordinary running convention: Send while idle, Stop while running, routed through `subagent.interrupt` ([interrupt contract](2026-08-06-continuable-subagent-interrupt.md)). Prompt failures retain the draft through the ordinary error behavior.
|
||||
A one-shot row always replaces the composer with copy explaining that the execution record is read-only. A continuable row does so only while `parentAvailable` is false and the child is not running; a running parent-offline child keeps the ordinary composer with its input and Send action disabled so independent Stop stays reachable, and the read-only takeover returns once it stops. With a live parent, Enter and Send admit another FIFO turn even while the child runs, while independent Stop routes through `subagent.interrupt` ([interrupt contract](2026-08-06-continuable-subagent-interrupt.md)). Prompt failures retain the draft through the ordinary error behavior.
|
||||
|
||||
Agent-bound auxiliary controls are unavailable in addressed child views. In particular, the model selector and `/model` contribution do not call ordinary `session.models` or `session.selectModel`; the Host also rejects any accidental call instead of activating persisted child history outside the direct-parent continuation seam.
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ Figma 中的 [subagent 列表](https://www.figma.com/design/jRBBK7zBgcszdVWQ0Fh5
|
||||
|
||||
选择一行后,系统会先记录其确切地址,再打开常驻客户端 `Session`。历史分页、事件 fold、工具渲染意图、title 与实时 mux 归并都会复用普通对话机制。面包屑导航使用目录 label,只会沿 `origin: 'subagent'` 行的父链接逐级回溯,包含第一个普通 owner,并让普通 fork 保持单层。从已寻址 subagent 创建 fork 时,会生成具有直接源谱系的普通 fork,并将其附加到最近拥有 Workspace 的祖先。目录是一棵 ARIA 树,支持懒加载式 ArrowRight/ArrowLeft 展开与折叠、线性 ArrowUp/ArrowDown 导航、Home/End、Escape 以及焦点恢复。
|
||||
|
||||
one-shot 行始终会用文案替代输入框,说明执行记录为只读。可继续行仅在 `parentAvailable` 为 false 且 child 未在运行时如此;parent 离线但仍在运行的 child 保留普通输入框,只禁用其输入区,让同一个 primary Stop 保持可达,停止后只读替代恢复。parent 在线时,即使 child 正在运行,Enter 也会准入另一个 FIFO 轮次,而唯一的 primary 操作遵循普通运行约定:空闲时为 Send,运行时为 Stop,经由 `subagent.interrupt` 路由([中断契约](2026-08-06-continuable-subagent-interrupt.md))。提示词失败会通过普通错误行为保留草稿。
|
||||
one-shot 行始终会用文案替代输入框,说明执行记录为只读。可继续行仅在 `parentAvailable` 为 false 且 child 未在运行时如此;parent 离线但仍在运行的 child 保留普通输入框,并禁用其输入区和 Send 操作,让独立的 Stop 保持可达,停止后只读替代恢复。parent 在线时,即使 child 正在运行,Enter 和 Send 也会准入另一个 FIFO 轮次,而独立的 Stop 经由 `subagent.interrupt` 路由([中断契约](2026-08-06-continuable-subagent-interrupt.md))。提示词失败会通过普通错误行为保留草稿。
|
||||
|
||||
已寻址 child 视图不提供绑定到 agent 的辅助控件。具体而言,模型选择器与 `/model` contribution 不会调用普通 `session.models` 或 `session.selectModel`;Host 也会拒绝任何意外调用,而不是在直接 parent 继续执行 seam 之外激活持久化 child 历史。
|
||||
|
||||
|
||||
@@ -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-08-06-continuable-subagent-interrupt.md
|
||||
2026-08-06-continuable-subagent-interrupt.md: 0f663ac6a58b9c3d658aa572ca2fea18dbef6449
|
||||
2026-08-06-continuable-subagent-interrupt.zh.md: b8a5b557b73b2845e3c0b1dabdec0163f56eca48
|
||||
2026-08-06-continuable-subagent-interrupt.md: cb97a77434b43ee71fa30deee9811f7a45c53f1a
|
||||
2026-08-06-continuable-subagent-interrupt.zh.md: ee05afd963584331def8c7d6acfa26830b7c1d0f
|
||||
|
||||
@@ -39,10 +39,10 @@ A human or ancestor can stop a runaway continuable turn without losing the child
|
||||
|
||||
The address-only RPC exposes one bit of live residency: an absent target is accepted while a live target under a mismatched parent returns `subagent-unauthorized`. The single-user local Host trust model accepts that observability; a future multi-principal Host must revisit both authority and response indistinguishability.
|
||||
|
||||
The Web surface reuses the existing primary Send/Stop toggle rather than adding a second action: the client `Session.cancel()` routes a continuable address through `subagent.interrupt` (one-shot addresses stay uncancellable, ordinary sessions keep `session.cancel`), and a running parent-offline continuable child keeps the default composer with disabled input so that same primary Stop remains reachable, returning to the read-only takeover once it stops ([Web subagent conversations](2026-07-27-web-subagent-conversations.md) owns the surrounding catalog and composer contract).
|
||||
The Web surface keeps Send and Stop as independent actions for a running continuable child: the client `Session.cancel()` routes Stop through `subagent.interrupt` (one-shot addresses stay uncancellable, ordinary sessions keep their existing primary Send/Stop toggle through `session.cancel`), while Send continues to queue follow-ups. A running parent-offline continuable child keeps the default composer with input and Send disabled but Stop reachable, returning to the read-only takeover once it stops ([Web subagent conversations](2026-07-27-web-subagent-conversations.md) owns the surrounding catalog and composer contract).
|
||||
|
||||
The model-facing `interrupt_agent(agent_id)` tool in `dsh-tool-subagent-control` passes `exec.agent` as the `ancestor` authority and adds none of its own: the core primitive verifies live registry identity and recorded lineage, so the tool can name a direct child or a deeper descendant with the same generic `agent_id` parameter — deliberately not `subagent_id`, which would imply direct children only. Discovery rides `list_agents({ scope: 'descendants' })` over the new `SubagentService.listDescendants()` one-trace pre-order walk with verified `parentId`/`depth` per entry ([durable catalog note](2026-07-22-durable-subagent-catalog-and-list-agents.md) owns the listing contract); discovery is a hint, never authority. `send_message` keeps its exact-direct-parent authority — only interrupt is ancestor-wide.
|
||||
|
||||
## Testing
|
||||
|
||||
Core coverage in `packages/subagent/subagent/tests/continuation.spec.ts` proves the durable `turn/end` abort, parked-then-FIFO-resumed queue, untouched descendant, both authority kinds with their cancel causes, self/sibling/stale/non-ancestor rejection, absent/one-shot/disposal-race no-ops, and the unchanged `keepInbox` loop behavior. Host coverage in `packages/host/apiproxy/tests` proves the RPC calls only the core primitive (no agents/catalog/history reads), the `subagent-unauthorized`/`internal` mappings, the wire schema's continuable-mode fence, and carrier round-trips. Client coverage pins the address-routed `Session.cancel()`, the InputBar Send/Stop toggle with the parent-offline locked-input state, and the read-only-composer selector's running exception; the keyless assembled Web scenarios (`apps/web/tests/subagent-interrupt.e2e.ts`, `subagent-interrupt-ui.e2e.ts`) hold a real child turn open with a replay hang entry and prove the interrupt transport, the aborted `turn/end`, the parked follow-up, and the FIFO resume end to end. Tool coverage in `packages/subagent/tool-subagent-control/tests` proves direct and deep ancestor interrupts with the `parent` cause and parked queue, self/sibling/stranger rejection without touching the target, absent-target no-ops without cold resume, and the descendants listing's pre-order positions; the keyless ACP snapshots pin the new tool schemas in every recorded request header.
|
||||
Core coverage in `packages/subagent/subagent/tests/continuation.spec.ts` proves the durable `turn/end` abort, parked-then-FIFO-resumed queue, untouched descendant, both authority kinds with their cancel causes, self/sibling/stale/non-ancestor rejection, absent/one-shot/disposal-race no-ops, and the unchanged `keepInbox` loop behavior. Host coverage in `packages/host/apiproxy/tests` proves the RPC calls only the core primitive (no agents/catalog/history reads), the `subagent-unauthorized`/`internal` mappings, the wire schema's continuable-mode fence, and carrier round-trips. Client coverage pins the address-routed `Session.cancel()`, the InputBar's independent Send and Stop actions with the parent-offline locked-input/Send state, and the read-only-composer selector's running exception; the keyless assembled Web scenarios (`apps/web/tests/subagent-interrupt.e2e.ts`, `subagent-interrupt-ui.e2e.ts`) hold real child turns open with replay hang entries and prove the parent-offline UI-to-RPC abort path, queued Send, the parked follow-up, and the FIFO resume end to end. Tool coverage in `packages/subagent/tool-subagent-control/tests` proves direct and deep ancestor interrupts with the `parent` cause and parked queue, self/sibling/stranger rejection without touching the target, absent-target no-ops without cold resume, and the descendants listing's pre-order positions; the keyless ACP snapshot executes `list_agents({ scope: 'descendants' })` and `interrupt_agent` through the assembled application against one settled child, while recorded request headers continue to pin both schemas.
|
||||
|
||||
@@ -39,10 +39,10 @@ Host RPC `subagent.interrupt` 接收 continuable 的 `SubagentAddress` 并返回
|
||||
|
||||
仅凭地址的 RPC 会暴露一位在线驻留信息:不存在的目标会被接受,而 parent 不匹配的在线目标会返回 `subagent-unauthorized`。单用户本地 Host 的信任模型接受这种可观察性;未来的多主体 Host 必须重新审视权限和响应不可区分性。
|
||||
|
||||
Web 侧复用现有的 primary Send/Stop 切换而不新增第二个操作:客户端 `Session.cancel()` 将 continuable 地址路由到 `subagent.interrupt`(one-shot 地址保持不可取消,普通会话仍走 `session.cancel`);parent 离线但仍在运行的 continuable child 保留默认 composer 并禁用其输入区,让同一个 primary Stop 保持可达,停止后恢复只读替代(周边目录与 composer 契约由 [Web subagent 对话](2026-07-27-web-subagent-conversations.md)拥有)。
|
||||
在 Web 侧,正在运行的 continuable child 使用相互独立的 Send 与 Stop 操作:客户端 `Session.cancel()` 将 Stop 路由到 `subagent.interrupt`(one-shot 地址保持不可取消,普通会话仍通过 `session.cancel` 保留既有的 primary Send/Stop 切换),同时 Send 继续将后续消息加入队列。parent 离线但仍在运行的 continuable child 保留默认 composer,禁用输入区与 Send,但 Stop 仍然可达;停止后恢复只读替代(周边目录与 composer 契约由 [Web subagent 对话](2026-07-27-web-subagent-conversations.md)拥有)。
|
||||
|
||||
`dsh-tool-subagent-control` 中面向模型的 `interrupt_agent(agent_id)` 工具把 `exec.agent` 作为 `ancestor` 授权传入,自身不增加任何权限:核心原语校验在线注册表身份与记录的 lineage,因此该工具可以用同一个通用 `agent_id` 参数指定直接 child 或更深的后代——刻意不用会暗示仅限直接 child 的 `subagent_id`。发现依赖 `list_agents({ scope: 'descendants' })`,其底层是新的 `SubagentService.listDescendants()` 单次追踪 pre-order 遍历,每个条目带经校验的 `parentId`/`depth`(列表契约由[持久化目录 note](2026-07-22-durable-subagent-catalog-and-list-agents.md)拥有);发现只是提示,绝非权限。`send_message` 保持其确切直接 parent 权限——只有中断是 ancestor 级的。
|
||||
|
||||
## Testing
|
||||
|
||||
`packages/subagent/subagent/tests/continuation.spec.ts` 中的核心覆盖证明了持久化 `turn/end` 中止、队列先暂停后按 FIFO 恢复、后代不受影响、两种授权及其取消 cause、self/sibling/stale/非 ancestor 拒绝、absent/一次性/disposal 竞态 no-op,以及 `keepInbox` 循环行为不变。`packages/host/apiproxy/tests` 中的 Host 覆盖证明 RPC 只调用核心原语(不读 agents/目录/历史)、`subagent-unauthorized`/`internal` 映射、wire schema 的 continuable 模式围栏以及 carrier 往返。客户端覆盖固定按地址路由的 `Session.cancel()`、InputBar 的 Send/Stop 切换及 parent 离线时锁定输入的状态,以及只读 composer selector 的运行例外;keyless 组装 Web 场景(`apps/web/tests/subagent-interrupt.e2e.ts`、`subagent-interrupt-ui.e2e.ts`)用 replay hang 条目保持真实 child 轮次打开,端到端证明中断传输、中止的 `turn/end`、follow-up 暂停以及 FIFO 恢复。`packages/subagent/tool-subagent-control/tests` 中的工具覆盖证明直接与更深 ancestor 以 `parent` cause 中断并暂停队列、self/sibling/陌生调用方被拒绝且不触碰目标、目标不存在时 no-op 且不冷恢复,以及 descendants 列表的 pre-order 位置;keyless ACP 快照把新工具 schema 固定在每个已录制请求 header 中。
|
||||
`packages/subagent/subagent/tests/continuation.spec.ts` 中的核心覆盖证明了持久化 `turn/end` 中止、队列先暂停后按 FIFO 恢复、后代不受影响、两种授权及其取消 cause、self/sibling/stale/非 ancestor 拒绝、absent/一次性/disposal 竞态 no-op,以及 `keepInbox` 循环行为不变。`packages/host/apiproxy/tests` 中的 Host 覆盖证明 RPC 只调用核心原语(不读 agents/目录/历史)、`subagent-unauthorized`/`internal` 映射、wire schema 的 continuable 模式围栏以及 carrier 往返。客户端覆盖固定按地址路由的 `Session.cancel()`、InputBar 的独立 Send 与 Stop 操作及 parent 离线时锁定输入区和 Send 的状态,以及只读 composer selector 的运行例外;keyless 组装 Web 场景(`apps/web/tests/subagent-interrupt.e2e.ts`、`subagent-interrupt-ui.e2e.ts`)通过多条 replay hang 条目保持多个真实 child 轮次打开,端到端证明 parent 离线时从 UI 到 RPC 的中止路径、Send 入队、follow-up 暂停以及 FIFO 恢复。`packages/subagent/tool-subagent-control/tests` 中的工具覆盖证明直接与更深 ancestor 以 `parent` cause 中断并暂停队列、self/sibling/陌生调用方被拒绝且不触碰目标、目标不存在时 no-op 且不冷恢复,以及 descendants 列表的 pre-order 位置;keyless ACP 快照通过组装应用,针对一个已结算的 child 执行 `list_agents({ scope: 'descendants' })` 与 `interrupt_agent`,同时已录制的请求 header 仍固定这两个 schema。
|
||||
|
||||
Reference in New Issue
Block a user