diff --git a/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.i18n.yaml index ca846c7118..0c7cce4b83 100644 --- a/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.i18n.yaml @@ -3,14 +3,14 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md /agent-note-agent-teams-teammates-are-subagents: - en: 51e2fc51a6e1622a - zh: 7e09ea6292e5921d + en: d74a90e6568d278a + zh: 538f5fa098e2e925 /agent-note-agent-teams-teammates-are-subagents/problem: en: ca2848e3bbc3a009 zh: 0551e32ff6d187d0 /agent-note-agent-teams-teammates-are-subagents/decision: - en: 9da65aee28d19268 - zh: 20191b1ec59697b1 + en: 6e1d8ea634e8e3fc + zh: c9af24066de755a5 /agent-note-agent-teams-teammates-are-subagents/alternatives-considered: en: ef0cf648ff54784f zh: 2a0b3745a54e7072 diff --git a/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md b/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md index 426750813c..0c6147d7a2 100644 --- a/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md +++ b/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md @@ -4,7 +4,7 @@ Status: implemented English | [中文](2026-10-15-agent-teams-as-subagents.zh.md) -The adjacency and parent-lifetime restrictions are superseded by [tree-wide communication](2026-10-16-tree-wide-agent-communication.md). [Task-board inbox coordination](2026-10-19-team-task-board-inbox.md) supersedes the separate roster and Team-owned wait; the single subagent identity decision remains active. +The name-free model interface below is superseded by [hierarchical agent names](2026-10-17-hierarchical-agent-names.md). Session identity, lineage authority, and Team reuse remain current. The adjacency and parent-lifetime restrictions are superseded by [tree-wide communication](2026-10-16-tree-wide-agent-communication.md). [Task-board inbox coordination](2026-10-19-team-task-board-inbox.md) supersedes the separate roster and Team-owned wait; the single subagent identity decision remains active. ## Problem @@ -18,7 +18,7 @@ Use one subagent identity: the Session id. The existing `description` tool argum Every root Agent and its descendants form the Team identified by the root Session id. Team creates no agents and contributes task tools, task policy, and required Team identity. Ordinary subagent controls supply discovery, messaging, interruption, and inbox waiting. Parent and fork children receive identical tool schemas and shared policies. -Task ownership uses `ownerId`; root reassignment accepts itself or any reachable continuable descendant. Historical roster records remain decode-only. Team persists task changes and signals active waits after commit without starting idle turns. Historical `team/message/*` records remain readable but are never delivered or acknowledged. +Model-facing task ownership uses `ownerName`; host ownership retains `ownerId`; root reassignment accepts itself or any reachable continuable descendant. Historical roster records remain decode-only. Team persists task changes and signals active waits after commit without starting idle turns. Historical `team/message/*` records remain readable but are never delivered or acknowledged. This supersedes the roster, provisioning, recovery, and messaging decisions in [Agent Teams](../feature/2026-08-05-agent-teams.md) and the [Team direct inbox decision](../../archived/simplification/2026-09-26-team-direct-inbox.md). The task-board and shared-checkout decisions remain active. [Adjacent Agent messaging](2026-08-27-adjacent-agent-steer-messaging.md) includes sibling routing, while [required runtime-context identity](2026-10-13-team-identity-runtime-context.md) owns the role statement. The [upgrade guide](../../../../docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.md) lists removed tools, methods, and configuration. diff --git a/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.zh.md b/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.zh.md index dfd80205de..45c2d8ef22 100644 --- a/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.zh.md +++ b/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.zh.md @@ -4,7 +4,7 @@ Status: implemented [English](2026-10-15-agent-teams-as-subagents.md) | 中文 -邻接和父级生命周期限制由[整树通信](2026-10-16-tree-wide-agent-communication.zh.md)取代。[任务板收件箱协调](2026-10-19-team-task-board-inbox.zh.md)取代独立名册和 Team 所有的等待;单一 subagent 身份决策仍有效。 +下述不使用名字的模型接口由[分层 agent 名字](2026-10-17-hierarchical-agent-names.zh.md)取代。Session 身份、谱系权限和 Team 复用仍然有效。邻接和父级生命周期限制由[整树通信](2026-10-16-tree-wide-agent-communication.zh.md)取代。[任务板收件箱协调](2026-10-19-team-task-board-inbox.zh.md)取代独立名册和 Team 所有的等待;单一 subagent 身份决策仍有效。 ## Problem @@ -18,7 +18,7 @@ Status: implemented 每个根 Agent 及其后代组成以根 Session id 标识的 Team。Team 不创建 agent,提供任务工具、任务策略和必需 Team 身份。普通 subagent 控制工具提供发现、通信、中断和收件箱等待。父级和 fork 子级接收相同工具 schema 和共享策略。 -任务归属使用 `ownerId`;根重新分配可指定自身或任意可达的可继续执行后代。历史名册记录仅供解码。Team 持久化任务变更,并在提交后通知当前等待,不启动空闲轮次。历史 `team/message/*` 记录仍可读取,但绝不再投递或确认。 +面向模型的任务归属使用 `ownerName`;host 归属保留 `ownerId`;根重新分配可指定自身或任意可达的可继续执行后代。历史名册记录仅供解码。Team 持久化任务变更,并在提交后通知当前等待,不启动空闲轮次。历史 `team/message/*` 记录仍可读取,但绝不再投递或确认。 本决策取代 [Agent Teams](../feature/2026-08-05-agent-teams.zh.md) 的成员列表、创建、恢复和消息决策,以及 [Team 直接收件箱决策](../../archived/simplification/2026-09-26-team-direct-inbox.md)。任务板和共享工作树决策仍有效。[相邻 Agent 消息](2026-08-27-adjacent-agent-steer-messaging.zh.md)纳入同级路由,[必需运行时身份](2026-10-13-team-identity-runtime-context.zh.md)负责角色声明。[升级指南](../../../../docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.zh.md)列出移除的工具、方法和配置。 diff --git a/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.i18n.yaml index ee52c30615..89cf05fabb 100644 --- a/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.i18n.yaml @@ -9,8 +9,8 @@ en: c583c3cdb894e417 zh: 6edc0354b79f4f26 /agent-note-communication-follows-the-subagent-tree/decision: - en: da35a75e069fa1e4 - zh: 7edf62ad66ab04ec + en: d613dd690cb06596 + zh: fab70b2107e33054 /agent-note-communication-follows-the-subagent-tree/alternatives-considered: en: 470f9cf8c3e32af9 zh: 73bbe29767c5d02a diff --git a/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.md b/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.md index 30dee6781a..1fad34c523 100644 --- a/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.md +++ b/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.md @@ -10,13 +10,13 @@ A Team shares one task board across the root's entire subagent tree. Restricting ## Decision -`list_agents` starts at the caller's live root and lists that root and all reachable continuable descendants, including the caller. Parent ids and depth remain relative to the root. The existing parent catalogs remain authoritative; no Team directory is stored separately. +`list_agents` starts at the caller's live root and lists that root and all reachable continuable descendants, including the caller. Parent paths and depth remain relative to the root. The existing parent catalogs remain authoritative; no Team directory is stored separately. `send_message` accepts another root or continuable member of the same tree. The sender must be the exact live root or a resident continuable subagent. Cross-branch delivery verifies the target descriptor and creation catalogs, then restores only the target under the Team root. Final inbox admission rechecks the sender and root. Cancellation or rejection before acceptance releases the new target instance; accepted input follows normal Agent persistence. The existing Agent registry records runtime ownership by the Team root. Creation parent ids and catalogs preserve ancestry for discovery and interrupt authority. A member finishing or being released affects only its own instance; Team-root shutdown joins all startup and cleanup. This removes ancestor restoration, parent retention, and recursive disposal without adding a Team runtime registry. Saved target presets and permission events supply restoration configuration. -This supersedes the adjacency, discovery, and parent-lifetime restrictions in [adjacent messaging](2026-08-27-adjacent-agent-steer-messaging.md) and [subagent teammates](2026-10-15-agent-teams-as-subagents.md). Their shared inbox, scheduling, attribution, and Team reuse decisions remain active. Members use Session ids for addressing; ordinary Session forks lead independent Teams. +This supersedes the adjacency, discovery, and parent-lifetime restrictions in [adjacent messaging](2026-08-27-adjacent-agent-steer-messaging.md) and [subagent teammates](2026-10-15-agent-teams-as-subagents.md). Their shared inbox, scheduling, attribution, and Team reuse decisions remain active. Model tools use [hierarchical agent paths](2026-10-17-hierarchical-agent-names.md); host delivery and authorization use Session ids; ordinary Session forks lead independent Teams. ## Alternatives considered diff --git a/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.zh.md b/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.zh.md index 58d075e9a6..cfae0bbc1d 100644 --- a/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.zh.md +++ b/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.zh.md @@ -10,13 +10,13 @@ Status: implemented ## Decision -`list_agents` 从调用方的在线根开始,列出该根与所有可达的可继续后代,包括调用方。父级 id 和深度始终相对根计算。现有父级目录仍是权威来源;不单独存储 Team 目录。 +`list_agents` 从调用方的在线根开始,列出该根与所有可达的可继续后代,包括调用方。父级路径和深度始终相对根计算。现有父级目录仍是权威来源;不单独存储 Team 目录。 `send_message` 接受同一棵树中的其他根或可继续成员。发送者必须是确切在线根或驻留的可继续子代理。跨分支投递验证目标描述符和创建目录后,仅在 Team 根下恢复目标。最终收件箱准入重新检查发送者与根。接受前的取消或拒绝释放新建目标实例;已接受输入遵循普通 Agent 持久化。 现有 Agent 注册表记录 Team 根对运行时实例的所有权。创建父级 id 和目录保留用于发现及中断授权的祖先关系。成员完成或释放只影响自身实例;关闭 Team 根会等待全部启动和清理。这删除了祖先恢复、父级保留和递归释放,无需增加 Team 运行时注册表。目标保存的 preset 和权限事件提供恢复配置。 -本决策替代[相邻消息](2026-08-27-adjacent-agent-steer-messaging.zh.md)与[子代理队友](2026-10-15-agent-teams-as-subagents.zh.md)中的邻接、发现及父子生命周期限制。它们的共享收件箱、调度、发送者归属和 Team 复用决策仍有效。成员使用 Session id 寻址;普通 Session 分叉领导独立 Team。 +本决策替代[相邻消息](2026-08-27-adjacent-agent-steer-messaging.zh.md)与[子代理队友](2026-10-15-agent-teams-as-subagents.zh.md)中的邻接、发现及父子生命周期限制。它们的共享收件箱、调度、发送者归属和 Team 复用决策仍有效。模型工具使用[分层 agent 路径](2026-10-17-hierarchical-agent-names.zh.md);host 投递与授权使用 Session id;普通 Session 分叉领导独立 Team。 ## Alternatives considered diff --git a/.agents/notes/implemented/architecture/2026-10-17-hierarchical-agent-names.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-17-hierarchical-agent-names.i18n.yaml new file mode 100644 index 0000000000..9cff3fde14 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-17-hierarchical-agent-names.i18n.yaml @@ -0,0 +1,22 @@ +# Bilingual-pair consistency record for 2026-10-17-hierarchical-agent-names.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-17-hierarchical-agent-names.md +/agent-note-hierarchical-agent-names: + en: 4a1223d881f03a6f + zh: acced71e5ee769b2 +/agent-note-hierarchical-agent-names/problem: + en: 78696f33320218ec + zh: 13d8aa85ef049e6e +/agent-note-hierarchical-agent-names/decision: + en: a7a8a0eba5840222 + zh: ae45ee6461659fb1 +/agent-note-hierarchical-agent-names/alternatives-considered: + en: 1447580f9c1f10e5 + zh: 147e2cc7062af05c +/agent-note-hierarchical-agent-names/consequences: + en: be8d7efb0185c48f + zh: c5a9e77faf4c64f4 +/agent-note-hierarchical-agent-names/testing: + en: 5c13031cb2de1faf + zh: 680ba292f9c27864 diff --git a/.agents/notes/implemented/architecture/2026-10-17-hierarchical-agent-names.md b/.agents/notes/implemented/architecture/2026-10-17-hierarchical-agent-names.md new file mode 100644 index 0000000000..bbe654c4c7 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-17-hierarchical-agent-names.md @@ -0,0 +1,31 @@ +# Agent Note: Hierarchical agent names + +Status: implemented + +English | [中文](2026-10-17-hierarchical-agent-names.zh.md) + +## Problem + +Session ids are durable execution identities but difficult to read in model coordination. Descriptions may repeat. Flat tree-wide names require every parent to coordinate reservations and publish descendant assignments in the root, introducing a second Session write and incomplete-registration recovery. + +## Decision + +Keep Session ids for persistence and authorization. Each child has an immutable name segment unique among its direct siblings. Model tools return and accept full paths rooted at `lead`, such as `lead/reviewer` and `lead/explore/alice`. Parent catalogs already record the required relationships; local descriptors retain the child’s segment. Path lookup reads only intermediate parent catalogs. Temporary reservations belong to the exact live parent and reject concurrent sibling duplicates before provider dispatch. A parent catalog append commits creation, and committed names survive settlement and restart. + +This consolidates the tree-wide naming decision and supersedes the name-free interface in [Agent Teams as subagents](2026-10-15-agent-teams-as-subagents.md). Its lineage authority and shared subagent lifecycle remain current. Resolution grants no messaging, interruption, or assignment authority. Historical unnamed children use `legacy:` segments; historical Team members without a catalog grant no assignment authority. + +## Alternatives considered + +**Flat tree-wide names.** Short addresses are convenient and root registration avoids intermediate catalog reads. That convenience requires shared name admission, a root assignment record, and recovery when the parent write succeeds but root registration fails. Paths give every caller the same unambiguous address while reusing parent-owned records. Reintroducing a flat namespace would require a demonstrated need for globally unique short aliases that outweighs these costs. + +**Recyclable names.** Reuse could redirect delayed work after the original agent settles. Retaining assignments keeps recipient identity stable. + +**Checking only before asynchronous startup.** Two concurrent sibling creations could both pass. Parent-local temporary reservations prevent duplicate external work before it starts; failed creation releases the reservation. + +## Consequences + +Creation takes one segment; tool results, child guidance, message prefixes, task owners, and settlement notices use full paths. Different parents and independent roots can reuse segments. Cold path lookup depends on intermediate catalogs, but not unrelated branches. A failed parent catalog append can leave a local descriptor; a named child cannot resume without its matching parent entry. Internal Session and task-owner ids stay stable. The [upgrade guide](../../../../docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.md) describes caller changes. + +## Testing + +Focused tests cover same-name children in separate branches, concurrent sibling rejection before dispatch, failed-start release, parent catalog failure, restoration, fork isolation, legacy paths, and ambiguous persisted names. Recorded Headless and SDK sessions cover creation, nested paths, messages, listing, and completion notices; Team tests reject historical membership as assignment or message authority. diff --git a/.agents/notes/implemented/architecture/2026-10-17-hierarchical-agent-names.zh.md b/.agents/notes/implemented/architecture/2026-10-17-hierarchical-agent-names.zh.md new file mode 100644 index 0000000000..0581df506f --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-17-hierarchical-agent-names.zh.md @@ -0,0 +1,31 @@ +# Agent Note: 分层 agent 名字 + +Status: implemented + +[English](2026-10-17-hierarchical-agent-names.md) | 中文 + +## 问题 + +Session id 是持久执行身份,但不便于阅读模型协作内容。描述可以重复。树内全局短名要求所有父级协调名字预留,并将后代分配发布到根,引入第二次 Session 写入和不完整登记恢复。 + +## 决策 + +保留 Session id 用于持久化和授权。每个子级有一个不可变的名字路径段,只在直接同级中唯一。模型工具返回并接受以 `lead` 为根的完整路径,如 `lead/reviewer` 和 `lead/explore/alice`。父级 catalog 已记录所需关系;本地 descriptor 保留子级路径段。路径查找只读取沿途父级 catalog。临时预留属于确切在线的父级,在 provider 派发前拒绝并发同级重名。父级 catalog 追加提交创建,已提交名字在执行结束和重启后保留。 + +本记录合并树内唯一名字决策,并取代 [Agent Teams 作为 subagent](2026-10-15-agent-teams-as-subagents.zh.md) 中不使用名字的接口。其谱系权限与共享 subagent 生命周期仍然有效。解析不授予消息、中断或分配权限。历史无名子级使用 `legacy:` 路径段;没有 catalog 的历史 Team 成员不授予分配权限。 + +## 考虑的替代方案 + +**树内全局短名。** 短地址便于使用,根登记避免读取中间 catalog。但这要求共享名字准入、根分配记录,以及父级写入成功但根登记失败后的恢复。路径复用父级已有记录,仍为所有调用方提供相同的无歧义地址。重新引入全局短名空间需要证明确有足以抵偿这些成本的短别名需求。 + +**可回收名字。** 执行结束后复用名字,可能让延迟工作转向另一个接收方。保留分配可维持接收方身份稳定。 + +**只在异步启动前检查。** 两个并发同级创建都可能通过检查。父级局部临时预留在外部工作开始前阻止重复派发;创建失败会释放预留。 + +## 影响 + +创建接受一个路径段;工具结果、子级指引、消息前缀、任务负责人和完成通知使用完整路径。不同父级和独立根可复用路径段。冷路径查找依赖沿途中间 catalog,不依赖无关分支。父级 catalog 追加失败可能留下本地 descriptor;命名子级缺少匹配的父级条目时无法恢复。内部 Session 和任务负责人 id 保持稳定。[升级指南](../../../../docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.zh.md) 说明调用方变更。 + +## 测试 + +定向测试覆盖不同分支同名子级、派发前拒绝并发同级重名、启动失败释放、父级 catalog 失败、恢复、fork 隔离、legacy 路径和有歧义的持久化名字。录制的 Headless 与 SDK 会话覆盖创建、嵌套路径、消息、列表和完成通知;Team 测试拒绝将历史成员关系作为分配或消息权限。 diff --git a/apps/cli/tests/agent-team-headless.e2e.ts b/apps/cli/tests/agent-team-headless.e2e.ts index a7c7f34e99..68efd6b4c0 100644 --- a/apps/cli/tests/agent-team-headless.e2e.ts +++ b/apps/cli/tests/agent-team-headless.e2e.ts @@ -91,8 +91,9 @@ describe('dsh run with Agent Teams enabled', () => { const workflowChild = parsed.find(log => log.some(record => record.type === 'user/message' && JSON.stringify((record.data as { content: unknown }).content) === JSON.stringify([{ type: 'text', text: 'TEAM_WORKFLOW_CHILD' }]))) expect(workflowChild).toBeDefined() - expect(workflowChild!.find(record => record.type === 'subagent/descriptor')?.data) - .toMatchObject({ mode: 'continuable', provider: 'spawn' }) + const workflowDescriptor = workflowChild!.find(record => record.type === 'subagent/descriptor')?.data as Record + expect(workflowDescriptor).toMatchObject({ mode: 'continuable', provider: 'spawn' }) + expect(workflowDescriptor.name).toMatch(/^workflow-/) expect(workflowChild!.filter(record => record.type === 'user/message' && (record.data as { source: { kind: string } }).source.kind === 'user').map(record => record.data)) .toEqual([expect.objectContaining({ content: [{ type: 'text', text: 'TEAM_WORKFLOW_CHILD' }] })]) @@ -105,10 +106,10 @@ describe('dsh run with Agent Teams enabled', () => { expect(eventTypes).not.toContain('team/member') const catalog = root!.filter(record => record.type === 'subagent/catalog').map(record => record.data) expect(catalog).toEqual(expect.arrayContaining([ - expect.objectContaining({ version: 0, label: 'Own deterministic implementation.' }), - expect.objectContaining({ version: 0, label: 'Own deterministic research.' }), + expect.objectContaining({ version: 0, name: workflowDescriptor.name }), + expect.objectContaining({ version: 0, name: 'implementer', label: 'Own deterministic implementation.' }), + expect.objectContaining({ version: 0, name: 'researcher', label: 'Own deterministic research.' }), ])) - for (const entry of catalog) expect(entry).not.toHaveProperty('name') expect(eventTypes).not.toContain('team/message/queued') expect(eventTypes).not.toContain('team/message/delivered') const peerMessages = parsed.flatMap(log => log.filter(record => record.type === 'user/message' diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-diagnostic/parent.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-diagnostic/parent.expected.jsonl index df8f92debd..e6b850123f 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-diagnostic/parent.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-diagnostic/parent.expected.jsonl @@ -22,7 +22,7 @@ {"type":"session/title","data":{"title":"Start a background job.","messageSeqs":[3],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"list-once","name":"list_agents","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"list-once","name":"list_agents","args":["{}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"list-once","name":"list_agents","arguments":"{}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":2,"step":1,"callId":"list-once","name":"list_agents","arguments":"{}"}} -{"type":"tool/result","data":{"turn":2,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"list-once"},"toolCallId":"list-once","content":[{"type":"text","text":"{{sessionId}} [running] — team lead\n{{sessionId}} [inactive] parent={{sessionId}} depth=1 — descriptorless child"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":2,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"list-once"},"toolCallId":"list-once","content":[{"type":"text","text":"lead [running] — team lead\nlead/legacy:{{sessionId}} [inactive] parent=lead depth=1 — descriptorless child"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"step/start","data":{"turn":2,"step":2}} {"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"The catalogued subagent is ready. PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["The catalogued subagent is ready. PARENT_DONE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"The catalogued subagent is ready. PARENT_DONE"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/child.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/child.expected.jsonl index 66fa764ab9..acdd066441 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/child.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/child.expected.jsonl @@ -1,13 +1,13 @@ {"type":"session","version":4,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","parentSession":"{{sessionId}}","isSeeded":false,"origin":"subagent","delegationDepth":1} -{"type":"subagent/descriptor","data":{"version":3,"mode":"continuable","provider":"spawn","label":"Delegated write probe","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash","agentReasoningEffort":"low"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"continuable","provider":"spawn","name":"write-probe","label":"Delegated write probe","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash","agentReasoningEffort":"low"}} {"type":"sandbox/mode","data":{"mode":"read-only","source":"delegation"}} {"type":"approval/policy","data":{"policy":"never","source":"delegation"}} -{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE."},{"type":"text","text":"Your parent agent id is \"{{sessionId}}\". Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. Use send_message({ agent_id: \"{{sessionId}}\", message: \"\" }) when a finding changes what the parent should do next; sending a message does not end your turn."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE."},{"type":"text","text":"Your name is \"lead/write-probe\". Your parent agent name is \"lead\". Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. Use send_message({ name: \"lead\", message: \"\" }) when a finding changes what the parent should do next; sending a message does not end your turn."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE."},{"type":"text","text":"Your parent agent id is \"{{sessionId}}\". Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. Use send_message({ agent_id: \"{{sessionId}}\", message: \"\" }) when a finding changes what the parent should do next; sending a message does not end your turn."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE."},{"type":"text","text":"Your name is \"lead/write-probe\". Your parent agent name is \"lead\". Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. Use send_message({ name: \"lead\", message: \"\" }) when a finding changes what the parent should do next; sending a message does not end your turn."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/parent.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/parent.expected.jsonl index 79f0cbebea..1cabc4a582 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/parent.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/parent.expected.jsonl @@ -21,15 +21,15 @@ {"type":"developer/message","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"bash"},{"type":"tool-addition","toolName":"create_goal"},{"type":"tool-addition","toolName":"edit"},{"type":"tool-addition","toolName":"exit_plan_mode"},{"type":"tool-addition","toolName":"get_goal"},{"type":"tool-addition","toolName":"glob"},{"type":"tool-addition","toolName":"grep"},{"type":"tool-addition","toolName":"interrupt_agent"},{"type":"tool-addition","toolName":"job_kill"},{"type":"tool-addition","toolName":"job_list"},{"type":"tool-addition","toolName":"job_output"},{"type":"tool-addition","toolName":"list_agents"},{"type":"tool-addition","toolName":"read"},{"type":"tool-addition","toolName":"read_image"},{"type":"tool-addition","toolName":"send_message"},{"type":"tool-addition","toolName":"skill"},{"type":"tool-addition","toolName":"subagent"},{"type":"tool-addition","toolName":"subagent_fork"},{"type":"tool-addition","toolName":"todo_write"},{"type":"tool-addition","toolName":"update_goal"},{"type":"tool-addition","toolName":"wait_agent"},{"type":"tool-addition","toolName":"web_fetch"},{"type":"tool-addition","toolName":"web_search"},{"type":"tool-addition","toolName":"workflow"},{"type":"tool-addition","toolName":"working_directory"},{"type":"tool-addition","toolName":"write"}],"role":"developer","id":"{{sessionId}}"},"headerSeq":18},"surfaceOp":"append"} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Tighten this session to read-only.","messageSeqs":[3],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"delegate-write","name":"subagent","arguments":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"delegate-write","name":"subagent","args":["{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"delegate-write","name":"subagent","arguments":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} -{"type":"tool/call","data":{"turn":2,"step":1,"callId":"delegate-write","name":"subagent","arguments":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{sessionId}}","childCreatedAt":0,"mode":"continuable","label":"Delegated write probe"}} -{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_DENIED [sandbox: file access denied under read-only mode]"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"}]}} -{"type":"tool/result","data":{"turn":2,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"delegate-write"},"toolCallId":"delegate-write","content":[{"type":"text","text":"started subagent {{sessionId}}"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[23],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"delegate-write","name":"subagent","arguments":"{\"name\":\"write-probe\",\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"delegate-write","name":"subagent","args":["{\"name\":\"write-probe\",\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"delegate-write","name":"subagent","arguments":"{\"name\":\"write-probe\",\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"tool/call","data":{"turn":2,"step":1,"callId":"delegate-write","name":"subagent","arguments":"{\"name\":\"write-probe\",\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}} +{"type":"subagent/catalog","data":{"version":0,"childId":"{{sessionId}}","childCreatedAt":0,"mode":"continuable","label":"Delegated write probe","name":"write-probe"}} +{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent lead/write-probe finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_DENIED [sandbox: file access denied under read-only mode]"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent lead/write-probe finished and will do no further work unless you send it more.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"}]}} +{"type":"tool/result","data":{"turn":2,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"delegate-write"},"toolCallId":"delegate-write","content":[{"type":"text","text":"started subagent lead/write-probe"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[23],"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":2}} -{"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_DENIED [sandbox: file access denied under read-only mode]"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent lead/write-probe finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_DENIED [sandbox: file access denied under read-only mode]"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent lead/write-probe finished and will do no further work unless you send it more.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"The delegated child was denied by the sandbox. PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["The delegated child was denied by the sandbox. PARENT_DONE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"The delegated child was denied by the sandbox. PARENT_DONE"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":2}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/replay.override.json b/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/replay.override.json index 1c0fe9ffad..e6da428e6c 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/replay.override.json +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/replay.override.json @@ -1 +1 @@ -[{"kind": "chunks", "chunks": [{"type": "block-start", "index": 0, "blockType": "tool-call"}, {"type": "tool-call-delta", "index": 0, "id": "delegate-write", "name": "subagent", "argumentsDelta": "{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}, {"type": "block-end", "index": 0, "block": {"type": "tool-call", "id": "delegate-write", "name": "subagent", "arguments": "{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}}, {"type": "usage", "usage": {"inputTokens": 10, "outputTokens": 5}}, {"type": "finish", "reason": {"kind": "tool-calls"}}]}, {"kind": "chunks", "chunks": [{"type": "block-start", "index": 0, "blockType": "text"}, {"type": "text-delta", "index": 0, "text": "The delegated child was denied by the sandbox. PARENT_DONE"}, {"type": "block-end", "index": 0, "block": {"type": "text", "text": "The delegated child was denied by the sandbox. PARENT_DONE"}}, {"type": "usage", "usage": {"inputTokens": 10, "outputTokens": 5}}, {"type": "finish", "reason": {"kind": "stop"}}]}] +[{"kind": "chunks", "chunks": [{"type": "block-start", "index": 0, "blockType": "tool-call"}, {"type": "tool-call-delta", "index": 0, "id": "delegate-write", "name": "subagent", "argumentsDelta": "{\"name\":\"write-probe\",\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}, {"type": "block-end", "index": 0, "block": {"type": "tool-call", "id": "delegate-write", "name": "subagent", "arguments": "{\"name\":\"write-probe\",\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}}, {"type": "usage", "usage": {"inputTokens": 10, "outputTokens": 5}}, {"type": "finish", "reason": {"kind": "tool-calls"}}]}, {"kind": "chunks", "chunks": [{"type": "block-start", "index": 0, "blockType": "text"}, {"type": "text-delta", "index": 0, "text": "The delegated child was denied by the sandbox. PARENT_DONE"}, {"type": "block-end", "index": 0, "block": {"type": "text", "text": "The delegated child was denied by the sandbox. PARENT_DONE"}}, {"type": "usage", "usage": {"inputTokens": 10, "outputTokens": 5}}, {"type": "finish", "reason": {"kind": "stop"}}]}] diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/child.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/child.expected.jsonl index ec1440c9f8..29da572429 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/child.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/child.expected.jsonl @@ -1,13 +1,13 @@ {"type":"session","version":4,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","parentSession":"{{sessionId}}","isSeeded":false,"origin":"subagent","delegationDepth":1} -{"type":"subagent/descriptor","data":{"version":3,"mode":"continuable","provider":"spawn","label":"Return child result","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash"}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"continuable","provider":"spawn","name":"settlement-child","label":"Return child result","agentProvider":"deepseek-official","agentModel":"deepseek-v4-flash"}} {"type":"sandbox/mode","data":{"mode":"workspace-write","source":"delegation"}} {"type":"approval/policy","data":{"policy":"never","source":"delegation"}} -{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message."},{"type":"text","text":"Your parent agent id is \"{{sessionId}}\". Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. Use send_message({ agent_id: \"{{sessionId}}\", message: \"\" }) when a finding changes what the parent should do next; sending a message does not end your turn."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message."},{"type":"text","text":"Your name is \"lead/settlement-child\". Your parent agent name is \"lead\". Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. Use send_message({ name: \"lead\", message: \"\" }) when a finding changes what the parent should do next; sending a message does not end your turn."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message."},{"type":"text","text":"Your parent agent id is \"{{sessionId}}\". Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. Use send_message({ agent_id: \"{{sessionId}}\", message: \"\" }) when a finding changes what the parent should do next; sending a message does not end your turn."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message."},{"type":"text","text":"Your name is \"lead/settlement-child\". Your parent agent name is \"lead\". Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. Use send_message({ name: \"lead\", message: \"\" }) when a finding changes what the parent should do next; sending a message does not end your turn."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/parent.override.json b/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/parent.override.json index 91428bb387..1ce2e97ca7 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/parent.override.json +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/parent.override.json @@ -3,8 +3,8 @@ "kind": "chunks", "chunks": [ { "type": "block-start", "index": 0, "blockType": "tool-call" }, - { "type": "tool-call-delta", "index": 0, "id": "start-child", "name": "subagent", "argumentsDelta": "{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}" }, - { "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "start-child", "name": "subagent", "arguments": "{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}" } }, + { "type": "tool-call-delta", "index": 0, "id": "start-child", "name": "subagent", "argumentsDelta": "{\"name\":\"settlement-child\",\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}" }, + { "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "start-child", "name": "subagent", "arguments": "{\"name\":\"settlement-child\",\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}" } }, { "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 5 } }, { "type": "finish", "reason": { "kind": "tool-calls" } } ] diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl index f06a75700f..17bf291b34 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl @@ -9,19 +9,19 @@ {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"Start one continuable subagent and","messageSeqs":[8],"source":{"kind":"fallback"}}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.\nIf the messages give little to name, still return a short best-effort title, such as Greeting, instead of explaining.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"Start one continuable subagent and acknowledge its completion notice. Do not call list_agents or send_message.\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":4096}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":14,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"start-child","name":"subagent","args":["{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":15,"time":0,"data":{"turn":1,"step":1,"callId":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":14,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"start-child","name":"subagent","arguments":"{\"name\":\"settlement-child\",\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"start-child","name":"subagent","args":["{\"name\":\"settlement-child\",\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"start-child","name":"subagent","arguments":"{\"name\":\"settlement-child\",\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":15,"time":0,"data":{"turn":1,"step":1,"callId":"start-child","name":"subagent","arguments":"{\"name\":\"settlement-child\",\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":16,"time":0,"data":{"title":"Subagent settlement","messageSeqs":[8],"source":{"kind":"provider","provider":"session-title-first-prompt-llm","model":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"subagent/catalog","seq":17,"time":0,"data":{"version":0,"childId":"{{sessionId}}","childCreatedAt":0,"mode":"continuable","label":"Return child result"}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":18,"time":0,"data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"start-child"},"toolCallId":"start-child","content":[{"type":"text","text":"started subagent {{sessionId}}"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"subagent/catalog","seq":17,"time":0,"data":{"version":0,"childId":"{{sessionId}}","childCreatedAt":0,"mode":"continuable","label":"Return child result","name":"settlement-child"}}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":18,"time":0,"data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"start-child"},"toolCallId":"start-child","content":[{"type":"text","text":"started subagent lead/settlement-child"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":19,"time":0,"data":{"turn":1,"step":1}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":20,"time":0,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_RESULT"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"}]}}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":20,"time":0,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent lead/settlement-child finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_RESULT"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent lead/settlement-child finished and will do no further work unless you send it more.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"}]}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":21,"time":0,"data":{"turn":1,"step":2}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":22,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["STARTED"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"STARTED"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":23,"time":0,"data":{"turn":1,"step":2}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":24,"time":0,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":25,"time":0,"data":{"turn":1,"step":3}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":26,"time":0,"data":{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_RESULT"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":26,"time":0,"data":{"content":[{"type":"text","text":"Background subagent lead/settlement-child finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_RESULT"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent lead/settlement-child finished and will do no further work unless you send it more.","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":27,"time":0,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_RECEIVED_SETTLEMENT_NOTICE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["PARENT_RECEIVED_SETTLEMENT_NOTICE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_RECEIVED_SETTLEMENT_NOTICE"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":28,"time":0,"data":{"turn":1,"step":3}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":29,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}} diff --git a/apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs b/apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs index 2cba5a8ffc..f3b521e948 100644 --- a/apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs +++ b/apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs @@ -87,13 +87,13 @@ function researcher(messages) { if (!names.includes('send_message')) { return toolChunks([ { name: 'team_task_update', args: { task_id: 'task-1', expected_revision: 2, action: 'complete' } }, - { name: 'send_message', args: { agent_id: sibling, message: 'Research complete: use the deterministic finding.' } }, + { name: 'send_message', args: { name: sibling, message: 'Research complete: use the deterministic finding.' } }, ]) } return textChunks('Research teammate complete.') } -function implementer(messages, parentId) { +function implementer(messages, parentName) { const names = calls(messages) const last = latestAssistantCalls(messages) const text = latestToolText(messages) @@ -134,7 +134,7 @@ function implementer(messages, parentId) { if (!names.includes('send_message')) { return toolChunks([ { name: 'team_task_update', args: { task_id: 'task-2', expected_revision: 2, action: 'complete' } }, - { name: 'send_message', args: { agent_id: parentId, message: 'Implementation complete and verified.' } }, + { name: 'send_message', args: { name: parentName, message: 'Implementation complete and verified.' } }, ]) } return textChunks('Implementation teammate complete.') @@ -154,6 +154,7 @@ function lead(messages) { return toolChunks([{ name: 'subagent', args: { + name: 'implementer', description: 'Own deterministic implementation.', prompt: 'IMPLEMENTER_MARK: wait for research, complete dependent task 2, report to lead.', }, @@ -163,6 +164,7 @@ function lead(messages) { return toolChunks([{ name: 'subagent', args: { + name: 'researcher', description: 'Own deterministic research.', prompt: 'RESEARCHER_MARK: complete research task 1, message implementer, then finish.', }, @@ -194,7 +196,7 @@ class TeamFixtureAdapter extends LlmAdapter { const context = options.messages.findLast(message => message.source?.kind === 'runtime-context') const section = name => context?.source.sections?.find(candidate => candidate.name === name)?.text const role = section('team:identity') - const parentId = role?.match(/Team led by agent "([^"]+)"/u)?.[1] + const parentName = role?.match(/Team led by "([^"]+)"/u)?.[1] const task = initial?.content.filter(block => block.type === 'text').map(block => block.text).join('\n') if (!workflowChild && (!tools.includes('subagent') || !tools.includes('workflow') || !tools.includes('team_task_create'))) { throw new Error('Team profile is missing delegation, workflow, or task tools') @@ -202,7 +204,7 @@ class TeamFixtureAdapter extends LlmAdapter { let chunks if (workflowChild) chunks = textChunks('Fresh workflow child complete.') else if (task?.startsWith('RESEARCHER_MARK:')) chunks = researcher(options.messages) - else if (task?.startsWith('IMPLEMENTER_MARK:')) chunks = implementer(options.messages, parentId) + else if (task?.startsWith('IMPLEMENTER_MARK:')) chunks = implementer(options.messages, parentName) else if (role?.includes('You are the Team Lead.')) chunks = lead(options.messages) else throw new Error('Team runtime context is missing a recognized identity') for (const chunk of chunks) { diff --git a/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts b/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts index a4a1666b4d..f0150c6412 100644 --- a/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts +++ b/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts @@ -1255,8 +1255,8 @@ describe('headless stream-json snapshots', () => { "checkedAgents": true, "deliveredMessages": 0, "identities": [ - "You are a teammate in the Team led by agent "".", - "You are a teammate in the Team led by agent "".", + "Your name is "lead/implementer". You are a teammate in the Team led by "lead".", + "Your name is "lead/researcher". You are a teammate in the Team led by "lead".", ], "queuedMessages": 0, "sessions": 4, diff --git a/apps/cli/tests/profiles/headless/tests/subagent-diagnostic.expected.e2e.ts b/apps/cli/tests/profiles/headless/tests/subagent-diagnostic.expected.e2e.ts index 3aff474038..f4f70361cf 100644 --- a/apps/cli/tests/profiles/headless/tests/subagent-diagnostic.expected.e2e.ts +++ b/apps/cli/tests/profiles/headless/tests/subagent-diagnostic.expected.e2e.ts @@ -132,7 +132,7 @@ describe('parent-owned cold child catalog snapshot', () => { if (parent === undefined) throw new Error('missing persisted parent log') // Discovery reads child catalogs without requiring activation descriptors. - expect(parent).toContain(`${childId} [inactive] parent=${parentId} depth=1 — descriptorless child`) + expect(parent).toContain(`legacy:${childId} [inactive] parent=lead depth=1 — descriptorless child`) expect(parent).not.toContain('[diagnostic: corrupt]') const context: NormalizeContext = { sessionIds: [parentId, childId], cwd } diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts index f67ef6fa59..7d095ddbeb 100644 --- a/apps/cli/tests/web-agent-presets.e2e.ts +++ b/apps/cli/tests/web-agent-presets.e2e.ts @@ -723,7 +723,7 @@ describe.each(['web', 'desktop'] as const)('%s native subagent bundles', (surfac }) try { for (const product of installed) { - expect(toolParameterNames(productCtx, handle.agent, toolName(product))).toEqual(['cwd', 'description', 'prompt']) + expect(toolParameterNames(productCtx, handle.agent, toolName(product))).toEqual(['cwd', 'description', 'name', 'prompt']) } } finally { await handle.dispose() } expect(spawn).not.toHaveBeenCalled() diff --git a/apps/web/tests/official-subagent-bundle.e2e.ts b/apps/web/tests/official-subagent-bundle.e2e.ts index 8a21671b29..92a1858b6a 100644 --- a/apps/web/tests/official-subagent-bundle.e2e.ts +++ b/apps/web/tests/official-subagent-bundle.e2e.ts @@ -156,7 +156,7 @@ describe.skipIf(MODE === 'record')('web snapshot: Official native subagent bundl if (notice?.data.source.kind !== 'subagent-settled') throw new Error('Native completion was not delivered to the parent') expect(notice.data.source.senderSessionId).toBe(catalog[0]?.childId) expect(notice.data.content).toContainEqual({ type: 'text', text: RESPONSE }) - expect(dispatches[0]?.content).toEqual([{ type: 'text', text: `started subagent ${notice.data.source.senderSessionId}` }]) + expect(dispatches[0]?.content).toEqual([{ type: 'text', text: 'started subagent lead/native-worker' }]) expect(events.filter(event => event.type === 'turn/end')).toHaveLength(2) expect(server.requests).toHaveLength(1) expect(server.requests[0]?.body).toMatchObject({ model: 'fixture-model' }) diff --git a/apps/web/tests/shipped-composition.e2e.ts b/apps/web/tests/shipped-composition.e2e.ts index d906cb2e27..ff55a49625 100644 --- a/apps/web/tests/shipped-composition.e2e.ts +++ b/apps/web/tests/shipped-composition.e2e.ts @@ -279,6 +279,7 @@ class ShippedChildAutoAdapter extends LlmAdapter { this.parentPhase += 1 if (phase === 0 && prompt.includes(AUTO_PARENT_AUTHORIZED)) { return toolChunks('parent-authorized', 'subagent', { + name: 'auto-authorized', description: 'Auto authorized child', prompt: [ AUTO_CHILD_AUTHORIZED, @@ -291,6 +292,7 @@ class ShippedChildAutoAdapter extends LlmAdapter { if (phase === 1) return textChunks('PARENT_AUTHORIZED_SETTLED') if (phase === 2 && prompt.includes(AUTO_PARENT_CONTINUABLE)) { return toolChunks('parent-continuable', 'subagent', { + name: 'auto-continuable', description: 'Auto continuable child', prompt: [ AUTO_CHILD_CONTINUABLE, @@ -305,7 +307,7 @@ class ShippedChildAutoAdapter extends LlmAdapter { throw new Error('continuable child id was not observed before parent adjustment') } return toolChunks('parent-adjust', 'send_message', { - agent_id: this.continuableChildId, + name: 'lead/auto-continuable', message: [ AUTO_CHILD_ADJUSTED, `Replace the earlier no-deletion restriction: permanently delete exactly ${this.continuableDeletePath}.`, diff --git a/apps/web/tests/sidebar-subagent-activity.e2e.ts b/apps/web/tests/sidebar-subagent-activity.e2e.ts index f2c8eb3434..33290d6287 100644 --- a/apps/web/tests/sidebar-subagent-activity.e2e.ts +++ b/apps/web/tests/sidebar-subagent-activity.e2e.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto' import { mkdir } from 'node:fs/promises' import { join } from 'node:path' import { fileURLToPath } from 'node:url' @@ -96,7 +97,7 @@ describe('web e2e: sidebar subagent activity', () => { source: { kind: 'user' }, })) await parentHandle.agent.whenIdle() - const started = await scaffold.ctx.subagents.startActivation({ + const started = await scaffold.ctx.subagents.startActivation({ name: `child-${randomUUID()}`, delivery: 'parent', provider: 'spawn', label: 'sidebar activity child', diff --git a/apps/web/tests/subagent-conversation.e2e.ts b/apps/web/tests/subagent-conversation.e2e.ts index e903e3c21e..9d02e9e4b7 100644 --- a/apps/web/tests/subagent-conversation.e2e.ts +++ b/apps/web/tests/subagent-conversation.e2e.ts @@ -144,7 +144,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = await parentInput.press('Enter') expect(await parentSettled).toBe(parent.id) - const started = await scaffold.ctx.subagents.startActivation({ + const started = await scaffold.ctx.subagents.startActivation({ name: 'researcher', delivery: 'parent', provider: 'spawn', label: LABEL, @@ -431,7 +431,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = await page.getByRole('button', { name: '2 subagents' }).hover() await page.getByRole('treeitem', { name: new RegExp(LABEL) }).getByRole('button', { name: `Open ${LABEL} in sidebar`, exact: true }).click() const sidebarChat = page.locator('[data-sidebar-chat]') - await sidebarChat.getByText(/^Explain event sourcing in one sentence\.Your parent agent id is /).waitFor({ timeout: 15_000 }) + await sidebarChat.getByText(/^Explain event sourcing in one sentence\.Your name is /).waitFor({ timeout: 15_000 }) await compareOrRefreshGolden( SIDEBAR_CHAT_EXPECTED, await captureStableAria(page, '[data-sidebar-chat]', scaffold.workspaceCwd), @@ -454,7 +454,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = await page.getByRole('button', { name: '2 subagents' }).hover() await page.getByRole('treeitem', { name: new RegExp(LABEL) }).click() await expect.poll( - () => page.getByText(/^Explain event sourcing in one sentence\.Your parent agent id is /).count(), + () => page.getByText(/^Explain event sourcing in one sentence\.Your name is /).count(), { timeout: 15_000 }, ).toBe(1) if (scaffold.ctx.agents.get(childId) !== undefined) { @@ -490,7 +490,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = await expect.poll(() => requested, { timeout: 15_000 }).toBe(true) expect(await page.getByText('This subagent is read-only for now', { exact: true }).count()).toBe(0) expect(await page.getByRole('navigation', { name: 'Session hierarchy' }).count()).toBe(0) - expect(await page.getByText(/^Explain event sourcing in one sentence\.Your parent agent id is /).count()).toBe(0) + expect(await page.getByText(/^Explain event sourcing in one sentence\.Your name is /).count()).toBe(0) releaseCatalog() await page.getByRole('button', { name: `Switch subagent: ${LABEL}` }).waitFor({ timeout: 15_000 }) const input = page.getByRole('textbox', { name: 'Message or run a task, / commands, @ files or sessions' }) diff --git a/apps/web/tests/subagent-fork-schedule.snapshot.ts b/apps/web/tests/subagent-fork-schedule.snapshot.ts index 62f42cb69a..387963bd0d 100644 --- a/apps/web/tests/subagent-fork-schedule.snapshot.ts +++ b/apps/web/tests/subagent-fork-schedule.snapshot.ts @@ -45,7 +45,7 @@ it.skipIf(MODE === 'record')('replays inherited schedule tools and refusals in a const childPrompt = parseSessionLog(expected[1]!).findLast(event => event.type === 'user/message' && event.data.source.kind === 'user') if (childPrompt?.type !== 'user/message') throw new Error('child fixture has no user prompt') - const activation = await scaffold.ctx.subagents.startActivation({ + const activation = await scaffold.ctx.subagents.startActivation({ name: 'scheduled-child', provider: 'fork', label: 'Check child reminder refusal', delivery: 'caller', signal: new AbortController().signal, request: { parent, prompt: [...childPrompt.data.content] }, diff --git a/apps/web/tests/subagent-interrupt-ui.e2e.ts b/apps/web/tests/subagent-interrupt-ui.e2e.ts index 9a7a706314..1201f4dac5 100644 --- a/apps/web/tests/subagent-interrupt-ui.e2e.ts +++ b/apps/web/tests/subagent-interrupt-ui.e2e.ts @@ -148,7 +148,7 @@ describe.skipIf(MODE === 'record')('web e2e: composer interrupt for a running co if (root === undefined) throw new Error('fresh workspace did not publish its parent Agent') parent = root // The child's first model call claims the primary override and holds. - const started = await scaffold.ctx.subagents.startActivation({ + const started = await scaffold.ctx.subagents.startActivation({ name: 'interrupt-worker', delivery: 'parent', provider: 'spawn', label: LABEL, @@ -213,7 +213,7 @@ describe.skipIf(MODE === 'record')('web e2e: composer interrupt for a running co await page.getByRole('treeitem', { name: new RegExp(LABEL) }).click() const input = page.locator('[data-composer-input]').first() await input.waitFor({ timeout: 15_000 }) - await page.getByText(/^Explain event sourcing in one sentence\.Your parent agent id is /) + await page.getByText(/^Explain event sourcing in one sentence\.Your name is /) .waitFor({ timeout: 15_000 }) expect(await input.isDisabled()).toBe(false) const stop = page.getByRole('button', { name: 'Stop generating' }) @@ -368,7 +368,7 @@ describe.skipIf(MODE === 'record')('web e2e: composer interrupt for a running co ? event.data.content.flatMap(block => block.type === 'text' ? [block.text] : []) : []) expect(userTexts[0]).toBe(INITIAL) - expect(userTexts[1]).toMatch(/^Your parent agent id is .+send_message\(\{ agent_id: /) + expect(userTexts[1]).toMatch(/^Your name is .+Your parent agent name is .+send_message\(\{ name: /) expect(userTexts.slice(2)).toEqual([REARM, REARM_WAKE, EDITED_FOLLOWUP, WAKING]) const imageMessage = events.find(event => event.type === 'user/message' && event.data.content.some(block => block.type === 'image')) diff --git a/apps/web/tests/subagent-interrupt.e2e.ts b/apps/web/tests/subagent-interrupt.e2e.ts index a1b1284a19..27d8cdc44f 100644 --- a/apps/web/tests/subagent-interrupt.e2e.ts +++ b/apps/web/tests/subagent-interrupt.e2e.ts @@ -108,7 +108,7 @@ describe.skipIf(MODE === 'record')('web e2e: subagents/interruptByParent over th const parent = scaffold.ctx.agents.get(parentId) if (parent === undefined) throw new Error('created parent session did not publish a live Agent') - const started = await scaffold.ctx.subagents.startActivation({ + const started = await scaffold.ctx.subagents.startActivation({ name: `child-${randomUUID()}`, delivery: 'parent', provider: 'spawn', label: 'event-sourcing researcher', @@ -189,7 +189,7 @@ describe.skipIf(MODE === 'record')('web e2e: subagents/interruptByParent over th ? event.data.content.flatMap(block => block.type === 'text' ? [block.text] : []) : []) expect(userTexts[0]).toBe(INITIAL) - expect(userTexts[1]).toMatch(/^Your parent agent id is .+send_message\(\{ agent_id: /) + expect(userTexts[1]).toMatch(/^Your name is .+Your parent agent name is .+send_message\(\{ name: /) expect(userTexts.slice(2)).toEqual([FOLLOWUP, WAKING]) const turnEndKinds = events .filter(event => event.type === 'turn/end') diff --git a/docs/config-catalog.md b/docs/config-catalog.md index b987fab3c6..cf6f70048c 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -2098,7 +2098,7 @@ export type PiAiThinkingTokenBudgetField = NonNullable @@ -954,7 +954,7 @@ Source: [`packages/subagent/subagent/src/catalog.ts:44`](../packages/subagent/su 'subagent/descriptor': SubagentDescriptorData ``` -Source: [`packages/subagent/subagent/src/descriptor.ts:38`](../packages/subagent/subagent/src/descriptor.ts) +Source: [`packages/subagent/subagent/src/descriptor.ts:39`](../packages/subagent/subagent/src/descriptor.ts) @@ -3322,7 +3322,7 @@ SHA-256: `c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262` Array of [`ContextSnapshotSection`](#persistence-type-sha256-86e1e384495ee09ac2d60c9db3610f1f1fddac2d248f6f1d6000e78054680219). - + @@ -3330,9 +3330,9 @@ Array of [`ContextSnapshotSection`](#persistence-type-sha256-86e1e384495ee09ac2d ### `ContinuableSubagentDescriptorData` -SHA-256: `6487b10ba7542f3c447b1e41a1c6a590b7bfafe0b9b66b49f34028da85bb970f` +SHA-256: `f37e394b9557d1a228d6b2f29f9586c394d75fe9578ebb25aa143565aba50182` -Sources: [`packages/subagent/subagent/src/descriptor.ts:72`](../packages/subagent/subagent/src/descriptor.ts) +Sources: [`packages/subagent/subagent/src/descriptor.ts:78`](../packages/subagent/subagent/src/descriptor.ts) | Property | Presence | Type | |---|---|---| @@ -3341,6 +3341,7 @@ Sources: [`packages/subagent/subagent/src/descriptor.ts:72`](../packages/subagen | `agentReasoningEffort` | optional | `string` | | `label` | required | `string` | | `mode` | required | `"continuable"` | +| `name` | optional | `string` | | `persona` | optional | `string` | | `provider` | required | `string` | | `toolFilter` | optional | [`ToolRestriction`](#persistence-type-sha256-3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f) | @@ -4362,7 +4363,7 @@ Sources: [`packages/schedule/schedule/src/types.ts:237`](../packages/schedule/sc | `operation` | required | `"dispatch"` | | `version` | required | `1` | - + @@ -4370,14 +4371,15 @@ Sources: [`packages/schedule/schedule/src/types.ts:237`](../packages/schedule/sc ### `OneShotSubagentDescriptorData` -SHA-256: `5f758f3de5f00c18968a7c6d347083aac5c9ae13f6a79c78f6611b23c4973564` +SHA-256: `f73798a0676cd6a7d7890fcd09bf2a9cd8ad0967b6c7afd7d3edd3f5fbc1c124` -Sources: [`packages/subagent/subagent/src/descriptor.ts:61`](../packages/subagent/subagent/src/descriptor.ts) +Sources: [`packages/subagent/subagent/src/descriptor.ts:67`](../packages/subagent/subagent/src/descriptor.ts) | Property | Presence | Type | |---|---|---| | `label` | optional | `string` | | `mode` | required | `"one-shot"` | +| `name` | optional | `string` | | `provider` | required | `string` | | `version` | required | `number` | @@ -4817,7 +4819,7 @@ One of: - [`{ type: "finish" }`](#persistence-type-sha256-a2b49331716ba0a7078b026d145306a70eae64b7313c3e6204bd931e75ec3bb1) - [`{ type: "usage" }`](#persistence-type-sha256-f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515) - + @@ -4825,20 +4827,20 @@ One of: ### `SubagentCatalogEvent` -SHA-256: `f731ffe80810b0289b3e694d8f40af6f01865c02a622ee457be90ca808b66e87` +SHA-256: `d356f2b4478fe252cf18d8017a277417a9d1b1f88b7bbdfbf5600092a7b7a09d` -Sources: [`packages/subagent/subagent/src/catalog.ts:28`](../packages/subagent/subagent/src/catalog.ts) +Sources: [`packages/subagent/subagent/src/catalog.ts:29`](../packages/subagent/subagent/src/catalog.ts) One of: -- [`{ childCreatedAt, childId, label, mode, … }`](#persistence-type-sha256-e2ad8f5bcd813f268ffef1b10a12d7ddd2b8ced8b9b6d0275b3f6b7ed094a187) -- [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-cbbe8377ee744964537dbafd83607219e58ba79a13f6db0d0645c7ad3d5e8a09) -- [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-5d542ac7bdf13310c46e1a884dec9fb18ad8c6e153cff207e639cd7bf9d7e694) -- [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-b6896938660a1dfe4ddd100fb3164d9f8fa57feeabfe37441a80106c9942697a) -- [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-a0f25b5aeeebf2bf1542ce67726b095e3b139a39c71861d57e827f12dea1f499) -- [`{ childCreatedAt, childId, label, mode, … }`](#persistence-type-sha256-0e10d7c096b47e61defe372a5fc8bd37d835199410eac44367f95c45082498bd) +- [`{ childCreatedAt, childId, label, mode, … }`](#persistence-type-sha256-d64be7afff109c07a5496c2d7d0f7c84d145e9abddbe4f340c15d84d42ea4b05) +- [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-6b70feb8fff1ac400560495d7e0156daed0e4cf2f73f0439f2e3b5d7038b2512) +- [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-4237004e46a01bdef8c8923aa7d5c1d90cf1cd5467dca15dd0da5668b4d43b69) +- [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-d452d8c6f47c6fa32773048a0cb44b825ca2d8e7aa28abb5f6f1afb1b965ba3a) +- [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-3ec336899b3fcfeaeb163a3fc407e6ad3108993d9925ef439fe3fbb3cd6d3c25) +- [`{ childCreatedAt, childId, label, mode, … }`](#persistence-type-sha256-9f808a869b1ef076e20a960c51b0c1c919b03025fa2661516cc36b3f452f60ee) - + @@ -4846,14 +4848,14 @@ One of: ### `SubagentDescriptorData` -SHA-256: `86d1d0e9cfebb240421187e3dc37fb71cc38a1501885cf32f8f9adef7b0a2bde` +SHA-256: `e17928cecf8a76d8a2f4574d1bf86e5451d1af445032e5626e0711bfe0392509` -Sources: [`packages/subagent/subagent/src/descriptor.ts:89`](../packages/subagent/subagent/src/descriptor.ts) +Sources: [`packages/subagent/subagent/src/descriptor.ts:95`](../packages/subagent/subagent/src/descriptor.ts) One of: -- [`ContinuableSubagentDescriptorData`](#persistence-type-sha256-6487b10ba7542f3c447b1e41a1c6a590b7bfafe0b9b66b49f34028da85bb970f) -- [`OneShotSubagentDescriptorData`](#persistence-type-sha256-5f758f3de5f00c18968a7c6d347083aac5c9ae13f6a79c78f6611b23c4973564) +- [`ContinuableSubagentDescriptorData`](#persistence-type-sha256-f37e394b9557d1a228d6b2f29f9586c394d75fe9578ebb25aa143565aba50182) +- [`OneShotSubagentDescriptorData`](#persistence-type-sha256-f73798a0676cd6a7d7890fcd09bf2a9cd8ad0967b6c7afd7d3edd3f5fbc1c124) @@ -5888,11 +5890,11 @@ SHA-256: `df843efd87c78cf8de98f8e0d0216956cd568d0d0e56a22c853663e274aa1a18` Array of [`{ capturedFormatVersion?, capturedThroughSeq, compacted, inputIndex, … }`](#persistence-type-sha256-4f91f187baeabef5c7950c4780d31bbe39f3345dff591a6cf26ac7e135ac1cf4). - + ### `{ childCreatedAt, childId, label, mode, … }` -SHA-256: `0e10d7c096b47e61defe372a5fc8bd37d835199410eac44367f95c45082498bd` +SHA-256: `9f808a869b1ef076e20a960c51b0c1c919b03025fa2661516cc36b3f452f60ee` | Property | Presence | Type | |---|---|---| @@ -5900,13 +5902,14 @@ SHA-256: `0e10d7c096b47e61defe372a5fc8bd37d835199410eac44367f95c45082498bd` | `childId` | required | `string` | | `label` | required | `string` | | `mode` | required | `"continuable"` | +| `name` | optional | `string` | | `version` | required | `0` | - + ### `{ childCreatedAt, childId, label, mode, … }` -SHA-256: `e2ad8f5bcd813f268ffef1b10a12d7ddd2b8ced8b9b6d0275b3f6b7ed094a187` +SHA-256: `d64be7afff109c07a5496c2d7d0f7c84d145e9abddbe4f340c15d84d42ea4b05` | Property | Presence | Type | |---|---|---| @@ -5914,27 +5917,14 @@ SHA-256: `e2ad8f5bcd813f268ffef1b10a12d7ddd2b8ced8b9b6d0275b3f6b7ed094a187` | `childId` | required | `string` | | `label` | required | `string` | | `mode` | required | `"continuable"` | +| `name` | optional | `string` | | `version` | required | `1` | - + ### `{ childCreatedAt, childId, label?, mode, … }` -SHA-256: `5d542ac7bdf13310c46e1a884dec9fb18ad8c6e153cff207e639cd7bf9d7e694` - -| Property | Presence | Type | -|---|---|---| -| `childCreatedAt` | required | `number` | -| `childId` | required | `string` | -| `label` | optional | `string` | -| `mode` | required | `"one-shot"` | -| `version` | required | `0` | - - - -### `{ childCreatedAt, childId, label?, mode, … }` - -SHA-256: `a0f25b5aeeebf2bf1542ce67726b095e3b139a39c71861d57e827f12dea1f499` +SHA-256: `3ec336899b3fcfeaeb163a3fc407e6ad3108993d9925ef439fe3fbb3cd6d3c25` | Property | Presence | Type | |---|---|---| @@ -5942,13 +5932,14 @@ SHA-256: `a0f25b5aeeebf2bf1542ce67726b095e3b139a39c71861d57e827f12dea1f499` | `childId` | required | `string` | | `label` | optional | `string` | | `mode` | required | `"unknown"` | +| `name` | optional | `string` | | `version` | required | `1` | - + ### `{ childCreatedAt, childId, label?, mode, … }` -SHA-256: `b6896938660a1dfe4ddd100fb3164d9f8fa57feeabfe37441a80106c9942697a` +SHA-256: `4237004e46a01bdef8c8923aa7d5c1d90cf1cd5467dca15dd0da5668b4d43b69` | Property | Presence | Type | |---|---|---| @@ -5956,13 +5947,14 @@ SHA-256: `b6896938660a1dfe4ddd100fb3164d9f8fa57feeabfe37441a80106c9942697a` | `childId` | required | `string` | | `label` | optional | `string` | | `mode` | required | `"one-shot"` | -| `version` | required | `1` | +| `name` | optional | `string` | +| `version` | required | `0` | - + ### `{ childCreatedAt, childId, label?, mode, … }` -SHA-256: `cbbe8377ee744964537dbafd83607219e58ba79a13f6db0d0645c7ad3d5e8a09` +SHA-256: `6b70feb8fff1ac400560495d7e0156daed0e4cf2f73f0439f2e3b5d7038b2512` | Property | Presence | Type | |---|---|---| @@ -5970,8 +5962,24 @@ SHA-256: `cbbe8377ee744964537dbafd83607219e58ba79a13f6db0d0645c7ad3d5e8a09` | `childId` | required | `string` | | `label` | optional | `string` | | `mode` | required | `"external"` | +| `name` | optional | `string` | | `version` | required | `2` | + + +### `{ childCreatedAt, childId, label?, mode, … }` + +SHA-256: `d452d8c6f47c6fa32773048a0cb44b825ca2d8e7aa28abb5f6f1afb1b965ba3a` + +| Property | Presence | Type | +|---|---|---| +| `childCreatedAt` | required | `number` | +| `childId` | required | `string` | +| `label` | optional | `string` | +| `mode` | required | `"one-shot"` | +| `name` | optional | `string` | +| `version` | required | `1` | + ### `{ code, name, reason? }` @@ -8651,33 +8659,33 @@ SHA-256: `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | `time` | required | `number` | | `type` | required | `"step/start"` | - + ### `{ type: "subagent/catalog" }` -SHA-256: `53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac` +SHA-256: `8521c4e8b3ddb8a7835fe8e707061bf4a9a52b9e4c2e98e5fc5fef2ac4998ff3` | Property | Presence | Type | |---|---|---| -| `data` | required | [`SubagentCatalogEvent`](#persistence-type-sha256-f731ffe80810b0289b3e694d8f40af6f01865c02a622ee457be90ca808b66e87) | +| `data` | required | [`SubagentCatalogEvent`](#persistence-type-sha256-d356f2b4478fe252cf18d8017a277417a9d1b1f88b7bbdfbf5600092a7b7a09d) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `time` | required | `number` | | `type` | required | `"subagent/catalog"` | - + ### `{ type: "subagent/descriptor" }` -SHA-256: `b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5` +SHA-256: `194530445ad15cc68b8a708321a4d8d118dfe54600864fcb965ffca9b1afb8cc` | Property | Presence | Type | |---|---|---| -| `data` | required | [`SubagentDescriptorData`](#persistence-type-sha256-86d1d0e9cfebb240421187e3dc37fb71cc38a1501885cf32f8f9adef7b0a2bde) | +| `data` | required | [`SubagentDescriptorData`](#persistence-type-sha256-e17928cecf8a76d8a2f4574d1bf86e5451d1af445032e5626e0711bfe0392509) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `time` | required | `number` | diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 1306974dc3..1f48105f8a 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -63,8 +63,8 @@ | `event:session/title-llm-request` | event | `79a4c87f8bfb4d6d05cd9952b99210d6a7db3b478c4a01d94b4c50602308c056` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-79a4c87f8bfb4d6d05cd9952b99210d6a7db3b478c4a01d94b4c50602308c056) | | `event:step/end` | event | `e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05` | [`{ type: "step/end" }`](#persistence-type-sha256-e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05) | | `event:step/start` | event | `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | [`{ type: "step/start" }`](#persistence-type-sha256-4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986) | -| `event:subagent/catalog` | event | `53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac) | -| `event:subagent/descriptor` | event | `b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5` | [`{ type: "subagent/descriptor" }`](#persistence-type-sha256-b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5) | +| `event:subagent/catalog` | event | `8521c4e8b3ddb8a7835fe8e707061bf4a9a52b9e4c2e98e5fc5fef2ac4998ff3` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-8521c4e8b3ddb8a7835fe8e707061bf4a9a52b9e4c2e98e5fc5fef2ac4998ff3) | +| `event:subagent/descriptor` | event | `194530445ad15cc68b8a708321a4d8d118dfe54600864fcb965ffca9b1afb8cc` | [`{ type: "subagent/descriptor" }`](#persistence-type-sha256-194530445ad15cc68b8a708321a4d8d118dfe54600864fcb965ffca9b1afb8cc) | | `event:subagent/model-selection-policy` | event | `a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc` | [`{ type: "subagent/model-selection-policy" }`](#persistence-type-sha256-a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc) | | `event:system/message` | event | `69081694be231d56fd9580ba14645fd5e35373202605d5c5c841a9435b5fa3b1` | [`{ type: "system/message" }`](#persistence-type-sha256-69081694be231d56fd9580ba14645fd5e35373202605d5c5c841a9435b5fa3b1) | | `event:team/member` | event | `4fb59762612c3e3ac3a3bd4f84c9c148d3c3893bd422ba2b201cc039fabd49bc` | [`{ type: "team/member" }`](#persistence-type-sha256-4fb59762612c3e3ac3a3bd4f84c9c148d3c3893bd422ba2b201cc039fabd49bc) | @@ -939,7 +939,7 @@ export type SessionEvent = { 'subagent/catalog': SubagentCatalogEvent ``` -来源:[`packages/subagent/subagent/src/catalog.ts:44`](../packages/subagent/subagent/src/catalog.ts) +来源:[`packages/subagent/subagent/src/catalog.ts:47`](../packages/subagent/subagent/src/catalog.ts) @@ -956,7 +956,7 @@ export type SessionEvent = { 'subagent/descriptor': SubagentDescriptorData ``` -来源:[`packages/subagent/subagent/src/descriptor.ts:38`](../packages/subagent/subagent/src/descriptor.ts) +来源:[`packages/subagent/subagent/src/descriptor.ts:39`](../packages/subagent/subagent/src/descriptor.ts) @@ -3324,7 +3324,7 @@ SHA-256: `c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262` [`ContextSnapshotSection`](#persistence-type-sha256-86e1e384495ee09ac2d60c9db3610f1f1fddac2d248f6f1d6000e78054680219) 的数组。 - + @@ -3332,9 +3332,9 @@ SHA-256: `c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262` ### `ContinuableSubagentDescriptorData` -SHA-256: `6487b10ba7542f3c447b1e41a1c6a590b7bfafe0b9b66b49f34028da85bb970f` +SHA-256: `f37e394b9557d1a228d6b2f29f9586c394d75fe9578ebb25aa143565aba50182` -来源:[`packages/subagent/subagent/src/descriptor.ts:72`](../packages/subagent/subagent/src/descriptor.ts) +来源:[`packages/subagent/subagent/src/descriptor.ts:78`](../packages/subagent/subagent/src/descriptor.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -3343,6 +3343,7 @@ SHA-256: `6487b10ba7542f3c447b1e41a1c6a590b7bfafe0b9b66b49f34028da85bb970f` | `agentReasoningEffort` | 可选 | `string` | | `label` | 必需 | `string` | | `mode` | 必需 | `"continuable"` | +| `name` | 可选 | `string` | | `persona` | 可选 | `string` | | `provider` | 必需 | `string` | | `toolFilter` | 可选 | [`ToolRestriction`](#persistence-type-sha256-3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f) | @@ -4364,7 +4365,7 @@ SHA-256: `8e8e91646a5cae3fc78f7532a0013ff2088ae47077642c2edd507a508209b8af` | `operation` | 必需 | `"dispatch"` | | `version` | 必需 | `1` | - + @@ -4372,14 +4373,15 @@ SHA-256: `8e8e91646a5cae3fc78f7532a0013ff2088ae47077642c2edd507a508209b8af` ### `OneShotSubagentDescriptorData` -SHA-256: `5f758f3de5f00c18968a7c6d347083aac5c9ae13f6a79c78f6611b23c4973564` +SHA-256: `f73798a0676cd6a7d7890fcd09bf2a9cd8ad0967b6c7afd7d3edd3f5fbc1c124` -来源:[`packages/subagent/subagent/src/descriptor.ts:61`](../packages/subagent/subagent/src/descriptor.ts) +来源:[`packages/subagent/subagent/src/descriptor.ts:67`](../packages/subagent/subagent/src/descriptor.ts) | 属性 | 存在性 | 类型 | |---|---|---| | `label` | 可选 | `string` | | `mode` | 必需 | `"one-shot"` | +| `name` | 可选 | `string` | | `provider` | 必需 | `string` | | `version` | 必需 | `number` | @@ -4819,7 +4821,7 @@ SHA-256: `47e9095591ae36e7c39ee62553097772f2bfa16304536a72dbbc3f691f605180` - [`{ type: "finish" }`](#persistence-type-sha256-a2b49331716ba0a7078b026d145306a70eae64b7313c3e6204bd931e75ec3bb1) - [`{ type: "usage" }`](#persistence-type-sha256-f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515) - + @@ -4827,20 +4829,20 @@ SHA-256: `47e9095591ae36e7c39ee62553097772f2bfa16304536a72dbbc3f691f605180` ### `SubagentCatalogEvent` -SHA-256: `f731ffe80810b0289b3e694d8f40af6f01865c02a622ee457be90ca808b66e87` +SHA-256: `d356f2b4478fe252cf18d8017a277417a9d1b1f88b7bbdfbf5600092a7b7a09d` -来源:[`packages/subagent/subagent/src/catalog.ts:28`](../packages/subagent/subagent/src/catalog.ts) +来源:[`packages/subagent/subagent/src/catalog.ts:29`](../packages/subagent/subagent/src/catalog.ts) 以下类型之一: -- [`{ childCreatedAt, childId, label, mode, … }`](#persistence-type-sha256-e2ad8f5bcd813f268ffef1b10a12d7ddd2b8ced8b9b6d0275b3f6b7ed094a187) -- [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-cbbe8377ee744964537dbafd83607219e58ba79a13f6db0d0645c7ad3d5e8a09) -- [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-5d542ac7bdf13310c46e1a884dec9fb18ad8c6e153cff207e639cd7bf9d7e694) -- [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-b6896938660a1dfe4ddd100fb3164d9f8fa57feeabfe37441a80106c9942697a) -- [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-a0f25b5aeeebf2bf1542ce67726b095e3b139a39c71861d57e827f12dea1f499) -- [`{ childCreatedAt, childId, label, mode, … }`](#persistence-type-sha256-0e10d7c096b47e61defe372a5fc8bd37d835199410eac44367f95c45082498bd) +- [`{ childCreatedAt, childId, label, mode, … }`](#persistence-type-sha256-d64be7afff109c07a5496c2d7d0f7c84d145e9abddbe4f340c15d84d42ea4b05) +- [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-6b70feb8fff1ac400560495d7e0156daed0e4cf2f73f0439f2e3b5d7038b2512) +- [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-4237004e46a01bdef8c8923aa7d5c1d90cf1cd5467dca15dd0da5668b4d43b69) +- [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-d452d8c6f47c6fa32773048a0cb44b825ca2d8e7aa28abb5f6f1afb1b965ba3a) +- [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-3ec336899b3fcfeaeb163a3fc407e6ad3108993d9925ef439fe3fbb3cd6d3c25) +- [`{ childCreatedAt, childId, label, mode, … }`](#persistence-type-sha256-9f808a869b1ef076e20a960c51b0c1c919b03025fa2661516cc36b3f452f60ee) - + @@ -4848,14 +4850,14 @@ SHA-256: `f731ffe80810b0289b3e694d8f40af6f01865c02a622ee457be90ca808b66e87` ### `SubagentDescriptorData` -SHA-256: `86d1d0e9cfebb240421187e3dc37fb71cc38a1501885cf32f8f9adef7b0a2bde` +SHA-256: `e17928cecf8a76d8a2f4574d1bf86e5451d1af445032e5626e0711bfe0392509` -来源:[`packages/subagent/subagent/src/descriptor.ts:89`](../packages/subagent/subagent/src/descriptor.ts) +来源:[`packages/subagent/subagent/src/descriptor.ts:95`](../packages/subagent/subagent/src/descriptor.ts) 以下类型之一: -- [`ContinuableSubagentDescriptorData`](#persistence-type-sha256-6487b10ba7542f3c447b1e41a1c6a590b7bfafe0b9b66b49f34028da85bb970f) -- [`OneShotSubagentDescriptorData`](#persistence-type-sha256-5f758f3de5f00c18968a7c6d347083aac5c9ae13f6a79c78f6611b23c4973564) +- [`ContinuableSubagentDescriptorData`](#persistence-type-sha256-f37e394b9557d1a228d6b2f29f9586c394d75fe9578ebb25aa143565aba50182) +- [`OneShotSubagentDescriptorData`](#persistence-type-sha256-f73798a0676cd6a7d7890fcd09bf2a9cd8ad0967b6c7afd7d3edd3f5fbc1c124) @@ -5890,11 +5892,11 @@ SHA-256: `df843efd87c78cf8de98f8e0d0216956cd568d0d0e56a22c853663e274aa1a18` [`{ capturedFormatVersion?, capturedThroughSeq, compacted, inputIndex, … }`](#persistence-type-sha256-4f91f187baeabef5c7950c4780d31bbe39f3345dff591a6cf26ac7e135ac1cf4) 的数组。 - + ### `{ childCreatedAt, childId, label, mode, … }` -SHA-256: `0e10d7c096b47e61defe372a5fc8bd37d835199410eac44367f95c45082498bd` +SHA-256: `9f808a869b1ef076e20a960c51b0c1c919b03025fa2661516cc36b3f452f60ee` | 属性 | 存在性 | 类型 | |---|---|---| @@ -5902,13 +5904,14 @@ SHA-256: `0e10d7c096b47e61defe372a5fc8bd37d835199410eac44367f95c45082498bd` | `childId` | 必需 | `string` | | `label` | 必需 | `string` | | `mode` | 必需 | `"continuable"` | +| `name` | 可选 | `string` | | `version` | 必需 | `0` | - + ### `{ childCreatedAt, childId, label, mode, … }` -SHA-256: `e2ad8f5bcd813f268ffef1b10a12d7ddd2b8ced8b9b6d0275b3f6b7ed094a187` +SHA-256: `d64be7afff109c07a5496c2d7d0f7c84d145e9abddbe4f340c15d84d42ea4b05` | 属性 | 存在性 | 类型 | |---|---|---| @@ -5916,27 +5919,14 @@ SHA-256: `e2ad8f5bcd813f268ffef1b10a12d7ddd2b8ced8b9b6d0275b3f6b7ed094a187` | `childId` | 必需 | `string` | | `label` | 必需 | `string` | | `mode` | 必需 | `"continuable"` | +| `name` | 可选 | `string` | | `version` | 必需 | `1` | - + ### `{ childCreatedAt, childId, label?, mode, … }` -SHA-256: `5d542ac7bdf13310c46e1a884dec9fb18ad8c6e153cff207e639cd7bf9d7e694` - -| 属性 | 存在性 | 类型 | -|---|---|---| -| `childCreatedAt` | 必需 | `number` | -| `childId` | 必需 | `string` | -| `label` | 可选 | `string` | -| `mode` | 必需 | `"one-shot"` | -| `version` | 必需 | `0` | - - - -### `{ childCreatedAt, childId, label?, mode, … }` - -SHA-256: `a0f25b5aeeebf2bf1542ce67726b095e3b139a39c71861d57e827f12dea1f499` +SHA-256: `3ec336899b3fcfeaeb163a3fc407e6ad3108993d9925ef439fe3fbb3cd6d3c25` | 属性 | 存在性 | 类型 | |---|---|---| @@ -5944,13 +5934,14 @@ SHA-256: `a0f25b5aeeebf2bf1542ce67726b095e3b139a39c71861d57e827f12dea1f499` | `childId` | 必需 | `string` | | `label` | 可选 | `string` | | `mode` | 必需 | `"unknown"` | +| `name` | 可选 | `string` | | `version` | 必需 | `1` | - + ### `{ childCreatedAt, childId, label?, mode, … }` -SHA-256: `b6896938660a1dfe4ddd100fb3164d9f8fa57feeabfe37441a80106c9942697a` +SHA-256: `4237004e46a01bdef8c8923aa7d5c1d90cf1cd5467dca15dd0da5668b4d43b69` | 属性 | 存在性 | 类型 | |---|---|---| @@ -5958,13 +5949,14 @@ SHA-256: `b6896938660a1dfe4ddd100fb3164d9f8fa57feeabfe37441a80106c9942697a` | `childId` | 必需 | `string` | | `label` | 可选 | `string` | | `mode` | 必需 | `"one-shot"` | -| `version` | 必需 | `1` | +| `name` | 可选 | `string` | +| `version` | 必需 | `0` | - + ### `{ childCreatedAt, childId, label?, mode, … }` -SHA-256: `cbbe8377ee744964537dbafd83607219e58ba79a13f6db0d0645c7ad3d5e8a09` +SHA-256: `6b70feb8fff1ac400560495d7e0156daed0e4cf2f73f0439f2e3b5d7038b2512` | 属性 | 存在性 | 类型 | |---|---|---| @@ -5972,8 +5964,24 @@ SHA-256: `cbbe8377ee744964537dbafd83607219e58ba79a13f6db0d0645c7ad3d5e8a09` | `childId` | 必需 | `string` | | `label` | 可选 | `string` | | `mode` | 必需 | `"external"` | +| `name` | 可选 | `string` | | `version` | 必需 | `2` | + + +### `{ childCreatedAt, childId, label?, mode, … }` + +SHA-256: `d452d8c6f47c6fa32773048a0cb44b825ca2d8e7aa28abb5f6f1afb1b965ba3a` + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `childCreatedAt` | 必需 | `number` | +| `childId` | 必需 | `string` | +| `label` | 可选 | `string` | +| `mode` | 必需 | `"one-shot"` | +| `name` | 可选 | `string` | +| `version` | 必需 | `1` | + ### `{ code, name, reason? }` @@ -8653,33 +8661,33 @@ SHA-256: `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | `time` | 必需 | `number` | | `type` | 必需 | `"step/start"` | - + ### `{ type: "subagent/catalog" }` -SHA-256: `53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac` +SHA-256: `8521c4e8b3ddb8a7835fe8e707061bf4a9a52b9e4c2e98e5fc5fef2ac4998ff3` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`SubagentCatalogEvent`](#persistence-type-sha256-f731ffe80810b0289b3e694d8f40af6f01865c02a622ee457be90ca808b66e87) | +| `data` | 必需 | [`SubagentCatalogEvent`](#persistence-type-sha256-d356f2b4478fe252cf18d8017a277417a9d1b1f88b7bbdfbf5600092a7b7a09d) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `time` | 必需 | `number` | | `type` | 必需 | `"subagent/catalog"` | - + ### `{ type: "subagent/descriptor" }` -SHA-256: `b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5` +SHA-256: `194530445ad15cc68b8a708321a4d8d118dfe54600864fcb965ffca9b1afb8cc` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`SubagentDescriptorData`](#persistence-type-sha256-86d1d0e9cfebb240421187e3dc37fb71cc38a1501885cf32f8f9adef7b0a2bde) | +| `data` | 必需 | [`SubagentDescriptorData`](#persistence-type-sha256-e17928cecf8a76d8a2f4574d1bf86e5451d1af445032e5626e0711bfe0392509) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `time` | 必需 | `number` | diff --git a/docs/persistence-changes/2026-10-16-tree-wide-agent-names.i18n.yaml b/docs/persistence-changes/2026-10-16-tree-wide-agent-names.i18n.yaml new file mode 100644 index 0000000000..333e5d5a13 --- /dev/null +++ b/docs/persistence-changes/2026-10-16-tree-wide-agent-names.i18n.yaml @@ -0,0 +1,28 @@ +# Bilingual-pair consistency record for 2026-10-16-tree-wide-agent-names.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/persistence-changes/2026-10-16-tree-wide-agent-names.md +/: + en: 3d5a424ae7d7e46b + zh: c22a8e4a62d8cc1a +/2026-10-16-tree-wide-agent-names: + en: a5bae46eec7e5c5b + zh: 190923c7b9b397d9 +/2026-10-16-tree-wide-agent-names/summary: + en: 463dba8c02a47e8a + zh: cb324e84f53ed9b1 +/2026-10-16-tree-wide-agent-names/table-of-contents: + en: 6cf314a069ceb451 + zh: 9adff678e9ecab3b +/2026-10-16-tree-wide-agent-names/declaration: + en: 6907136f28e48c90 + zh: 02f8298485f3d8e7 +/2026-10-16-tree-wide-agent-names/compatibility: + en: e1e34d058c4935ae + zh: 1c5f3e0c60575f24 +/2026-10-16-tree-wide-agent-names/verification: + en: c65708eceaa5547b + zh: 60eb3e524a0d8b30 +/2026-10-16-tree-wide-agent-names/dev-note: + en: b75c8458928b72b9 + zh: 003ba4e17b4b5269 diff --git a/docs/persistence-changes/2026-10-16-tree-wide-agent-names.md b/docs/persistence-changes/2026-10-16-tree-wide-agent-names.md new file mode 100644 index 0000000000..8f590bea62 --- /dev/null +++ b/docs/persistence-changes/2026-10-16-tree-wide-agent-names.md @@ -0,0 +1,52 @@ +--- +description: "Records a persistence type transition and its compatibility acknowledgement." +kind: persistence-change +--- + +# 2026-10-16-tree-wide-agent-names + +English | [中文](2026-10-16-tree-wide-agent-names.zh.md) + +## Summary + +Stores immutable sibling name segments in existing subagent catalog and descriptor records. Full agent paths are derived from parent catalogs and live ancestry. + +## Table of Contents + +- [Declaration](#declaration) +- [Compatibility](#compatibility) +- [Verification](#verification) +- [Dev Note](#dev-note) + + +## Declaration + +```yaml persistence-change +schemaVersion: 1 +id: 2026-10-16-tree-wide-agent-names +baseline: false +changes: + - root: "event:subagent/catalog" + previous: "2026-09-19-external-subagent-catalog" + after: "8521c4e8b3ddb8a7835fe8e707061bf4a9a52b9e4c2e98e5fc5fef2ac4998ff3" + decision: same-version + - root: "event:subagent/descriptor" + previous: "2026-09-11-initial" + after: "194530445ad15cc68b8a708321a4d8d118dfe54600864fcb965ffca9b1afb8cc" + decision: same-version +``` + + +## Compatibility + +New readers accept earlier unnamed catalog and descriptor records, using legacy Session-id segments for those children. Earlier strict catalog and descriptor readers reject added name fields instead of interpreting names incorrectly. The unmerged root-only catalog v3 registration variant is removed; published catalog v0–v2 variants and Session ids remain unchanged. The Session writer version stays the same. Catalog projection checkpoints are invalidated by a state-version increment. Only the direct parent records a child; a named child cannot resume without its matching parent catalog entry. + + +## Verification + +Focused subagent, control-tool, Team, and V3-to-V4 reader suites pass 1,109 tests. The 10 selected changed implementation files have 100% statement, branch, function, and line coverage. Cases cover sibling-local reservations, repeated segments in separate branches, historical path resolution, parent catalog failure and cold-resume refusal, fork isolation, and rejection of unsupported name-only catalog rows. + + +## Dev Note + +None. diff --git a/docs/persistence-changes/2026-10-16-tree-wide-agent-names.schema.json b/docs/persistence-changes/2026-10-16-tree-wide-agent-names.schema.json new file mode 100644 index 0000000000..162d8b0c37 --- /dev/null +++ b/docs/persistence-changes/2026-10-16-tree-wide-agent-names.schema.json @@ -0,0 +1,501 @@ +{ + "formatVersion": 2, + "roots": [ + { + "key": "event:subagent/catalog", + "kind": "event", + "event": "subagent/catalog", + "surface": false, + "digest": "8521c4e8b3ddb8a7835fe8e707061bf4a9a52b9e4c2e98e5fc5fef2ac4998ff3", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 17, + "optional": true + }, + { + "name": "seq", + "type": 3, + "optional": false + }, + { + "name": "time", + "type": 3, + "optional": false + }, + { + "name": "type", + "type": 18, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 2, + 7, + 9, + 12, + 14, + 15 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "childCreatedAt", + "type": 3, + "optional": false + }, + { + "name": "childId", + "type": 4, + "optional": false + }, + { + "name": "label", + "type": 4, + "optional": false + }, + { + "name": "mode", + "type": 5, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": true + }, + { + "name": "version", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "continuable" + }, + { + "kind": "literal", + "value": 0 + }, + { + "kind": "object", + "properties": [ + { + "name": "childCreatedAt", + "type": 3, + "optional": false + }, + { + "name": "childId", + "type": 4, + "optional": false + }, + { + "name": "label", + "type": 4, + "optional": false + }, + { + "name": "mode", + "type": 5, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": true + }, + { + "name": "version", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "childCreatedAt", + "type": 3, + "optional": false + }, + { + "name": "childId", + "type": 4, + "optional": false + }, + { + "name": "label", + "type": 4, + "optional": true + }, + { + "name": "mode", + "type": 10, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": true + }, + { + "name": "version", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "external" + }, + { + "kind": "literal", + "value": 2 + }, + { + "kind": "object", + "properties": [ + { + "name": "childCreatedAt", + "type": 3, + "optional": false + }, + { + "name": "childId", + "type": 4, + "optional": false + }, + { + "name": "label", + "type": 4, + "optional": true + }, + { + "name": "mode", + "type": 13, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": true + }, + { + "name": "version", + "type": 6, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "one-shot" + }, + { + "kind": "object", + "properties": [ + { + "name": "childCreatedAt", + "type": 3, + "optional": false + }, + { + "name": "childId", + "type": 4, + "optional": false + }, + { + "name": "label", + "type": 4, + "optional": true + }, + { + "name": "mode", + "type": 13, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": true + }, + { + "name": "version", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "childCreatedAt", + "type": 3, + "optional": false + }, + { + "name": "childId", + "type": 4, + "optional": false + }, + { + "name": "label", + "type": 4, + "optional": true + }, + { + "name": "mode", + "type": 16, + "optional": false + }, + { + "name": "name", + "type": 4, + "optional": true + }, + { + "name": "version", + "type": 8, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "unknown" + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "subagent/catalog" + } + ] + } + }, + { + "key": "event:subagent/descriptor", + "kind": "event", + "event": "subagent/descriptor", + "surface": false, + "digest": "194530445ad15cc68b8a708321a4d8d118dfe54600864fcb965ffca9b1afb8cc", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 10, + "optional": true + }, + { + "name": "seq", + "type": 7, + "optional": false + }, + { + "name": "time", + "type": 7, + "optional": false + }, + { + "name": "type", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 2, + 8 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "agentModel", + "type": 3, + "optional": true + }, + { + "name": "agentProvider", + "type": 3, + "optional": true + }, + { + "name": "agentReasoningEffort", + "type": 3, + "optional": true + }, + { + "name": "label", + "type": 3, + "optional": false + }, + { + "name": "mode", + "type": 4, + "optional": false + }, + { + "name": "name", + "type": 3, + "optional": true + }, + { + "name": "persona", + "type": 3, + "optional": true + }, + { + "name": "provider", + "type": 3, + "optional": false + }, + { + "name": "toolFilter", + "type": 5, + "optional": true + }, + { + "name": "version", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "continuable" + }, + { + "kind": "object", + "properties": [ + { + "name": "allow", + "type": 6, + "optional": true + }, + { + "name": "deny", + "type": 6, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 3 + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "object", + "properties": [ + { + "name": "label", + "type": 3, + "optional": true + }, + { + "name": "mode", + "type": 9, + "optional": false + }, + { + "name": "name", + "type": 3, + "optional": true + }, + { + "name": "provider", + "type": 3, + "optional": false + }, + { + "name": "version", + "type": 7, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "one-shot" + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "subagent/descriptor" + } + ] + } + } + ], + "types": [] +} diff --git a/docs/persistence-changes/2026-10-16-tree-wide-agent-names.zh.md b/docs/persistence-changes/2026-10-16-tree-wide-agent-names.zh.md new file mode 100644 index 0000000000..5aeae14001 --- /dev/null +++ b/docs/persistence-changes/2026-10-16-tree-wide-agent-names.zh.md @@ -0,0 +1,52 @@ +--- +description: "记录持久化类型更改及其兼容性确认。" +kind: persistence-change +--- + +# 2026-10-16-tree-wide-agent-names + +[English](2026-10-16-tree-wide-agent-names.md) | 中文 + +## 概述 + +在现有 subagent catalog 与 descriptor 记录中保存不可变的同级名字路径段。完整 agent 路径由父级 catalog 与在线谱系派生。 + +## 目录 + +- [声明](#declaration) +- [兼容性](#compatibility) +- [验证](#verification) +- [开发备注](#dev-note) + + +## 声明 + +```yaml persistence-change +schemaVersion: 1 +id: 2026-10-16-tree-wide-agent-names +baseline: false +changes: + - root: "event:subagent/catalog" + previous: "2026-09-19-external-subagent-catalog" + after: "8521c4e8b3ddb8a7835fe8e707061bf4a9a52b9e4c2e98e5fc5fef2ac4998ff3" + decision: same-version + - root: "event:subagent/descriptor" + previous: "2026-09-11-initial" + after: "194530445ad15cc68b8a708321a4d8d118dfe54600864fcb965ffca9b1afb8cc" + decision: same-version +``` + + +## 兼容性 + +新读取器接受此前无名的 catalog 与 descriptor 记录,为这些子级使用带 Session id 的 legacy 路径段。此前严格的 catalog 与 descriptor 读取器会拒绝新增的 name 字段,避免错误解释名字。移除尚未合并的仅用于根登记的 catalog v3 变体;已发布的 catalog v0–v2 变体与 Session id 保持不变。Session 写入版本不变。通过递增状态版本使 catalog 投影检查点失效。只有直接父级记录子级;命名子级缺少匹配的父级 catalog 条目时无法恢复。 + + +## 验证 + +定向 subagent、控制工具、Team 与 V3-to-V4 读取器测试套件共 1,109 个测试通过。选定的 10 个变更实现文件语句、分支、函数和行覆盖率均为 100%。用例覆盖同级局部预留、不同分支重复路径段、历史路径解析、父级 catalog 失败与冷恢复拒绝、fork 隔离,以及拒绝不支持的仅含名字的 catalog 行。 + + +## 开发备注 + +无。 diff --git a/docs/persistence-schema.json b/docs/persistence-schema.json index 433e2613a3..0df1a7419f 100644 --- a/docs/persistence-schema.json +++ b/docs/persistence-schema.json @@ -15367,7 +15367,7 @@ "kind": "event", "event": "subagent/catalog", "surface": false, - "digest": "53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac", + "digest": "8521c4e8b3ddb8a7835fe8e707061bf4a9a52b9e4c2e98e5fc5fef2ac4998ff3", "schema": { "root": 0, "nodes": [ @@ -15436,6 +15436,11 @@ "type": 5, "optional": false }, + { + "name": "name", + "type": 4, + "optional": true + }, { "name": "version", "type": 6, @@ -15483,6 +15488,11 @@ "type": 5, "optional": false }, + { + "name": "name", + "type": 4, + "optional": true + }, { "name": "version", "type": 8, @@ -15518,6 +15528,11 @@ "type": 10, "optional": false }, + { + "name": "name", + "type": 4, + "optional": true + }, { "name": "version", "type": 11, @@ -15557,6 +15572,11 @@ "type": 13, "optional": false }, + { + "name": "name", + "type": 4, + "optional": true + }, { "name": "version", "type": 6, @@ -15592,6 +15612,11 @@ "type": 13, "optional": false }, + { + "name": "name", + "type": 4, + "optional": true + }, { "name": "version", "type": 8, @@ -15623,6 +15648,11 @@ "type": 16, "optional": false }, + { + "name": "name", + "type": 4, + "optional": true + }, { "name": "version", "type": 8, @@ -15651,7 +15681,7 @@ "kind": "event", "event": "subagent/descriptor", "surface": false, - "digest": "b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5", + "digest": "194530445ad15cc68b8a708321a4d8d118dfe54600864fcb965ffca9b1afb8cc", "schema": { "root": 0, "nodes": [ @@ -15721,6 +15751,11 @@ "type": 4, "optional": false }, + { + "name": "name", + "type": 3, + "optional": true + }, { "name": "persona", "type": 3, @@ -15789,6 +15824,11 @@ "type": 9, "optional": false }, + { + "name": "name", + "type": 3, + "optional": true + }, { "name": "provider", "type": 3, @@ -22596,11 +22636,6 @@ "packages/subagent/tool-subagent/src/model-selection-state.ts:17" ] }, - { - "digest": "0e10d7c096b47e61defe372a5fc8bd37d835199410eac44367f95c45082498bd", - "names": [], - "sources": [] - }, { "digest": "0e7b2a97f54f1afdb8cdfad1635abe9145327db539bfb69b4045c19d8c17755d", "names": [], @@ -22792,6 +22827,11 @@ "names": [], "sources": [] }, + { + "digest": "194530445ad15cc68b8a708321a4d8d118dfe54600864fcb965ffca9b1afb8cc", + "names": [], + "sources": [] + }, { "digest": "1986c2664009c99155e44795676c887fa67f7c55cd2d5e6b648ff763e0ab033a", "names": [], @@ -23373,6 +23413,11 @@ "packages/compaction/compaction/src/types.ts:24" ] }, + { + "digest": "3ec336899b3fcfeaeb163a3fc407e6ad3108993d9925ef439fe3fbb3cd6d3c25", + "names": [], + "sources": [] + }, { "digest": "3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796", "names": [], @@ -23402,6 +23447,11 @@ "names": [], "sources": [] }, + { + "digest": "4237004e46a01bdef8c8923aa7d5c1d90cf1cd5467dca15dd0da5668b4d43b69", + "names": [], + "sources": [] + }, { "digest": "42e0916e0dda5f6d1e7bb05d8514717147c036a79c9f36085683469516c1fd3f", "names": [], @@ -23708,11 +23758,6 @@ "packages/core/session/src/types.ts:274" ] }, - { - "digest": "53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac", - "names": [], - "sources": [] - }, { "digest": "53cf1704ae0709c7678bcf070c83c619fc48ca1b5c173b74cfa8702f22a8e492", "names": [], @@ -23857,11 +23902,6 @@ "names": [], "sources": [] }, - { - "digest": "5d542ac7bdf13310c46e1a884dec9fb18ad8c6e153cff207e639cd7bf9d7e694", - "names": [], - "sources": [] - }, { "digest": "5d7a64ed6aac5551ebf998dc25e1e7b48a144769913f89702840180e61dc3995", "names": [], @@ -23898,15 +23938,6 @@ "names": [], "sources": [] }, - { - "digest": "5f758f3de5f00c18968a7c6d347083aac5c9ae13f6a79c78f6611b23c4973564", - "names": [ - "packages/subagent/subagent/src/descriptor.ts#OneShotSubagentDescriptorData" - ], - "sources": [ - "packages/subagent/subagent/src/descriptor.ts:61" - ] - }, { "digest": "6085488d223aeffdef51d7ae17655fc26ab22ee13cbed82a50e62e2b6fb5b918", "names": [], @@ -23946,15 +23977,6 @@ "names": [], "sources": [] }, - { - "digest": "6487b10ba7542f3c447b1e41a1c6a590b7bfafe0b9b66b49f34028da85bb970f", - "names": [ - "packages/subagent/subagent/src/descriptor.ts#ContinuableSubagentDescriptorData" - ], - "sources": [ - "packages/subagent/subagent/src/descriptor.ts:72" - ] - }, { "digest": "6598cd6da253948d6b207c6374a45335804c79b576866d6215649c2d1bc85e25", "names": [ @@ -24042,6 +24064,11 @@ "names": [], "sources": [] }, + { + "digest": "6b70feb8fff1ac400560495d7e0156daed0e4cf2f73f0439f2e3b5d7038b2512", + "names": [], + "sources": [] + }, { "digest": "6bbe793f6a93dc8d50a4d4d3fbb6f8ed15650bcb7e151d28669628f7b7c3fc25", "names": [ @@ -24439,6 +24466,11 @@ "names": [], "sources": [] }, + { + "digest": "8521c4e8b3ddb8a7835fe8e707061bf4a9a52b9e4c2e98e5fc5fef2ac4998ff3", + "names": [], + "sources": [] + }, { "digest": "854db3efdfb0cd39d30953b28703e519c121f1115cebfffc40369fe86c406dd1", "names": [], @@ -24464,15 +24496,6 @@ "names": [], "sources": [] }, - { - "digest": "86d1d0e9cfebb240421187e3dc37fb71cc38a1501885cf32f8f9adef7b0a2bde", - "names": [ - "packages/subagent/subagent/src/descriptor.ts#SubagentDescriptorData" - ], - "sources": [ - "packages/subagent/subagent/src/descriptor.ts:89" - ] - }, { "digest": "86e1e384495ee09ac2d60c9db3610f1f1fddac2d248f6f1d6000e78054680219", "names": [ @@ -24803,6 +24826,11 @@ "names": [], "sources": [] }, + { + "digest": "9f808a869b1ef076e20a960c51b0c1c919b03025fa2661516cc36b3f452f60ee", + "names": [], + "sources": [] + }, { "digest": "a0a2e5c42e1c929445ecd1cd70f49be6b66441894ec72c08e8ae332821d4a3cb", "names": [], @@ -24813,11 +24841,6 @@ "names": [], "sources": [] }, - { - "digest": "a0f25b5aeeebf2bf1542ce67726b095e3b139a39c71861d57e827f12dea1f499", - "names": [], - "sources": [] - }, { "digest": "a0f906856fc06f1c6f2083eb970e5a657a82b1a84c06c095445e5d6d161cda80", "names": [], @@ -25216,11 +25239,6 @@ "packages/schedule/schedule/src/types.ts:167" ] }, - { - "digest": "b6896938660a1dfe4ddd100fb3164d9f8fa57feeabfe37441a80106c9942697a", - "names": [], - "sources": [] - }, { "digest": "b6ef9c6210be8350cd465f485cf520031fffa6bf3809c61a1e713e834c5982d0", "names": [], @@ -25248,11 +25266,6 @@ "names": [], "sources": [] }, - { - "digest": "b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5", - "names": [], - "sources": [] - }, { "digest": "b815d7b91cdd9fc6ccc7dcba05cfc5ce1d55fab5fc0c0801803cd3630cedb602", "names": [], @@ -25474,11 +25487,6 @@ "names": [], "sources": [] }, - { - "digest": "cbbe8377ee744964537dbafd83607219e58ba79a13f6db0d0645c7ad3d5e8a09", - "names": [], - "sources": [] - }, { "digest": "cbca49bde16b656d5b8fbe4a8a507769f7b594c6287782b809c03814941bc5e3", "names": [], @@ -25534,6 +25542,20 @@ "names": [], "sources": [] }, + { + "digest": "d356f2b4478fe252cf18d8017a277417a9d1b1f88b7bbdfbf5600092a7b7a09d", + "names": [ + "packages/subagent/subagent/src/catalog.ts#SubagentCatalogEvent" + ], + "sources": [ + "packages/subagent/subagent/src/catalog.ts:29" + ] + }, + { + "digest": "d452d8c6f47c6fa32773048a0cb44b825ca2d8e7aa28abb5f6f1afb1b965ba3a", + "names": [], + "sources": [] + }, { "digest": "d4b739491689cc6bb13d07ffac1a32239875abda364c0d5edd8f74faf5e9c00d", "names": [], @@ -25561,6 +25583,11 @@ "names": [], "sources": [] }, + { + "digest": "d64be7afff109c07a5496c2d7d0f7c84d145e9abddbe4f340c15d84d42ea4b05", + "names": [], + "sources": [] + }, { "digest": "d66e11dddf5dfa3c71547e2cbd7b0608824cd8d6243fc16f532017ff28e8fba5", "names": [], @@ -25699,9 +25726,13 @@ "sources": [] }, { - "digest": "e2ad8f5bcd813f268ffef1b10a12d7ddd2b8ced8b9b6d0275b3f6b7ed094a187", - "names": [], - "sources": [] + "digest": "e17928cecf8a76d8a2f4574d1bf86e5451d1af445032e5626e0711bfe0392509", + "names": [ + "packages/subagent/subagent/src/descriptor.ts#SubagentDescriptorData" + ], + "sources": [ + "packages/subagent/subagent/src/descriptor.ts:95" + ] }, { "digest": "e2d0367e5716bc188435e51718ebff8ce873190625fa93b1c5331102d98758fa", @@ -25977,6 +26008,15 @@ "packages/llm/llm/src/types.ts:414" ] }, + { + "digest": "f37e394b9557d1a228d6b2f29f9586c394d75fe9578ebb25aa143565aba50182", + "names": [ + "packages/subagent/subagent/src/descriptor.ts#ContinuableSubagentDescriptorData" + ], + "sources": [ + "packages/subagent/subagent/src/descriptor.ts:78" + ] + }, { "digest": "f4a66f654996c093aae700ea4c434ff2ee3569fea9c0e5813ccf6afdccabd163", "names": [], @@ -26039,12 +26079,12 @@ "sources": [] }, { - "digest": "f731ffe80810b0289b3e694d8f40af6f01865c02a622ee457be90ca808b66e87", + "digest": "f73798a0676cd6a7d7890fcd09bf2a9cd8ad0967b6c7afd7d3edd3f5fbc1c124", "names": [ - "packages/subagent/subagent/src/catalog.ts#SubagentCatalogEvent" + "packages/subagent/subagent/src/descriptor.ts#OneShotSubagentDescriptorData" ], "sources": [ - "packages/subagent/subagent/src/catalog.ts:28" + "packages/subagent/subagent/src/descriptor.ts:67" ] }, { diff --git a/docs/subsystems/subagent.i18n.yaml b/docs/subsystems/subagent.i18n.yaml index cb4ecf1336..7c4c99b892 100644 --- a/docs/subsystems/subagent.i18n.yaml +++ b/docs/subsystems/subagent.i18n.yaml @@ -12,8 +12,8 @@ en: 6f87231d18d82916 zh: 9f064b23058f8766 /subagents/messages-and-interruption: - en: f1921c4d9f85890f - zh: c16d41aaa3697171 + en: b1debfea952d2e4e + zh: fff0a6db056d7bb6 /subagents/durable-enumeration: en: 30c3607a969696ce zh: e6359cdf22b1e235 @@ -24,5 +24,8 @@ en: 226ba8094a8cf96c zh: 719678662905d1eb /subagents/durable-enumeration-listchildren-listdescendants-and-their-entries: - en: d8ce7969b65ad86e - zh: d39dac6592681bf2 + en: e07231af88af10b7 + zh: f99ed1d058da1002 +/subagents/agent-names: + en: a27c933826093f3e + zh: 2df9d1e683d15b57 diff --git a/docs/subsystems/subagent.md b/docs/subsystems/subagent.md index 21e71c2267..d97c6ba452 100644 --- a/docs/subsystems/subagent.md +++ b/docs/subsystems/subagent.md @@ -29,6 +29,8 @@ interface SubagentCapabilities { ```ts type-equiv /** One managed child execution and its result delivery policy. */ interface SubagentActivationSpec { + /** Immutable name segment, unique among the parent’s direct children. */ + readonly name: string /** Registered backend to use. */ readonly provider: string /** Short task label retained in cataloged child membership. */ @@ -47,6 +49,8 @@ interface SubagentActivationSpec { ```ts type-equiv /** A managed execution; disposal addresses this exact activation, never a later resume. */ interface SubagentActivation { + /** Full path used by model-facing collaboration tools. */ + readonly name: string /** Stable identity of the child. */ readonly childId: SessionId /** Accepted inbox message, when the backend has a local inbox. */ @@ -173,7 +177,7 @@ interface ContinuableCreateSpec { ## Messages and interruption -sendMessage requires the exact live sender and a target Session id. Every sender can reach a direct continuable child; a resident continuable child can also reach its direct parent and continuable siblings. Sibling delivery uses the shared live parent’s authority, preserving the actual sender’s id. Running targets receive Steer; absent children and siblings cold-resume. External executions reject follow-ups. Local and external executions report final text, structured output, diagnostics, and status through parent settlement notices; caller delivery suppresses those notices. drainDescendants and drainChildren await owned teardown. +sendMessage requires the exact live root or resident continuable sender and another member’s Session id. Delivery preserves the actual sender, verifies the target’s durable branch, and restores cold continuable ancestors under their recorded parents without delivering the target’s message to them. Running targets receive Steer; inactive targets start a turn. External executions reject follow-ups. Interruption still requires ancestor authority. Local and external executions report final text, structured output, diagnostics, and status through parent settlement notices; caller delivery suppresses those notices. drainDescendants and drainChildren await owned teardown. ```ts type-equiv /** Durable attribution for one model-authored message between Agents. */ @@ -378,7 +382,7 @@ The model-facing `list_agents` adapter reports current activity as `running` or `SubagentRuntime.listChildren(parentSessionId, signal?)` reads the parent's `subagentCatalog` view through a live-preferred Session observation and releases that observation on success or failure. It returns direct-child entries in parent event order without reading child logs or enumerating the Session corpus. Query failures propagate; a missing catalog projection fails explicitly. Browser rows derive membership from the shared projection store and add activity from Session status; the control stream pushes complete catalog updates. `listDescendants()` recursively reads those catalogs and derives `hasChildren` from each child catalog. [The parent-catalog Agent Note](../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md) owns creation, fork isolation, ordering, and persistence costs. -`SubagentRuntime.listDescendants(rootSessionId)` recursively calls the same catalog reader in stable pre-order, preserving each parent's event order. External entries are leaves without local Sessions. One-shot and unknown-mode entries remain traversal nodes; unknown modes produce `unsupported` diagnostics. An unreadable child catalog produces `corrupt` or `unavailable` and stops only that branch. Root read failures, missing services or projections, and cancellation reject the listing. Each reachable catalog is observed once and released before the next read; repeated ids and cycles are skipped. Sessions absent from reachable catalogs are not discovered, including ordinary Session forks and any subagents below those forks. Each row carries its catalog parent and root-relative depth: +`SubagentRuntime.listDescendants(rootSessionId)` recursively calls the same catalog reader in stable pre-order, preserving each parent's event order. External entries are leaves without local Sessions. One-shot and unknown-mode entries remain traversal nodes; unknown modes produce `unsupported` diagnostics. An unreadable child catalog produces `corrupt` or `unavailable` and stops only that branch. Root read failures, missing services or projections, and cancellation reject the listing. Each reachable child catalog is observed once; repeated ids and cycles are skipped. Sessions absent from reachable catalogs are not discovered, including ordinary Session forks and any subagents below those forks. Each row carries its catalog parent and root-relative depth: ```ts type-equiv /** One catalog descendant with its direct parent and edge distance from the requested root. */ @@ -392,6 +396,10 @@ type SubagentDescendantListEntry = SubagentListEntry & { +## Agent names + +Sibling names are unique under one parent; full paths start at `lead`. Parent catalogs and local child descriptors retain name segments. Temporary reservations belong to the live parent. See the [naming rules](../../packages/subagent/subagent/README.md#local-and-external-children). + @@ -436,6 +444,23 @@ startActivation(spec: SubagentActivationSpec & { readonly childId: SessionId }): */ startActivation(spec: SubagentActivationSpec): Promise +/** + * Derive an Agent's full path from creation ancestry and descriptor names. + * @param agent - exact live Agent whose name is requested. + * @returns lead followed by named or legacy child segments. + */ +nameOf(agent: Agent): string + +/** + * Resolve a full agent path without granting authority to act on its Agent. + * @param caller - exact live Agent selecting the tree. + * @param name - a full path returned by creation or discovery. + * @param signal - cancellation during historical discovery. + * @returns the durable Session identity for the consuming operation to authorize. + * @throws when the path is absent or a required ancestor is unavailable. + */ +resolveName(caller: Agent, name: string, signal?: AbortSignal): Promise + /** * Resolve the root shared by an Agent and its teammates. * @param caller - exact live Agent whose subagent ancestry selects the tree. @@ -511,7 +536,7 @@ async drainChildren(parent: Agent, childIds: readonly SessionId[]): Promise +## Agent 名字 + +同级名字在一个父级下唯一;完整路径以 `lead` 开头。父级 catalog 与本地子级 descriptor 保留名字路径段。临时预留属于在线父级。详见[命名规则](../../packages/subagent/subagent/README.zh.md#local-and-external-children)。 + @@ -438,6 +446,23 @@ startActivation(spec: SubagentActivationSpec & { readonly childId: SessionId }): */ startActivation(spec: SubagentActivationSpec): Promise +/** + * Derive an Agent's full path from creation ancestry and descriptor names. + * @param agent - exact live Agent whose name is requested. + * @returns lead followed by named or legacy child segments. + */ +nameOf(agent: Agent): string + +/** + * Resolve a full agent path without granting authority to act on its Agent. + * @param caller - exact live Agent selecting the tree. + * @param name - a full path returned by creation or discovery. + * @param signal - cancellation during historical discovery. + * @returns the durable Session identity for the consuming operation to authorize. + * @throws when the path is absent or a required ancestor is unavailable. + */ +resolveName(caller: Agent, name: string, signal?: AbortSignal): Promise + /** * Resolve the root shared by an Agent and its teammates. * @param caller - exact live Agent whose subagent ancestry selects the tree. @@ -513,7 +538,7 @@ async drainChildren(parent: Agent, childIds: readonly SessionId[]): Promise @@ -2178,13 +2188,13 @@ Ask a subagent to stop its current work. This call returns without waiting for i { "type": "object", "properties": { - "agent_id": { + "name": { "type": "string", - "description": "The id of an agent created under you: your direct child or a deeper descendant." + "description": "The full path (such as lead/explore/alice) of an agent created under you: your direct child or a deeper descendant." } }, "required": [ - "agent_id" + "name" ] } ``` @@ -2193,7 +2203,7 @@ Source: [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/sub ### `list_agents` -List your entire Team: the root agent and all reachable continuable descendants, including yourself. Rows carry ids, descriptions, and status; descendant rows carry parent ids and depth from the root. You can message any other Team member listed as running or inactive. You can interrupt only your own descendants. running means it is working; inactive means it is not currently working. While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation. +List your entire Team: the root agent and all reachable continuable descendants, including yourself. Rows carry full agent paths, descriptions, and status; descendant rows carry parent paths and depth from the root. You can message any other Team member listed as running or inactive. You can interrupt only your own descendants. running means it is working; inactive means it is not currently working. While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation. ```json { @@ -2212,9 +2222,9 @@ Send a message to an agent. A working agent receives it at its next step; an idl { "type": "object", "properties": { - "agent_id": { + "name": { "type": "string", - "description": "The agent id of another member of your Team, as shown by list_agents." + "description": "The full path (such as lead/reviewer) of another member of your Team, as shown by list_agents." }, "message": { "type": "string", @@ -2222,7 +2232,7 @@ Send a message to an agent. A working agent receives it at its next step; an idl } }, "required": [ - "agent_id", + "name", "message" ] } @@ -2407,7 +2417,7 @@ List shared tasks, including readiness, owner, revision, blockers, and write-sco }, "owner": { "type": "string", - "description": "Optional exact ownerId filter; use unowned for tasks without an owner." + "description": "Optional exact ownerName filter; use unowned for tasks without an owner." }, "ready": { "type": "boolean", @@ -2481,7 +2491,7 @@ Compare-and-set a shared task action using the latest revision from team_task_ge }, "owner": { "type": "string", - "description": "Agent id of the root or any continuable descendant for root-only reassign; omit to unassign." + "description": "Full agent path of the root or any continuable descendant for root-only reassign; omit to unassign." } }, "required": [ diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index 016a6f9b82..81ae217417 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -43,7 +43,7 @@ | `@deepseek-ai/dsh-tool-skill` | `skill` | `ctx.workingDirectory`、`ctx.tools`、`ctx.agents`、`ctx.skills` | `tool/call`、`tool/result`、`user/message replacement catalogs via agent.inject()` | - | - | | `@deepseek-ai/dsh-tool-session-query` | `session_event_read`、`session_event_search`、`session_event_trace`、`session_search`、`session_trace` | `ctx.tools`、`ctx.systemPrompt`、`ctx.sessionQuery`、`ctx.agents`、`a calling Agent for workspace authority` | `tool/call`、`tool/result` | - | 这 5 个只读工具会隐藏提供方游标,并根据不可变的调用 agent 会话为每个结果授权。该包需要选择启用;需要强制截止时间或限制行内输出的组合还会挂载通用超时或 spill 策略。 | | `@deepseek-ai/dsh-experimental-tool-subagent-oar` | `subagent_oar` | `ctx.tools`, `ctx.subagents` | `tool/call`, `tool/result`, `parent subagent catalog and completion notices` | - | subagent_oar 工具在为外部提供方配置的别名之间选择。本目录展示 Codex 和 Claude Code 别名;每个部署自行选择提供方。原生模型和权限设置由所选提供方的配置决定。 | -| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`ctx.sessionProjections`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 加载时的 `toolName` 选择委派名称(默认为 `subagent`);`modelSelection` 增加可选路由字段。`modelDiscovery` 默认关闭,每个作用域由一个实例注册共享发现工具。Web 与 Desktop preset 启用 spawn 发现和路由字段,并为 `subagent_fork` 保持路由字段关闭。Host 设置服务为每个新顶层 Session 记录授权;子 Session 继承该授权。没有已记录策略时,发现和显式选择只允许当前父级路由。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 | +| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`ctx.sessionProjections`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 加载时的 `toolName` 选择委派名称(默认为 `subagent`);`modelSelection` 增加可选路由字段。`modelDiscovery` 默认关闭,每个作用域由一个实例注册共享发现工具。Web 与 Desktop preset 启用 spawn 发现和路由字段,并为 `subagent_fork` 保持路由字段关闭。Host 设置服务为每个新顶层 Session 记录授权;子 Session 继承该授权。没有已记录策略时,发现和显式选择只允许当前父级路由。每次委派都会立即返回唯一的 agent 路径;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 | | `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`、`list_agents`、`send_message`、`wait_agent` | `ctx.tools`、`ctx.subagents`、`ctx.agents` | `tool/call`、`tool/result`、`child session events through ctx.subagents` | - | 共享控制工具发现并联系 subagent 树中的 Agent,中断有权控制的后代,并等待调用者收件箱中的输入。绑定提供方的 `tool-subagent` 实例提供委派工具。 | | `@deepseek-ai/dsh-tool-jobs` | `job_kill`、`job_list`、`job_output` | `ctx.tools`、`ctx.jobs`、`ctx.systemPrompt` | `tool/call`、`tool/result`、`user/message via agent.inject() for background completion notices` | - | 与任务种类无关的后台任务控制器:后台 bash 命令与 PTY 发送 都通过相同的 3 个工具读取、列出和终止。加载该插件会挂接控制器,从而启用生产方的 `ctx.jobs.start()`。 | | `@deepseek-ai/dsh-experimental-tool-agent-team` | `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `ctx.subagents`, `an exact live Team member Agent` | `tool/call`, `team/task`, `tool/result` | - | 四个任务板工具共享 subagent 树的根 Session。可选 Agent Teams profile 在标准子代理委派和控制工具之外增加任务板指导,以及从 subagent 根派生的必需 Team 身份。 | @@ -2075,12 +2075,16 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, ### `subagent_oar` -将上下文完整的任务委派给已配置的外部 agent。每次调用选择一个运行时。subagent 在独立上下文中工作,无法看到当前对话。工具返回子级 id,后续完成通知包含其最终回答。subagent 不接受后续消息。并发启动相互独立的任务,并在其运行期间继续其他有用工作。只剩等待结果时结束当前轮次,不要轮询。 +将上下文完整的任务委派给已配置的外部 agent。每次调用选择一个运行时。subagent 在独立上下文中工作,无法看到当前对话。工具返回完整 agent 路径,后续完成通知包含其最终回答。subagent 不接受后续消息。并发启动相互独立的任务,并在其运行期间继续其他有用工作。只剩等待结果时结束当前轮次,不要轮询。 ```json { "type": "object", "properties": { + "name": { + "type": "string", + "description": "A sibling-unique name segment: start with a lower-case letter and use lower-case letters, digits, underscores, or hyphens." + }, "runtime": { "type": "string", "description": "Configured external runtime for this task.", @@ -2103,6 +2107,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, } }, "required": [ + "name", "runtime", "description", "prompt" @@ -2142,12 +2147,16 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 ### `subagent` -将一项自包含任务委派给 subagent(在自身上下文中工作的独立 agent),用它卸载聚焦且独立的工作,例如研究、限定范围的实现或分析,以免消耗当前对话的上下文。subagent 会返回结果,但不会返回中间步骤。此工具启动独立管理的 subagent 并立即返回其 id。如果它结束时你仍在线,运行时会自动递送其完成通知及已有的最终回答。你可以使用 `send_message` 在运行时引导它,或在结束后继续对话。 +将一项自包含任务委派给 subagent(在自身上下文中工作的独立 agent),用它卸载聚焦且独立的工作,例如研究、限定范围的实现或分析,以免消耗当前对话的上下文。subagent 会返回结果,但不会返回中间步骤。此工具启动独立管理的 subagent 并立即返回其名字。如果它结束时你仍在线,运行时会自动递送其完成通知及已有的最终回答。你可以使用 `send_message` 在运行时引导它,或在结束后继续对话。 ```json { "type": "object", "properties": { + "name": { + "type": "string", + "description": "Name unique among your direct children. Use one segment starting with a lower-case letter, followed by lower-case letters, digits, underscores, or hyphens. Names remain taken after agents finish. The result returns the full path, such as lead/reviewer." + }, "cwd": { "type": "string", "description": "Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent." @@ -2162,6 +2171,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 } }, "required": [ + "name", "description", "prompt" ] @@ -2170,7 +2180,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 来源:[`packages/subagent/tool-subagent/src/index.ts`](../packages/subagent/tool-subagent/src/index.ts) -加载时的 `toolName` 选择委派名称(默认为 `subagent`);`modelSelection` 增加可选路由字段。`modelDiscovery` 默认关闭,每个作用域由一个实例注册共享发现工具。Web 与 Desktop preset 启用 spawn 发现和路由字段,并为 `subagent_fork` 保持路由字段关闭。Host 设置服务为每个新顶层 Session 记录授权;子 Session 继承该授权。没有已记录策略时,发现和显式选择只允许当前父级路由。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 +加载时的 `toolName` 选择委派名称(默认为 `subagent`);`modelSelection` 增加可选路由字段。`modelDiscovery` 默认关闭,每个作用域由一个实例注册共享发现工具。Web 与 Desktop preset 启用 spawn 发现和路由字段,并为 `subagent_fork` 保持路由字段关闭。Host 设置服务为每个新顶层 Session 记录授权;子 Session 继承该授权。没有已记录策略时,发现和显式选择只允许当前父级路由。每次委派都会立即返回唯一的 agent 路径;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 @@ -2178,19 +2188,19 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 ### `interrupt_agent` -根据 agent id 请求取消后台 agent 的当前轮次。目标可以是你的直接子级,也可以是在你下方创建的更深层 agent。对于本地 agent,只有当前轮次会停止:已经排队发给该 agent 的消息会一直搁置到后续的 send_message;它启动的 agent 会继续运行;该 agent 本身仍可接受后续操作。外部执行会永久停止,无法接收后续消息。停止请求被接受后,此调用立即返回,因此目标可能还会短暂运行;中断一个已经完成的 agent 是可接受的空操作。 +根据 agent 完整路径请求取消后台 agent 的当前轮次。目标可以是你的直接子级,也可以是在你下方创建的更深层 agent。对于本地 agent,只有当前轮次会停止:已经排队发给该 agent 的消息会一直搁置到后续的 send_message;它启动的 agent 会继续运行;该 agent 本身仍可接受后续操作。外部执行会永久停止,无法接收后续消息。停止请求被接受后,此调用立即返回,因此目标可能还会短暂运行;中断一个已经完成的 agent 是可接受的空操作。 ```json { "type": "object", "properties": { - "agent_id": { + "name": { "type": "string", - "description": "The id of an agent created under you: your direct child or a deeper descendant." + "description": "The full path (such as lead/explore/alice) of an agent created under you: your direct child or a deeper descendant." } }, "required": [ - "agent_id" + "name" ] } ``` @@ -2199,7 +2209,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 ### `list_agents` -列出整个 Team:根代理和所有可达的可继续后代,包括调用方自身。行包含 id、描述和状态;后代行包含父级 id 和相对根的深度。可以向列为 running 或 inactive 的任意其他 Team 成员发送消息;只能中断自己的后代。running 表示正在工作;inactive 表示当前没有工作。自身仍在线时,直属子代理结束会收到通知,无需反复查询其状态。使用 send_message 继续对话。 +列出整个 Team:根代理和所有可达的可继续后代,包括调用方自身。行包含完整 agent 路径、描述和状态;后代行包含父级路径 和相对根的深度。可以向列为 running 或 inactive 的任意其他 Team 成员发送消息;只能中断自己的后代。running 表示正在工作;inactive 表示当前没有工作。自身仍在线时,直属子代理结束会收到通知,无需反复查询其状态。使用 send_message 继续对话。 ```json { @@ -2218,9 +2228,9 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 { "type": "object", "properties": { - "agent_id": { + "name": { "type": "string", - "description": "The agent id of another member of your Team, as shown by list_agents." + "description": "The full path (such as lead/reviewer) of another member of your Team, as shown by list_agents." }, "message": { "type": "string", @@ -2228,7 +2238,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 } }, "required": [ - "agent_id", + "name", "message" ] } @@ -2413,7 +2423,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 }, "owner": { "type": "string", - "description": "Optional exact ownerId filter; use unowned for tasks without an owner." + "description": "Optional exact ownerName filter; use unowned for tasks without an owner." }, "ready": { "type": "boolean", @@ -2487,7 +2497,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 }, "owner": { "type": "string", - "description": "Agent id of the root or any continuable descendant for root-only reassign; omit to unassign." + "description": "Full agent path of the root or any continuable descendant for root-only reassign; omit to unassign." } }, "required": [ diff --git a/docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.i18n.yaml index 2c27ed403c..479b1905c1 100644 --- a/docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.i18n.yaml +++ b/docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.i18n.yaml @@ -3,14 +3,14 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.md /: - en: 1f073189776760d8 - zh: cfbb0403aea96f84 -/agent-teams-uses-subagents-and-session-ids: - en: 703c7766dd80e1d1 - zh: 157c67164c1cbc7d -/agent-teams-uses-subagents-and-session-ids/change: - en: 16641bbd65e005af - zh: ffe57e39b9c33843 -/agent-teams-uses-subagents-and-session-ids/migration: - en: 51e3139edc8042dd - zh: f022c84d731168fc + en: 74f7ef4250caee88 + zh: 944be1c0a0cb9f74 +/agent-teams-uses-subagents-and-hierarchical-names: + en: 8ff32d071efaba78 + zh: 4bc6f62f6863fca8 +/agent-teams-uses-subagents-and-hierarchical-names/change: + en: f603e50bd6aee1fa + zh: 82358818c016e017 +/agent-teams-uses-subagents-and-hierarchical-names/migration: + en: 5a3eeb5f43980b31 + zh: 290eda22dac21d29 diff --git a/docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.md b/docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.md index cba2f28a11..5648866240 100644 --- a/docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.md +++ b/docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.md @@ -1,30 +1,30 @@ --- kind: upgrade-guide -description: "Agent Teams uses the standard subagent tools and Session ids; Team-specific creation, messaging, listing, interruption, and their configuration keys are removed." +description: "Agent Teams uses the standard subagent tools and hierarchical names; Team-specific creation, messaging, listing, interruption, and their configuration keys are removed." --- -# Agent Teams uses subagents and Session ids +# Agent Teams uses subagents and hierarchical names English | [中文](guide.zh.md) ## Change -Agent Teams uses subagent tools and Session ids. Descriptions label responsibilities; only ids identify recipients. +Agent Teams uses subagent tools. Full paths rooted at `lead` identify recipients; descriptions label responsibilities. Creation requires a sibling-unique name segment. -- `spawn_teammate` and the Team-scoped `send_message`, `list_agents`, and `interrupt_agent` are removed. The standard control tools use `agent_id`, not `target`. Members of one root tree can message each other across branches by id; interruption remains ancestor-only. -- `list_agents` takes no arguments and lists the entire tree. Remove scripted `scope` arguments. Rows use `kind: root` or `kind: child`; descendants include parent ids and root-relative depth. The root can assign tasks to itself or any discovered continuable descendant. -- Team adds four task tools, task policy, and required identity. `wait_agent` moves to ordinary subagent controls and waits for the caller’s next-step inbox, including pending input. Status changes and unrelated inboxes do not wake it; `noProgress` is removed. Timeout bounds and default are unchanged. +- `spawn_teammate` and the Team-scoped `send_message`, `list_agents`, and `interrupt_agent` are removed. The standard control tools use `name`, not `target`. Members can message across branches of their root tree; interruption remains ancestor-only. +- `list_agents` takes no arguments and lists the entire tree. Remove scripted `scope` arguments. Rows use `kind: root` or `kind: child`; descendants include parent paths and root-relative depth. The root can assign tasks to itself or any discovered continuable descendant. +- Team adds four task tools, policy, and required identity. `wait_agent` moves to ordinary subagent controls and waits for the caller’s next-step inbox, including pending input. Status changes and unrelated inboxes do not wake it; `noProgress` is removed. Timeout bounds and default are unchanged. - `agent-team` rejects `maxMembers`, `maxMessageBytes`, and `disposalTimeoutMs`; `tool-agent-team` rejects `interrupt`, `freshProvider`, and `forkProvider`. -- `ctx.agentTeams` removes creation, messaging, interruption, roster, membership, and wait APIs. `TeamMembership` and `TeamMembers` are removed; use subagent root resolution and discovery. Task ownership uses `ownerId`, including the root’s Session id. -- Runtime instances share root ownership without changing creation ancestry. Member settlement does not dispose descendants. Messages restore only their target. Hosts close the tree when its root closes; members obtain required results explicitly. -- Cold resume uses saved presets and permissions, not the creator’s current generation. Missing presets fail restoration; historical targets without one use host composition plus descriptor overrides. Lifecycle dispatch uses the root scope while retaining the direct creator in `parentSessionId`. Browser continuation requires the root online; failure uses `subagent/delivery-unavailable`. -- Historical Team roster and mailbox records remain readable. Historical names do not resolve message recipients or task owners. Subagent catalog payloads are unchanged. -- The Web Team panel shows tasks only; ordinary subagent navigation owns member browsing. Historical `agentTeam.members` remains decodable but grants no authority. After flush, task commits end all resident Agents’ active waits in the tree. Signals retain no unread state and start no agents; changes outside a wait cannot wake later waits. +- `ctx.agentTeams` removes creation, messaging, interruption, roster, membership, and wait APIs. `TeamMembership` and `TeamMembers` are removed; use subagent root resolution and discovery. Service task ownership retains `ownerId`; model tool results use `ownerName`. +- Runtime instances share root ownership; creation ancestry is unchanged. Member settlement does not dispose descendants. Messages restore only their target. Root closure closes the tree; members obtain results explicitly. +- Cold resume uses saved presets and permissions. Missing presets fail restoration; historical targets without one use host composition plus descriptor overrides. Lifecycle dispatch uses the root scope while retaining the direct creator in `parentSessionId`. Browser continuation requires the root online; failure uses `subagent/delivery-unavailable`. +- Historical Team rosters and mailboxes remain readable; their names grant no recipient or task-owner authority. New child descriptors and parent catalog entries carry an optional `name` segment; historical unnamed children use `legacy:` path segments. +- The Web Team panel shows tasks; subagent navigation browses members. Historical `agentTeam.members` remains decodable but grants no authority. After flush, task commits end all resident Agents’ active waits in the tree. Signals retain no unread state and start no agents; changes outside a wait cannot wake later waits. ## Migration -1. Remove the obsolete configuration keys and any patch rows that disable subagent tools solely for Team support. -2. Replace `spawn_teammate({ name, description, prompt, context })` with `subagent({ description, prompt })`, or `subagent_fork` for inherited conversation history. Save the returned agent id. -3. Use `send_message({ agent_id, message })` and `interrupt_agent({ agent_id })`. Discover all Team member ids with `list_agents`. -4. Pass member Session ids to task `owner` fields and read `ownerId` in results. Use the Lead's Session id instead of the alias `lead`. -5. Replace removed service calls with `ctx.subagents` operations. Verify member navigation, assignment to a nested descendant, and task-board wakeups during an active wait. Re-read tasks after waiting. +1. Remove obsolete configuration keys and patches disabling subagent tools solely for Team support. +2. Replace `spawn_teammate({ name, description, prompt, context })` with `subagent({ name, description, prompt })`; use `subagent_fork` to inherit conversation history. Save the returned path. `subagent_oar` also requires `name` and returns a path. +3. Use `send_message({ name, message })` and `interrupt_agent({ name })`; discover member paths with `list_agents`. +4. Task `owner` fields accept paths; results expose `ownerName`. Use `lead` for the root. Host APIs retain Session ids. +5. Replace removed services with `ctx.subagents`. Verify member navigation, nested-descendant assignment, and task-board wakeups during active waits. Re-read tasks after waiting. diff --git a/docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.zh.md index 1be7765643..62392ea577 100644 --- a/docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.zh.md +++ b/docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.zh.md @@ -1,30 +1,30 @@ --- kind: upgrade-guide -description: "Agent Teams 使用标准 subagent 工具和 Session id;移除 Team 专属创建、消息、列举、中断操作及其配置键。" +description: "Agent Teams 使用标准 subagent 工具和分层名字;移除 Team 专属创建、消息、列举、中断操作及其配置键。" --- -# Agent Teams 使用子代理和 Session id +# Agent Teams 使用子代理和分层名字 [English](guide.md) | 中文 ## 变更 -Agent Teams 使用子代理工具和 Session id。描述标注职责;仅 id 标识接收者。 +Agent Teams 使用子代理工具。以 `lead` 为根的完整路径标识接收者;描述标注职责。创建时必须提供同级唯一的名字片段。 -- 移除 `spawn_teammate` 以及 Team 作用域的 `send_message`、`list_agents` 和 `interrupt_agent`。标准控制工具使用 `agent_id` 而非 `target`。同一根下的成员可以通过 id 跨分支互发消息;中断仍只允许祖先执行。 -- `list_agents` 不接受参数,列出整棵树。移除脚本中的 `scope` 参数。行使用 `kind: root` 或 `kind: child`;后代包含父 id 和相对根的深度。根可将任务分配给自身或任意已发现的可继续执行后代。 +- 移除 `spawn_teammate` 以及 Team 作用域的 `send_message`、`list_agents` 和 `interrupt_agent`。标准控制工具使用 `name` 而非 `target`。同一根下的成员可以通过完整路径跨分支互发消息;中断仍只允许祖先执行。 +- `list_agents` 不接受参数,列出整棵树。移除脚本中的 `scope` 参数。行使用 `kind: root` 或 `kind: child`;后代包含父路径和相对根的深度。根可将任务分配给自身或任意已发现的可继续执行后代。 - Team 添加四个任务工具、任务策略和必需身份。`wait_agent` 移至普通 subagent 控制工具,等待调用者的下一步收件箱,包括待处理输入。状态变化和其他收件箱不会唤醒它;移除 `noProgress`。超时范围和默认值不变。 - `agent-team` 拒绝 `maxMembers`、`maxMessageBytes` 和 `disposalTimeoutMs`;`tool-agent-team` 拒绝 `interrupt`、`freshProvider` 和 `forkProvider`。 -- `ctx.agentTeams` 移除创建、通信、中断、名册、成员关系和等待 API。移除 `TeamMembership` 和 `TeamMembers`;改用 subagent 根解析和发现。任务归属使用 `ownerId`,包括根的 Session id。 +- `ctx.agentTeams` 移除创建、通信、中断、名册、成员关系和等待 API。移除 `TeamMembership` 和 `TeamMembers`;改用 subagent 根解析和发现。服务任务归属保留 `ownerId`;模型工具结果使用 `ownerName`。 - 运行时实例共享根所有权,不改变创建祖先关系。成员收尾不释放后代。消息仅恢复目标。根关闭时 host 关闭整棵树;成员显式获取所需结果。 -- 冷恢复使用已保存的 preset 和权限,而非创建者当前代次。缺失 preset 会导致恢复失败;历史目标没有保存 preset 时,使用 host 组合及描述符覆盖。生命周期分派使用根作用域,同时在 `parentSessionId` 中保留直接创建者。浏览器继续执行需要根在线;失败使用 `subagent/delivery-unavailable`。 -- 历史 Team 名册和邮箱记录仍可读取。其中的名字仅作为历史数据,不用于解析消息收件人或任务所有者。Subagent catalog 载荷不变。 +- 冷恢复使用已保存的 preset 和权限。缺失 preset 会导致恢复失败;历史目标没有保存 preset 时,使用 host 组合及描述符覆盖。生命周期分派使用根作用域,同时在 `parentSessionId` 中保留直接创建者。浏览器继续执行需要根在线;失败使用 `subagent/delivery-unavailable`。 +- 历史 Team 名册和邮箱记录仍可读取。其中的名字仅作为历史数据,不用于解析消息收件人或任务所有者。新子级描述符和父级目录条目携带可选的 `name` 片段;历史未命名子级使用 `legacy:` 路径片段。 - Web Team 面板仅显示任务;普通 subagent 导航负责浏览成员。历史 `agentTeam.members` 仍可解码,但不授予权限。任务提交刷盘后结束同树所有驻留 Agent 的当前等待。信号不保留未读状态,也不启动 Agent;等待之外的变更不能唤醒后续等待。 ## 迁移 -1. 删除废弃配置键,以及仅为支持 Team 而禁用 subagent 工具的 patch 行。 -2. 将 `spawn_teammate({ name, description, prompt, context })` 替换为 `subagent({ description, prompt })`;继承对话历史时使用 `subagent_fork`。保存返回的 agent id。 -3. 使用 `send_message({ agent_id, message })` 和 `interrupt_agent({ agent_id })`。通过 `list_agents` 查询所有 Team 成员的 id。 -4. 任务的 `owner` 字段传入成员 Session id,结果读取 `ownerId`。以 Lead 的 Session id 取代 `lead` 别名。 -5. 将已移除服务调用替换为 `ctx.subagents` 操作。验证成员导航、向嵌套后代分配任务,以及当前等待期间的任务板唤醒。等待结束后重新读取任务。 +1. 删除废弃配置键,以及仅为支持 Team 而禁用 subagent 工具的 patch。 +2. 将 `spawn_teammate({ name, description, prompt, context })` 替换为 `subagent({ name, description, prompt })`;使用 `subagent_fork` 继承对话历史。保存返回的路径。`subagent_oar` 也要求 `name` 并返回路径。 +3. 使用 `send_message({ name, message })` 和 `interrupt_agent({ name })`;通过 `list_agents` 查询成员路径。 +4. 任务的 `owner` 字段接受路径,结果提供 `ownerName`。根使用 `lead`。Host API 保留 Session id。 +5. 将已移除服务替换为 `ctx.subagents`。验证成员导航、嵌套后代分配,以及当前等待期间的任务板唤醒。等待结束后重新读取任务。 diff --git a/packages/acp/acp/tests/turns.spec.ts b/packages/acp/acp/tests/turns.spec.ts index 1ef2bc94ce..9ed8c85fc7 100644 --- a/packages/acp/acp/tests/turns.spec.ts +++ b/packages/acp/acp/tests/turns.spec.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto' import { mountWorkingDirectoryFixture } from '../../../subagent/subagent/tests/working-directory-fixture.ts' import { createUserMessage, type StreamChunk } from '@deepseek-ai/dsh-llm' import type { ContextFormed } from '@deepseek-ai/dsh-llm' @@ -47,7 +48,7 @@ async function installDelegationTool(ctx: BridgeHarness['ctx']): Promise<() => b name: 'delegate_again', description: 'Run a later child.', parameters: {}, execute: async (_args, exec) => { if (exec.agent === undefined) throw new Error('delegation fixture requires an Agent') - const child = await ctx.subagents.startActivation({ + const child = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`, provider: 'local-test', label: 'later child', delivery: 'caller', signal: exec.signal, request: { parent: exec.agent, prompt: [{ type: 'text', text: 'later child task' }] }, }) diff --git a/packages/bundle/headless/tests/headless.spec.ts b/packages/bundle/headless/tests/headless.spec.ts index 68720d75f4..33bbb06f50 100644 --- a/packages/bundle/headless/tests/headless.spec.ts +++ b/packages/bundle/headless/tests/headless.spec.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto' /** Direct one-shot Agent driving, exact Session adoption, machine-readable projection, and exit mapping. */ import { Readable } from 'node:stream' @@ -375,7 +376,7 @@ describe('headless runner', () => { async afterPrompt(session, message, parent) { turns += 1 if (turns === 1) { - await test.ctx.subagents.startActivation({ + await test.ctx.subagents.startActivation({ name: `child-${randomUUID()}`, provider: 'external', label: 'work', delivery: 'parent', signal: new AbortController().signal, request: { parent, prompt: [] }, }) diff --git a/packages/client/ui-tool/README.i18n.yaml b/packages/client/ui-tool/README.i18n.yaml index 994c8139a7..6c8ba540c9 100644 --- a/packages/client/ui-tool/README.i18n.yaml +++ b/packages/client/ui-tool/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-client-ui-tool/use-this-package: - en: c6d4e666191521b2 - zh: 2498020e7080e172 + en: 34321c376896ba9c + zh: b847f7a33458caf0 /deepseek-ai-dsh-client-ui-tool/use-this-package/registering-a-business-tool-view: en: 2e3c306b13d3bc25 zh: 77127f01763b3976 diff --git a/packages/client/ui-tool/README.md b/packages/client/ui-tool/README.md index 8c9c1d6279..20e7a8ac3a 100644 --- a/packages/client/ui-tool/README.md +++ b/packages/client/ui-tool/README.md @@ -25,7 +25,7 @@ English | [中文](README.zh.md) ## Use this package -Tool calls appear in the conversation as cards: a root call tree with its nested subcalls, each atomic call rendered by its owning view. Every lifecycle state retains the tool's ordinary business glyph; failure and interruption remain explicit through the frozen call/result state, accessible status text, and failure summary. Users can open files or inspect calls through the Host callbacks. A collapsed `web_fetch` row links its http(s) URL, which opens in a new browser tab. +Tool calls appear in the conversation as cards: a root call tree with its nested subcalls, each atomic call rendered by its owning view. Every lifecycle state retains the tool's ordinary business glyph; failure and interruption remain explicit through the frozen call/result state, accessible status text, and failure summary. Users can open files or inspect calls through the Host callbacks. A collapsed `web_fetch` row links its http(s) URL, which opens in a new browser tab. Control receipts display agent `name` arguments; historical `agent_id` recordings remain readable. Settled foreground commands with a nonzero exit code, a terminating signal, or an explicit missing exit code keep their description in the ordinary gray text and show a neutral capsule with a red dot immediately after it. The capsule displays only the recorded code, signal, or localized missing-code status and stays on the same line; the description truncates with an ellipsis to reserve its space. Running, successful, and background calls omit it. Generic execution errors and stopped summaries retain their error and warning colors, including on hover. diff --git a/packages/client/ui-tool/README.zh.md b/packages/client/ui-tool/README.zh.md index 6bd8ba9809..8a22194567 100644 --- a/packages/client/ui-tool/README.zh.md +++ b/packages/client/ui-tool/README.zh.md @@ -25,7 +25,7 @@ kind: "package-reference" ## 使用本包 -工具调用在对话中显示为卡片:一个根调用树带其嵌套子调用,每个原子调用由所属视图渲染。所有生命周期状态都保留工具的普通业务图标;失败与中断仍通过冻结调用/结果状态、无障碍状态文本和失败摘要明确表达。用户可通过宿主回调打开文件或检查调用。折叠的 `web_fetch` 行把其 http(s) URL 显示为链接,在新浏览器标签页中打开。 +工具调用在对话中显示为卡片:一个根调用树带其嵌套子调用,每个原子调用由所属视图渲染。所有生命周期状态都保留工具的普通业务图标;失败与中断仍通过冻结调用/结果状态、无障碍状态文本和失败摘要明确表达。用户可通过宿主回调打开文件或检查调用。折叠的 `web_fetch` 行把其 http(s) URL 显示为链接,在新浏览器标签页中打开。 控制工具回执显示 agent 的 `name` 参数;历史 `agent_id` 记录仍可读取。 已结束的前台命令若返回非零退出码、终止信号,或明确记录为无退出码,描述保持普通灰色,后面紧接带红点的中性胶囊。胶囊仅显示记录的退出码、信号或本地化的“未正常退出”状态,并保持在同一行;描述超出可用宽度时显示省略号,为胶囊预留空间。运行中、成功和后台调用不显示胶囊。通用执行错误与停止摘要仍保留错误色和警告色,悬停时也不改变。 diff --git a/packages/client/ui-tool/src/client/tool/models/control-details-model.ts b/packages/client/ui-tool/src/client/tool/models/control-details-model.ts index 47d6edceb8..b8074b1885 100644 --- a/packages/client/ui-tool/src/client/tool/models/control-details-model.ts +++ b/packages/client/ui-tool/src/client/tool/models/control-details-model.ts @@ -109,7 +109,7 @@ function lspDetails(args: Record, text: string, t: DetailTransl export function controlDetails( name: string, args: Record, text: string, json: unknown, t: DetailTranslate, ): ToolDetailsModel | null { - const target = arg(args, 'target') || arg(args, 'agent_id') || arg(args, 'sessionId') || arg(args, 'job_id') + const target = arg(args, 'name') || arg(args, 'target') || arg(args, 'agent_id') || arg(args, 'sessionId') || arg(args, 'job_id') switch (name) { case 'list_agents': return agentList(text, json, t) case 'job_list': return jobList(text, t) diff --git a/packages/client/ui-tool/tests/details-models.client.spec.ts b/packages/client/ui-tool/tests/details-models.client.spec.ts index d6e32cff64..46e5865121 100644 --- a/packages/client/ui-tool/tests/details-models.client.spec.ts +++ b/packages/client/ui-tool/tests/details-models.client.spec.ts @@ -89,7 +89,7 @@ describe('control detail adapters', () => { expect(details('team_task_create', '{"id":"task-1","subject":"Review","status":"pending"}')?.summary).toBe('Review') expect(details('team_task_list', '{"tasks":[{"id":"task-1","subject":"Review"}],"nextCursor":3}')?.caption).toBe('More tasks available; next cursor is 3') expect(details('send_message', '{"status":"queued"}', { agent_id: 'a1', message: 'hello' })?.items[0]?.badge?.label).toBe('Queued') - expect(details('send_message', 'message delivered to agent a1', { agent_id: 'a1', message: 'hello' })?.summary).toBe('a1 · Message delivered') + expect(details('send_message', 'message delivered to agent reviewer', { name: 'reviewer', message: 'hello' })?.summary).toBe('reviewer · Message delivered') expect(details('interrupt_agent', '{"previousStatus":"running"}', { agent_id: 'a1' })?.items[0]?.fields).toContainEqual({ label: 'Previous status', value: 'Running' }) expect(details('wait_agent', '{"timedOut":true}')?.summary).toBe('Wait result · Wait timed out') expect(details('wait_agent', '{"timedOut":false,"noProgress":{"message":"No peer"}}')?.items[0]?.description).toBe('No peer') @@ -110,7 +110,7 @@ describe('control detail adapters', () => { expect(details('unknown', 'x')).toBeNull() expect(details('team_task_list', '{"tasks":[],"nextCursor":"bad"}')).toBeNull() expect(details('terminal_read', 'bad', { sessionId: 'pty-1' })).toBeNull() - expect(details('interrupt_agent', 'interrupt requested for agent a1', { agent_id: 'a1' })?.summary).toContain('Interrupt') + expect(details('interrupt_agent', 'interrupt requested for agent reviewer', { name: 'reviewer' })?.summary).toContain('Interrupt') expect(details('interrupt_agent', 'unexpected', { agent_id: 'a1' })).toBeNull() expect(details('job_kill', 'unexpected', { job_id: 'j1' })).toBeNull() expect(details('terminal_close', 'unexpected', { sessionId: 'pty-1' })).toBeNull() diff --git a/packages/core/agent-loop/tests/resource-lifecycle.spec.ts b/packages/core/agent-loop/tests/resource-lifecycle.spec.ts index eececf3f92..7ab6132bb1 100644 --- a/packages/core/agent-loop/tests/resource-lifecycle.spec.ts +++ b/packages/core/agent-loop/tests/resource-lifecycle.spec.ts @@ -116,7 +116,7 @@ it.each(['loop', 'root'] as const)('unloads %s while a background child is stopp const started = await ctx.tools.execute({ agent: parent, signal: new AbortController().signal, callId: ToolCallId('background-start'), name: 'subagent', - arguments: { description: 'child', prompt: 'wait' }, + arguments: { name: 'child', description: 'child', prompt: 'wait' }, }) expect(started.isError).toBe(false) const child = await requestEntered.promise diff --git a/packages/experimental/agent-team/README.i18n.yaml b/packages/experimental/agent-team/README.i18n.yaml index 9b53942f81..da1d864f7f 100644 --- a/packages/experimental/agent-team/README.i18n.yaml +++ b/packages/experimental/agent-team/README.i18n.yaml @@ -24,14 +24,14 @@ en: e316c9ce74bb9630 zh: e7874163c70d8d67 /deepseek-ai-dsh-experimental-agent-team/use-this-package/teammates: - en: 501812f9e9174d1d - zh: 55303d71b67cc0be + en: a52935db7fa328e1 + zh: 6081f1e8bb75d0ca /deepseek-ai-dsh-experimental-agent-team/use-this-package/messages-between-teammates: - en: 07dce4ca3b85c8e4 - zh: 6c872836fa785aa9 + en: ea0bf3fb3213feef + zh: 0af8ce3160d2fd5d /deepseek-ai-dsh-experimental-agent-team/use-this-package/shared-task-board: - en: dfecbb3adbc360da - zh: 4855fd99f03675f4 + en: 8ace88f5b3c32eeb + zh: 2e0111b4a7f72651 /deepseek-ai-dsh-experimental-agent-team/use-this-package/waiting-and-interruption: en: 2a8172e07ec20808 zh: 8aa93e8556517b92 @@ -81,8 +81,8 @@ en: 8b29cb821f80db40 zh: 43e402736a553ce5 /deepseek-ai-dsh-experimental-agent-team/model-experience/task-views-and-errors/what-the-model-sees: - en: 42e3a8e77cd1f549 - zh: 52460903771fa74b + en: bc7d5135ea812e71 + zh: 24fd4df413683aad /deepseek-ai-dsh-experimental-agent-team/model-experience/task-views-and-errors/token-effect: en: f8857619242bb16e zh: aca99dc785e83d56 diff --git a/packages/experimental/agent-team/README.md b/packages/experimental/agent-team/README.md index d3ecef5a7f..14d8127e63 100644 --- a/packages/experimental/agent-team/README.md +++ b/packages/experimental/agent-team/README.md @@ -53,19 +53,19 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a ### Teammates -Ask the Lead to start a subagent with a `description` of its work: `subagent` starts fresh, while `subagent_fork` inherits completed turns. The returned Session id addresses the teammate; descriptions are display text and may repeat. +Ask the Lead to start a subagent with a sibling-unique `name` and a `description` of its work: `subagent` starts fresh, while `subagent_fork` inherits completed turns. The returned full path addresses the teammate; descriptions may repeat. Every root agent and its descendants form one Team. Task assignment accepts the root or any reachable continuable descendant discovered by the subagent runtime, at any depth. Historical roster records remain readable but grant no assignment authority. ### Messages between teammates -Members use standard `list_agents` to discover the tree and `send_message` to contact another member by Session id. The subagent runtime owns routing, sender attribution, and cold restoration. Task-board signals address all resident Agents in the same tree and never restore a cold member. +Members use standard `list_agents` to discover the tree and `send_message` to contact another member by full agent path. The subagent runtime owns routing, sender attribution, and cold restoration. Task-board signals address all resident Agents in the same tree and never restore a cold member. ### Shared task board Any member can add a task with a title, details, optional dependencies on other tasks, and optional hints about which files it will touch. A task is claimable only when everything it depends on is complete. -A member claims a task to start work, completes it, releases it, or reopens it. The root can reassign a task to itself or any reachable continuable descendant by Session id. Other members claim tasks themselves. Every change is compare-and-set, so stale updates cannot overwrite newer work. +A member claims a task to start work, completes it, releases it, or reopens it. The root can reassign a task to itself or any reachable continuable descendant by full agent path. Other members claim tasks themselves. Every change is compare-and-set, so stale updates cannot overwrite newer work. File hints produce warnings when two in-progress tasks plan to touch overlapping paths — they never block anything. Deleted tasks remain in history but disappear from the active list. @@ -173,7 +173,7 @@ Read these pages when the package-level contract is not enough. They move from t #### What the model sees -The model reads task views through task-tool results. Views identify owners by `ownerId`; stale updates report the current revision. A task-board signal ends an active `wait_agent` call without supplying task contents. The model re-reads current tasks before acting. +The model reads task views through task-tool results. Model-facing views identify owners by `ownerName`; host views retain `ownerId`; stale updates report the current revision. A task-board signal ends an active `wait_agent` call without supplying task contents. The model re-reads current tasks before acting. #### Token effect diff --git a/packages/experimental/agent-team/README.zh.md b/packages/experimental/agent-team/README.zh.md index 6515e5f1e0..8479eb140e 100644 --- a/packages/experimental/agent-team/README.zh.md +++ b/packages/experimental/agent-team/README.zh.md @@ -53,19 +53,19 @@ kind: "package-reference" ### Teammate -让 Lead 使用 `description` 描述工作并启动子代理:`subagent` 从全新上下文开始,`subagent_fork` 继承已完成轮次。返回的 Session id 用于寻址队友;描述用于展示,可以重复。 +让 Lead 使用同级内唯一的 `name` 和工作 `description` 启动子代理:`subagent` 从全新上下文开始,`subagent_fork` 继承已完成轮次。返回的完整路径用于寻址队友;描述可以重复。 每个根 agent 及其后代组成一个 Team。任务可分配给根 agent 或 subagent 运行时发现的任意深度的可继续执行后代。历史名册记录仍可读取,但不授予分配权限。 ### teammate 之间的消息 -成员通过标准 `list_agents` 发现整棵树,并通过 `send_message` 按 Session id 联系另一成员。subagent 运行时负责路由、发送方归属和冷恢复。任务板信号面向同树所有驻留 Agent,绝不恢复冷成员。 +成员通过标准 `list_agents` 发现整棵树,并通过 `send_message` 按完整 agent 路径联系另一成员。subagent 运行时负责路由、发送方归属和冷恢复。任务板信号面向同树所有驻留 Agent,绝不恢复冷成员。 ### 共享任务板 任何成员都可以添加任务,包含标题、详情、对其他任务的可选依赖,以及可选的文件触及提示。只有其全部依赖完成后,任务才可 claim。 -成员可认领任务开始工作、完成任务、释放任务或重新打开任务。根 agent 可按 Session id 将任务重新分配给自身或任意可达的可继续执行后代。其他成员自行认领任务。所有变更均采用比较并交换,过期更新无法覆盖较新的工作。 +成员可认领任务开始工作、完成任务、释放任务或重新打开任务。根 agent 可按完整 agent 路径将任务重新分配给自身或任意可达的可继续执行后代。其他成员自行认领任务。所有变更均采用比较并交换,过期更新无法覆盖较新的工作。 当两个 in-progress 任务计划触及重叠路径时,文件提示会产生警告——它们绝不阻止任何操作。已删除任务保留在历史中,但从活动列表中消失。 @@ -173,7 +173,7 @@ subagent 服务通过 `subagents.rootOf()` 解析当前在线调用者的根。 #### 模型看到什么 -模型通过任务工具结果读取任务视图。视图以 `ownerId` 标识所有者;过期更新报告当前修订。任务板信号结束正在进行的 `wait_agent` 调用,不提供任务内容。模型在行动前重新读取当前任务。 +模型通过任务工具结果读取任务视图。面向模型的视图以 `ownerName` 标识所有者,host 视图保留 `ownerId`;过期更新报告当前修订。任务板信号结束正在进行的 `wait_agent` 调用,不提供任务内容。模型在行动前重新读取当前任务。 #### Token 影响 diff --git a/packages/experimental/agent-team/tests/persistence.spec.ts b/packages/experimental/agent-team/tests/persistence.spec.ts index da9a2e41bf..c33e4f4155 100644 --- a/packages/experimental/agent-team/tests/persistence.spec.ts +++ b/packages/experimental/agent-team/tests/persistence.spec.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto' import { afterEach, describe, expect, it, vi } from 'vitest' import { mkdtempSync } from 'node:fs' import { rm } from 'node:fs/promises' @@ -117,7 +118,7 @@ for (const backend of backends) { const first = await stack(backend, storageRoot, [textResponse('initial teammate answer')]) const firstLead = await first.ctx.agentLoop.create(rootId, { provider: 'mock', model: 'mock' }) - const started = await first.ctx.subagents.startActivation({ + const started = await first.ctx.subagents.startActivation({ name: `child-${randomUUID()}`, provider: 'spawn', label: 'restart worker', request: { prompt: [{ type: 'text', text: 'finish before restart' }], parent: firstLead }, diff --git a/packages/experimental/agent-team/tests/team.spec.ts b/packages/experimental/agent-team/tests/team.spec.ts index 9a12b9b31c..a293e91d2b 100644 --- a/packages/experimental/agent-team/tests/team.spec.ts +++ b/packages/experimental/agent-team/tests/team.spec.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto' import { afterEach, describe, expect, it, vi } from 'vitest' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' @@ -74,7 +75,7 @@ function content(text: string) { /** Start one teammate: a continuable child of the Lead. */ async function spawn(ctx: Context, lead: Agent, description: string | undefined, provider = 'spawn'): Promise { - const started = await ctx.subagents.startActivation({ + const started = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`, provider, label: `${description ?? 'shared'} responsibility`, request: { prompt: content(`${description ?? 'shared'} initial`), parent: lead }, @@ -134,7 +135,7 @@ describe('Tree-owned task boards', () => { const { ctx, lead } = await setup(['hang', 'hang', 'hang']) const named = await waitRunning(ctx, await spawn(ctx, lead, 'alpha')) const unnamed = await waitRunning(ctx, await spawn(ctx, lead, undefined)) - const grandchildId = (await ctx.subagents.startActivation({ + const grandchildId = (await ctx.subagents.startActivation({ name: `child-${randomUUID()}`, provider: 'spawn', label: 'nested', delivery: 'caller', signal: SIGNAL, request: { prompt: content('nested'), parent: named }, })).childId diff --git a/packages/experimental/auto-review/tests/auto-review.spec.ts b/packages/experimental/auto-review/tests/auto-review.spec.ts index 828d43e6d8..69d46d870d 100644 --- a/packages/experimental/auto-review/tests/auto-review.spec.ts +++ b/packages/experimental/auto-review/tests/auto-review.spec.ts @@ -1340,6 +1340,7 @@ describe('out-of-process delegation boundary', () => { if (schema === undefined) throw new Error('remote delegation tool schema is missing') appendHeader(session, [schema]) const args = { + name: 'remote-inspector', description: 'remote boundary', prompt: 'Inspect the child runtime permission boundary.', } diff --git a/packages/experimental/tool-agent-team/README.i18n.yaml b/packages/experimental/tool-agent-team/README.i18n.yaml index 4f98c7c116..05ce9ff684 100644 --- a/packages/experimental/tool-agent-team/README.i18n.yaml +++ b/packages/experimental/tool-agent-team/README.i18n.yaml @@ -21,11 +21,11 @@ en: c27d5457e1183675 zh: 3c36d513e6400446 /deepseek-ai-dsh-experimental-tool-agent-team/use-this-package/smallest-working-example: - en: 24b61d5204ef2adc - zh: 720620ab65464cb1 + en: 6af7bd1afa2f61c4 + zh: 16c479ead42a2b35 /deepseek-ai-dsh-experimental-tool-agent-team/use-this-package/what-the-model-can-do: - en: 4aee48f5debcc7d1 - zh: 4eb148aa5e42398b + en: f4c91b124496d1a6 + zh: a2149d32e11d634a /deepseek-ai-dsh-experimental-tool-agent-team/use-this-package/what-success-and-failure-look-like: en: bf1eb0e693610f5b zh: 2b29e5cf63623909 @@ -69,8 +69,8 @@ en: ed8642015d306d6d zh: 31496981d115ba40 /deepseek-ai-dsh-experimental-tool-agent-team/model-experience/team-identity-runtime-context/what-the-model-sees: - en: f53d44b775dadb2f - zh: ff7d8144fc003d53 + en: c34616cd061c33be + zh: c039b9dd9804f8ac /deepseek-ai-dsh-experimental-tool-agent-team/model-experience/team-identity-runtime-context/what-the-model-sees/root-identity: en: 58d2b04f507baa5f zh: 773786c59605a154 diff --git a/packages/experimental/tool-agent-team/README.md b/packages/experimental/tool-agent-team/README.md index 0a3f4568a8..17252ddd55 100644 --- a/packages/experimental/tool-agent-team/README.md +++ b/packages/experimental/tool-agent-team/README.md @@ -42,7 +42,7 @@ The plugin takes no configuration; mount it beside the Team domain as in the [ag A row that sets `interrupt`, `freshProvider`, or `forkProvider` does not activate, and dsh names the key in its startup warning. The [upgrade guide](../../../docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.md) maps the removed tools and keys to their replacements, and the generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-experimental-tool-agent-team) records the empty configuration. -Ask the Lead to start a subagent to review the diff, add a review task, and wait for its result. The `description` explains the subagent’s work; the returned id addresses it. +Ask the Lead to start a subagent to review the diff, add a review task, and wait for its result. The `description` explains the subagent’s work; the returned full path addresses it. ### What the model can do @@ -52,7 +52,7 @@ The task tools complement the ordinary subagent controls: - **Wait for inbox input** — `wait_agent` belongs to `@deepseek-ai/dsh-tool-subagent-control` and remains available without this package. - **Create and reach teammates** — `subagent`, `subagent_fork`, `send_message`, `list_agents`, and `interrupt_agent` come from the subagent tool packages and are unchanged by this package. -Task `owner` arguments and `ownerId` results use Session ids. The root may reassign to itself or any reachable continuable descendant; historical roster records grant no authority. Other agents claim ready tasks themselves. Updates enforce ownership and revision checks. +Task `owner` arguments and `ownerName` results use full agent paths. The root may reassign to itself or any reachable continuable descendant; historical roster records grant no authority. Other agents claim ready tasks themselves. Updates enforce ownership and revision checks. ### What success and failure look like @@ -122,7 +122,7 @@ Every Team member's system prompt carries one `team:policy` section; the Lead an ##### Team policy ```markdown -A shared task board is available to every agent in your subagent tree. Task owners are agent ids from list_agents. The root can reassign tasks to itself or any continuable descendant; other agents claim ready tasks themselves. +A shared task board is available to every agent in your subagent tree. Task owners are full agent paths from list_agents. The root can reassign tasks to itself or any continuable descendant; other agents claim ready tasks themselves. Read tasks with team_task_list or team_task_get, claim with the current revision, perform the work, then complete the task. Dependencies determine readiness; write scopes describe expected edits and overlapping scopes are advisory, not locks. Task readiness does not start an agent. @@ -141,18 +141,18 @@ The text is identical for the Lead and every descendant and does not change whil #### What the model sees -Required `team:identity` context uses the following text, replacing `` or `` with the JSON-encoded Session id derived from the subagent tree. If the root is unavailable, the context reports that fact and permits independent work. Optional-context suppression cannot remove this identity; the ordinary runtime-context mechanism restores it after compaction. +Required `team:identity` context uses the following text, replacing `` with the JSON-encoded full path derived from the subagent tree. If the root is unavailable, the context reports that fact and permits independent work. Optional-context suppression cannot remove this identity; the ordinary runtime-context mechanism restores it after compaction. ##### Root identity ```markdown -You are the Team Lead. Your agent id is . +You are the Team Lead. Your name is lead. ``` ##### Teammate identity ```markdown -You are a teammate in the Team led by agent . +Your name is . You are a teammate in the Team led by "lead". ``` #### Token effect diff --git a/packages/experimental/tool-agent-team/README.zh.md b/packages/experimental/tool-agent-team/README.zh.md index a5323e3b28..a3db90d59d 100644 --- a/packages/experimental/tool-agent-team/README.zh.md +++ b/packages/experimental/tool-agent-team/README.zh.md @@ -42,7 +42,7 @@ kind: "package-reference" 设置 `interrupt`、`freshProvider` 或 `forkProvider` 的配置行不会激活,dsh 会在启动警告中指出该键。[升级指南](../../../docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.zh.md)把已移除的工具与键对应到替代方式,生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-experimental-tool-agent-team)记录空配置。 -让 Lead 启动子代理审查差异、创建审查任务并等待结果。`description` 说明子代理的工作;返回的 id 用于寻址。 +让 Lead 启动子代理审查差异、创建审查任务并等待结果。`description` 说明子代理的工作;返回的完整路径用于寻址。 ### 模型能做什么 @@ -52,7 +52,7 @@ kind: "package-reference" - **等待收件箱输入** — `wait_agent` 属于 `@deepseek-ai/dsh-tool-subagent-control`,无需此包即可使用。 - **创建和联系队友**——`subagent`、`subagent_fork`、`send_message`、`list_agents` 和 `interrupt_agent` 来自子代理工具包,本包不修改它们。 -任务 `owner` 参数和 `ownerId` 结果使用 Session id。根可将任务重新分配给自身或任意可达的可继续执行后代;历史名册记录不授予权限。其他 agent 自行认领就绪任务。更新会检查归属和修订。 +任务 `owner` 参数和 `ownerName` 结果使用完整 agent 路径。根可将任务重新分配给自身或任意可达的可继续执行后代;历史名册记录不授予权限。其他 agent 自行认领就绪任务。更新会检查归属和修订。 ### 成功与失败的表现 @@ -122,7 +122,7 @@ kind: "package-reference" ##### Team 策略 ```markdown -A shared task board is available to every agent in your subagent tree. Task owners are agent ids from list_agents. The root can reassign tasks to itself or any continuable descendant; other agents claim ready tasks themselves. +A shared task board is available to every agent in your subagent tree. Task owners are full agent paths from list_agents. The root can reassign tasks to itself or any continuable descendant; other agents claim ready tasks themselves. Read tasks with team_task_list or team_task_get, claim with the current revision, perform the work, then complete the task. Dependencies determine readiness; write scopes describe expected edits and overlapping scopes are advisory, not locks. Task readiness does not start an agent. @@ -141,18 +141,18 @@ Task changes end current wait_agent calls in your subagent tree. Changes outside #### 模型看到什么 -必需的 `team:identity` 上下文使用以下文本,将 `` 或 `` 替换为从 subagent 树派生的 JSON 编码 Session id。根不可用时,上下文明确报告该事实并允许独立工作。可选上下文抑制不能移除该身份;普通运行时上下文机制会在压缩后恢复它。 +必需的 `team:identity` 上下文使用以下文本,将 `` 替换为从 subagent 树派生的 JSON 编码完整路径。根不可用时,上下文明确报告该事实并允许独立工作。可选上下文抑制不能移除该身份;普通运行时上下文机制会在压缩后恢复它。 ##### 根身份 ```markdown -You are the Team Lead. Your agent id is . +You are the Team Lead. Your name is lead. ``` ##### 队友身份 ```markdown -You are a teammate in the Team led by agent . +Your name is . You are a teammate in the Team led by "lead". ``` #### Token 影响 diff --git a/packages/experimental/tool-agent-team/src/index.ts b/packages/experimental/tool-agent-team/src/index.ts index 303cf57659..0e4c995ba1 100644 --- a/packages/experimental/tool-agent-team/src/index.ts +++ b/packages/experimental/tool-agent-team/src/index.ts @@ -3,9 +3,9 @@ import type { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import type { Agent } from '@deepseek-ai/dsh-agent' -import { SessionId } from '@deepseek-ai/dsh-session' +import type { SessionId } from '@deepseek-ai/dsh-session' import { SubagentError } from '@deepseek-ai/dsh-subagent' -import { TeamTaskId } from '@deepseek-ai/dsh-experimental-agent-team' +import { TeamTaskId, type TeamTaskView } from '@deepseek-ai/dsh-experimental-agent-team' import { defineTool } from '@deepseek-ai/dsh-tools' import type { InferValue, ValueSchemaSpec } from '@deepseek-ai/dsh-tools' @@ -24,7 +24,7 @@ export const Config: z = z.object({}) const REMOVED_CONFIG_KEYS = ['interrupt', 'freshProvider', 'forkProvider'] as const /** Model-facing collaboration guidance shared by Lead and teammates. */ -const TEAM_POLICY = `A shared task board is available to every agent in your subagent tree. Task owners are agent ids from list_agents. The root can reassign tasks to itself or any continuable descendant; other agents claim ready tasks themselves. +const TEAM_POLICY = `A shared task board is available to every agent in your subagent tree. Task owners are full agent paths from list_agents. The root can reassign tasks to itself or any continuable descendant; other agents claim ready tasks themselves. Read tasks with team_task_list or team_task_get, claim with the current revision, perform the work, then complete the task. Dependencies determine readiness; write scopes describe expected edits and overlapping scopes are advisory, not locks. Task readiness does not start an agent. @@ -40,7 +40,7 @@ const TASK_VIEW_SCHEMA = { subject: { type: 'string', required: true }, description: { type: 'string', required: true }, status: { type: 'string', required: true, enum: ['pending', 'in_progress', 'completed', 'deleted'] }, - ownerId: { type: 'string' }, + ownerName: { type: 'string' }, blockedBy: { type: 'array', required: true, items: { type: 'string' } }, writeScopes: { type: 'array', required: true, items: { type: 'string' } }, ready: { type: 'boolean', required: true }, @@ -74,6 +74,31 @@ function jsonOutput(schema: S): { } } +type NamedTaskView = Omit & { readonly ownerName?: string } + +/** Project task owners from live descriptors or the Team’s direct-child catalog. */ +async function namedTasks(ctx: Context, caller: Agent, tasks: readonly TeamTaskView[], signal: AbortSignal): Promise { + const root = ctx.subagents.rootOf(caller) + const names = new Map([[root.id, 'lead']]) + const missing = new Set() + for (const { ownerId } of tasks) { + if (ownerId === undefined || names.has(ownerId)) continue + const agent = ctx.agents.get(ownerId) + if (agent === undefined) missing.add(ownerId) + else names.set(ownerId, ctx.subagents.nameOf(agent)) + } + if (missing.size > 0) { + for (const entry of await ctx.subagents.listDescendants(root.id, signal)) { + const parent = names.get(entry.parentId) as string + names.set(entry.id, `${parent}/${entry.name ?? `legacy:${entry.id}`}`) + } + } + return tasks.map(({ ownerId, ...view }) => ({ + ...view, + ...ownerId === undefined ? {} : { ownerName: names.get(ownerId) ?? `legacy:${ownerId}` }, + })) +} + /** Recover the exact caller guaranteed by Agent-scoped tool discovery. */ function callingAgent(agent: Agent | undefined, toolName: string): Agent { /* v8 ignore next 2 -- Team tools are registered only in an exact Agent scope, so discovery supplies this carrier. */ @@ -101,8 +126,8 @@ function install(agent: Agent, ctx: Context): () => void { try { const root = ctx.subagents.rootOf(agent) return root === agent - ? `You are the Team Lead. Your agent id is ${JSON.stringify(agent.id)}.` - : `You are a teammate in the Team led by agent ${JSON.stringify(root.id)}.` + ? 'You are the Team Lead. Your name is lead.' + : `Your name is ${JSON.stringify(ctx.subagents.nameOf(agent))}. You are a teammate in the Team led by ${JSON.stringify(ctx.subagents.nameOf(root))}.` } catch (error: unknown) { if (!(error instanceof SubagentError) || error.code !== 'UNAUTHORIZED') throw error const parentId = agent.session.header.parentSession @@ -126,12 +151,14 @@ function install(agent: Agent, ctx: Context): () => void { }, output: jsonOutput(TASK_VIEW_SCHEMA), async execute(args, exec) { - return await ctx.agentTeams.createTask(callingAgent(exec.agent, 'team_task_create'), { + const caller = callingAgent(exec.agent, 'team_task_create') + const task = await ctx.agentTeams.createTask(caller, { subject: args.subject, description: args.description, ...args.blocked_by === undefined ? {} : { blockedBy: args.blocked_by.map(TeamTaskId) }, ...args.write_scopes === undefined ? {} : { writeScopes: args.write_scopes }, }) + return (await namedTasks(ctx, caller, [task], exec.signal))[0] as NamedTaskView }, }))) @@ -144,17 +171,18 @@ function install(agent: Agent, ctx: Context): () => void { enum: ['pending', 'in_progress', 'completed'], description: 'Optional exact status filter.', }, - owner: { type: 'string', description: 'Optional exact ownerId filter; use unowned for tasks without an owner.' }, + owner: { type: 'string', description: 'Optional exact ownerName filter; use unowned for tasks without an owner.' }, ready: { type: 'boolean', description: 'Optional readiness filter.' }, cursor: { type: 'integer', description: 'Zero-based result offset. Defaults to 0.' }, limit: { type: 'integer', description: 'Number of rows, 1 through 100. Defaults to 50.' }, }, output: jsonOutput(TASK_LIST_VALUE_SCHEMA), - execute(args, exec) { + async execute(args, exec) { const status = args.status - const filtered = ctx.agentTeams.listTasks(callingAgent(exec.agent, 'team_task_list')).filter(task => + const caller = callingAgent(exec.agent, 'team_task_list') + const filtered = (await namedTasks(ctx, caller, ctx.agentTeams.listTasks(caller), exec.signal)).filter(task => (status === undefined || task.status === status) - && (args.owner === undefined || (args.owner === 'unowned' ? task.ownerId === undefined : task.ownerId === args.owner)) + && (args.owner === undefined || (args.owner === 'unowned' ? task.ownerName === undefined : task.ownerName === args.owner)) && (args.ready === undefined || task.ready === args.ready)) const cursor = args.cursor ?? 0 const limit = args.limit ?? 50 @@ -175,10 +203,9 @@ function install(agent: Agent, ctx: Context): () => void { }, output: jsonOutput(TASK_VIEW_SCHEMA), async execute(args, exec) { - return Promise.resolve(ctx.agentTeams.getTask( - callingAgent(exec.agent, 'team_task_get'), - TeamTaskId(args.task_id), - )) + const caller = callingAgent(exec.agent, 'team_task_get') + const task = ctx.agentTeams.getTask(caller, TeamTaskId(args.task_id)) + return (await namedTasks(ctx, caller, [task], exec.signal))[0] as NamedTaskView }, }))) @@ -198,11 +225,14 @@ function install(agent: Agent, ctx: Context): () => void { description: { type: 'string', description: 'Replacement details for edit.' }, blocked_by: { type: 'array', items: { type: 'string' }, description: 'Complete blocker list for set_dependencies.' }, write_scopes: { type: 'array', items: { type: 'string' }, description: 'Replacement advisory write scopes for edit.' }, - owner: { type: 'string', description: 'Agent id of the root or any continuable descendant for root-only reassign; omit to unassign.' }, + owner: { type: 'string', description: 'Full agent path of the root or any continuable descendant for root-only reassign; omit to unassign.' }, }, output: jsonOutput(TASK_VIEW_SCHEMA), async execute(args, exec) { - return await ctx.agentTeams.updateTask(callingAgent(exec.agent, 'team_task_update'), { + const caller = callingAgent(exec.agent, 'team_task_update') + const owner = args.owner === undefined ? undefined + : await ctx.subagents.resolveName(caller, args.owner, exec.signal) + const task = await ctx.agentTeams.updateTask(caller, { taskId: TeamTaskId(args.task_id), expectedRevision: args.expected_revision, action: args.action, @@ -210,8 +240,9 @@ function install(agent: Agent, ctx: Context): () => void { ...args.description === undefined ? {} : { description: args.description }, ...args.blocked_by === undefined ? {} : { blockedBy: args.blocked_by.map(TeamTaskId) }, ...args.write_scopes === undefined ? {} : { writeScopes: args.write_scopes }, - ...args.owner === undefined ? {} : { owner: SessionId(args.owner) }, + ...owner === undefined ? {} : { owner }, }) + return (await namedTasks(ctx, caller, [task], exec.signal))[0] as NamedTaskView }, }))) } catch (error: unknown) { diff --git a/packages/experimental/tool-agent-team/tests/tool-team.spec.ts b/packages/experimental/tool-agent-team/tests/tool-team.spec.ts index 0909034817..816692d9bb 100644 --- a/packages/experimental/tool-agent-team/tests/tool-team.spec.ts +++ b/packages/experimental/tool-agent-team/tests/tool-team.spec.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto' import { afterEach, describe, expect, it, vi } from 'vitest' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' @@ -22,7 +23,7 @@ import { serialize } from '@deepseek-ai/dsh-llm-deepseek/src/serialize.ts' import type { GenerateOptions } from '@deepseek-ai/dsh-llm' import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' import { mountWorkingDirectoryFixture } from '../../../subagent/subagent/tests/working-directory-fixture.ts' -import TeamService from '../../agent-team/src/index.ts' +import TeamService, { TeamId } from '../../agent-team/src/index.ts' import * as toolTeam from '../src/index.ts' function serializeRequest(request: GenerateOptions) { @@ -42,7 +43,7 @@ const TEAM_TOOLS = [ 'team_task_list', 'team_task_update', ] -const leadIdentity = (agent: Agent): string => `You are the Team Lead. Your agent id is ${JSON.stringify(agent.id)}.` +const leadIdentity = (_agent: Agent): string => 'You are the Team Lead. Your name is lead.' const roots: string[] = [] const contexts = new Set() @@ -125,7 +126,7 @@ async function startWait(ctx: Context, agent: Agent) { /** Start a continuable child of the Lead with a task description. */ async function spawn(ctx: Context, lead: Agent, description: string | undefined, prompt: string, provider = 'spawn'): Promise { - const started = await ctx.subagents.startActivation({ + const started = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`, provider, label: `${description ?? 'shared'} task`, request: { prompt: [{ type: 'text', text: prompt }], parent: lead }, @@ -145,8 +146,8 @@ function teamTools(assembled: Awaited>): string[] { return assembled.tools.map(schema => schema.name).filter(name => TEAM_TOOLS.includes(name)).sort() } -function teammateIdentity(lead: Agent): string { - return `You are a teammate in the Team led by agent ${JSON.stringify(lead.id)}.` +function teammateIdentity(_lead: Agent): string { + return 'You are a teammate in the Team led by \"lead\".' } async function runTurn(agent: Agent, text: string): Promise { @@ -260,7 +261,7 @@ describe('dsh-tool-team', () => { meta: { parentSession: child.agent.id, origin: 'subagent' }, }) expect(renderContextSnapshot(await assembly(ctx, nested.agent))).toContain(teammateIdentity(lead)) - expect(renderContextSnapshot(await assembly(ctx, nested.agent))).not.toContain(teammateIdentity(child.agent)) + expect(renderContextSnapshot(await assembly(ctx, nested.agent))).not.toContain(`Team led by ${JSON.stringify(ctx.subagents.nameOf(child.agent))}`) }) it.each([undefined, 'in-history'] as const)('appends current Lead identity to a teammate fork with update mode %s', async (systemPromptUpdate) => { @@ -371,7 +372,7 @@ describe('dsh-tool-team', () => { expected_revision: task.revision, action: 'claim', }) - expect(JSON.parse(text(claimed))).toMatchObject({ status: 'in_progress', ownerId: childId }) + expect(JSON.parse(text(claimed))).toMatchObject({ status: 'in_progress', ownerName: ctx.subagents.nameOf(ctx.agents.get(childId)!) }) const stale = await execute(ctx, lead, 'team_task_update', { task_id: task.id, expected_revision: task.revision, @@ -411,7 +412,7 @@ describe('dsh-tool-team', () => { const claim = JSON.parse(text(claimed)) as { revision: number } expect(JSON.parse(text(await execute(ctx, lead, 'team_task_list', { - status: 'in_progress', owner: lead.id, cursor: 0, limit: 1, + status: 'in_progress', owner: 'lead', cursor: 0, limit: 1, })))).toMatchObject({ tasks: [{ id: first.id }] }) expect(JSON.parse(text(await execute(ctx, lead, 'team_task_list', { owner: 'unowned', limit: 1, @@ -446,7 +447,7 @@ describe('dsh-tool-team', () => { task_id: first.id, expected_revision: dependency.revision, action: 'reassign', - owner: childId, + owner: ctx.subagents.nameOf(ctx.agents.get(childId)!), })).isError).toBe(true) const child = ctx.agents.get(childId)! @@ -460,6 +461,42 @@ describe('dsh-tool-team', () => { await waitNoAgent(ctx, childId) }) + it('retains a settled task owner’s catalog name and rejects its legacy alias', async () => { + const { ctx, lead } = await setup([textResponse('done')]) + const id = await spawn(ctx, lead, 'reviewer', 'review') + await waitNoAgent(ctx, id) + const [entry] = await ctx.subagents.listChildren(lead.id) + const task = await ctx.agentTeams.createTask(lead, { subject: 'Review', description: 'Review history' }) + const updated = await execute(ctx, lead, 'team_task_update', { + task_id: task.id, expected_revision: task.revision, action: 'reassign', owner: `lead/${entry!.name}`, + }) + expect(JSON.parse(text(updated))).toMatchObject({ ownerName: `lead/${entry!.name}` }) + expect(JSON.parse(text(await execute(ctx, lead, 'team_task_list', {})))) + .toMatchObject({ tasks: [{ ownerName: `lead/${entry!.name}` }] }) + expect((await execute(ctx, lead, 'team_task_update', { + task_id: task.id, expected_revision: 2, action: 'reassign', owner: `legacy:${id}`, + })).isError).toBe(true) + }) + + it.each([false, true])('rejects historical roster assignments without a readable continuable target (cataloged: %s)', async (cataloged) => { + const { ctx, lead } = await setup([]) + const id = SessionId('historical-worker') + for (const phase of ['provisioning', 'active'] as const) lead.session.append('team/member', { + version: 2, teamId: TeamId(lead.id), + member: { id, name: 'old-name', description: 'historical member', provider: 'spawn', context: 'fresh', phase }, + }) + if (cataloged) lead.session.append('subagent/catalog', { version: 0, childId: id, childCreatedAt: 1, mode: 'continuable', label: 'Old child' }) + const created = await ctx.agentTeams.createTask(lead, { subject: 'Review', description: 'Review history' }) + expect((await execute(ctx, lead, 'team_task_update', { + task_id: created.id, expected_revision: created.revision, action: 'reassign', + owner: cataloged ? 'lead/legacy:historical-worker' : 'legacy:historical-worker', + })).isError).toBe(true) + expect((await execute(ctx, lead, 'send_message', { name: 'legacy:historical-worker', message: 'hello' })).isError).toBe(true) + expect((await execute(ctx, lead, 'team_task_update', { + task_id: created.id, expected_revision: 2, action: 'reassign', owner: 'legacy:missing', + })).isError).toBe(true) + }) + it('removes and reinstalls every scoped registration across plugin HMR without stopping the child', async () => { const { ctx, lead, fiber } = await setup(['hang']) const childId = await spawn(ctx, lead, 'hmr-worker', 'wait') diff --git a/packages/experimental/tool-subagent-oar/README.i18n.yaml b/packages/experimental/tool-subagent-oar/README.i18n.yaml index a652d96c43..b7fffed28d 100644 --- a/packages/experimental/tool-subagent-oar/README.i18n.yaml +++ b/packages/experimental/tool-subagent-oar/README.i18n.yaml @@ -9,8 +9,8 @@ en: 520d9dc51e2b0142 zh: 8f332776243107a9 /deepseek-ai-dsh-experimental-tool-subagent-oar/summary: - en: 8adbaf2511d2c47f - zh: 1495f6bdb84a30ea + en: c04d4c7d89864380 + zh: 9c1724f34943c24a /deepseek-ai-dsh-experimental-tool-subagent-oar/table-of-contents: en: d152484eb41ac6b4 zh: 8d1e20b826450ffe @@ -21,8 +21,8 @@ en: 368805e5bcffeb0a zh: 9340ff5074ada9e1 /deepseek-ai-dsh-experimental-tool-subagent-oar/use-this-package/minimal-configuration: - en: 5b2b33c1d0fae20a - zh: 26bff1e013e2bfcd + en: c73dc23aaf8e0061 + zh: 62fecd115dfbd793 /deepseek-ai-dsh-experimental-tool-subagent-oar/understand-the-implementation: en: 3e8a9fffa9c5afd0 zh: 932b6d0b682208ba @@ -36,8 +36,8 @@ en: 8febbb031aa0452d zh: da3f9011631652da /deepseek-ai-dsh-experimental-tool-subagent-oar/model-experience/external-delegation-tool/what-the-model-sees: - en: 2e0f5274092f794b - zh: 83a14e5f7018717f + en: 74d769e3d3b38809 + zh: 73a11b00fe199679 /deepseek-ai-dsh-experimental-tool-subagent-oar/model-experience/external-delegation-tool/token-effect: en: 60d7ffec7f4dc858 zh: 09f286978df6b4ec diff --git a/packages/experimental/tool-subagent-oar/README.md b/packages/experimental/tool-subagent-oar/README.md index 991dfc0c3e..78048fd17e 100644 --- a/packages/experimental/tool-subagent-oar/README.md +++ b/packages/experimental/tool-subagent-oar/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -Use one `subagent_oar` tool to choose a configured external agent for each standalone text task. Configuration fixes the available runtime aliases and their providers; the model selects an alias without changing native arguments or permissions. Each task uses independent context and returns a child id, with a later completion notice delivered to the parent. +Use one `subagent_oar` tool to choose a configured external agent for each standalone text task. Configuration fixes the available runtime aliases and their providers; the model selects an alias without changing native arguments or permissions. Each task uses independent context and returns a full child path, with a later completion notice delivered to the parent. ## Table of Contents @@ -48,7 +48,7 @@ With `oar-codex` and `oar-claude` already registered, mount this row in the comp | `providers` | required | Nonempty map from model-visible runtime aliases to registered provider names | | `toolName` | `subagent_oar` | Tool name; every mounted instance needs a distinct name | -The [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-experimental-tool-subagent-oar) owns accepted fields and validation. A call requires `runtime`, `description`, and `prompt`; optional `cwd` selects the child directory, with relative paths resolved from the parent's current directory. The prompt must contain the context the child needs. Native installation, authentication, model settings, and permissions belong to the selected provider. +The [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-experimental-tool-subagent-oar) owns accepted fields and validation. A call requires a sibling-unique `name` segment, `runtime`, `description`, and `prompt`; optional `cwd` selects the child directory, with relative paths resolved from the parent's current directory. The prompt must contain the context the child needs. Native installation, authentication, model settings, and permissions belong to the selected provider. ----- @@ -80,7 +80,7 @@ The [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-exper #### What the model sees -The [tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-experimental-tool-subagent-oar) owns the description and argument schema. The `runtime` enum contains the configured aliases. After acceptance, the tool returns `started subagent `, where `` identifies the child. The shared subagent service later supplies the child's answer or failure notice. +The [tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-experimental-tool-subagent-oar) owns the description and argument schema. The `runtime` enum contains the configured aliases. After acceptance, the tool returns `started subagent `, where `` is the full child path. The shared subagent service later supplies the child's answer or failure notice. #### Token effect diff --git a/packages/experimental/tool-subagent-oar/README.zh.md b/packages/experimental/tool-subagent-oar/README.zh.md index 3213e82786..0d912a779e 100644 --- a/packages/experimental/tool-subagent-oar/README.zh.md +++ b/packages/experimental/tool-subagent-oar/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -使用一个 `subagent_oar` 工具,为每个独立文本任务选择已配置的外部智能体。配置固定可用的运行时别名及其提供方;模型选择别名,不能更改原生参数或权限。每个任务使用独立上下文并返回子任务 id,完成通知稍后送达父智能体。 +使用一个 `subagent_oar` 工具,为每个独立文本任务选择已配置的外部智能体。配置固定可用的运行时别名及其提供方;模型选择别名,不能更改原生参数或权限。每个任务使用独立上下文并返回子级完整路径,完成通知稍后送达父智能体。 ## 目录 @@ -48,7 +48,7 @@ kind: "package-reference" | `providers` | 必填 | 从模型可见运行时别名映射到已注册提供方名称的非空映射 | | `toolName` | `subagent_oar` | 工具名称;每个挂载实例需要不同名称 | -[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-experimental-tool-subagent-oar)定义接受的字段和校验规则。调用要求提供 `runtime`、`description` 和 `prompt`;可选的 `cwd` 选择子任务目录,相对路径从父智能体的当前目录解析。提示必须包含子任务需要的上下文。原生安装、认证、模型设置和权限由所选提供方负责。 +[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-experimental-tool-subagent-oar)定义接受的字段和校验规则。调用要求提供同级唯一的 `name` 片段、`runtime`、`description` 和 `prompt`;可选的 `cwd` 选择子任务目录,相对路径从父智能体的当前目录解析。提示必须包含子任务需要的上下文。原生安装、认证、模型设置和权限由所选提供方负责。 ----- @@ -80,7 +80,7 @@ kind: "package-reference" #### 模型看到什么 -[工具目录](../../../docs/tool-catalog.zh.md#deepseek-aidsh-experimental-tool-subagent-oar)定义描述和参数 schema。`runtime` 枚举包含配置的别名。接受任务后,工具返回 `started subagent `,其中 `` 标识子任务。共享子智能体服务稍后提供子任务的答复或失败通知。 +[工具目录](../../../docs/tool-catalog.zh.md#deepseek-aidsh-experimental-tool-subagent-oar)定义描述和参数 schema。`runtime` 枚举包含配置的别名。接受任务后,工具返回 `started subagent `,其中 `` 是子级完整路径。共享子智能体服务稍后提供子任务的答复或失败通知。 #### Token 影响 diff --git a/packages/experimental/tool-subagent-oar/src/index.ts b/packages/experimental/tool-subagent-oar/src/index.ts index a5ca008a7f..7e23d17fbb 100644 --- a/packages/experimental/tool-subagent-oar/src/index.ts +++ b/packages/experimental/tool-subagent-oar/src/index.ts @@ -36,10 +36,15 @@ export function apply(ctx: Context, config: Config): void { name: toolName, description: 'Delegate a self-contained task to a configured external agent. Choose its runtime for each call. ' + 'The child works in its own context and does not see this conversation. ' - + 'This tool returns a child id; a later completion notice includes its final answer. ' + + 'This tool returns a full agent path; a later completion notice includes its final answer. ' + 'The child does not accept follow-up messages. Start independent tasks together and continue useful work while they run. ' + 'When only waiting for their results remains, end your turn instead of polling.', parameters: { + name: { + type: 'string', + required: true, + description: 'A sibling-unique name segment: start with a lower-case letter and use lower-case letters, digits, underscores, or hyphens.', + }, runtime: { type: 'string', required: true, @@ -69,10 +74,10 @@ export function apply(ctx: Context, config: Config): void { additionalProperties: false, properties: { kind: { type: 'string', required: true, const: 'activation' }, - subagentId: { type: 'string', required: true }, + name: { type: 'string', required: true }, }, }, - render: (_args, value) => [{ type: 'text', text: `started subagent ${value.subagentId}` }], + render: (_args, value) => [{ type: 'text', text: `started subagent ${value.name}` }], }, /* jscpd:ignore-end */ isConcurrencySafe: () => true, @@ -86,6 +91,7 @@ export function apply(ctx: Context, config: Config): void { throw new Error('subagent_oar requires a one-shot provider with independent conversation context') } const activation = await ctx.subagents.startActivation({ + name: args.name, provider: providerName, label: args.description, request: { @@ -96,7 +102,7 @@ export function apply(ctx: Context, config: Config): void { signal: exec.signal, delivery: 'parent', }) - return { kind: 'activation' as const, subagentId: activation.childId } + return { kind: 'activation' as const, name: activation.name } }, presentCall: args => ({ card: 'generic', title: args.description, rawInput: args }), })) diff --git a/packages/experimental/tool-subagent-oar/tests/tool-subagent-oar.spec.ts b/packages/experimental/tool-subagent-oar/tests/tool-subagent-oar.spec.ts index a457588684..22b6053986 100644 --- a/packages/experimental/tool-subagent-oar/tests/tool-subagent-oar.spec.ts +++ b/packages/experimental/tool-subagent-oar/tests/tool-subagent-oar.spec.ts @@ -42,14 +42,14 @@ it('selects one configured provider per call and exposes only task inputs', asyn providers: { codex: 'codex-provider', claude: 'claude-provider', future: 'future-provider' }, }) const schema = ctx.tools.schemas().find(tool => tool.name === 'subagent_oar') - expect(schema?.parameters.required).toEqual(['runtime', 'description', 'prompt']) - expect(Object.keys(schema?.parameters.properties as Record).sort()).toEqual(['cwd', 'description', 'prompt', 'runtime']) + expect(schema?.parameters.required).toEqual(['name', 'runtime', 'description', 'prompt']) + expect(Object.keys(schema?.parameters.properties as Record).sort()).toEqual(['cwd', 'description', 'name', 'prompt', 'runtime']) expect(schema?.parameters.properties).toMatchObject({ runtime: { enum: ['codex', 'claude', 'future'] } }) for (const runtime of ['codex', 'claude', 'future']) { const result = await ctx.tools.execute({ agent: parent, name: 'subagent_oar', signal: test.signal, callId: ToolCallId(randomUUID()), - arguments: { runtime, description: `Use ${runtime}`, prompt: 'independent task' }, + arguments: { name: `child-${randomUUID()}`, runtime, description: `Use ${runtime}`, prompt: 'independent task' }, }) expect(result.isError, JSON.stringify(result.content)).toBe(false) expect(result.content).toHaveLength(1) @@ -62,7 +62,7 @@ it('selects one configured provider per call and exposes only task inputs', asyn expect(calls[0]?.request.parent).toBe(parent) expect(calls[0]?.request.cwd).toBe(process.cwd()) expect(ctx.tools.executionMode({ - name: 'subagent_oar', arguments: { runtime: 'codex', description: 'task', prompt: 'task' }, + name: 'subagent_oar', arguments: { name: `child-${randomUUID()}`, runtime: 'codex', description: 'task', prompt: 'task' }, callId: ToolCallId(randomUUID()), signal: test.signal, })).toEqual({ kind: 'parallel' }) await fiber.dispose() @@ -76,7 +76,7 @@ it('rejects unconfigured runtime names before starting any child', async (test) const result = await ctx.tools.execute({ agent: parent, name: 'subagent_oar', signal: test.signal, callId: ToolCallId(randomUUID()), - arguments: { runtime: 'unconfigured', description: 'task', prompt: 'task' }, + arguments: { name: `child-${randomUUID()}`, runtime: 'unconfigured', description: 'task', prompt: 'task' }, }) expect(result.isError).toBe(true) expect(calls).toEqual([]) @@ -89,7 +89,7 @@ it('fails visibly when the selected provider disappears', async (test) => { const result = await ctx.tools.execute({ agent: parent, name: 'subagent_oar', signal: test.signal, callId: ToolCallId(randomUUID()), - arguments: { runtime: 'missing', description: 'task', prompt: 'task' }, + arguments: { name: `child-${randomUUID()}`, runtime: 'missing', description: 'task', prompt: 'task' }, }) expect(result.isError).toBe(true) expect(calls).toEqual([]) @@ -113,7 +113,7 @@ it('requires a caller and independently scoped one-shot providers', async (test) start: async () => { throw new Error('must not run') }, }) await ctx.plugin(externalTool, { providers: { inherited: 'inherited' } }) - const args = { runtime: 'inherited', description: 'task', prompt: 'task' } + const args = { name: 'inspector', runtime: 'inherited', description: 'task', prompt: 'task' } const noParent = await ctx.tools.execute({ name: 'subagent_oar', arguments: args, callId: ToolCallId(randomUUID()), signal: test.signal, }) @@ -129,7 +129,7 @@ it('passes an explicit child directory and presents the task description', async const { ctx, parent, calls } = await setup() test.onTestFinished(() => ctx.fiber.dispose()) await ctx.plugin(externalTool, { providers: { codex: 'codex-provider' } }) - const args = { runtime: 'codex', description: 'Inspect child', prompt: 'task', cwd: 'packages' } + const args = { name: 'inspector', runtime: 'codex', description: 'Inspect child', prompt: 'task', cwd: 'packages' } expect(ctx.tools.get('subagent_oar')?.presentCall?.(args)).toEqual({ card: 'generic', title: 'Inspect child', rawInput: args, }) diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 03c3daa591..1431dc7fb3 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -3040,6 +3040,19 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ parameters: [{ name: 'spec', description: 'task, backend, and result recipient.' }], returns: 'activation with a message id only for local children.', }, + { + signature: 'nameOf(agent: Agent): string', + description: 'Derive an Agent\'s full path from creation ancestry and descriptor names.', + parameters: [{ name: 'agent', description: 'exact live Agent whose name is requested.' }], + returns: 'lead followed by named or legacy child segments.', + }, + { + signature: 'resolveName(caller: Agent, name: string, signal?: AbortSignal): Promise', + description: 'Resolve a full agent path without granting authority to act on its Agent.', + parameters: [{ name: 'caller', description: 'exact live Agent selecting the tree.' }, { name: 'name', description: 'a full path returned by creation or discovery.' }, { name: 'signal', description: 'cancellation during historical discovery.' }], + returns: 'the durable Session identity for the consuming operation to authorize.', + throws: ['when the path is absent or a required ancestor is unavailable.'], + }, { signature: 'rootOf(caller: Agent): Agent', description: 'Resolve the root shared by an Agent and its teammates.', @@ -3084,7 +3097,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ signature: 'listChildren(parentSessionId: SessionId, signal?: AbortSignal): Promise', description: 'Read the parent\'s durable direct-child catalog without loading or resuming an Agent. The service owns and releases the live-preferred Session observation.', parameters: [{ name: 'parentSessionId', description: 'parent whose direct children are requested.' }, { name: 'signal', description: 'cancellation forwarded to the Session query.' }], - returns: 'catalog children in parent event order.', + returns: 'committed catalog children in parent event order.', throws: ['{@link SubagentError} when query or catalog projection is unavailable.', 'SessionQueryError when the parent cannot be read or the query is cancelled.'], }, { @@ -7857,11 +7870,11 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SubagentActivation', - declaration: 'export interface SubagentActivation {\n readonly childId: SessionId;\n readonly messageId?: MessageId;\n readonly result: Promise;\n dispose(): Promise;\n}', + declaration: 'export interface SubagentActivation {\n readonly name: string;\n readonly childId: SessionId;\n readonly messageId?: MessageId;\n readonly result: Promise;\n dispose(): Promise;\n}', }, { name: 'SubagentActivationSpec', - declaration: 'export interface SubagentActivationSpec {\n readonly provider: string;\n readonly label: string;\n readonly childId?: SessionId;\n readonly request: Omit;\n readonly signal: AbortSignal;\n readonly delivery: \'parent\' | \'caller\';\n}', + declaration: 'export interface SubagentActivationSpec {\n readonly name: string;\n readonly provider: string;\n readonly label: string;\n readonly childId?: SessionId;\n readonly request: Omit;\n readonly signal: AbortSignal;\n readonly delivery: \'parent\' | \'caller\';\n}', }, { name: 'SubagentCapabilities', @@ -7869,7 +7882,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SubagentCatalogEntry', - declaration: 'export type SubagentCatalogEntry = {\n readonly id: SessionId;\n readonly createdAt: number;\n} & ({\n readonly mode: \'one-shot\';\n readonly label?: string;\n} | {\n readonly mode: \'external\';\n readonly label?: string;\n} | {\n readonly mode: \'continuable\';\n readonly label: string;\n} | {\n readonly mode: \'unknown\';\n readonly label?: string;\n});', + declaration: 'export type SubagentCatalogEntry = {\n readonly name?: string;\n readonly id: SessionId;\n readonly createdAt: number;\n} & ({\n readonly mode: \'one-shot\';\n readonly label?: string;\n} | {\n readonly mode: \'external\';\n readonly label?: string;\n} | {\n readonly mode: \'continuable\';\n readonly label: string;\n} | {\n readonly mode: \'unknown\';\n readonly label?: string;\n});', }, { name: 'SubagentCatalogRow', @@ -7889,7 +7902,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SubagentListEntry', - declaration: 'export type SubagentListEntry = SubagentCatalogRow & {\n readonly kind: \'child\';\n readonly hasChildren: boolean;\n} | {\n readonly kind: \'diagnostic\';\n readonly id: SessionId;\n readonly reason: \'corrupt\' | \'unsupported\' | \'unavailable\';\n};', + declaration: 'export type SubagentListEntry = {\n readonly name?: string;\n} & (SubagentCatalogRow & {\n readonly kind: \'child\';\n readonly hasChildren: boolean;\n} | {\n readonly kind: \'diagnostic\';\n readonly id: SessionId;\n readonly reason: \'corrupt\' | \'unsupported\' | \'unavailable\';\n});', }, { name: 'SubagentPromptReceipt', @@ -7929,7 +7942,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SubagentRuntime', - declaration: 'export class SubagentRuntime extends TypertRemoteService {\n static Config;\n static inject;\n constructor(ctx: Context, private config: Config);\n resolveMaxDepth(configured?: number | \'provider-managed\'): number | undefined;\n startActivation(spec: SubagentActivationSpec & {\n readonly childId: SessionId;\n }): Promise;\n startActivation(spec: SubagentActivationSpec): Promise;\n async startActivation(spec: SubagentActivationSpec): Promise;\n rootOf(caller: Agent): Agent;\n async waitForChildren(parent: Agent): Promise;\n async sendMessage(sender: Agent, targetId: SessionId, content: ContentBlock[], options: SubagentSendMessageOptions): Promise;\n interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void;\n async drainDescendants(parents: readonly Agent[]): Promise;\n async drainChildren(parent: Agent, childIds: readonly SessionId[]): Promise;\n listChildren(parentSessionId: SessionId, signal?: AbortSignal): Promise;\n listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise;\n @Remote(\'prompt\')\n async prompt(request: SubagentPromptRequest, signal: AbortSignal): Promise;\n @Remote(\'interruptByParent\')\n interruptByParent(childSessionId: SessionId, parentSessionId: SessionId /* …truncated — full shape in source */', + declaration: 'export class SubagentRuntime extends TypertRemoteService {\n static Config;\n static inject;\n constructor(ctx: Context, private config: Config);\n resolveMaxDepth(configured?: number | \'provider-managed\'): number | undefined;\n startActivation(spec: SubagentActivationSpec & {\n readonly childId: SessionId;\n }): Promise;\n startActivation(spec: SubagentActivationSpec): Promise;\n async startActivation(spec: SubagentActivationSpec): Promise;\n nameOf(agent: Agent): string;\n resolveName(caller: Agent, name: string, signal?: AbortSignal): Promise;\n rootOf(caller: Agent): Agent;\n async waitForChildren(parent: Agent): Promise;\n async sendMessage(sender: Agent, targetId: SessionId, content: ContentBlock[], options: SubagentSendMessageOptions): Promise;\n interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void;\n async drainDescendants(parents: readonly Agent[]): Promise;\n async drainChildren(parent: Agent, childIds: readonly SessionId[]): Promise;\n listChildren(parentSessionId: SessionId, signal?: AbortSignal): Promise;\n listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise;\n @Remote(\'prompt\')\n async prompt(request: SubagentPromptRequest, signal: AbortSignal): Promise { const start = async (parent: Agent, id: SessionId): Promise => { const gate = { entered: Promise.withResolvers(), release: Promise.withResolvers() } gates.set(id, gate) - const receipt = await ctx.subagents.startActivation({ + const receipt = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`, provider: 'local-test', childId: id, label: 'parked child', delivery: 'caller', signal: new AbortController().signal, request: { parent, prompt: [{ type: 'text', text: 'work' }] }, }) diff --git a/packages/sdk/server/tests/wait.spec.ts b/packages/sdk/server/tests/wait.spec.ts index 7da1b35f21..b147e366a1 100644 --- a/packages/sdk/server/tests/wait.spec.ts +++ b/packages/sdk/server/tests/wait.spec.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto' import { mountWorkingDirectoryFixture } from '../../../subagent/subagent/tests/working-directory-fixture.ts' /** SDK task completion retains live failures that have no durable terminal outcome. */ import { Context } from '@deepseek-ai/cordis' @@ -64,7 +65,7 @@ describe('SDK session/wait with parked child input', () => { let owner = parent let child = parent for (let level = 0; level < depth; level++) { - const started = await ctx.subagents.startActivation({ + const started = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`, provider: 'local-test', label: 'interruptible child', delivery: 'caller', signal: new AbortController().signal, request: { parent: owner, prompt: [{ type: 'text', text: 'work' }] }, }) @@ -116,7 +117,7 @@ describe('SDK session/wait failures', () => { prepareContinuable: () => Promise.resolve({}), }) const warnings = vi.spyOn(ctx.logger, 'warn') - const child = await ctx.subagents.startActivation({ + const child = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`, provider: 'local-test', label: 'child failure', delivery: 'caller', signal: new AbortController().signal, request: { parent, prompt: [{ type: 'text', text: 'delegated task' }] }, }) diff --git a/packages/session/session-format-v3-to-v4/README.i18n.yaml b/packages/session/session-format-v3-to-v4/README.i18n.yaml index 84f41abbe2..53ac0eb404 100644 --- a/packages/session/session-format-v3-to-v4/README.i18n.yaml +++ b/packages/session/session-format-v3-to-v4/README.i18n.yaml @@ -51,8 +51,8 @@ en: 610f35b8c44538a2 zh: 17e5c07f09783106 /deepseek-ai-dsh-session-format-v3-to-v4/native-v4-admission/lifecycle-and-reference-relationships: - en: bae15764867cf91c - zh: 09a56ed1a8c9622a + en: 514fac0b2e5ca2be + zh: f02c30f07d0ad245 /deepseek-ai-dsh-session-format-v3-to-v4/native-v4-admission/developer-changes-and-deferred-schemas: en: 471f0e087806057a zh: 76b2ee1f58118d71 diff --git a/packages/session/session-format-v3-to-v4/README.md b/packages/session/session-format-v3-to-v4/README.md index c5b33a53a6..2c11026870 100644 --- a/packages/session/session-format-v3-to-v4/README.md +++ b/packages/session/session-format-v3-to-v4/README.md @@ -245,7 +245,7 @@ Current common admission does not validate each user/tool/developer content bloc | `compaction/start`, `compaction/summary`, `compaction/end` | Match compaction id, source command, and the active turn context. Summary spans name exact current-surface nodes and exclude the protected head; successful completion has one summary. Inherited unfinished compactions expire at the end-seed marker. | | `compaction/prune` | Its span names exact current-surface nodes and excludes the protected head; it does not require a compaction transaction or its owner fields. | | Compact checkpoint replacement | Its `compact-checkpoint` source identifies the active compaction. | -| Native `subagent/catalog` | Validates own version-0/version-1/version-2 payloads after the inherited cut. Each child id must be unique. Native reads neither collect child logs nor compare their physical facts; inherited entries do not establish own membership. | +| Native `subagent/catalog` | Validates own version-0/version-1/version-2 child payloads after the inherited cut, including optional sibling name segments. Each child id must be unique. Native reads neither collect child logs nor compare their physical facts; inherited entries do not establish own membership. | | Inherited cut and delivery | Apply the marker, coordinate, and generation-ownership rules stated above. | These checks are generation-owned in [relationships.ts](src/relationships.ts). Full common message/envelope acceptance and plugin-owned message projections additionally use the installed Session; the exported V4 restorer alone is not a replacement for complete catalog restoration. diff --git a/packages/session/session-format-v3-to-v4/README.zh.md b/packages/session/session-format-v3-to-v4/README.zh.md index 87f5256f3c..15aff92337 100644 --- a/packages/session/session-format-v3-to-v4/README.zh.md +++ b/packages/session/session-format-v3-to-v4/README.zh.md @@ -245,7 +245,7 @@ System image 接纳要求非空 attachment id、PNG/JPEG/WebP/GIF MIME 类 | `compaction/start`、`compaction/summary`、`compaction/end` | 匹配 compaction id、源 command 和活动 turn 上下文。Summary 区间引用精确的当前表面节点且排除 protected head;成功完成需要一个 summary。继承的未完成 compaction 在 end-seed marker 处过期。 | | `compaction/prune` | 其区间引用精确的当前表面节点且排除 protected head;它不要求存在 compaction 事务或其所有者字段。 | | Compact checkpoint 替换 | 其 `compact-checkpoint` 来源标识活动 compaction。 | -| 原生 `subagent/catalog` | 校验继承截点之后的自身 version-0/version-1/version-2 载荷。每个子 id 必须唯一。原生读取不收集子日志,也不比较其物理事实;继承条目不建立自身成员关系。 | +| 原生 `subagent/catalog` | 校验继承切点之后自有的第 0/1/2 版子级载荷,包括可选的同级名字路径段。每个 child id 必须唯一。原生读取既不收集子日志,也不比较其物理事实;继承条目不建立自有成员关系。 | | 继承截点与 delivery | 应用上文的 marker、坐标及代际归属规则。 | 这些检查由 [relationships.ts](src/relationships.ts) 按代际拥有。完整的通用消息/信封接纳与插件拥有的消息投影还使用已安装 Session;单独的导出 V4 恢复器不能替代完整 catalog 恢复。 diff --git a/packages/session/session-format-v3-to-v4/tests/validation.spec.ts b/packages/session/session-format-v3-to-v4/tests/validation.spec.ts index 7142c6ab45..459dd6c4f0 100644 --- a/packages/session/session-format-v3-to-v4/tests/validation.spec.ts +++ b/packages/session/session-format-v3-to-v4/tests/validation.spec.ts @@ -129,6 +129,12 @@ describe('restoreReleasedV4Artifact', () => { data: { ...data, mode: 'one-shot' } }]), known)).toThrow('duplicate catalog child') }) + it('rejects catalog records that do not describe a direct child', () => { + const event = { type: 'subagent/catalog', seq: 0, time: 1, + data: { version: 3, kind: 'name', name: 'writer', childId: 'grandchild' } } + expect(() => restoreReleasedV4Artifact(artifact([event]), new Set([...KNOWN, 'subagent/catalog']))).toThrow() + }) + it('validates only the envelope in physical mode', () => { const unknown = conversation() unknown.splice(2, 0, { type: 'campaign/unknown', seq: 0, time: 1, data: { x: 1 } }) diff --git a/packages/subagent/subagent-acp/tests/fixtures/loader/mock-delegating-llm.ts b/packages/subagent/subagent-acp/tests/fixtures/loader/mock-delegating-llm.ts index d1e51f5ee5..07f52d9b42 100644 --- a/packages/subagent/subagent-acp/tests/fixtures/loader/mock-delegating-llm.ts +++ b/packages/subagent/subagent-acp/tests/fixtures/loader/mock-delegating-llm.ts @@ -14,7 +14,7 @@ class MockDelegatingAdapter extends LlmAdapter { : '' if (!options.messages.some(message => message.role === 'tool')) { - const args = JSON.stringify({ description: 'cwd probe', prompt: 'report your workspace' }) + const args = JSON.stringify({ name: 'loader-child', description: 'cwd probe', prompt: 'report your workspace' }) yield { type: 'block-start', index: 0, blockType: 'tool-call' } yield { type: 'tool-call-delta', index: 0, id: ToolCallId('call-delegate'), name: 'subagent', argumentsDelta: args } yield { type: 'block-end', index: 0, block: { type: 'tool-call', id: ToolCallId('call-delegate'), name: 'subagent', arguments: args } } diff --git a/packages/subagent/subagent-acp/tests/loader-composition.e2e.ts b/packages/subagent/subagent-acp/tests/loader-composition.e2e.ts index 7d32f04fee..922f166a60 100644 --- a/packages/subagent/subagent-acp/tests/loader-composition.e2e.ts +++ b/packages/subagent/subagent-acp/tests/loader-composition.e2e.ts @@ -43,7 +43,7 @@ function settlementText(events: SessionEvent[]): string[] { const notice = notices[0]! if (notice.type !== 'user/message' || notice.data.source.kind !== 'subagent-settled') throw new Error('missing completion notice') expect(results[0]!.data.message.content).toEqual([ - { type: 'text', text: `started subagent ${notice.data.source.senderSessionId}` }, + { type: 'text', text: 'started subagent lead/loader-child' }, ]) return notice.data.content.slice(1).filter(block => block.type === 'text').map(block => block.text) } diff --git a/packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts b/packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts index 26543fb219..7784c19243 100644 --- a/packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts +++ b/packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts @@ -79,23 +79,23 @@ describe('product-provider public Loader composition', () => { tools: [ { name: 'subagent_codex', - parameterNames: ['cwd', 'description', 'prompt'], - required: ['description', 'prompt'], + parameterNames: ['cwd', 'description', 'name', 'prompt'], + required: ['name', 'description', 'prompt'], }, { name: 'subagent_claude_code', - parameterNames: ['cwd', 'description', 'prompt'], - required: ['description', 'prompt'], + parameterNames: ['cwd', 'description', 'name', 'prompt'], + required: ['name', 'description', 'prompt'], }, { name: 'subagent_claude_primary', - parameterNames: ['cwd', 'description', 'prompt'], - required: ['description', 'prompt'], + parameterNames: ['cwd', 'description', 'name', 'prompt'], + required: ['name', 'description', 'prompt'], }, { name: 'subagent_claude_secondary', - parameterNames: ['cwd', 'description', 'prompt'], - required: ['description', 'prompt'], + parameterNames: ['cwd', 'description', 'name', 'prompt'], + required: ['name', 'description', 'prompt'], }, ], jobTools: ['job_kill', 'job_list', 'job_output'], diff --git a/packages/subagent/subagent-codex/tests/loader-composition.e2e.ts b/packages/subagent/subagent-codex/tests/loader-composition.e2e.ts index a9972381ae..ad39873df1 100644 --- a/packages/subagent/subagent-codex/tests/loader-composition.e2e.ts +++ b/packages/subagent/subagent-codex/tests/loader-composition.e2e.ts @@ -69,18 +69,18 @@ describe('Codex provider public Loader composition', () => { tools: [ { name: 'subagent_codex', - parameterNames: ['cwd', 'description', 'prompt'], - required: ['description', 'prompt'], + parameterNames: ['cwd', 'description', 'name', 'prompt'], + required: ['name', 'description', 'prompt'], }, { name: 'subagent_codex_primary', - parameterNames: ['cwd', 'description', 'prompt'], - required: ['description', 'prompt'], + parameterNames: ['cwd', 'description', 'name', 'prompt'], + required: ['name', 'description', 'prompt'], }, { name: 'subagent_codex_secondary', - parameterNames: ['cwd', 'description', 'prompt'], - required: ['description', 'prompt'], + parameterNames: ['cwd', 'description', 'name', 'prompt'], + required: ['name', 'description', 'prompt'], }, ], jobTools: ['job_kill', 'job_list', 'job_output'], diff --git a/packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/mock-delegating-llm.ts b/packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/mock-delegating-llm.ts index dee4ac0f45..c11bb25269 100644 --- a/packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/mock-delegating-llm.ts +++ b/packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/mock-delegating-llm.ts @@ -32,7 +32,7 @@ class MockDelegatingAdapter extends LlmAdapter { const selectedRoute = process.env.DSH_TEST_CHILD_DEFAULT_ROUTE === '1' ? { reasoning_effort: 'max' } : { provider: 'mock', model: 'mock-routed', reasoning_effort: 'max' } - const args = JSON.stringify({ + const args = JSON.stringify({ name: 'loader-child', description: 'route probe', prompt: 'report your route and workspace', ...selectedRoute, diff --git a/packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/nested-mock-llm.ts b/packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/nested-mock-llm.ts index 25748da58a..d20609db83 100644 --- a/packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/nested-mock-llm.ts +++ b/packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/nested-mock-llm.ts @@ -20,7 +20,7 @@ class NestedAdapter extends LlmAdapter { } answer = 'nested child result' } else if (!options.messages.some(message => message.role === 'tool')) { - const args = JSON.stringify({ description: 'nested fork', prompt: 'nested-child-task' }) + const args = JSON.stringify({ name: 'loader-child', description: 'nested fork', prompt: 'nested-child-task' }) const id = ToolCallId('nested-fork') yield { type: 'block-start', index: 0, blockType: 'tool-call' } yield { type: 'tool-call-delta', index: 0, id, name: 'subagent_fork', argumentsDelta: args } diff --git a/packages/subagent/subagent-dsh-sdk/tests/loader-composition.e2e.ts b/packages/subagent/subagent-dsh-sdk/tests/loader-composition.e2e.ts index 3e8b4c2b4d..210ae3cfcb 100644 --- a/packages/subagent/subagent-dsh-sdk/tests/loader-composition.e2e.ts +++ b/packages/subagent/subagent-dsh-sdk/tests/loader-composition.e2e.ts @@ -47,7 +47,7 @@ function settlementText(events: SessionEvent[]): string[] { const notice = notices[0]! if (notice.type !== 'user/message' || notice.data.source.kind !== 'subagent-settled') throw new Error('missing completion notice') expect(results[0]!.data.message.content).toEqual([ - { type: 'text', text: `started subagent ${notice.data.source.senderSessionId}` }, + { type: 'text', text: 'started subagent lead/loader-child' }, ]) return notice.data.content.slice(1).filter(block => block.type === 'text').map(block => block.text) } diff --git a/packages/subagent/subagent/README.i18n.yaml b/packages/subagent/subagent/README.i18n.yaml index 73f8196ff8..74efd28771 100644 --- a/packages/subagent/subagent/README.i18n.yaml +++ b/packages/subagent/subagent/README.i18n.yaml @@ -18,17 +18,17 @@ en: 924b8c15f9529028 zh: 9fb0c399b5a79e3e /deepseek-ai-dsh-subagent/use-this-package/enabling-delegation: - en: 025605eddae02478 - zh: 5659cba4340735d8 + en: 998f10b308f8196e + zh: c19cd3fa6e6a7832 /deepseek-ai-dsh-subagent/use-this-package/delegation-settings: en: 86c982223fab1a3a zh: 3d220d8ed84eab9a /deepseek-ai-dsh-subagent/use-this-package/activation-capacity: - en: 4fc839e4615959e6 - zh: 39948945b77ab8fa + en: 985ceb258fbab1e4 + zh: fef5a066f7f8bf43 /deepseek-ai-dsh-subagent/use-this-package/local-and-external-children: - en: 013644290e74493e - zh: 05014c3c3773edeb + en: b5531f2976d64dd2 + zh: a65ff6ace501a94f /deepseek-ai-dsh-subagent/use-this-package/child-presets: en: a740e5e9bdd661d9 zh: 712a5458fc8b3615 @@ -51,8 +51,8 @@ en: 196f7b89e7fad0fb zh: 87d2f8f7257ff145 /deepseek-ai-dsh-subagent/understand-the-implementation/activation-settlement: - en: 166d385d83a23e2f - zh: 8ea3de8baea043e4 + en: 314301c4854ed0b5 + zh: 966e0d333888eec0 /deepseek-ai-dsh-subagent/understand-the-implementation/restoring-child-composition: en: 843f9f4319ca2ae3 zh: 218bf355816d4ff6 diff --git a/packages/subagent/subagent/README.md b/packages/subagent/subagent/README.md index bdd9bce617..5aad61e30c 100644 --- a/packages/subagent/subagent/README.md +++ b/packages/subagent/subagent/README.md @@ -42,7 +42,7 @@ Mount the service with `@deepseek-ai/dsh-session-projection`, a provider, and th toolName: subagent ``` -The tool returns a child session id immediately. The parent receives a completion notice when the background activation settles. Mounting the service alone enables no delegation. +The tool requires a sibling-unique name segment and immediately returns the full child path. The parent receives a completion notice when the background activation settles. Mounting the service alone enables no delegation. ### Delegation settings @@ -56,9 +56,12 @@ The current `maxActiveSubagents` value is sampled before every new or cold-resum At capacity, creation or cold resume rejects with `ACTIVATION_LIMIT_REACHED` (browser prompts receive `subagent/delivery-unavailable`): wait for a child to finish or continue using the existing agents. Admission does not queue, because a parent waiting for descendants must not wait for its own occupied slot. Slots are process-local and do not constrain cumulative Session history or token usage. + ### Local and external children -`startActivation({ provider, label, request, signal, delivery })` returns `{ childId, messageId?, result, dispose }`. Supplying `childId` selects a local backend and guarantees `messageId`. Local children accept later messages and can cold-resume; external children expose one execution and reject continuation. `request.agentOptions` overrides supported child model settings. `delivery: 'parent'` supplies the model-facing completion notice; `delivery: 'caller'` lets workflows await the result without a parent notice or initial return guidance. Local parent delivery requires persistence; caller delivery can use an ephemeral local session. Cold resume requires persistence and Session query. +`startActivation({ name, provider, label, request, signal, delivery })` returns `{ childId, name, messageId?, result, dispose }`. Supplying `childId` selects a local backend and guarantees `messageId`. Local children accept later messages and can cold-resume; external children expose one execution and reject continuation. `request.agentOptions` overrides supported child model settings. `delivery: 'parent'` supplies the model-facing completion notice; `delivery: 'caller'` lets workflows await the result without a parent notice or initial return guidance. Local parent delivery requires persistence; caller delivery can use an ephemeral local session. Cold resume requires persistence and Session query. + +Agent names are immutable segments unique among one parent’s direct children. Creation accepts a lower-case letter followed by lower-case letters, digits, underscores, or hyphens. The returned `name` is a full path: `lead`, `lead/reviewer`, or `lead/explore/alice`. Each parent’s catalog and each local child’s descriptor retain the segment; ancestry supplies the prefix. Pending reservations are scoped to the live parent and released when creation returns or fails. Cataloged names remain taken after settlement. `nameOf(agent)` derives the path from creation ancestry; `resolveName()` follows only the catalogs along a full path and grants no operation authority. Unrelated unreadable branches do not block creation or lookup. Historical unnamed children use `legacy:` as their path segment. ### Child presets @@ -120,7 +123,7 @@ The manager reserves child identity and capacity, materializes a local Agent or A local Agent failure without a subsequently committed `turn/end` settles as `error` independently of its descendants. Unclaimed input cannot keep that failed activation resident. A later committed turn ending supersedes the live failure and supplies the recovered outcome. -Successful local creation appends a `subagent/catalog` fact to the parent Session regardless of result delivery. External creation appends a catalog fact only with `delivery: 'parent'`; caller-owned external executions retain their caller's membership records. External entries use `mode: 'external'` to identify children without a local Session. Catalog entries record membership once at creation and carry no execution status. Complete results go to the caller or the parent completion notice. Projections exclude inherited facts. Direct lists read one parent projection, so completed local workflow children remain discoverable and can be opened or continued. Catalog payload v0 records local modes; v1 also accepts unknown mode; v2 records external executions. Readers support all three versions. Historical migration appends a v1 `subagent/catalog` from a readable child header when its descriptor is unavailable; local creation retains v0. Its `mode: 'unknown'` projection keeps the child visible without claiming continuation support; an existing complete entry remains authoritative. +Local and external creation appends a direct-child `subagent/catalog` fact to the parent Session regardless of result delivery. That append commits the child’s name; nested children require no additional root record. A failed append releases the pending name and disposes the activation. A local descriptor can remain, but a named child can resume only when its direct parent catalog contains the same name and id. After the append, execution or notification failure does not release the name. Persistence flush remains separate from the synchronous append. Historical unnamed continuation retains its existing behavior. ### Restoring child composition diff --git a/packages/subagent/subagent/README.zh.md b/packages/subagent/subagent/README.zh.md index 2ab0b4a033..640eb6cf2e 100644 --- a/packages/subagent/subagent/README.zh.md +++ b/packages/subagent/subagent/README.zh.md @@ -42,7 +42,7 @@ kind: "package-reference" toolName: subagent ``` -工具立即返回子会话 ID,后台 activation 结算后父级收到完成通知。仅挂载服务本身不会启用委派。 +工具要求同级唯一的名字片段,并立即返回子级完整路径。后台 activation 结束时,父级收到完成通知。只挂载服务不会启用委派。 ### 委派设置 @@ -56,9 +56,12 @@ kind: "package-reference" 容量耗尽时,新建或冷恢复以 `ACTIVATION_LIMIT_REACHED` 拒绝(浏览器消息返回 `subagent/delivery-unavailable`):等待子代理完成,或继续使用现有代理。接纳不会排队,避免等待后代的父代理又等待自己占用的名额。名额仅存在于当前进程,不限制累计 Session 历史或 token 用量。 + ### 本地与外部子级 -`startActivation({ provider, label, request, signal, delivery })` 返回 `{ childId, messageId?, result, dispose }`。提供 `childId` 会选择本地后端,并保证返回 `messageId`。本地子级接受后续消息并支持冷恢复;外部子级只暴露一次执行,拒绝继续输入。`request.agentOptions` 覆盖后端支持的子级模型设置。`delivery: 'parent'` 提供面向模型的完成通知;`delivery: 'caller'` 让工作流等待结果,不发送父级通知,也不附加初始返回指导。本地父级投递需要持久化,调用方投递可以使用临时本地会话;冷恢复需要持久化与 Session 查询服务。 +`startActivation({ name, provider, label, request, signal, delivery })` 返回 `{ childId, name, messageId?, result, dispose }`。提供 `childId` 会选择本地后端,并保证返回 `messageId`。本地子级接受后续消息,支持冷恢复;外部子级只执行一次,拒绝继续执行。`request.agentOptions` 覆盖受支持的子级模型设置。`delivery: 'parent'` 提供面向模型的完成通知;`delivery: 'caller'` 允许 workflow 等待结果,不发送父级通知或初始返回指引。本地 parent 交付要求持久化;caller 交付可使用临时本地会话。冷恢复要求持久化和 Session query。 + +agent 名字是不可变的路径段,只在同一父级的直接子级中唯一。创建时接受以小写字母开头、后续只含小写字母、数字、下划线或连字符的名字。返回的 `name` 是完整路径:`lead`、`lead/reviewer` 或 `lead/explore/alice`。每个父级的 catalog 和本地子级自身的 descriptor 保留路径段;祖先关系提供前缀。临时名字预留属于在线父级,在创建返回或失败时释放。已写入 catalog 的名字在执行结束后仍被占用。`nameOf(agent)` 从创建祖先关系推导路径;`resolveName()` 只沿完整路径读取 catalog,不授予操作权限。无关的不可读分支不阻止创建或查找。历史无名子级使用 `legacy:` 作为路径段。 ### 子级 preset @@ -120,7 +123,7 @@ kind: "package-reference" 本地 Agent 发生失败且后续没有成功提交 `turn/end` 时,会独立于后代以 `error` 结算。未认领的输入不会使该失败 activation 一直驻留。后续成功提交的轮次结束记录会取代实时失败,提供恢复后的结果。 -本地创建成功后,无论结果交付给谁,都会向父 Session 追加 `subagent/catalog` 事实。外部创建仅在使用 `delivery: 'parent'` 时追加目录事实;调用方拥有的外部执行保留调用方自己的成员记录。外部条目以 `mode: 'external'` 标记没有本地 Session 的子级。目录仅在创建时记录一次成员关系,不携带执行状态。完整结果交给调用方或父级完成通知。投影排除继承事实。直接列表读取一个父级投影,因此已完成的本地工作流子级仍可被发现、打开或继续。Catalog 载荷 v0 记录本地模式,v1 还接受未知模式,v2 记录外部执行,读取器支持三版。历史迁移在 descriptor 不可用时根据可读子 header 追加 v1 `subagent/catalog`;本地创建保留 v0。其 `mode: 'unknown'` 投影让子会话保持可见,但不表示支持继续执行;已有完整条目仍具有权威性。 +本地与外部创建均向父 Session 追加直接子级 `subagent/catalog` 事实,不受结果交付方式影响。这次追加提交子级名字;嵌套子级无需额外的根记录。追加失败会释放临时名字并清理 activation。本地 descriptor 可能残留,但命名子级只有在直接父级 catalog 含相同名字与 id 时才能恢复。追加成功后,执行或通知失败不会释放名字。持久化 flush 仍独立于同步追加。历史无名子级的继续执行保留原有行为。 ### 恢复子级组装 diff --git a/packages/subagent/subagent/src/activation.ts b/packages/subagent/subagent/src/activation.ts index cdba5d4d0c..4908a5efd2 100644 --- a/packages/subagent/subagent/src/activation.ts +++ b/packages/subagent/subagent/src/activation.ts @@ -40,6 +40,8 @@ export class ActivationPool { * The manager owns admission, execution handles, and resource release. */ interface ActivationBase { + /** Immutable model-facing name retained through teardown. */ + readonly name: string /** Return this epoch's slot after its handle has finished disposal. */ readonly releaseSlot: () => void /** The durable child this Activation is an epoch of. */ diff --git a/packages/subagent/subagent/src/catalog.ts b/packages/subagent/subagent/src/catalog.ts index 416625c594..b346b600e3 100644 --- a/packages/subagent/subagent/src/catalog.ts +++ b/packages/subagent/subagent/src/catalog.ts @@ -15,6 +15,7 @@ import type { SessionLogOffset, } from '@deepseek-ai/dsh-session' import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection' +import { subagentNameSchema } from './descriptor.ts' import type { SubagentCatalogEntry } from './projection-types.ts' /** Catalog payload version emitted by local child creation. */ @@ -24,9 +25,11 @@ type KnownCatalogMode = | { readonly mode: 'one-shot'; readonly label?: string } | { readonly mode: 'continuable'; readonly label: string } -/** Parent catalog v0 records local modes, v1 adds unknown children, and v2 records external executions. */ +/** Child discovery records: v0 local modes, v1 unknown children, and v2 external executions. */ export type SubagentCatalogEvent = & { + /** Immutable sibling name; absent in older catalogs. */ + readonly name?: string readonly childId: SessionId readonly childCreatedAt: number } & ( @@ -54,6 +57,7 @@ export interface SubagentCatalogState { const sessionIdSchema = z.string() as unknown as z.ZodType const oneShotCatalogSchema = z.object({ version: z.union([z.literal(0), z.literal(1)]), + name: subagentNameSchema.optional(), childId: sessionIdSchema, childCreatedAt: z.number().int().nonnegative(), mode: z.literal('one-shot'), @@ -61,6 +65,7 @@ const oneShotCatalogSchema = z.object({ }).strict() const continuableCatalogSchema = z.object({ version: z.union([z.literal(0), z.literal(1)]), + name: subagentNameSchema.optional(), childId: sessionIdSchema, childCreatedAt: z.number().int().nonnegative(), mode: z.literal('continuable'), @@ -102,7 +107,7 @@ const stateSchema: z.ZodType = z.object({ * @param state - parent catalog fold state. * @returns current direct-child rows in parent catalog event order. */ -function subagentCatalogEntries(state: SubagentCatalogState): SubagentCatalogEntry[] { +export function subagentCatalogEntries(state: SubagentCatalogState): SubagentCatalogEntry[] { const entries: SubagentCatalogEntry[] = [] for (const data of iterateChunkedList(state.head)) { const { version: _version, childId, childCreatedAt, ...descriptor } = data @@ -120,7 +125,7 @@ export const subagentCatalogProjectionDefinition = { if (event.type !== 'subagent/catalog' || event.seq < state.inheritedEventCount) return state return { ...state, head: appendChunkedList(state.head, eventDataSchema.parse(event.data)) } }, - stateVersion: 6, + stateVersion: 9, wire: { viewSchema, view: subagentCatalogEntries }, } satisfies ProjectionDefinition<'subagentCatalog', SubagentCatalogState> @@ -133,7 +138,7 @@ export const subagentCatalogProjectionDefinition = { export function establishCatalogChild( parent: Session, child: SessionHeader, - descriptor: { readonly mode: 'continuable'; readonly label: string }, + descriptor: { readonly mode: 'continuable'; readonly label: string; readonly name?: string }, ): void { parent.append('subagent/catalog', { version: SUBAGENT_CATALOG_VERSION, @@ -141,6 +146,7 @@ export function establishCatalogChild( childCreatedAt: child.createdAt, mode: descriptor.mode, label: descriptor.label, + ...descriptor.name === undefined ? {} : { name: descriptor.name }, }) } @@ -152,13 +158,15 @@ const EXTERNAL_SUBAGENT_CATALOG_VERSION = 2 * @param parent - durable direct parent receiving the discovery fact. * @param childId - identity returned by the external provider. * @param label - display label assigned by the caller. + * @param name - immutable sibling name assigned at creation. */ -export function establishExternalCatalogChild(parent: Session, childId: SessionId, label: string): void { +export function establishExternalCatalogChild(parent: Session, childId: SessionId, label: string, name: string): void { parent.append('subagent/catalog', { version: EXTERNAL_SUBAGENT_CATALOG_VERSION, childId, childCreatedAt: Date.now(), mode: 'external', label, + name, }) } diff --git a/packages/subagent/subagent/src/continuation-messages.ts b/packages/subagent/subagent/src/continuation-messages.ts index a5f89b15c8..5fd4649f3c 100644 --- a/packages/subagent/subagent/src/continuation-messages.ts +++ b/packages/subagent/subagent/src/continuation-messages.ts @@ -56,15 +56,17 @@ function agentMessageSource(sender: Agent): AgentMessageSource { * Build the model-visible and durable representation of one adjacent-Agent message. * @param sender - exact live Agent that authored the message. * @param content - model-visible message blocks supplied by the sender. + * @param senderName - full agent path shown to the recipient. * @returns the durable user-message representation delivered to the recipient. */ export function createAgentMessage( sender: Agent, content: ContentBlock[], + senderName: string, ): ReturnType { return createUserMessage({ content: [ - { type: 'text' as const, text: `Agent ${sender.id} sent a message: ` }, + { type: 'text' as const, text: `Agent ${senderName} sent a message: ` }, ...content, ], source: agentMessageSource(sender), @@ -73,23 +75,25 @@ export function createAgentMessage( /** * Append adjacent-Agent return guidance to a continuable child's initial task. - * @param parentId - durable parent session id named in the guidance. + * @param parentName - name of the direct parent. + * @param childName - name assigned to this child. * @param prompt - initial model-visible task blocks. * @returns task blocks followed by the continuable return guidance. */ export function withContinuableReturnGuidance( - parentId: SessionId, + parentName: string, + childName: string, prompt: ContentBlock[], ): ContentBlock[] { - const encodedParentId = JSON.stringify(parentId) + const encodedParentName = JSON.stringify(parentName) return [ ...prompt, { type: 'text', - text: `Your parent agent id is ${encodedParentId}. Your final answer is delivered automatically if your ` + text: `Your name is ${JSON.stringify(childName)}. Your parent agent name is ${encodedParentName}. Your final answer is delivered automatically if your ` + 'parent is still online. Include the result and relevant evidence in that answer; the parent does not ' + 'receive your full transcript, tool output, or reasoning. Use ' - + `send_message({ agent_id: ${encodedParentId}, message: "" }) when a finding changes what the ` + + `send_message({ name: ${encodedParentName}, message: "" }) when a finding changes what the ` + 'parent should do next; sending a message does not end your turn.', }, ] @@ -98,13 +102,13 @@ export function withContinuableReturnGuidance( /** * One line telling a parent that a background child is finished and why, in * the parent's own task vocabulary. - * @param childId - the durable child the parent knows by id. + * @param childName - the immutable name the parent uses for the child. * @param stopReason - how the child's last ordinary turn ended. * @param continuable - whether the child accepts a later message. * @returns the model-facing opening line of the settlement notice. */ -function settlementSummary(childId: SessionId, stopReason: SubagentResult['stopReason'], continuable: boolean): string { - const subject = `Background subagent ${childId}` +function settlementSummary(childName: string, stopReason: SubagentResult['stopReason'], continuable: boolean): string { + const subject = `Background subagent ${childName}` switch (stopReason) { case 'completed': return continuable @@ -130,17 +134,19 @@ function settlementSummary(childId: SessionId, stopReason: SubagentResult['stopR /** * Build the runtime-owned settlement notice from the child's nonempty closing text. - * @param childId - durable child session id named in the notice. + * @param childId - durable child identity retained in message attribution. + * @param childName - immutable name shown in the notice. * @param terminal - recorded terminal state for the settled Activation. * @param continuable - whether the child can accept another task. * @returns the durable user-message representation delivered to the parent. */ export function createSettlementMessage( childId: SessionId, + childName: string, terminal: SubagentResult, continuable = true, ): ReturnType { - const summary = settlementSummary(childId, terminal.stopReason, continuable) + const summary = settlementSummary(childName, terminal.stopReason, continuable) // Parent providers receive this notice as a user message and may reject // nontext assistant blocks. Keep this conversion local so SDK/UI consumers // retain the complete child output. diff --git a/packages/subagent/subagent/src/control-types.ts b/packages/subagent/subagent/src/control-types.ts index 73137dcffb..4f5566365e 100644 --- a/packages/subagent/subagent/src/control-types.ts +++ b/packages/subagent/subagent/src/control-types.ts @@ -56,7 +56,10 @@ export type SubagentCatalogRow = * One recursive catalog row. Traversal may also return diagnostics for * unknown catalog modes or unreadable child catalogs. */ -export type SubagentListEntry = +export type SubagentListEntry = { + /** Immutable name from the parent catalog; absent for older children. */ + readonly name?: string +} & ( | SubagentCatalogRow & { readonly kind: 'child' /** Whether the child catalog contains a direct child. */ @@ -74,6 +77,7 @@ export type SubagentListEntry = */ readonly reason: 'corrupt' | 'unsupported' | 'unavailable' } +) /** Durable parent/child browsing address; unknown mode is resolved when child history is read. */ export type SubagentAddress = diff --git a/packages/subagent/subagent/src/descriptor.ts b/packages/subagent/subagent/src/descriptor.ts index a7469c664d..044f730061 100644 --- a/packages/subagent/subagent/src/descriptor.ts +++ b/packages/subagent/subagent/src/descriptor.ts @@ -21,6 +21,7 @@ * @module @deepseek-ai/dsh-subagent/descriptor */ +import { z } from 'zod' import { snapshotJsonValue } from '@deepseek-ai/dsh-util-values' import type { SessionEvent } from '@deepseek-ai/dsh-session' import type { ReasoningEffortId } from '@deepseek-ai/dsh-llm' @@ -47,8 +48,13 @@ declare module '@deepseek-ai/dsh-session/types' { */ export const SUBAGENT_DESCRIPTOR_VERSION = 3 +/** Persisted and requested sibling names, each containing one path segment. */ +export const subagentNameSchema = z.string().regex(/^[a-z][a-z0-9_-]*$/) + /** Fields shared by every supported `subagent/descriptor` payload. */ interface SubagentDescriptorBase { + /** Immutable sibling name; absent in older descriptors. */ + readonly name?: string /** Descriptor format version ({@link SUBAGENT_DESCRIPTOR_VERSION}). */ readonly version: number /** Whether the child is a terminal one-shot run or a resumable conversation. */ @@ -98,6 +104,7 @@ const DESCRIPTOR_BASE_KEYS = [ 'mode', 'provider', 'label', + 'name', ] as const const ONE_SHOT_DESCRIPTOR_KEYS = new Set(DESCRIPTOR_BASE_KEYS) const CONTINUABLE_DESCRIPTOR_KEYS = new Set([ @@ -184,6 +191,7 @@ function parseSubagentDescriptor(value: unknown): SubagentDescriptorData | undef mode === 'one-shot' ? ONE_SHOT_DESCRIPTOR_KEYS : CONTINUABLE_DESCRIPTOR_KEYS, 'payload', ) + const name = value['name'] === undefined ? undefined : subagentNameSchema.parse(value['name']) const provider = value['provider'] if (typeof provider !== 'string') { throw new Error('persisted subagent descriptor provider must be a string') @@ -194,6 +202,7 @@ function parseSubagentDescriptor(value: unknown): SubagentDescriptorData | undef version: SUBAGENT_DESCRIPTOR_VERSION, mode, provider, + ...name === undefined ? {} : { name }, ...label !== undefined ? { label } : {}, } } @@ -213,6 +222,7 @@ function parseSubagentDescriptor(value: unknown): SubagentDescriptorData | undef mode, provider, label, + ...name === undefined ? {} : { name }, ...agentProvider !== undefined ? { agentProvider } : {}, ...agentModel !== undefined ? { agentModel } : {}, ...agentReasoningEffort !== undefined ? { agentReasoningEffort } : {}, @@ -234,6 +244,7 @@ export function snapshotSubagentDescriptor( version: SUBAGENT_DESCRIPTOR_VERSION, mode: input.mode, provider: input.provider, + ...input.name === undefined ? {} : { name: input.name }, label: input.label, ...input.agentProvider !== undefined ? { agentProvider: input.agentProvider } : {}, ...input.agentModel !== undefined ? { agentModel: input.agentModel } : {}, diff --git a/packages/subagent/subagent/src/index.ts b/packages/subagent/subagent/src/index.ts index 50e0b4b3e6..99c9057654 100644 --- a/packages/subagent/subagent/src/index.ts +++ b/packages/subagent/subagent/src/index.ts @@ -76,7 +76,7 @@ import { installSubagentArchiveAdmission } from './archive-admission.ts' import type { SubagentCatalogEntry } from './projection-types.ts' import { subagentIdentityProjectionDefinition, subagentTimingProjectionDefinition } from './projection.ts' import type { IdentityState } from './projection.ts' -import { subagentCatalogProjectionDefinition } from './catalog.ts' +import { subagentCatalogProjectionDefinition, subagentCatalogEntries, type SubagentCatalogState } from './catalog.ts' import { deliverSubagentPrompt } from './internal.ts' export type {} from './catalog.ts' @@ -202,6 +202,7 @@ export class SubagentRuntime extends TypertRemoteService { static inject = ['workingDirectory', 'sessionProjections'] private providers = new Map() private manager: SubagentManager | undefined + private catalogProjectionHandle!: ProjectionHandle private identityProjectionHandle!: ProjectionHandle /** * The contained lifecycle-edge publisher. Built here because scoped dispatch @@ -215,6 +216,7 @@ export class SubagentRuntime extends TypertRemoteService { this.emitLifecycle = createLifecycleEmitter(this.ctx, parent => scopeTarget(this, parent)) ctx.inject(['agents'], (childCtx: Context) => { const manager = new SubagentManager(childCtx, { + catalogOf: session => subagentCatalogEntries(this.catalogProjectionHandle.stateOf(session)), identityOf: session => this.identityProjectionHandle.stateOf(session).identity, startExternal: (name, request) => { const provider = this.expectProvider(name) as SubagentProvider & Required> @@ -236,7 +238,7 @@ export class SubagentRuntime extends TypertRemoteService { protected async [Service.init](): Promise { const { sessionProjections } = this.ctx - await sessionProjections.register(subagentCatalogProjectionDefinition) + this.catalogProjectionHandle = await sessionProjections.register(subagentCatalogProjectionDefinition) await sessionProjections.register(subagentTimingProjectionDefinition) this.identityProjectionHandle = await sessionProjections.register(subagentIdentityProjectionDefinition) } @@ -290,6 +292,27 @@ export class SubagentRuntime extends TypertRemoteService { : manager.startLocal(spec) } + /** + * Derive an Agent's full path from creation ancestry and descriptor names. + * @param agent - exact live Agent whose name is requested. + * @returns lead followed by named or legacy child segments. + */ + nameOf(agent: Agent): string { + return this.requireManager().names.nameOf(agent) + } + + /** + * Resolve a full agent path without granting authority to act on its Agent. + * @param caller - exact live Agent selecting the tree. + * @param name - a full path returned by creation or discovery. + * @param signal - cancellation during historical discovery. + * @returns the durable Session identity for the consuming operation to authorize. + * @throws when the path is absent or a required ancestor is unavailable. + */ + resolveName(caller: Agent, name: string, signal?: AbortSignal): Promise { + return this.requireManager().names.resolve(caller, name, signal) + } + /** * Resolve the root shared by an Agent and its teammates. * @param caller - exact live Agent whose subagent ancestry selects the tree. @@ -412,7 +435,7 @@ export class SubagentRuntime extends TypertRemoteService { * The service owns and releases the live-preferred Session observation. * @param parentSessionId - parent whose direct children are requested. * @param signal - cancellation forwarded to the Session query. - * @returns catalog children in parent event order. + * @returns committed catalog children in parent event order. * @throws {@link SubagentError} when query or catalog projection is unavailable. * @throws SessionQueryError when the parent cannot be read or the query is cancelled. */ diff --git a/packages/subagent/subagent/src/list-children.ts b/packages/subagent/subagent/src/list-children.ts index 0a737b98d7..357541f712 100644 --- a/packages/subagent/subagent/src/list-children.ts +++ b/packages/subagent/subagent/src/list-children.ts @@ -25,7 +25,7 @@ export type SubagentDescendantListEntry = SubagentListEntry & { * @param ctx - context carrying the Session query service. * @param parentSessionId - parent whose direct children are requested. * @param signal - cancellation forwarded to the Session observation. - * @returns direct-child rows in parent catalog event order. + * @returns committed direct-child rows in parent catalog event order. * @throws {@link SubagentError} when query or catalog projection is unavailable. */ export async function listChildren( @@ -102,13 +102,15 @@ export async function listDescendants( const code = error instanceof Error && 'code' in error ? error.code : undefined result.push({ kind: 'diagnostic', id: entry.id, parentId, depth, + ...entry.name === undefined ? {} : { name: entry.name }, reason: code === 'SESSION_QUERY_CORRUPT_SESSION' || code === 'SESSION_QUERY_SOURCE_CONFLICT' ? 'corrupt' : 'unavailable', }) continue } if (entry.mode === 'unknown') { - result.push({ kind: 'diagnostic', id: entry.id, parentId, depth, reason: 'unsupported' }) + result.push({ kind: 'diagnostic', id: entry.id, parentId, depth, + ...entry.name === undefined ? {} : { name: entry.name }, reason: 'unsupported' }) } else { const { createdAt: _createdAt, ...identity } = entry result.push({ diff --git a/packages/subagent/subagent/src/manager.ts b/packages/subagent/subagent/src/manager.ts index 0f48df0444..6fcf414c61 100644 --- a/packages/subagent/subagent/src/manager.ts +++ b/packages/subagent/subagent/src/manager.ts @@ -32,9 +32,10 @@ import { foldSubagentDescriptor, snapshotSubagentDescriptor } from './descriptor import type { SubagentDescriptorData } from './descriptor.ts' import { establishCatalogChild, establishExternalCatalogChild } from './catalog.ts' import { SubagentError } from './error.ts' +import { SubagentNames } from './names.ts' import { isAdjacentAgentSendMessageTool } from './internal.ts' import type { ActivationObserver } from './lifecycle.ts' -import type { SubagentIdentityProjection } from './projection-types.ts' +import type { SubagentIdentityProjection, SubagentCatalogEntry } from './projection-types.ts' import type { ContinuableCreateRequest, ContinuableCreateSpec, SubagentInterruptAuthority, SubagentActivationSpec, SubagentActivation, SubagentStartRequest, SubagentRun, @@ -59,6 +60,7 @@ type ChildDeliveryOptions = { interface SubagentHost { /** Read the current descriptor identity through the service-owned projection. */ identityOf(session: Session): SubagentIdentityProjection | undefined + catalogOf(session: Session): SubagentCatalogEntry[] startExternal(name: string, request: ResolvedSubagentStartRequest): Promise /** Resolve one provider's detached continuable-creation contribution. */ prepareContinuable(name: string, request: ContinuableCreateRequest): Promise @@ -68,6 +70,7 @@ interface SubagentHost { /** Inputs shared by fresh and resumed Activation materialization. */ interface MaterializeBase { + name: string childId: SessionId provider: string root: Agent @@ -132,6 +135,10 @@ function failureMessage(error: unknown): string { /** Own subagent requests and the resources of every live activation. */ export class SubagentManager { + /** Agent paths and parent-local pending creation reservations. */ + readonly names: SubagentNames + /** Paths supplied to Agent initialization before its Activation is published. */ + private readonly initializing = new Map>() /** Child session id → its live Activation. Process-local, never durable. */ private readonly resident = new Map() /** Root identities retain their pool across child settlement without retaining dead roots. */ @@ -157,6 +164,14 @@ export class SubagentManager { private readonly host: SubagentHost, private readonly maxActiveSubagents: () => number, ) { + this.names = new SubagentNames(ctx, { + identityOf: session => this.host.identityOf(session), + catalogOf: session => this.host.catalogOf(session), + residencyOf: (agent) => { + const activation = this.resident.get(agent.id) + return activation?.kind === 'local' && activation.handle.agent === agent ? activation : this.initializing.get(agent.id) + }, + }) // Drain managed instances before Cordis releases their owning scope. const scope = ctx.plugin(function activationOwner() {}) this.ownerCtx = scope.ctx @@ -341,6 +356,7 @@ export class SubagentManager { const agentReasoningEffort = agentOptions.reasoningEffort const descriptor = snapshotSubagentDescriptor({ mode: 'continuable', + name: spec.name, provider: spec.provider, label: spec.label, ...agentProvider !== undefined ? { agentProvider } : {}, @@ -351,6 +367,7 @@ export class SubagentManager { // parent's future, not to this child. const delegatedPolicies = captureDelegatedPolicyOverrides(parent) + using reservation = this.names.reserve(parent, spec.name, signal, childId) const cwd = await this.resolveDirectory(request, signal) const prepared = await this.host.prepareContinuable(spec.provider, { sessionId: childId, @@ -379,6 +396,7 @@ export class SubagentManager { } const activation = await this.materialize({ kind: 'local', + name: reservation.name, childId, provider: spec.provider, ...team, @@ -403,13 +421,13 @@ export class SubagentManager { return await this.submitMaterialized( activation, spec.delivery !== 'caller' && isAdjacentAgentSendMessageTool(this.ctx.get('tools')?.get('send_message', child)) - ? withContinuableReturnGuidance(parent.id, request.prompt) + ? withContinuableReturnGuidance(this.names.nameOf(parent), reservation.name, request.prompt) : request.prompt, { source: { kind: 'user' }, signal, delivery: 'queue', authorize: () => { this.assertAdmitting(parent) } }, team.root, () => { this.assertAdmitting(parent) - establishCatalogChild(parent.session, childHeader, descriptor) + reservation.commit(childId, () => { establishCatalogChild(parent.session, childHeader, descriptor) }) }, ) }) @@ -430,9 +448,11 @@ export class SubagentManager { return this.trackStartup(team.root, spec.signal, async (signal) => { let activation: Activation | undefined try { + using reservation = this.names.reserve(parent, spec.name, signal) const cwd = await this.resolveDirectory(spec.request, signal) activation = await this.materialize({ kind: 'external', + name: reservation.name, childId: pendingId, provider: spec.provider, ...team, @@ -444,9 +464,8 @@ export class SubagentManager { }) this.assertAdmitting(parent) signal.throwIfAborted() - if (spec.delivery === 'parent') { - establishExternalCatalogChild(parent.session, activation.childId, spec.label) - } + const childId = activation.childId + reservation.commit(childId, () => { establishExternalCatalogChild(parent.session, childId, spec.label, spec.name) }) this.announce(activation) return this.receipt(activation) } catch (error: unknown) { @@ -486,7 +505,7 @@ export class SubagentManager { const root = this.ctx.agents.roots().find(root => root === caller && !delegated || this.ctx.agents.isOwnedBy(caller.id, root), ) - if (root === undefined) throw new SubagentError('agent has no active Team owner', 'UNAUTHORIZED') + if (root === undefined || this.ctx.agents.get(root.id) !== root) throw new SubagentError('agent has no active Team owner', 'UNAUTHORIZED') return root } @@ -512,7 +531,7 @@ export class SubagentManager { const root = this.rootOf(sender) if (sender.id === targetId) throw new SubagentError('send_message requires another Team member', 'UNAUTHORIZED') this.assertAdmitting(sender) - const message = createAgentMessage(sender, content) + const message = createAgentMessage(sender, content, this.names.nameOf(sender)) return this.deliverToMember(root, targetId, [...message.content], { signal: options.signal, delivery: 'steer', source: message.source, authorize: () => { this.assertAdmitting(sender) }, @@ -520,8 +539,9 @@ export class SubagentManager { } /** Resolve the live owner through verified creation edges without activating ancestors. */ - private async teamOf(source: SessionObservation, signal: AbortSignal, root?: Agent): Promise> { + private async teamOf(source: SessionObservation, signal: AbortSignal, root?: Agent): Promise> { const ancestors: SessionId[] = [] + const names: string[] = [] const seen = new Set([source.header.id]) let { header, projections, inheritedEventCount } = source while (true) { @@ -532,17 +552,20 @@ export class SubagentManager { if (seen.has(parentId)) throw new SubagentError('cyclic subagent creation ancestry', 'UNAUTHORIZED') seen.add(parentId) using parent = await this.observeContinuation(parentId, signal) - if (!parent.projections?.values.subagentCatalog?.some(entry => entry.id === header.id)) { + if (!parent.projections?.values.subagentCatalog?.some(entry => + entry.id === header.id && (identity.name === undefined || entry.name === identity.name), + )) { throw new SubagentError('subagent is absent from its creator catalog', 'UNAUTHORIZED') } + names.unshift(identity.name ?? `legacy:${header.id}`) ancestors.unshift(parentId) - if (parentId === root?.id) return { root, ancestors } + if (parentId === root?.id) return { root, ancestors, name: ['lead', ...names].join('/') } ;({ header, projections, inheritedEventCount } = parent) } if (root === undefined && ancestors.length > 0 && header.origin !== 'subagent') { const owner = this.ctx.agents.roots().find(agent => agent.id === header.id) if (owner === undefined) throw new SubagentError('Team root is not live', 'CONTINUATION_UNAVAILABLE') - return { root: owner, ancestors } + return { root: owner, ancestors, name: ['lead', ...names].join('/') } } throw new SubagentError(`agent "${source.header.id}" is not a member of this Team`, 'UNAUTHORIZED') } @@ -661,10 +684,11 @@ export class SubagentManager { inputs.signal.throwIfAborted() const pool = this.rootPool(inputs.root) const releaseSlot = pool.reserve(this.maxActiveSubagents()) + this.initializing.set(inputs.childId, inputs) return this.materializeTracked(inputs, releaseSlot).catch((error: unknown) => { releaseSlot() throw error - }) + }).finally(() => { this.initializing.delete(inputs.childId) }) } /** Keep Team shutdown and host waits joined to preparation, publication, and rollback. */ @@ -818,6 +842,7 @@ export class SubagentManager { const observer = this.host.observeActivation(provider, childId, root, inputs.ancestors.at(-1) as SessionId) const activation: Activation = { + name: inputs.name, releaseSlot, childId, ...resource, @@ -1033,7 +1058,7 @@ export class SubagentManager { const parent = this.ctx.agents.get(parentId) if (parent === undefined || this.rootOf(parent) !== activation.root) return const message = createSettlementMessage( - activation.childId, terminal, activationAgent(activation) !== undefined, + activation.childId, activation.name, terminal, activationAgent(activation) !== undefined, ) if (this.closingTeardownFor(parent) !== undefined) { parent.inject(message) @@ -1064,6 +1089,7 @@ export class SubagentManager { private receipt(activation: Activation): SubagentActivation { return { childId: activation.childId, + name: activation.name, result: activation.result.promise, dispose: () => this.dispose(activation), } @@ -1159,7 +1185,7 @@ export class SubagentManager { 'NOT_RESUMABLE', ) } - const { ancestors } = await this.teamOf(source, signal, root) + const { ancestors, name } = await this.teamOf(source, signal, root) let activation: Activation try { const projectedPreset = source.projections?.values.agentPreset @@ -1169,6 +1195,7 @@ export class SubagentManager { const agentPreset = projectedPreset ?? source.header.agentPreset activation = await this.materialize({ kind: 'local', + name, childId, provider: descriptor.provider, root, diff --git a/packages/subagent/subagent/src/names.ts b/packages/subagent/subagent/src/names.ts new file mode 100644 index 0000000000..8832736160 --- /dev/null +++ b/packages/subagent/subagent/src/names.ts @@ -0,0 +1,156 @@ +/** Parent-local name reservations and catalog-based agent paths. + * @module @deepseek-ai/dsh-subagent/names + */ + +import type { Context } from '@deepseek-ai/cordis' +import type { Agent } from '@deepseek-ai/dsh-agent' +import type { Session, SessionId } from '@deepseek-ai/dsh-session' +import type { SubagentCatalogEntry, SubagentIdentityProjection } from './projection-types.ts' +import { subagentNameSchema } from './descriptor.ts' +import { SubagentError } from './error.ts' +import { listChildren } from './list-children.ts' + +/** Service-owned projections and current managed residency. */ +interface NameSources { + catalogOf(session: Session): SubagentCatalogEntry[] + identityOf(session: Session): SubagentIdentityProjection | undefined + residencyOf(agent: Agent): { + readonly name: string + readonly root: Agent + readonly ancestors: readonly SessionId[] + } | undefined +} + +/** Pending names belong to their live parent; committed names belong to its catalog. */ +export class SubagentNames { + private readonly pending = new WeakMap>() + + /** + * @param ctx - subagent service context with live Agents and Session queries. + * @param sources - service-owned projection reads and managed residency. + */ + constructor(private readonly ctx: Context, private readonly sources: NameSources) {} + + /** + * Reserve a sibling name before preparation or external provider dispatch. + * @param parent - exact live spawning Agent. + * @param name - one lower-case path segment. + * @param signal - cancellation before reservation or publication. + * @param id - reserved local identity, when the provider does not allocate it. + * @returns the full path, parent catalog publication, and temporary-reservation release. + * @throws when the name or identity is taken, invalid, or cannot be checked. + */ + reserve(parent: Agent, name: string, signal: AbortSignal, id?: SessionId): { + readonly name: string + commit(id: SessionId, publish: () => void): void + [Symbol.dispose](): void + } { + if (!subagentNameSchema.safeParse(name).success) { + throw new SubagentError('agent name must start with a lower-case letter and contain only lower-case letters, digits, underscores, or hyphens', 'INVALID_AGENT_NAME') + } + const lineage = this.lineage(parent) + const path = `${this.nameOf(parent)}/${name}` + signal.throwIfAborted() + const children = () => this.sources.catalogOf(parent.session) + const pending = this.pending.get(parent) ?? new Set() + this.pending.set(parent, pending) + if (children().some(child => child.name === name) || pending.has(name)) { + throw new SubagentError(`agent name "${name}" is already in use under ${this.nameOf(parent)}; choose another name`, 'DUPLICATE_AGENT_NAME') + } + const checkId = (candidate: SessionId): void => { + if (lineage.some(agent => agent.id === candidate) + || this.sources.residencyOf(parent)?.ancestors.includes(candidate) + || children().some(child => child.id === candidate)) { + throw new SubagentError(`subagent "${candidate}" already exists`, 'DUPLICATE_CHILD') + } + } + if (id !== undefined) checkId(id) + pending.add(name) + return { + name: path, + commit: (childId, publish) => { + signal.throwIfAborted() + if (lineage.some(agent => this.ctx.agents.get(agent.id) !== agent)) { + throw new SubagentError('agent tree changed during creation', 'AGENT_TREE_UNAVAILABLE') + } + checkId(childId) + publish() + }, + [Symbol.dispose]: () => { pending.delete(name) }, + } + } + + /** + * Derive an Agent's full path from its creation ancestry and descriptor segments. + * @param agent - exact live Agent. + * @returns lead followed by named or legacy child segments. + */ + nameOf(agent: Agent): string { + const lineage = this.lineage(agent) + const resident = this.sources.residencyOf(agent) + if (resident !== undefined) return resident.name + return lineage.map((member, index) => { + if (index === 0) return 'lead' + const identity = this.sources.identityOf(member.session) + return identity !== undefined && member.session.isOwnSeq(identity.seq) && identity.name !== undefined + ? identity.name : `legacy:${member.id}` + }).join('/') + } + + /** + * Follow a full path through direct-child catalogs without granting operation authority. + * @param caller - exact live Agent selecting the tree. + * @param name - full path returned by creation or discovery. + * @param signal - cancellation during historical catalog reads. + * @returns the durable identity for the consuming operation to authorize. + * @throws when a path segment is absent, ambiguous, or its parent cannot be read. + */ + async resolve(caller: Agent, name: string, signal?: AbortSignal): Promise { + signal?.throwIfAborted() + const root = this.lineage(caller)[0] + if (name === 'lead') return root.id + const segments = name.split('/') + if (segments.shift() === 'lead') { + let children = this.sources.catalogOf(root.session) + for (const [index, segment] of segments.entries()) { + signal?.throwIfAborted() + const matches = children.filter(child => (child.name ?? `legacy:${child.id}`) === segment) + const child = matches[0] + if (child === undefined) break + if (matches.some(match => match.id !== child.id)) { + throw new SubagentError(`agent path "${name}" has an ambiguous segment; restore its parent catalog`, 'CORRUPT_AGENT_NAMES') + } + if (index === segments.length - 1) return child.id + if (child.mode === 'external') break + children = await listChildren(this.ctx, child.id, signal) + } + } + throw new SubagentError(`agent path "${name}" was not found; use list_agents to discover available paths`, 'AGENT_NAME_NOT_FOUND') + } + + /** Select the live root and caller; unmanaged Agents require their live creation ancestors. */ + private lineage(caller: Agent): [Agent, ...Agent[]] { + if (this.ctx.agents.get(caller.id) !== caller) { + throw new SubagentError('agent naming requires the exact live caller', 'UNAUTHORIZED') + } + const resident = this.sources.residencyOf(caller) + if (resident !== undefined) { + if (this.ctx.agents.get(resident.root.id) !== resident.root) { + throw new SubagentError('agent naming requires the live Team root', 'AGENT_TREE_UNAVAILABLE') + } + return [resident.root, caller] + } + const lineage: [Agent, ...Agent[]] = [caller] + let current = caller + while (current.session.header.origin === 'subagent') { + const id = current.session.header.parentSession + const parent = id === undefined ? undefined : this.ctx.agents.get(id) + if (parent === undefined || lineage.includes(parent)) { + throw new SubagentError('agent naming requires the live parent tree', 'AGENT_TREE_UNAVAILABLE') + } + lineage.unshift(parent) + current = parent + } + return lineage + } +} diff --git a/packages/subagent/subagent/src/projection-types.ts b/packages/subagent/subagent/src/projection-types.ts index c8d1b472a7..dc9096d37e 100644 --- a/packages/subagent/subagent/src/projection-types.ts +++ b/packages/subagent/subagent/src/projection-types.ts @@ -9,6 +9,8 @@ import type { SessionId, SessionSeq } from '@deepseek-ai/dsh-session/types' /** One current direct-child discovery row materialized from parent facts. */ export type SubagentCatalogEntry = & { + /** Immutable sibling name, absent for historical unnamed children. */ + readonly name?: string readonly id: SessionId readonly createdAt: number } @@ -43,7 +45,10 @@ export interface SubagentTimingProjection { * Label strength follows the descriptor schema: a continuable child always * carries one, a one-shot child may omit it. */ -export type SubagentIdentityProjection = +export type SubagentIdentityProjection = { + /** Immutable sibling name from the descriptor, absent for historical children. */ + readonly name?: string +} & ( | { /** A terminal one-shot child. */ mode: 'one-shot' @@ -65,6 +70,7 @@ export type SubagentIdentityProjection = /** Seq of the folded descriptor event; see the one-shot arm for the own-suffix proof. */ seq: SessionSeq } +) declare module '@deepseek-ai/dsh-session-projection/types' { interface SessionProjectionMap { diff --git a/packages/subagent/subagent/src/projection.ts b/packages/subagent/subagent/src/projection.ts index 3fe2eacc28..678509f3b1 100644 --- a/packages/subagent/subagent/src/projection.ts +++ b/packages/subagent/subagent/src/projection.ts @@ -9,7 +9,7 @@ import { z } from 'zod' import { SessionSeq } from '@deepseek-ai/dsh-session' import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection' import type { SessionEvent } from '@deepseek-ai/dsh-session' -import { foldSubagentDescriptor } from './descriptor.ts' +import { foldSubagentDescriptor, subagentNameSchema } from './descriptor.ts' import type { SubagentDescriptorData } from './descriptor.ts' import type { SubagentIdentityProjection, SubagentTimingProjection } from './projection-types.ts' @@ -113,23 +113,25 @@ export interface IdentityState { identity?: SubagentIdentityProjection | undefined } -// The cast bridges only the optional-label arm: Zod's optional output -// includes explicit `undefined`, which exactOptionalPropertyTypes excludes -// from the public interface. The no-value state itself is the serializable -// `null` arm — never `undefined` — so every registry read and push frame -// survives JSON.stringify losslessly. -const identityValueSchema = z.discriminatedUnion('mode', [ +const identityValueSchema: z.ZodType = z.discriminatedUnion('mode', [ z.object({ mode: z.literal('one-shot'), + name: subagentNameSchema.optional(), label: z.string().optional(), seq: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).transform(SessionSeq), }).strict(), z.object({ mode: z.literal('continuable'), + name: subagentNameSchema.optional(), label: z.string(), seq: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).transform(SessionSeq), }).strict(), -]) as unknown as z.ZodType +]).transform(({ name, ...identity }): SubagentIdentityProjection => { + const named = name === undefined ? {} : { name } + if (identity.mode === 'continuable') return { ...identity, ...named } + const { label, ...oneShot } = identity + return { ...oneShot, ...named, ...label === undefined ? {} : { label } } +}) const identitySchema = identityValueSchema.nullable() @@ -148,13 +150,13 @@ function descriptorIdentity(event: SessionEvent): SubagentIdentityProjection | u descriptor = undefined } if (descriptor === undefined) return undefined - return descriptor.mode === 'one-shot' - ? { - mode: 'one-shot', - ...descriptor.label !== undefined ? { label: descriptor.label } : {}, - seq: event.seq, - } - : { mode: 'continuable', label: descriptor.label, seq: event.seq } + return { + ...descriptor.name === undefined ? {} : { name: descriptor.name }, + ...descriptor.mode === 'one-shot' + ? { mode: 'one-shot', ...descriptor.label === undefined ? {} : { label: descriptor.label } } as const + : { mode: 'continuable', label: descriptor.label } as const, + seq: event.seq, + } } /** @@ -178,7 +180,5 @@ export const subagentIdentityProjectionDefinition = { return identity === undefined ? {} : { identity } }, wire: { viewSchema: identitySchema, view: state => state.identity ?? null }, - // Bumped when the identity gained its `seq` field: an older checkpoint row - // would replay into a value the schema rejects, so it must refold instead. - stateVersion: 2, + stateVersion: 3, } satisfies ProjectionDefinition<'subagent', IdentityState> diff --git a/packages/subagent/subagent/src/types.ts b/packages/subagent/subagent/src/types.ts index 129dab3ebb..9ea0815c20 100644 --- a/packages/subagent/subagent/src/types.ts +++ b/packages/subagent/subagent/src/types.ts @@ -29,6 +29,8 @@ export function SubagentRunId(id: string): SubagentRunId { /** One managed child execution and its result delivery policy. */ export interface SubagentActivationSpec { + /** Immutable name segment, unique among the parent’s direct children. */ + readonly name: string /** Registered backend to use. */ readonly provider: string /** Short task label retained in cataloged child membership. */ @@ -45,6 +47,8 @@ export interface SubagentActivationSpec { /** A managed execution; disposal addresses this exact activation, never a later resume. */ export interface SubagentActivation { + /** Full path used by model-facing collaboration tools. */ + readonly name: string /** Stable identity of the child. */ readonly childId: SessionId /** Accepted inbox message, when the backend has a local inbox. */ diff --git a/packages/subagent/subagent/tests/catalog.spec.ts b/packages/subagent/subagent/tests/catalog.spec.ts index ac576639fa..81bc4c2edd 100644 --- a/packages/subagent/subagent/tests/catalog.spec.ts +++ b/packages/subagent/subagent/tests/catalog.spec.ts @@ -12,6 +12,7 @@ import { import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session' import { establishExternalCatalogChild, + establishCatalogChild, subagentCatalogProjectionDefinition, } from '../src/catalog.ts' import type { SubagentCatalogState } from '../src/catalog.ts' @@ -58,12 +59,12 @@ describe('subagent catalog projection', () => { await ctx.plugin(SessionStore) const parent = ctx.sessions.create(header.id) const before = Date.now() - establishExternalCatalogChild(parent, SessionId('external'), 'External work') + establishExternalCatalogChild(parent, SessionId('external'), 'External work', 'worker') const events = parent.snapshotEvents() expect(events).toHaveLength(1) expect(events[0]).toMatchObject({ type: 'subagent/catalog', - data: { version: 2, childId: 'external', mode: 'external', label: 'External work' }, + data: { version: 2, childId: 'external', mode: 'external', label: 'External work', name: 'worker' }, }) const entries = subagentCatalogProjectionDefinition.wire.view(fold(events)) expect(entries).toHaveLength(1) @@ -74,6 +75,24 @@ describe('subagent catalog projection', () => { } }) + it('preserves a field-free historical descriptor when establishing its catalog', async () => { + const ctx = new Context() + try { + await ctx.plugin(SessionStore) + const parent = ctx.sessions.create(header.id) + establishCatalogChild(parent, { ...header, id: SessionId('legacy') }, { mode: 'continuable', label: 'Old child' }) + expect(subagentCatalogProjectionDefinition.wire.view(fold(parent.snapshotEvents()))) + .toEqual([{ id: 'legacy', createdAt: 0, mode: 'continuable', label: 'Old child' }]) + } finally { + await ctx.fiber.dispose() + } + }) + + it('rejects multi-segment names in persisted catalog entries', () => { + const event = fact(0, 'child', 1, { mode: 'continuable', label: 'Review' }) + expect(() => fold([{ ...event, data: { ...event.data, name: 'lead/reviewer' } }])).toThrow() + }) + it('publishes detached catalog views and changes only for new own facts', async () => { const ctx = new Context() try { diff --git a/packages/subagent/subagent/tests/continuation-inheritance.spec.ts b/packages/subagent/subagent/tests/continuation-inheritance.spec.ts index 17a099b804..49a7789df1 100644 --- a/packages/subagent/subagent/tests/continuation-inheritance.spec.ts +++ b/packages/subagent/subagent/tests/continuation-inheritance.spec.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto' import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts' /** * Continuable-child delegation policy: a fresh continuable start seeds the @@ -61,6 +62,7 @@ async function setup(script: Script) { function startSpec(parent: Agent, provider = 'spawn') { return { + name: `child-${randomUUID()}`, provider, label: 'child task', request: { prompt: [{ type: 'text' as const, text: 'child task' }], parent }, diff --git a/packages/subagent/subagent/tests/continuation-messages-adapter.spec.ts b/packages/subagent/subagent/tests/continuation-messages-adapter.spec.ts index 7d4a5a266c..3196cc058d 100644 --- a/packages/subagent/subagent/tests/continuation-messages-adapter.spec.ts +++ b/packages/subagent/subagent/tests/continuation-messages-adapter.spec.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto' import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' @@ -57,7 +58,7 @@ it('continues the parent through default Messages after a reasoning-bearing cont settled.resolve(undefined) }) - const started = await ctx.subagents.startActivation({ + const started = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`, delivery: 'parent', provider: 'spawn', label: 'child task', @@ -79,7 +80,7 @@ it('continues the parent through default Messages after a reasoning-bearing cont expect(requests.map(request => request.path)).toEqual(['/anthropic/v1/messages', '/anthropic/v1/messages']) const notice = parent.session.deriveMessages().find(message => message.source.kind === 'subagent-settled') const closingContent = [ - { type: 'text', text: `Background subagent ${started.childId} finished and will do no further work unless you send it more.` }, + { type: 'text', text: `Background subagent ${started.name} finished and will do no further work unless you send it more.` }, { type: 'text', text: 'Its closing message:' }, { type: 'text', text: 'child answer' }, ] diff --git a/packages/subagent/subagent/tests/continuation-messages.spec.ts b/packages/subagent/subagent/tests/continuation-messages.spec.ts index 9d91df3be3..20cf0eed7d 100644 --- a/packages/subagent/subagent/tests/continuation-messages.spec.ts +++ b/packages/subagent/subagent/tests/continuation-messages.spec.ts @@ -4,7 +4,7 @@ import { SessionId } from '@deepseek-ai/dsh-session' import { createSettlementMessage } from '../src/continuation-messages.ts' const childId = SessionId('settled-child') -const summary = { type: 'text', text: `Background subagent ${childId} finished and will do no further work unless you send it more.` } +const summary = { type: 'text', text: 'Background subagent reviewer finished and will do no further work unless you send it more.' } const reasoning: ContentBlock = { type: 'reasoning', text: 'private child reasoning' } const toolCall: ContentBlock = { type: 'tool-call', id: ToolCallId('child-call'), name: 'read', arguments: '{}' } @@ -14,7 +14,7 @@ describe('continuable settlement content', () => { ['a tool call after the answer', [{ type: 'text', text: 'answer' }, toolCall]], ] satisfies [string, ContentBlock[]][])('reports only the closing text with %s', (_label, output) => { const original = structuredClone(output) - const message = createSettlementMessage(childId, { stopReason: 'completed', output }) + const message = createSettlementMessage(childId, 'reviewer', { stopReason: 'completed', output }) expect(message.role).toBe('user') expect(message.content).toEqual([ @@ -30,7 +30,7 @@ describe('continuable settlement content', () => { ['reasoning-only output', [reasoning]], ['empty text', [{ type: 'text', text: '' }]], ] satisfies [string, ContentBlock[]][])('reports no closing message for %s', (_label, output) => { - const message = createSettlementMessage(childId, { stopReason: 'completed', output }) + const message = createSettlementMessage(childId, 'reviewer', { stopReason: 'completed', output }) expect(message.content).toEqual([ summary, @@ -41,7 +41,7 @@ describe('continuable settlement content', () => { it('preserves text block order and bytes around omitted reasoning and tool calls', () => { const first: ContentBlock = { type: 'text', text: ' first\n' } const second: ContentBlock = { type: 'text', text: '\n第二段 ' } - const message = createSettlementMessage(childId, { + const message = createSettlementMessage(childId, 'reviewer', { stopReason: 'completed', output: [reasoning, first, toolCall, second], }) @@ -56,7 +56,7 @@ describe('continuable settlement content', () => { }) it('includes closing text alongside structured results and diagnostics', () => { - expect(createSettlementMessage(childId, { + expect(createSettlementMessage(childId, 'reviewer', { stopReason: 'completed', output: [{ type: 'text', text: 'final answer' }], structured: { answer: 42 }, diagnostic: 'safe diagnostic', }).content).toEqual([ diff --git a/packages/subagent/subagent/tests/continuation-presets.spec.ts b/packages/subagent/subagent/tests/continuation-presets.spec.ts index 30ef9ea230..636f750d86 100644 --- a/packages/subagent/subagent/tests/continuation-presets.spec.ts +++ b/packages/subagent/subagent/tests/continuation-presets.spec.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto' /** Independent spawn presets and compatibility of persisted child compositions. */ import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts' @@ -80,6 +81,7 @@ function parkParent(ctx: Context, parent: Agent): void { function startSpec(parent: Agent, agentPreset?: string, provider = 'separate') { return { provider, + name: `child-${randomUUID()}`, label: 'preset work', delivery: 'parent' as const, request: { @@ -170,6 +172,7 @@ describe('continuable child presets', () => { mode: 'continuable', provider: 'separate', label: 'preset work', + name: started.name.slice('lead/'.length), agentProvider: 'mock', agentModel: 'mock', }) diff --git a/packages/subagent/subagent/tests/continuation.spec.ts b/packages/subagent/subagent/tests/continuation.spec.ts index 687f95d3f7..7ff123b7d5 100644 --- a/packages/subagent/subagent/tests/continuation.spec.ts +++ b/packages/subagent/subagent/tests/continuation.spec.ts @@ -1,4 +1,5 @@ import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' +import { randomUUID } from 'node:crypto' import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts' import { afterEach, describe, expect, it, vi } from 'vitest' import { mkdtempSync, rmSync } from 'node:fs' @@ -17,7 +18,7 @@ import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process' import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork-in-process' import * as SubagentControl from '@deepseek-ai/dsh-tool-subagent-control' import type { ContentBlock, GenerateOptions, MessageId, StreamChunk } from '@deepseek-ai/dsh-llm' -import { ToolCallId, createUserMessage, LlmAdapter, ReasoningEffortId } from '@deepseek-ai/dsh-llm' +import { boundContextSummary, ToolCallId, createUserMessage, LlmAdapter, ReasoningEffortId } from '@deepseek-ai/dsh-llm' import { defineTool } from '@deepseek-ai/dsh-tools' import { MockAdapter, maxTokensResponse, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' import SubagentRuntime, { @@ -28,6 +29,9 @@ import type { SubagentRunEndInfo, SubagentRunInfo } from '../src/index.ts' import type { SubagentPromptRequestId } from '../src/control-types.ts' import * as activationResults from '../src/activation.ts' import { requireLocalActivation } from '../src/activation.ts' +import { SubagentNames } from '../src/names.ts' +import { subagentCatalogProjectionDefinition, subagentCatalogEntries } from '../src/catalog.ts' +import { subagentIdentityProjectionDefinition } from '../src/projection.ts' import { TestSessionQuery } from './test-session-query.ts' import { loadStoredSession } from './persistence-helpers.ts' import { @@ -121,6 +125,7 @@ const testSignal = new AbortController().signal function startSpec(parent: Agent, provider = 'spawn', signal: AbortSignal = testSignal) { return { + name: `child-${randomUUID()}`, provider, label: 'child task', request: { prompt: [{ type: 'text' as const, text: 'child task' }], parent }, @@ -146,7 +151,7 @@ function hasAssistantText(events: readonly SessionEvent[], text: string): boolea function userTexts(events: readonly SessionEvent[]): string[] { return events.flatMap(event => event.type === 'user/message' && event.data.source.kind !== 'runtime-context' ? event.data.content.flatMap(block => block.type === 'text' - && !block.text.startsWith('Your parent agent id is ') + && !block.text.startsWith('Your name is ') ? [block.text] : []) : []) @@ -564,6 +569,10 @@ describe('SubagentRuntime.startActivation', () => { childId: reservedId, })).rejects.toMatchObject({ code: 'DUPLICATE_CHILD' }) expect(ctx.agents.get(reservedId)).toBeUndefined() + const other = await ctx.agentLoop.create(SessionId('other-root'), { provider: 'mock', model: 'mock' }) + await expect(ctx.subagents.startActivation({ + delivery: 'parent', ...startSpec(other), childId: reservedId, + })).rejects.toMatchObject({ code: 'DUPLICATE_CHILD' }) }) it('rejects reserved child ids for an external backend before dispatch', async () => { @@ -601,6 +610,7 @@ describe('SubagentRuntime.startActivation', () => { expect(descriptorIndex).toBeLessThan(turnStartIndex) const descriptor = loaded.events[descriptorIndex] as SessionEvent<'subagent/descriptor'> expect(descriptor.data).toEqual({ + name: started.name.slice('lead/'.length), version: SUBAGENT_DESCRIPTOR_VERSION, mode: 'continuable', provider: 'spawn', @@ -716,6 +726,44 @@ describe('SubagentRuntime.startActivation', () => { expect(parent.session.snapshotEvents().filter(event => event.type === 'subagent/catalog')).toEqual([]) }) + it('releases a sibling name after parent catalog failure and rejects the uncataloged child on resume', async () => { + const release = Promise.withResolvers() + const adapter = new GatedAdapter([ + { chunks: textResponse('parent done'), gate: release.promise }, + { chunks: textResponse('retry done') }, + { chunks: textResponse('notice received') }, + ]) + const { ctx, parent } = await setupWith(adapter) + const nested = await ctx.subagents.startActivation({ ...startSpec(parent), name: 'reviewer', delivery: 'parent' }) + const child = ctx.agents.get(nested.childId)! + const failedId = SessionId('unfinished-grandchild') + await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) }) + const append = vi.spyOn(child.session, 'append').mockImplementationOnce(() => { throw new Error('parent catalog failed') }) + try { + await expect(ctx.subagents.startActivation({ + ...startSpec(child), name: 'writer', childId: failedId, delivery: 'parent', + })).rejects.toThrow('parent catalog failed') + expect(append.mock.calls.find(([type]) => type === 'subagent/catalog')).toMatchObject(['subagent/catalog', { version: 0, childId: failedId, name: 'writer' }]) + append.mockRestore() + expect(ctx.agents.get(failedId)).toBeUndefined() + expect(child.session.snapshotEvents().filter(event => event.type === 'subagent/catalog')).toEqual([]) + expect(await ctx.subagents.listChildren(child.id)).toEqual([]) + await expect(ctx.subagents.resolveName(parent, 'lead/reviewer/writer')).rejects.toMatchObject({ code: 'AGENT_NAME_NOT_FOUND' }) + await expect(queuePrompt(ctx, child, failedId, message('resume unfinished child'))).rejects.toMatchObject({ code: 'UNAUTHORIZED' }) + + const retry = await ctx.subagents.startActivation({ ...startSpec(child), name: 'writer', delivery: 'caller' }) + await retry.result + expect(await ctx.subagents.resolveName(parent, 'lead/reviewer/writer')).toBe(retry.childId) + expect((await ctx.subagents.listChildren(child.id)).map(entry => entry.id)).toEqual([retry.childId]) + expect((await ctx.subagents.listChildren(parent.id)).map(entry => entry.id)).toEqual([nested.childId]) + await expect(queuePrompt(ctx, child, failedId, message('resume old attempt'))).rejects.toMatchObject({ code: 'UNAUTHORIZED' }) + } finally { + append.mockRestore() + release.resolve(undefined) + await nested.result + } + }) + it('preserves prompt admission failure when Activation disposal also fails', async () => { const { ctx, parent } = await setup([textResponse('unused')]) const childId = SessionId('00000000-0000-4000-8000-000000000322') @@ -797,6 +845,7 @@ describe('SubagentRuntime.startActivation', () => { const descriptor = child.session.snapshotEvents().find(event => event.type === 'subagent/descriptor') expect(descriptor?.data).toEqual({ + name: started.name.slice('lead/'.length), version: SUBAGENT_DESCRIPTOR_VERSION, mode: 'continuable', provider: 'spawn', @@ -1054,12 +1103,43 @@ describe('direct-child Queue residency routing', () => { const started = await ctx.subagents.startActivation({ ...startSpec(parent), delivery: 'parent' }) await waitNoActivation(ctx, started.childId) + const catalog = await ctx.sessionProjections.register(subagentCatalogProjectionDefinition) + const identity = await ctx.sessionProjections.register(subagentIdentityProjectionDefinition) + const restored = new SubagentNames(ctx, { + catalogOf: session => subagentCatalogEntries(catalog.stateOf(session)), + identityOf: session => identity.stateOf(session).identity, + residencyOf: () => undefined, + }) + const observe = vi.spyOn(ctx.sessionQuery, 'observeSession') + const reservations = await Promise.allSettled([ + Promise.resolve().then(() => restored.reserve(parent, 'next', new AbortController().signal)), + Promise.resolve().then(() => restored.reserve(parent, 'next', new AbortController().signal)), + ]) + try { + expect(reservations.filter(result => result.status === 'fulfilled')).toHaveLength(1) + const rejected = reservations.filter(result => result.status === 'rejected') + expect(rejected).toHaveLength(1) + for (const result of rejected) expect(result.reason).toMatchObject({ code: 'DUPLICATE_AGENT_NAME' }) + expect(await restored.resolve(parent, started.name)).toBe(started.childId) + const reads = observe.mock.calls.length + expect(reads).toBe(0) + expect(await restored.resolve(parent, started.name)).toBe(started.childId) + expect(observe).toHaveBeenCalledTimes(reads) + } finally { + for (const result of reservations) if (result.status === 'fulfilled') result.value[Symbol.dispose]() + observe.mockRestore() + } + const names: string[] = [] + ctx.on('agent/created', ({ agent }) => { + if (agent.id === started.childId) names.push(ctx.subagents.nameOf(agent)) + }) const messageId = await queuePrompt(ctx, parent, started.childId, message('continue please')) expect(messageId).toBeTypeOf('string') await waitNoActivation(ctx, started.childId) const loaded = await loadStoredSession(ctx.sessionPersistence, started.childId) expect(userTexts(loaded.events)).toEqual(['child task', 'continue please']) + expect(names).toEqual([started.name]) // One descriptor only: cold resume never re-seeds it. expect(loaded.events.filter(event => event.type === 'subagent/descriptor')).toHaveLength(1) }) @@ -2855,7 +2935,7 @@ describe('continuable adjacent-Agent delivery', () => { senderSessionId: started.childId, }) expect(delivered?.content).toEqual([ - { type: 'text', text: `Agent ${started.childId} sent a message: ` }, + { type: 'text', text: `Agent ${started.name} sent a message: ` }, { type: 'text', text: 'an explicit message' }, ]) @@ -2883,13 +2963,16 @@ describe('continuable adjacent-Agent delivery', () => { id === parent.id ? state === 'absent' ? undefined : { ...parent } : get(id) )) - await expect(ctx.subagents.sendMessage(child, parent.id, message('cannot arrive'), { - signal: testSignal, - })).rejects.toMatchObject({ code: 'UNAUTHORIZED' }) - - getSpy.mockRestore() - releaseChild.resolve(undefined) - await waitNoActivation(ctx, started.childId) + try { + expect(() => ctx.subagents.nameOf(child)).toThrow(expect.objectContaining({ code: 'AGENT_TREE_UNAVAILABLE' })) + await expect(ctx.subagents.sendMessage(child, parent.id, message('cannot arrive'), { + signal: testSignal, + })).rejects.toMatchObject({ code: 'UNAUTHORIZED' }) + } finally { + getSpy.mockRestore() + releaseChild.resolve(undefined) + await waitNoActivation(ctx, started.childId) + } }) it('propagates the Team root inbox rejection', async () => { @@ -2927,12 +3010,12 @@ describe('continuable settlement delivery', () => { const notice = settlementNotices(parent)[0]! expect(notice.sender).toBe(started.childId) expect(notice.text).toBe( - `Background subagent ${started.childId} finished and will do no further work unless you send it more.\nIts closing message:\nthe answer`, + `Background subagent ${started.name} finished and will do no further work unless you send it more.\nIts closing message:\nthe answer`, ) // The collapsed row states the outcome without the child's content. - expect(notice.summary).toBe( - `Background subagent ${started.childId} finished and will do no further work unless you send it more.`, - ) + expect(notice.summary).toBe(boundContextSummary( + `Background subagent ${started.name} finished and will do no further work unless you send it more.`, + )) }) it('delivers settlement even when the child already sent a message', async () => { @@ -2961,7 +3044,7 @@ describe('continuable settlement delivery', () => { await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) }) expect(settlementNotices(parent)[0]!.text).toBe( - `Background subagent ${started.childId} ran out of room before it finished.\nIts closing message:\nhalf an ans`, + `Background subagent ${started.name} ran out of room before it finished.\nIts closing message:\nhalf an ans`, ) }) @@ -2979,7 +3062,7 @@ describe('continuable settlement delivery', () => { await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) }) expect(settlementNotices(parent)[0]!.text).toBe( - `Background subagent ${started.childId} declined the task.\nIt left no closing message.`, + `Background subagent ${started.name} declined the task.\nIt left no closing message.`, ) }) @@ -3009,7 +3092,7 @@ describe('continuable settlement delivery', () => { const child = await loadStoredSession(ctx.sessionPersistence, started.childId) expect(hasUserText(child.events, 'second task')).toBe(false) expect(settlementNotices(parent)[0]!.text).toBe( - `Background subagent ${started.childId} failed before it finished.\nIts closing message:\nthe answer`, + `Background subagent ${started.name} failed before it finished.\nIts closing message:\nthe answer`, ) }) @@ -3044,7 +3127,7 @@ describe('continuable settlement delivery', () => { // task had finished. await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) }) expect(settlementNotices(parent)[0]!.text).toBe( - `Background subagent ${started.childId} was stopped before it finished.\nIts closing message:\nthe answer`, + `Background subagent ${started.name} was stopped before it finished.\nIts closing message:\nthe answer`, ) }) @@ -3070,7 +3153,7 @@ describe('continuable settlement delivery', () => { // that genuinely finished with no output. await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) }) expect(settlementNotices(parent)[0]!.text).toBe( - `Background subagent ${started.childId} was stopped before it finished.\nIt left no closing message.`, + `Background subagent ${started.name} was stopped before it finished.\nIt left no closing message.`, ) }) @@ -3096,7 +3179,7 @@ describe('continuable settlement delivery', () => { await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) }) expect(settlementNotices(parent)[0]!.text).toBe( - `Background subagent ${started.childId} was stopped before it finished.\nIt left no closing message.`, + `Background subagent ${started.name} was stopped before it finished.\nIt left no closing message.`, ) }) @@ -3133,7 +3216,7 @@ describe('continuable settlement delivery', () => { expect(hasUserText(child.session.snapshotEvents(), 'never runs')).toBe(false) await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) }) expect(settlementNotices(parent)[0]!.text).toBe( - `Background subagent ${started.childId} was stopped before it finished.\nIts closing message:\nthe answer`, + `Background subagent ${started.name} was stopped before it finished.\nIts closing message:\nthe answer`, ) }) @@ -3155,7 +3238,7 @@ describe('continuable settlement delivery', () => { await waitNoActivation(ctx, started.childId) await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) }) expect(settlementNotices(parent)[0]!.text).toBe( - `Background subagent ${started.childId} finished and will do no further work unless you send it more.\nIts closing message:\nthe answer`, + `Background subagent ${started.name} finished and will do no further work unless you send it more.\nIts closing message:\nthe answer`, ) await expect(started.result).resolves.toMatchObject({ stopReason: 'completed' }) await expect(started.dispose()).rejects.toThrow('scope unwind failed') @@ -3231,7 +3314,7 @@ describe('continuable settlement delivery', () => { // message when that parent is disposed next is pinned by the test below. expect(settlementNotices(parent)).toHaveLength(1) expect(settlementNotices(parent)[0]!.text).toBe( - `Background subagent ${started.childId} was stopped before it finished.\nIt left no closing message.`, + `Background subagent ${started.name} was stopped before it finished.\nIt left no closing message.`, ) expect(parent.session.snapshotEvents().some(event => event.type === 'agent/inbox/spliced')).toBe(true) expect(parent.session.snapshotEvents().some(event => event.type === 'turn/start')).toBe(false) diff --git a/packages/subagent/subagent/tests/external-activation-helpers.ts b/packages/subagent/subagent/tests/external-activation-helpers.ts index 8ac5f26057..174fd46e9a 100644 --- a/packages/subagent/subagent/tests/external-activation-helpers.ts +++ b/packages/subagent/subagent/tests/external-activation-helpers.ts @@ -68,7 +68,7 @@ export async function startExternalActivation( registry.set(request.parent, parent) } const { label, signal, ...execution } = request - return ctx.subagents.startActivation({ + return ctx.subagents.startActivation({ name: `child-${randomUUID()}`, provider, label: label ?? 'External test task', request: { ...execution, parent }, diff --git a/packages/subagent/subagent/tests/external-activation.spec.ts b/packages/subagent/subagent/tests/external-activation.spec.ts index 7d33de2bf6..4e5b086d64 100644 --- a/packages/subagent/subagent/tests/external-activation.spec.ts +++ b/packages/subagent/subagent/tests/external-activation.spec.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto' import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts' /** External execution ownership, result delivery, and process cleanup. */ @@ -26,7 +27,7 @@ async function setup(start: (request: SubagentStartRequest) => Promise ctx.subagents.startActivation({ + start: (delivery: 'parent' | 'caller' = 'caller') => ctx.subagents.startActivation({ name: `child-${randomUUID()}`, provider: 'external', label: 'External work', signal: controller.signal, delivery, request: { parent, prompt: [{ type: 'text', text: 'Work independently' }] }, }), @@ -102,7 +103,7 @@ describe('external subagent activations', () => { } }) try { - await expect(fixture.ctx.subagents.startActivation({ + await expect(fixture.ctx.subagents.startActivation({ name: `child-${randomUUID()}`, provider: 'external', label: 'Unpublished work', signal: fixture.controller.signal, delivery: 'parent', request: { parent, prompt: [{ type: 'text', text: 'Must not publish' }] }, })).rejects.toMatchObject({ code: change === 'drain' ? 'DRAINING' : 'UNAUTHORIZED' }) @@ -148,7 +149,7 @@ describe('external subagent activations', () => { await expect(waiting).resolves.toBe(true) await activation.dispose() expect(backend.dispose).toHaveBeenCalledTimes(1) - expect(fixture.records()).toEqual([]) + expect(fixture.records()).toMatchObject([{ id: activation.childId, name: activation.name.slice('lead/'.length), mode: 'external' }]) expect(fixture.parent.inbox.nextTurn).toEqual([]) }) @@ -285,7 +286,7 @@ describe('external subagent activations', () => { backend.result.resolve(failed) await expect(activation.result).resolves.toEqual(failed) await activation.dispose() - expect(fixture.records()).toMatchObject(delivery === 'parent' ? [{ id: activation.childId, mode: 'external' }] : []) + expect(fixture.records()).toMatchObject([{ id: activation.childId, name: activation.name.slice('lead/'.length), mode: 'external' }]) if (delivery === 'caller') { expect(fixture.parent.inbox.nextTurn).toEqual([]) } else { @@ -317,9 +318,9 @@ describe('external subagent activations', () => { expect((error as Error).message).not.toContain('/private/path') await expect(activation.result).resolves.toEqual(completed) expect(activation.dispose()).toBe(disposal) - expect(fixture.records()).toMatchObject(delivery === 'parent' ? [{ - id: activation.childId, mode: 'external', - }] : []) + expect(fixture.records()).toMatchObject([{ + id: activation.childId, name: activation.name.slice('lead/'.length), mode: 'external', + }]) expect(ended).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ stopReason: 'error' })) if (delivery === 'parent') { expect(fixture.parent.inbox.nextTurn).toHaveLength(1) @@ -365,7 +366,7 @@ it('records external membership at creation without publishing settlement update it('refuses a reserved Session id for an external provider before dispatch', async () => { const start = vi.fn() const fixture = await setup(start) - await expect(fixture.ctx.subagents.startActivation({ + await expect(fixture.ctx.subagents.startActivation({ name: `child-${randomUUID()}`, provider: 'external', childId: SessionId('reserved'), label: 'External', delivery: 'parent', signal: fixture.controller.signal, request: { parent: fixture.parent, prompt: [] }, })).rejects.toMatchObject({ code: 'UNSUPPORTED_CAPABILITY' }) diff --git a/packages/subagent/subagent/tests/external-lifecycle.spec.ts b/packages/subagent/subagent/tests/external-lifecycle.spec.ts index c6df558c36..3430941711 100644 --- a/packages/subagent/subagent/tests/external-lifecycle.spec.ts +++ b/packages/subagent/subagent/tests/external-lifecycle.spec.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto' import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts' /** External activation admission and resource ownership across parent lifetimes. */ @@ -25,7 +26,7 @@ async function setup(start: (request: SubagentStartRequest) => Promise ctx.subagents.startActivation({ + start: (owner: Agent = parent) => ctx.subagents.startActivation({ name: `child-${randomUUID()}`, provider: 'external', label: 'External work', signal: new AbortController().signal, delivery, request: { parent: owner, prompt: [{ type: 'text', text: 'work' }] }, }), diff --git a/packages/subagent/subagent/tests/list-children.spec.ts b/packages/subagent/subagent/tests/list-children.spec.ts index 2e64d9124f..543f028479 100644 --- a/packages/subagent/subagent/tests/list-children.spec.ts +++ b/packages/subagent/subagent/tests/list-children.spec.ts @@ -1,4 +1,5 @@ import { subagentCatalogProjectionDefinition } from '../src/catalog.ts' +import { randomUUID } from 'node:crypto' import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts' import { afterEach, describe, expect, it, vi } from 'vitest' import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs' @@ -72,8 +73,9 @@ async function startChild( ctx: Context, parent: Agent, label: string, + name = `child-${randomUUID()}`, ): Promise { - const started = await ctx.subagents.startActivation({ delivery: 'parent', + const started = await ctx.subagents.startActivation({ name, delivery: 'parent', provider: 'spawn', label, request: { prompt: [{ type: 'text', text: `task: ${label}` }], parent }, @@ -124,18 +126,18 @@ describe('SubagentRuntime.listChildren', () => { signal: testSignal, }) expect(await ctx.subagents.listChildren(parent.id)).toMatchObject([ - { id: callerOwned.childId, label: 'spawn', mode: 'continuable' }, + { id: callerOwned.childId, name: callerOwned.name.slice('lead/'.length), label: 'spawn', mode: 'continuable' }, ]) await callerOwned.result await callerOwned.dispose() - const continuableId = await startChild(ctx, parent, 'continuable child') + const continuableId = await startChild(ctx, parent, 'continuable child', 'continuable') const listSessions = vi.spyOn(ctx.sessionQuery, 'listSessions') const observeSession = vi.spyOn(ctx.sessionQuery, 'observeSession') const children = await ctx.subagents.listChildren(parent.id) expect(children.map(({ createdAt: _createdAt, ...child }) => child)).toEqual([ - { id: callerOwned.childId, label: 'spawn', mode: 'continuable' }, - { id: continuableId, label: 'continuable child', mode: 'continuable' }, + { id: callerOwned.childId, name: callerOwned.name.slice('lead/'.length), label: 'spawn', mode: 'continuable' }, + { id: continuableId, name: 'continuable', label: 'continuable child', mode: 'continuable' }, ]) expect(children.every(child => Number.isFinite(child.createdAt))).toBe(true) expect(listSessions).not.toHaveBeenCalled() @@ -336,18 +338,18 @@ describe('SubagentRuntime.listDescendants', () => { expect(ctx.sessions.get(id)).toBeUndefined() }) - it('traverses one-shot and unknown-mode catalog entries to reach continuable children', async () => { + it.each([false, true])('traverses one-shot and unknown-mode entries (named: %s)', async (named) => { const { ctx, parent } = await setup([]) const once = ctx.sessions.create(SessionId('once')) const unknown = ctx.sessions.create(SessionId('unknown')) const leaf = ctx.sessions.create(SessionId('leaf')) catalog(parent.session, [child('once', 'one-shot')]) - catalog(once, [child('unknown', 'unknown')]) + catalog(once, [{ ...child('unknown', 'unknown'), ...named ? { name: 'future-child' } : {} }]) catalog(unknown, [child('leaf')]) const rows = await ctx.subagents.listDescendants(parent.id) expect(rows).toHaveLength(3) expect(rows[0]).toMatchObject({ kind: 'child', id: once.id, hasChildren: true }) - expect(rows[1]).toEqual({ kind: 'diagnostic', id: unknown.id, reason: 'unsupported', parentId: once.id, depth: 2 }) + expect(rows[1]).toEqual({ kind: 'diagnostic', id: unknown.id, ...named ? { name: 'future-child' } : {}, reason: 'unsupported', parentId: once.id, depth: 2 }) expect(rows[2]).toMatchObject({ kind: 'child', id: leaf.id, parentId: unknown.id, depth: 3 }) }) @@ -392,7 +394,7 @@ describe('SubagentRuntime.listDescendants', () => { const branch = ctx.sessions.create(SessionId('branch')) const hidden = ctx.sessions.create(SessionId('hidden')) const sibling = ctx.sessions.create(SessionId('sibling')) - catalog(parent.session, [child('branch'), child('sibling')]) + catalog(parent.session, [{ ...child('branch'), name: 'broken-branch' }, child('sibling')]) catalog(branch, [child('hidden')]) const observe = ctx.sessionQuery.observeSession.bind(ctx.sessionQuery) vi.spyOn(ctx.sessionQuery, 'observeSession').mockImplementation((id, options) => { @@ -403,7 +405,7 @@ describe('SubagentRuntime.listDescendants', () => { return observe(id, options) }) const rows = await ctx.subagents.listDescendants(parent.id) - expect(rows[0]).toEqual({ kind: 'diagnostic', id: branch.id, parentId: parent.id, depth: 1, reason }) + expect(rows[0]).toEqual({ kind: 'diagnostic', id: branch.id, name: 'broken-branch', parentId: parent.id, depth: 1, reason }) expect(rows[1]).toMatchObject({ kind: 'child', id: sibling.id }) expect(rows.some(row => row.id === hidden.id)).toBe(false) }) diff --git a/packages/subagent/subagent/tests/local-activation.ts b/packages/subagent/subagent/tests/local-activation.ts index 02b9d594c5..5f0a4d51a8 100644 --- a/packages/subagent/subagent/tests/local-activation.ts +++ b/packages/subagent/subagent/tests/local-activation.ts @@ -36,7 +36,7 @@ export async function startTestActivation( request: Omit & { signal?: AbortSignal }, ) { const { signal = new AbortController().signal, label = provider, ...task } = request - const activation = await ctx.get('subagents')!.startActivation({ provider, label, request: task, signal, delivery: 'caller' }) + const activation = await ctx.get('subagents')!.startActivation({ name: `child-${randomUUID()}`, provider, label, request: task, signal, delivery: 'caller' }) return { ...activation, id: activation.childId, localAgent: ctx.get('agents')!.get(activation.childId)! } } diff --git a/packages/subagent/subagent/tests/names.spec.ts b/packages/subagent/subagent/tests/names.spec.ts new file mode 100644 index 0000000000..1dcc622bfb --- /dev/null +++ b/packages/subagent/subagent/tests/names.spec.ts @@ -0,0 +1,311 @@ +import { describe, expect, it, onTestFinished, vi } from 'vitest' +import { Context } from '@deepseek-ai/cordis' +import AgentLoop from '@deepseek-ai/dsh-agent-loop' +import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' +import { SessionId } from '@deepseek-ai/dsh-session' +import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process' +import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' +import SubagentRuntime from '../src/index.ts' +import { SubagentNames } from '../src/names.ts' +import { subagentCatalogProjectionDefinition, subagentCatalogEntries, establishCatalogChild, establishExternalCatalogChild } from '../src/catalog.ts' +import { subagentIdentityProjectionDefinition } from '../src/projection.ts' +import { snapshotSubagentDescriptor } from '../src/descriptor.ts' +import { externalTestParent } from './external-activation-helpers.ts' +import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts' +import { TestSessionQuery } from './test-session-query.ts' + +const signal = new AbortController().signal + +async function setup() { + const ctx = new Context() + const parent = await externalTestParent(ctx) + await mountWorkingDirectoryFixture(ctx) + await ctx.plugin(SubagentRuntime) + await ctx.plugin(TestSessionQuery) + const catalog = await ctx.sessionProjections.register(subagentCatalogProjectionDefinition) + const identity = await ctx.sessionProjections.register(subagentIdentityProjectionDefinition) + const sources = { + catalogOf: (session: typeof parent.session) => subagentCatalogEntries(catalog.stateOf(session)), + identityOf: (session: typeof parent.session) => identity.stateOf(session).identity, + residencyOf: () => undefined, + } + return { ctx, parent, sources, names: new SubagentNames(ctx, sources) } +} + +describe('agent paths', () => { + it('reserves lead for each root and isolates independent trees', async () => { + const { ctx, parent, names } = await setup() + const other = await externalTestParent(ctx) + expect(names.nameOf(parent)).toBe('lead') + expect(await names.resolve(other, 'lead')).toBe(other.id) + for (const root of [parent, other]) { + using reservation = names.reserve(root, 'reviewer', signal) + reservation.commit(SessionId('child'), () => { establishExternalCatalogChild(root.session, SessionId('child'), 'Review', 'reviewer') }) + expect(await names.resolve(root, 'lead/reviewer')).toBe('child') + } + }) + + it.each(['', 'Reviewer', 'has space', '1worker', 'legacy:old', 'a/b', '.', '..', 'a\\b'])( + 'rejects invalid name %j without writing a creation record', async (name) => { + const { parent, names } = await setup() + const seq = parent.session.seq + expect(() => names.reserve(parent, name, signal)).toThrow(expect.objectContaining({ code: 'INVALID_AGENT_NAME' })) + expect(parent.session.seq).toBe(seq) + }, + ) + + it('reuses sibling names in separate branches and resolves paths through only their parent catalogs', async () => { + const { ctx, parent, names, sources } = await setup() + const session = ctx.sessions.create(SessionId('nested'), { meta: { origin: 'subagent', parentSession: parent.id } }) + const nested = { ...parent, id: session.id, session } + onTestFinished(ctx.agents.enter(nested, undefined)) + const descriptor = snapshotSubagentDescriptor({ mode: 'continuable', provider: 'spawn', label: 'Review', name: 'reviewer' }) + session.append('subagent/descriptor', descriptor) + { + using reservation = names.reserve(parent, 'reviewer', signal, session.id) + expect(reservation.name).toBe('lead/reviewer') + await expect(names.resolve(parent, reservation.name)).rejects.toMatchObject({ code: 'AGENT_NAME_NOT_FOUND' }) + reservation.commit(session.id, () => { establishCatalogChild(parent.session, session.header, descriptor) }) + } + const rootEvents = parent.session.snapshotEvents() + { + using reservation = names.reserve(nested, 'reviewer', signal) + expect(reservation.name).toBe('lead/reviewer/reviewer') + reservation.commit(SessionId('external'), () => { establishExternalCatalogChild(session, SessionId('external'), 'Review', 'reviewer') }) + } + expect(parent.session.snapshotEvents()).toEqual(rootEvents) + expect(names.nameOf(nested)).toBe('lead/reviewer') + expect(ctx.subagents.nameOf(nested)).toBe('lead/reviewer') + const restored = new SubagentNames(ctx, sources) + const observe = vi.spyOn(ctx.sessionQuery, 'observeSession') + try { + expect(await restored.resolve(nested, 'lead/reviewer/reviewer')).toBe('external') + expect(observe.mock.calls.map(([id]) => id)).toEqual([session.id]) + expect(await restored.resolve(parent, 'lead/reviewer')).toBe(session.id) + expect(() => restored.reserve(nested, 'reviewer', signal)).toThrow(expect.objectContaining({ code: 'DUPLICATE_AGENT_NAME' })) + expect(() => restored.reserve(parent, 'another', signal, session.id)).toThrow(expect.objectContaining({ code: 'DUPLICATE_CHILD' })) + await expect(restored.resolve(nested, 'lead/reviewer/reviewer/child')).rejects.toMatchObject({ code: 'AGENT_NAME_NOT_FOUND' }) + } finally { + observe.mockRestore() + } + }) + + it('holds and releases equal pending names independently under two live parents', async () => { + const { ctx, parent, names } = await setup() + const branches = ['review', 'explore'].map((name) => { + const session = ctx.sessions.create(SessionId(name), { meta: { origin: 'subagent', parentSession: parent.id } }) + const agent = { ...parent, id: session.id, session } + onTestFinished(ctx.agents.enter(agent, undefined)) + const descriptor = snapshotSubagentDescriptor({ mode: 'continuable', provider: 'spawn', label: name, name }) + session.append('subagent/descriptor', descriptor) + establishCatalogChild(parent.session, session.header, descriptor) + return agent + }) + const first = names.reserve(branches[0]!, 'alice', signal) + using second = names.reserve(branches[1]!, 'alice', signal) + expect(first.name).toBe('lead/review/alice') + expect(second.name).toBe('lead/explore/alice') + first[Symbol.dispose]() + using retry = names.reserve(branches[0]!, 'alice', signal) + expect(() => names.reserve(branches[1]!, 'alice', signal)).toThrow(expect.objectContaining({ code: 'DUPLICATE_AGENT_NAME' })) + retry.commit(SessionId('a'), () => { establishExternalCatalogChild(branches[0]!.session, SessionId('a'), 'Review', 'alice') }) + second.commit(SessionId('b'), () => { establishExternalCatalogChild(branches[1]!.session, SessionId('b'), 'Explore', 'alice') }) + expect(await names.resolve(parent, retry.name)).toBe('a') + expect(await names.resolve(parent, second.name)).toBe('b') + }) + + it.each(['reviewer', '/lead/reviewer', 'lead//reviewer', 'lead/../reviewer', 'lead/reviewer/', 'lead/./reviewer'])( + 'rejects noncanonical path %j', async (path) => { + const { parent, names } = await setup() + establishExternalCatalogChild(parent.session, SessionId('child'), 'Review', 'reviewer') + await expect(names.resolve(parent, path)).rejects.toMatchObject({ code: 'AGENT_NAME_NOT_FOUND' }) + }, + ) + + it('allows root and task-filter words as child segments', async () => { + const { parent, names } = await setup() + for (const name of ['lead', 'unowned']) { + using reservation = names.reserve(parent, name, signal) + reservation.commit(SessionId(name), () => { establishExternalCatalogChild(parent.session, SessionId(name), 'Task', name) }) + expect(await names.resolve(parent, `lead/${name}`)).toBe(name) + } + }) + + it('releases failed startup reservations and refuses a concurrent duplicate before provider dispatch', async () => { + const { ctx, parent } = await setup() + const entered = Promise.withResolvers() + const gate = Promise.withResolvers() + const start = vi.fn(async () => { + entered.resolve(undefined) + await gate.promise + throw new Error('provider failed') + }) + ctx.subagents.registerProvider({ + name: 'external', inheritsParentContext: false, + capabilities: { agentOptions: false, agentPreset: false, outputSchema: false, depthLimit: false }, start, + }) + const spec = { + name: 'reviewer', provider: 'external', label: 'review', delivery: 'caller' as const, + request: { parent, prompt: [{ type: 'text' as const, text: 'Review' }] }, signal, + } + const first = ctx.subagents.startActivation(spec) + const rejected = expect(first).rejects.toThrow('provider failed') + try { + await entered.promise + await expect(ctx.subagents.startActivation(spec)).rejects.toMatchObject({ code: 'DUPLICATE_AGENT_NAME' }) + expect(start).toHaveBeenCalledTimes(1) + } finally { + gate.resolve(undefined) + await rejected + } + await expect(ctx.subagents.startActivation(spec)).rejects.toThrow('provider failed') + expect(start).toHaveBeenCalledTimes(2) + expect(parent.session.snapshotEvents().filter(event => event.type === 'subagent/catalog')).toEqual([]) + }) + + it('rejects reuse of a disposed ephemeral local identity before publishing another child', async () => { + const ctx = new Context() + onTestFinished(() => ctx.fiber.dispose()) + await mountAgentLoopTestDependencies(ctx) + await ctx.plugin(AgentLoop, { agents: [] }) + await mountWorkingDirectoryFixture(ctx) + await ctx.plugin(SubagentRuntime) + await ctx.plugin(SubagentSpawn, { providerName: 'spawn' }) + ctx.llm.registerAdapter(['mock'], new MockAdapter([textResponse('done')])) + const parent = await ctx.agentLoop.create(SessionId('root'), { provider: 'mock', model: 'mock' }) + const start = (name: string) => ctx.subagents.startActivation({ + name, childId: SessionId('ephemeral-child'), provider: 'spawn', label: 'Check', delivery: 'caller', signal, + request: { parent, prompt: [{ type: 'text', text: 'Reply' }] }, + }) + await expect(start('Reviewer')).rejects.toMatchObject({ code: 'INVALID_AGENT_NAME' }) + const first = await start('first') + await first.result + expect(ctx.agents.get(first.childId)).toBeUndefined() + expect(ctx.sessions.get(first.childId)).toBeUndefined() + const before = parent.session.snapshotEvents() + await expect(start('second')).rejects.toMatchObject({ code: 'DUPLICATE_CHILD' }) + expect(parent.session.snapshotEvents()).toEqual(before) + expect(await ctx.subagents.resolveName(parent, 'lead/first')).toBe(first.childId) + }) + + it('resolves only cataloged legacy addresses and rejects cancellation', async () => { + const { ctx, parent, names } = await setup() + const child = ctx.sessions.create(SessionId('old-child'), { meta: { origin: 'subagent', parentSession: parent.id } }) + parent.session.append('subagent/catalog', { version: 0, childId: child.id, childCreatedAt: 0, mode: 'continuable', label: 'Old child' }) + expect(await names.resolve(parent, 'lead/legacy:old-child')).toBe(child.id) + await expect(names.resolve(parent, 'lead/legacy:missing')).rejects.toMatchObject({ code: 'AGENT_NAME_NOT_FOUND' }) + await expect(names.resolve(parent, 'missing')).rejects.toMatchObject({ code: 'AGENT_NAME_NOT_FOUND' }) + await expect(names.resolve(parent, 'lead', AbortSignal.abort(new Error('cancel lookup')))).rejects.toThrow('cancel lookup') + const agent = { ...parent, id: child.id, session: child } + onTestFinished(ctx.agents.enter(agent, undefined)) + expect(names.nameOf(agent)).toBe('lead/legacy:old-child') + }) + + it('keeps unnamed direct-child ids occupied without reading their logs', async () => { + const { ctx, parent, names } = await setup() + parent.session.append('subagent/catalog', { version: 0, childId: SessionId('legacy-child'), childCreatedAt: 0, mode: 'continuable', label: 'Old child' }) + const observe = vi.spyOn(ctx.sessionQuery, 'observeSession') + try { + expect(() => names.reserve(parent, 'writer', signal, SessionId('legacy-child'))) + .toThrow(expect.objectContaining({ code: 'DUPLICATE_CHILD' })) + using reservation = names.reserve(parent, 'external', signal) + const publish = vi.fn() + expect(() => { reservation.commit(SessionId('legacy-child'), publish) }) + .toThrow(expect.objectContaining({ code: 'DUPLICATE_CHILD' })) + expect(publish).not.toHaveBeenCalled() + expect(observe).not.toHaveBeenCalled() + } finally { + observe.mockRestore() + } + }) + + it('resolves historical nested paths after their Agents unload', async () => { + const { ctx, parent, names } = await setup() + const child = ctx.sessions.create(SessionId('old'), { meta: { origin: 'subagent', parentSession: parent.id } }) + establishCatalogChild(parent.session, child.header, { mode: 'continuable', label: 'Old' }) + child.append('subagent/catalog', { version: 2, childId: SessionId('remote'), childCreatedAt: 0, mode: 'external' }) + expect(ctx.agents.get(child.id)).toBeUndefined() + expect(await names.resolve(parent, 'lead/legacy:old/legacy:remote')).toBe('remote') + await expect(names.resolve(parent, 'lead/legacy:missing')).rejects.toMatchObject({ code: 'AGENT_NAME_NOT_FOUND' }) + }) + + it('lists direct children without reading their parent ancestry', async () => { + const { ctx } = await setup() + const session = ctx.sessions.create(SessionId('orphan'), { meta: { origin: 'subagent' } }) + establishExternalCatalogChild(session, SessionId('remote'), 'Task', 'writer') + expect(await ctx.subagents.listChildren(session.id)).toMatchObject([{ id: 'remote', name: 'writer' }]) + }) + + it('checks sibling names without reading an unavailable historical branch', async () => { + const { ctx, parent, names } = await setup() + parent.session.append('subagent/catalog', { version: 0, childId: SessionId('missing'), childCreatedAt: 0, mode: 'continuable', label: 'Missing', name: 'reviewer' }) + const observe = vi.spyOn(ctx.sessionQuery, 'observeSession') + using _reservation = names.reserve(parent, 'writer', signal) + expect(await names.resolve(parent, 'lead/reviewer')).toBe('missing') + await expect(names.resolve(parent, 'lead/writer')).rejects.toMatchObject({ code: 'AGENT_NAME_NOT_FOUND' }) + expect(observe).not.toHaveBeenCalled() + observe.mockRestore() + }) + + it('rejects an ambiguous persisted sibling name', async () => { + const { parent, names } = await setup() + establishExternalCatalogChild(parent.session, SessionId('first'), 'Review', 'reviewer') + establishExternalCatalogChild(parent.session, SessionId('second'), 'Review again', 'reviewer') + await expect(names.resolve(parent, 'lead/reviewer')).rejects.toMatchObject({ code: 'CORRUPT_AGENT_NAMES' }) + }) + + it('refuses publication after ancestor replacement and releases the uncommitted name', async () => { + const { ctx, parent, names } = await setup() + { + using reservation = names.reserve(parent, 'reviewer', signal) + const get = ctx.agents.get.bind(ctx.agents) + const lookup = vi.spyOn(ctx.agents, 'get').mockImplementation(id => id === parent.id ? { ...parent } : get(id)) + const publish = vi.fn() + expect(() => { reservation.commit(SessionId('child'), publish) }).toThrow(expect.objectContaining({ code: 'AGENT_TREE_UNAVAILABLE' })) + expect(publish).not.toHaveBeenCalled() + lookup.mockRestore() + } + using retry = names.reserve(parent, 'reviewer', signal) + expect(() => { retry.commit(SessionId('child'), () => { throw new Error('append failed') }) }).toThrow('append failed') + }) + + it('rejects cyclic live ancestry', async () => { + const { ctx, parent, names } = await setup() + const session = ctx.sessions.create(SessionId('cycle'), { meta: { origin: 'subagent', parentSession: SessionId('cycle') } }) + const child = { ...parent, id: session.id, session } + onTestFinished(ctx.agents.enter(child, undefined)) + expect(() => names.nameOf(child)).toThrow(expect.objectContaining({ code: 'AGENT_TREE_UNAVAILABLE' })) + }) + + it('accepts repeated identical catalog facts and isolates fork-inherited assignments', async () => { + const { ctx, parent, names } = await setup() + establishExternalCatalogChild(parent.session, SessionId('old'), 'Review', 'reviewer') + establishExternalCatalogChild(parent.session, SessionId('old'), 'Review', 'reviewer') + expect(await names.resolve(parent, 'lead/reviewer')).toBe('old') + const fork = ctx.sessions.fork(parent.session, undefined, SessionId('fork')) + const agent = { ...parent, id: fork.id, session: fork } + onTestFinished(ctx.agents.enter(agent, undefined)) + expect(names.nameOf(agent)).toBe('lead') + using reservation = names.reserve(agent, 'reviewer', signal) + reservation.commit(SessionId('new'), () => { establishExternalCatalogChild(fork, SessionId('new'), 'Review', 'reviewer') }) + expect(await names.resolve(agent, 'lead/reviewer')).toBe('new') + }) + + it('rejects a subagent header without a parent', async () => { + const { ctx, parent, names } = await setup() + const session = ctx.sessions.create(SessionId('orphan'), { meta: { origin: 'subagent' } }) + const child = { ...parent, id: session.id, session } + onTestFinished(ctx.agents.enter(child, undefined)) + expect(() => names.reserve(child, 'writer', signal)).toThrow(expect.objectContaining({ code: 'AGENT_TREE_UNAVAILABLE' })) + }) + + it('rejects stale callers and detached children', async () => { + const { ctx, parent, names } = await setup() + expect(() => names.nameOf({ ...parent })).toThrow(expect.objectContaining({ code: 'UNAUTHORIZED' })) + const session = ctx.sessions.create(SessionId('orphan'), { meta: { origin: 'subagent', parentSession: SessionId('missing') } }) + const child = { ...parent, id: session.id, session } + onTestFinished(ctx.agents.enter(child, undefined)) + expect(() => names.nameOf(child)).toThrow(expect.objectContaining({ code: 'AGENT_TREE_UNAVAILABLE' })) + expect(() => names.reserve(child, 'writer', signal)).toThrow(expect.objectContaining({ code: 'AGENT_TREE_UNAVAILABLE' })) + }) +}) diff --git a/packages/subagent/subagent/tests/parent-result.spec.ts b/packages/subagent/subagent/tests/parent-result.spec.ts index 1019a7d58b..0ccf3699fd 100644 --- a/packages/subagent/subagent/tests/parent-result.spec.ts +++ b/packages/subagent/subagent/tests/parent-result.spec.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto' import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts' /** Parent result delivery independent of a local child's messaging tool. */ @@ -24,7 +25,7 @@ it.each(['absent', 'unused'] as const)('returns the closing answer when send_mes const adapter = new MockAdapter([textResponse('CHILD_RESULT_42'), textResponse('Parent received the answer.')]) ctx.llm.registerAdapter(['mock'], adapter) const parent = await ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' }) - const activation = await ctx.subagents.startActivation({ + const activation = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`, provider: 'spawn', label: 'Compute the answer', delivery: 'parent', signal: new AbortController().signal, request: { parent, prompt: [{ type: 'text', text: 'Find the answer.' }], @@ -58,7 +59,7 @@ it('delivers structured output to the parent when the child has send_message', a ]) ctx.llm.registerAdapter(['mock'], adapter) const parent = await ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' }) - const activation = await ctx.subagents.startActivation({ + const activation = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`, provider: 'spawn', label: 'Compute the answer', delivery: 'parent', signal: new AbortController().signal, request: { parent, prompt: [{ type: 'text', text: 'Find the answer.' }], diff --git a/packages/subagent/subagent/tests/parked-wait.spec.ts b/packages/subagent/subagent/tests/parked-wait.spec.ts index 83dd729732..02d0e816ea 100644 --- a/packages/subagent/subagent/tests/parked-wait.spec.ts +++ b/packages/subagent/subagent/tests/parked-wait.spec.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto' import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts' /** Host completion races around idle local children with preserved input. */ import { setImmediate } from 'node:timers/promises' @@ -49,7 +50,7 @@ async function setup() { } function start(owner = parent, provider = 'local') { - const started = ctx.subagents.startActivation({ + const started = ctx.subagents.startActivation({ name: `child-${randomUUID()}`, provider, label: 'child task', delivery: 'caller', diff --git a/packages/subagent/subagent/tests/schedule-tools.spec.ts b/packages/subagent/subagent/tests/schedule-tools.spec.ts index 11c44b9707..41f992e36f 100644 --- a/packages/subagent/subagent/tests/schedule-tools.spec.ts +++ b/packages/subagent/subagent/tests/schedule-tools.spec.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto' import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts' /** * A preset-mounted Schedule tool reaches a delegated child, and the Host @@ -93,7 +94,7 @@ function toolResultTexts(agent: Agent): string[] { describe('a delegated child under a preset that mounts the Schedule tools', () => { it('sees schedule_create and is refused with subagent_session without storing a task', async () => { const { ctx, parent, dispatched } = await setupScheduleHost() - const run = await ctx.subagents.startActivation({ + const run = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`, provider: 'spawn', label: 'child task', delivery: 'caller', diff --git a/packages/subagent/subagent/tests/service.spec.ts b/packages/subagent/subagent/tests/service.spec.ts index bfffcbcf3b..501755b21c 100644 --- a/packages/subagent/subagent/tests/service.spec.ts +++ b/packages/subagent/subagent/tests/service.spec.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto' import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts' import { describe, expect, expectTypeOf, it, onTestFinished, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' @@ -96,7 +97,7 @@ async function start(subagents: SubagentRuntime, provider: string, request: Suba const parent = ctx.agents.get(request.parent.id) ?? (await ctx.agents.create({ sessionId: request.parent.id })).agent const { label, signal, ...options } = request - return subagents.startActivation({ + return subagents.startActivation({ name: `child-${randomUUID()}`, provider, label: label ?? provider, request: { ...options, parent }, signal, delivery, }) } @@ -203,7 +204,7 @@ describe('SubagentRuntime', () => { it('rejects continuable operations when their runtime services are absent', async () => { const { subagents } = await service(false) - await expect(subagents.startActivation({ + await expect(subagents.startActivation({ name: `child-${randomUUID()}`, delivery: 'parent', provider: 'unused', label: 'unused child', @@ -222,7 +223,7 @@ describe('SubagentRuntime', () => { const { subagents } = await service(false) const provider = new StubProvider('external') subagents.registerProvider(provider) - await expect(subagents.startActivation({ + await expect(subagents.startActivation({ name: `child-${randomUUID()}`, provider: provider.name, label: 'Unavailable runtime', request: baseRequest(), @@ -250,7 +251,7 @@ describe('SubagentRuntime', () => { const provider = new StubProvider('external') subagents.registerProvider(provider) const { agent: parent } = await ctx.agents.create({ sessionId: SessionId('reserved-parent') }) - await expect(subagents.startActivation({ + await expect(subagents.startActivation({ name: `child-${randomUUID()}`, provider: provider.name, label: 'Reserved team child', childId: SessionId('reserved-child'), @@ -305,16 +306,23 @@ describe('SubagentRuntime', () => { expect(runIds[0]).toBe(runIds[1]) }) - it('mints distinct lifecycle identities when provider and child ids repeat', async () => { + it('mints distinct lifecycle identities when provider and child ids repeat across roots', async () => { const { ctx, subagents } = await service() - subagents.registerProvider(new StubProvider('reused')) + subagents.registerProvider({ + name: 'reused', capabilities: NO_CAPS, inheritsParentContext: false, + start: async () => ({ + id: SessionId('reused-child'), + result: Promise.resolve({ output: [], stopReason: 'completed' }), + async dispose() {}, + }), + }) const runIds: string[] = [] ctx.on('subagent/start', info => void runIds.push(info.runId)) const first = await start(subagents, 'reused', baseRequest()) await first.result await first.dispose() - const second = await start(subagents, 'reused', baseRequest()) + const second = await start(subagents, 'reused', baseRequest({ parent: fakeParent('parent-2') })) await Promise.all([first.result, second.result]) expect(runIds).toHaveLength(2) @@ -462,8 +470,8 @@ describe('subagent descriptors', () => { provider: 'spawn', } expect(foldSubagentDescriptor([event(minimal)])).toEqual(minimal) - expect(foldSubagentDescriptor([event({ ...minimal, label: 'child work' })])) - .toEqual({ ...minimal, label: 'child work' }) + expect(foldSubagentDescriptor([event({ ...minimal, label: 'child work', name: 'worker' })])) + .toEqual({ ...minimal, label: 'child work', name: 'worker' }) expect(snapshotSubagentDescriptor({ mode: 'continuable', provider: 'spawn', label: 'child', })).toEqual({ version: SUBAGENT_DESCRIPTOR_VERSION, mode: 'continuable', provider: 'spawn', label: 'child' }) @@ -519,6 +527,7 @@ describe('subagent descriptors', () => { }) it.each([ + ['invalid name', { version: SUBAGENT_DESCRIPTOR_VERSION, mode: 'one-shot', provider: 'spawn', name: 'lead/reviewer' }, 'Invalid string'], ['string payload', 'invalid', 'payload must be an object'], ['null payload', null, 'payload must be an object'], ['array payload', [], 'payload must be an object'], @@ -646,7 +655,7 @@ it('rejects a disposed parent before starting provider execution', async () => { subagents.registerProvider(provider) const handle = await ctx.agents.create({ sessionId: SessionId('disposed-parent') }) await handle.dispose() - await expect(subagents.startActivation({ + await expect(subagents.startActivation({ name: `child-${randomUUID()}`, provider: provider.name, label: 'work', delivery: 'caller', signal: new AbortController().signal, request: { parent: handle.agent, prompt: [] }, })).rejects.toMatchObject({ code: 'UNAUTHORIZED' }) diff --git a/packages/subagent/subagent/tests/sibling-messages.spec.ts b/packages/subagent/subagent/tests/sibling-messages.spec.ts index aa3b0a75ab..c599ccb8d9 100644 --- a/packages/subagent/subagent/tests/sibling-messages.spec.ts +++ b/packages/subagent/subagent/tests/sibling-messages.spec.ts @@ -81,7 +81,7 @@ const signal = new AbortController().signal /** Start one caller-delivery child whose history carries `TASK:`. */ async function start(ctx: Context, parent: Agent, marker: string, description = 'shared responsibility'): Promise { - const started = await ctx.subagents.startActivation({ + const started = await ctx.subagents.startActivation({ name: marker, provider: 'spawn', label: description, request: { prompt: [{ type: 'text', text: `TASK:${marker}` }], parent }, @@ -131,7 +131,7 @@ describe('subagent sibling messages', () => { await ctx.subagents.sendMessage(sender, b, [{ type: 'text', text: 'hello' }], { signal }) const delivered = (await storedUserMessages(ctx, b)).find(event => event.data.source.kind === 'agent-message')! - expect(textOf(delivered)).toBe(`Agent ${a} sent a message: hello`) + expect(textOf(delivered)).toBe('Agent lead/a sent a message: hello') expect(delivered.data.source).toEqual({ kind: 'agent-message', form: 'relay', senderSessionId: a }) ctx.subagents.interrupt(a, { kind: 'ancestor', agent: parent }) await settled(ctx, a) @@ -197,9 +197,9 @@ describe('tree-wide messages', () => { await ctx.subagents.sendMessage(parent, leafId, [{ type: 'text', text: 'grandchild' }], { signal }) await ctx.subagents.sendMessage(leaf, parent.id, [{ type: 'text', text: 'grandparent' }], { signal }) expect(received).toEqual([ - { target: leafId, sender: a, text: `Agent ${a} sent a message: cousin` }, - { target: leafId, sender: parent.id, text: `Agent ${parent.id} sent a message: grandchild` }, - { target: parent.id, sender: leafId, text: `Agent ${leafId} sent a message: grandparent` }, + { target: leafId, sender: a, text: 'Agent lead/a sent a message: cousin' }, + { target: leafId, sender: parent.id, text: 'Agent lead sent a message: grandchild' }, + { target: parent.id, sender: leafId, text: 'Agent lead/b/leaf sent a message: grandparent' }, ]) expect(() => { ctx.subagents.interrupt(leafId, { kind: 'ancestor', agent: sender }) }) .toThrow(expect.objectContaining({ code: 'UNAUTHORIZED' })) @@ -225,7 +225,7 @@ describe('tree-wide messages', () => { const messages = await storedUserMessages(ctx, leafId) const delivered = messages.find(event => event.data.source.kind === 'agent-message')! - expect(textOf(delivered)).toBe(`Agent ${a} sent a message: cold cousin`) + expect(textOf(delivered)).toBe('Agent lead/a sent a message: cold cousin') expect(delivered.data.source).toEqual({ kind: 'agent-message', form: 'relay', senderSessionId: a }) await settled(ctx, branchId) expect(resumed).toEqual([{ id: leafId, parent: branchId }]) @@ -354,7 +354,7 @@ describe('tree-wide messages', () => { expect(ctx.agents.get(branchId)).toBe(branch) release.resolve(undefined) const messages = await storedUserMessages(ctx, leafId) - expect(messages.some(message => textOf(message) === `Agent ${parent.id} sent a message: after close`)).toBe(true) + expect(messages.some(message => textOf(message) === 'Agent lead sent a message: after close')).toBe(true) await settled(ctx, branchId) } finally { release.resolve(undefined) diff --git a/packages/subagent/subagent/tests/structured.spec.ts b/packages/subagent/subagent/tests/structured.spec.ts index 08950f7805..f787e9af72 100644 --- a/packages/subagent/subagent/tests/structured.spec.ts +++ b/packages/subagent/subagent/tests/structured.spec.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto' import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts' import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' @@ -384,7 +385,7 @@ describe('in-process structured output', () => { name: 'start_descendant', description: 'Start delegated work', parameters: {}, async execute(_args, exec): Promise { if (exec.agent === undefined) throw new Error('expected the structured parent') - descendant = await ctx.subagents.startActivation({ + descendant = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`, provider: 'spawn', label: 'Nested work', signal: testToolSignal, delivery: 'parent', request: { parent: exec.agent, prompt: [{ type: 'text', text: 'Work until released' }], @@ -429,7 +430,7 @@ describe('in-process structured output', () => { name: 'delegate_answer', description: 'Delegate answer computation', parameters: {}, async execute(_args, exec): Promise { if (exec.agent === undefined) throw new Error('expected the structured parent') - await ctx.subagents.startActivation({ + await ctx.subagents.startActivation({ name: `child-${randomUUID()}`, provider: 'delayed-answer', label: 'Compute answer', signal: testToolSignal, delivery: 'parent', request: { parent: exec.agent, prompt: [{ type: 'text', text: 'Compute the answer' }] }, }) diff --git a/packages/subagent/subagent/tests/timing-projection.spec.ts b/packages/subagent/subagent/tests/timing-projection.spec.ts index dd8af27c69..8a738c3757 100644 --- a/packages/subagent/subagent/tests/timing-projection.spec.ts +++ b/packages/subagent/subagent/tests/timing-projection.spec.ts @@ -29,13 +29,15 @@ function fold(events: SessionEvent[]) { } describe('subagent timing projection', () => { - it('preserves historical one-shot identity without inventing a label', () => { + it.each([undefined, 'historical child'])('preserves historical one-shot identity (label: %s)', (label) => { const descriptor = { ...event('subagent/descriptor', 1, 110), - data: { version: SUBAGENT_DESCRIPTOR_VERSION, mode: 'one-shot', provider: 'spawn' }, + data: { version: SUBAGENT_DESCRIPTOR_VERSION, mode: 'one-shot', provider: 'spawn', ...label === undefined ? {} : { label } }, } as SessionEvent const state = subagentIdentityProjectionDefinition.apply(subagentIdentityProjectionDefinition.init(), descriptor) - expect(subagentIdentityProjectionDefinition.wire.view(state)).toEqual({ mode: 'one-shot', seq: 1 }) + const view = subagentIdentityProjectionDefinition.wire.view(state) + expect(view).toEqual({ mode: 'one-shot', seq: 1, ...label === undefined ? {} : { label } }) + expect(subagentIdentityProjectionDefinition.wire.viewSchema.parse(JSON.parse(JSON.stringify(view)))).toEqual(view) }) it('waits for the session projection registry and releases its projections on unload', async () => { diff --git a/packages/subagent/subagent/tests/working-directory.spec.ts b/packages/subagent/subagent/tests/working-directory.spec.ts index bd8e680cd8..69b018bdee 100644 --- a/packages/subagent/subagent/tests/working-directory.spec.ts +++ b/packages/subagent/subagent/tests/working-directory.spec.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto' import { startTestActivation } from './local-activation.ts' import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts' import { mkdtemp, mkdir, realpath, rm } from 'node:fs/promises' @@ -68,7 +69,7 @@ describe('subagent working directories', () => { it.each(['relative', 'absolute'])('retains an explicit %s directory when a continuable child cold-resumes', async (form) => { const { ctx, parent, origin, first, second } = await fixture() const selected = await realpath(second) - const started = await ctx.subagents.startActivation({ + const started = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`, delivery: 'caller', provider: 'spawn', label: 'directory child', request: { parent, cwd: form === 'absolute' ? second : 'second', prompt: [{ type: 'text', text: 'child' }] }, @@ -96,7 +97,7 @@ describe('subagent working directories', () => { setup: (childCtx, child) => options.setup?.(childCtx.isolate('workingDirectory'), child), })) try { - await expect(ctx.subagents.startActivation({ + await expect(ctx.subagents.startActivation({ name: `child-${randomUUID()}`, delivery: 'caller', provider: 'spawn', label: 'unavailable directory service', childId, request: { parent, cwd: second, prompt: [{ type: 'text', text: 'child' }] }, diff --git a/packages/subagent/tool-subagent-control/README.i18n.yaml b/packages/subagent/tool-subagent-control/README.i18n.yaml index aba1e85c3b..4f1f0fd83a 100644 --- a/packages/subagent/tool-subagent-control/README.i18n.yaml +++ b/packages/subagent/tool-subagent-control/README.i18n.yaml @@ -9,8 +9,8 @@ en: 465d36925c792167 zh: 269cac314f638827 /deepseek-ai-dsh-tool-subagent-control/summary: - en: ae2e4f23feb5b492 - zh: 11b736f3485a3a62 + en: bb6d3aa01e0d70b9 + zh: ff16dadb2e976be4 /deepseek-ai-dsh-tool-subagent-control/table-of-contents: en: d152484eb41ac6b4 zh: 09388d293f9be9cb @@ -21,17 +21,17 @@ en: ab49da7fb0b66bb1 zh: 331ecc5ee6851456 /deepseek-ai-dsh-tool-subagent-control/use-this-package/send-message: - en: d49274c955c540b8 - zh: d6bece8efc944ac1 + en: 75c5b5ad3adeca51 + zh: e2be953f9c2609b2 /deepseek-ai-dsh-tool-subagent-control/use-this-package/interrupt-agent: - en: de45d5a4b98d3c91 - zh: 666aa187170b9c0d + en: eb4a2f0d5e188345 + zh: ad53c7ce5880d8a3 /deepseek-ai-dsh-tool-subagent-control/use-this-package/wait-agent: en: 41ed5ec6e82e0282 zh: 0545e0719d5565d8 /deepseek-ai-dsh-tool-subagent-control/use-this-package/list-agents: - en: 0a94b7351a55c776 - zh: d63aad67d6fe79e0 + en: 96edd3b4f5ca3324 + zh: fce8840fe7e0d933 /deepseek-ai-dsh-tool-subagent-control/understand-the-implementation: en: c5cdf01a78c990f2 zh: 53c180d726ce4502 @@ -39,8 +39,8 @@ en: 0b3853eaa8859d72 zh: 16358081c7362589 /deepseek-ai-dsh-tool-subagent-control/understand-the-implementation/delivery-and-signal-ownership: - en: 0f56ed14a185d4c9 - zh: 676bb9484e3f45d4 + en: 81632e94d10ce33f + zh: 36958764906bdb68 /deepseek-ai-dsh-tool-subagent-control/understand-the-implementation/listing-projection: en: ee3136b09f0c9ef2 zh: 5cbe189bb857fdc7 @@ -57,8 +57,8 @@ en: e4c7376d4f29dbd5 zh: 6b7a9eaf69c9ea53 /deepseek-ai-dsh-tool-subagent-control/model-experience/tool-schema/what-the-model-sees: - en: 1ea7f529eb698c45 - zh: c8af269730f179c1 + en: 3181d0ba99edecc1 + zh: 91142b27c34481a9 /deepseek-ai-dsh-tool-subagent-control/model-experience/tool-schema/token-effect: en: 282c65ac0eebfbdd zh: e9610d49f72918b0 @@ -69,8 +69,8 @@ en: d64e936e83e89a2b zh: b9905c33af6cca45 /deepseek-ai-dsh-tool-subagent-control/model-experience/interrupt-result/what-the-model-sees: - en: 89305619897680ba - zh: e610a844fc09c53f + en: f62d986ac7c1f6cc + zh: 047e2f72a7e34614 /deepseek-ai-dsh-tool-subagent-control/model-experience/interrupt-result/token-effect: en: 5d1bcef28b5f18da zh: 027d67d93d660e76 @@ -81,11 +81,11 @@ en: ab964e307bd37dca zh: 03f975252f838a42 /deepseek-ai-dsh-tool-subagent-control/model-experience/delivery-result/what-the-model-sees: - en: e990979eba8811c0 - zh: e9c21ab21c11c3f1 + en: 5b50b601447c7378 + zh: 766f040863ba024c /deepseek-ai-dsh-tool-subagent-control/model-experience/delivery-result/token-effect: - en: 2da85f2635791a2d - zh: c35ce0b7f9d88adc + en: 2f8fb7fc2b1afca1 + zh: 5e008f5b97babb2d /deepseek-ai-dsh-tool-subagent-control/model-experience/delivery-result/kv-cache-effect: en: f5d5f97a153f7c70 zh: 3d6bc05b9eadfbc2 @@ -93,8 +93,8 @@ en: 094e55d258f2ec2e zh: f364bc9046530bff /deepseek-ai-dsh-tool-subagent-control/model-experience/listing-result/what-the-model-sees: - en: f433b1ca6ddf6ad4 - zh: e19625218e74749a + en: a3524d40d2b1c9af + zh: bec98179547a65aa /deepseek-ai-dsh-tool-subagent-control/model-experience/listing-result/token-effect: en: cf5518c977befa64 zh: 4735a2d3e965874f diff --git a/packages/subagent/tool-subagent-control/README.md b/packages/subagent/tool-subagent-control/README.md index 348def37db..f6206acca4 100644 --- a/packages/subagent/tool-subagent-control/README.md +++ b/packages/subagent/tool-subagent-control/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -`dsh-tool-subagent-control` lets agents discover and message their subagent tree by Session id, interrupt descendants, and wait for their own next-step inbox. One plugin instance registers all four tools; members inherit the same definitions and ordering. +`dsh-tool-subagent-control` lets agents discover and message their subagent tree by full agent path, interrupt descendants, and wait for their own next-step inbox. One plugin instance registers all four tools; members inherit the same definitions and ordering. ## Table of Contents @@ -44,11 +44,11 @@ This package takes no configuration. Its main entry registers `send_message`, `i ### send_message -Sends a message to another Team member by `agent_id`. The caller must be the live root or a resident continuable subagent. A working target receives Steer input at its next step; an inactive target starts or resumes a turn. Only the target resumes; its creation ancestors stay offline. Success returns inbox acceptance (`messageId`), never a reply. Self, unrelated trees, unsupported targets, unknown ids, unavailable history, and rejected admission return errors. +Sends a message to another Team member by `name`. The caller must be the live root or a resident continuable subagent. A working target receives Steer input at its next step; an inactive target starts or resumes a turn. Only the target resumes; its creation ancestors stay offline. Success returns inbox acceptance (`messageId`), never a reply. Self, unrelated trees, unsupported targets, unknown paths, unavailable history, and rejected admission return errors. ### interrupt_agent -`agent_id` identifies a descendant by Session id. External executions stop permanently and cannot receive follow-ups. Local targets stop only their current turn: queued messages stay parked until a later `send_message`, descendants keep running, and the child stays available for follow-ups. The call returns when the stop request is accepted, not when the target is quiet; interrupting an already-finished agent is an accepted no-op, and self, sibling, stale, and non-ancestor callers get errored results. +`name` identifies a descendant. External executions stop permanently and cannot receive follow-ups. Local targets stop only their current turn: queued messages stay parked until a later `send_message`, descendants keep running, and the child stays available for follow-ups. The call returns when the stop request is accepted, not when the target is quiet; interrupting an already-finished agent is an accepted no-op, and self, sibling, stale, and non-ancestor callers get errored results. ### wait_agent @@ -57,7 +57,7 @@ Waits for the exact caller’s next-step inbox. Pending input returns immediatel ### list_agents -Lists the root and all reachable continuable descendants, including the caller, in stable pre-order. Descendant rows carry their direct parent id and depth from the root. Live status is `running` or `inactive`. External entries are omitted without reading child Sessions; readable one-shot children remain traversal nodes. Unknown modes and unreadable catalogs produce diagnostics. Ordinary Session forks lead separate Teams. +Lists the root and all reachable continuable descendants, including the caller, in stable pre-order. Descendant rows carry their direct parent path and depth from the root. Live status is `running` or `inactive`. External entries are omitted without reading child Sessions; readable one-shot children remain traversal nodes. Unknown modes and unreadable catalogs produce diagnostics. Ordinary Session forks lead separate Teams. ----- @@ -75,7 +75,7 @@ Messaging, interruption, and discovery delegate to `ctx.subagents`; residency, c ### Delivery and signal ownership -The execution signal owns work only until inbox acceptance. Accepted messages cannot be cancelled through this tool. Every message is framed as `Agent sent a message:` and recorded with `{ kind: 'agent-message', form: 'relay', senderSessionId: sender.id }`. Tree membership authorizes communication; the Team root owns runtime instances and capacity. +The execution signal owns work only until inbox acceptance. Accepted messages cannot be cancelled through this tool. Every message is framed as `Agent sent a message:` and recorded with `{ kind: 'agent-message', form: 'relay', senderSessionId: sender.id }`. Tree membership authorizes communication; the Team root owns runtime instances and capacity. ### Listing projection @@ -111,7 +111,7 @@ Read these pages when the package-level contract is not enough; they move from t #### What the model sees -The generated [schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent-control) define id-addressed messaging and interruption, whole-tree discovery, and inbox waiting with `timeout_ms`. `wait_agent` returns `{ "timedOut": false }` for available input or a wait signal, and `{ "timedOut": true }` on timeout. It returns no message body or peer-activity diagnosis. +The generated [schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent-control) define name-addressed messaging and interruption, whole-tree discovery, and inbox waiting with `timeout_ms`. `wait_agent` returns `{ "timedOut": false }` for available input or a wait signal, and `{ "timedOut": true }` on timeout. It returns no message body or peer-activity diagnosis. #### Token effect @@ -125,7 +125,7 @@ Prefix-stable; the schemas do not change at runtime, and a parent and its contin #### What the model sees -`interrupt requested for agent ` on acceptance. An unauthorized caller — self, sibling, stale, or non-ancestor — is an errored result naming the rejection; an absent or settled target still renders the acceptance line. +`interrupt requested for agent ` on acceptance. An unknown name or unauthorized caller — self, sibling, stale, or non-ancestor — returns an error; a known settled target still renders the acceptance line. #### Token effect @@ -139,11 +139,11 @@ Append-only; each result follows the reusable request prefix. #### What the model sees -`message delivered to agent ` on acceptance; canonical output carries the accepted `messageId`. A target outside the caller’s Team or rejected admission produces an errored result. +`message delivered to agent ` on acceptance; canonical output carries the accepted `messageId`. A target outside the caller’s Team or rejected admission produces an errored result. #### Token effect -One short acknowledgement per call; replies arrive as separately addressed messages. Use ids from `list_agents` for follow-ups across the Team. +One short acknowledgement per call; replies arrive as separately addressed messages. Use names from `list_agents` for follow-ups across the Team. #### KV Cache effect @@ -153,7 +153,7 @@ Append-only; newly visible content follows the reusable request prefix and does #### What the model sees -The first row is ` [] — team lead`. Each continuable descendant follows as ` [] parent= depth= —