From b9e59cbaeed2db90b66b25ef57cea7d89b6ab949 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Tue, 15 Sep 2026 16:34:09 +0800 Subject: [PATCH] refactor: unify Team availability in the service --- ...-agent-availability-and-team-targets.i18n.yaml | 4 ++-- ...5-model-agent-availability-and-team-targets.md | 8 ++++---- ...odel-agent-availability-and-team-targets.zh.md | 8 ++++---- .../snapshots/agent-team-panel/task.expected.md | 2 +- docs/subsystems/agent-team.i18n.yaml | 4 ++-- docs/subsystems/agent-team.md | 6 +++--- docs/subsystems/agent-team.zh.md | 6 +++--- packages/experimental/agent-team/README.i18n.yaml | 4 ++-- packages/experimental/agent-team/README.md | 4 ++-- packages/experimental/agent-team/README.zh.md | 4 ++-- packages/experimental/agent-team/src/index.ts | 2 +- packages/experimental/agent-team/src/roster.ts | 15 ++++++++++----- packages/experimental/agent-team/src/types.ts | 2 +- .../experimental/agent-team/tests/team.spec.ts | 6 +++--- .../client-ui-agent-team/README.i18n.yaml | 4 ++-- .../experimental/client-ui-agent-team/README.md | 2 +- .../client-ui-agent-team/README.zh.md | 2 +- .../src/client/TeamAction.tsx | 2 -- .../client-ui-agent-team/src/client/locales.ts | 2 -- .../tests/browser-plugin.client.spec.ts | 4 ++-- .../tests/team-action.client.spec.tsx | 4 ++-- .../experimental/tool-agent-team/src/index.ts | 12 ++++-------- .../tool-agent-team/tests/tool-team.spec.ts | 12 +++++++----- .../extensions/tool-cordis/src/api-catalog.ts | 4 ++-- snapshots/session/team-targets/session.v3.jsonl | 4 ++-- 25 files changed, 63 insertions(+), 64 deletions(-) diff --git a/.agents/notes/implemented/simplification/2026-09-15-model-agent-availability-and-team-targets.i18n.yaml b/.agents/notes/implemented/simplification/2026-09-15-model-agent-availability-and-team-targets.i18n.yaml index ebf8b988ee..165022b7dc 100644 --- a/.agents/notes/implemented/simplification/2026-09-15-model-agent-availability-and-team-targets.i18n.yaml +++ b/.agents/notes/implemented/simplification/2026-09-15-model-agent-availability-and-team-targets.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-09-15-model-agent-availability-and-team-targets.md -2026-09-15-model-agent-availability-and-team-targets.md: f826750bb8465a2e85040b4f09bbb270d5468daf -2026-09-15-model-agent-availability-and-team-targets.zh.md: 0bd339c60e7177c459055cb2089c86bbf072a11a +2026-09-15-model-agent-availability-and-team-targets.md: e1f7f95cfa333be6d05677b4b19775c744f2817c +2026-09-15-model-agent-availability-and-team-targets.zh.md: 0447d7d4059eb75d1fb9169389cd83a54292aadf diff --git a/.agents/notes/implemented/simplification/2026-09-15-model-agent-availability-and-team-targets.md b/.agents/notes/implemented/simplification/2026-09-15-model-agent-availability-and-team-targets.md index f826750bb8..e1f7f95cfa 100644 --- a/.agents/notes/implemented/simplification/2026-09-15-model-agent-availability-and-team-targets.md +++ b/.agents/notes/implemented/simplification/2026-09-15-model-agent-availability-and-team-targets.md @@ -12,18 +12,18 @@ Team operations accept member names, but returning both names and Session UUIDs The Team tool adapter returns `target` from the existing member name and omits member Session IDs from creation and listing results. Messaging, interruption, and task assignment consume that same name. Task `ownerName`, task IDs, and message IDs retain their existing meanings. Internal services, Web views, and persisted Team records retain Session identity. -Normal subagent and Team tool results project loaded-between-turns and stored agents as `inactive`. Only an executing turn is `running`; Team retains `provisioning` and `failed` for member creation, and ordinary subagent diagnostic rows retain their read-failure reasons. Interruption projects its previous status by the same rule. Availability does not report task outcomes or waiting dependencies. +Normal subagent tool results and Team service views project loaded-between-turns and stored agents as `inactive`. Only an executing turn is `running`; Team retains `provisioning` and `failed` for member creation, and ordinary subagent diagnostic rows retain their read-failure reasons. Interruption projects its previous status by the same rule. Availability does not report task outcomes or waiting dependencies. -These projections belong to the tools. Runtime loading, cold resume, interruption authority, and mailbox delivery retain their existing behavior. Ordinary subagents continue to use `agent_id`; stable subagent names and historical-data handling are deferred. No persistence type or Session format changes. +The Team service owns roster and interruption availability for both tools and Web. The ordinary subagent projection belongs to its list tool. Runtime loading, cold resume, interruption authority, and mailbox delivery retain their existing behavior. Ordinary subagents continue to use `agent_id`; stable subagent names and historical-data handling are deferred. No persistence type or Session format changes. ## Alternatives considered **Add stable names to ordinary subagents now.** This requires a durable naming policy and explicit handling of historical children without names. It is independent of removing misleading Team identities and model status distinctions. -**Merge internal runtime states.** Residency determines whether delivery starts a loaded agent or restores one from storage. The model can use one availability status while services and Web keep that distinction. +**Merge internal runtime states.** Residency determines whether delivery starts a loaded agent or restores one from storage. Underlying Agent status and registry presence retain that distinction; Team views expose only turn availability. ## Consequences Models can copy a Team result's target directly into applicable operations. They lose member UUIDs and residency details from these tool results; neither is needed for Team addressing. Ordinary subagent and Team addressing remain different until a separate naming decision. -Focused tool tests cover every status projection, returned-target operations, diagnostic rows, and live versus stored children. Recorded headless Team and SDK subagent scenarios pin the assembled model output and schemas. The [Agent Teams decision](../feature/2026-08-05-agent-teams.md) continues to own roster durability, scoping, and authority. +Focused service and tool tests cover every status projection, returned-target operations, diagnostic rows, and live versus stored children. Recorded headless Team and SDK subagent scenarios pin the assembled model output and schemas. The [Agent Teams decision](../feature/2026-08-05-agent-teams.md) continues to own roster durability, scoping, and authority. diff --git a/.agents/notes/implemented/simplification/2026-09-15-model-agent-availability-and-team-targets.zh.md b/.agents/notes/implemented/simplification/2026-09-15-model-agent-availability-and-team-targets.zh.md index 0bd339c60e..0447d7d405 100644 --- a/.agents/notes/implemented/simplification/2026-09-15-model-agent-availability-and-team-targets.zh.md +++ b/.agents/notes/implemented/simplification/2026-09-15-model-agent-availability-and-team-targets.zh.md @@ -12,18 +12,18 @@ Team 操作接受成员名称,但同时返回名称和 Session UUID 容易使 Team 工具适配器从现有成员名称生成 `target`,并从创建和列表结果中省略成员 Session ID。消息、中断和任务分配使用同一名称。任务的 `ownerName`、任务 ID 和消息 ID 保持现有含义。内部服务、Web 视图和持久化 Team 记录保留 Session 身份。 -普通 subagent 与 Team 工具结果把已加载但处于轮次之间的 agent 和仅存储的 agent 都投影为 `inactive`。只有正在执行轮次时才是 `running`;Team 保留表示成员创建状态的 `provisioning` 与 `failed`,普通 subagent 的诊断行保留读取失败原因。中断结果按同一规则投影之前的状态。可用状态不报告任务结果或等待依赖。 +普通 subagent 工具结果与 Team 服务视图把已加载但处于轮次之间的 agent 和仅存储的 agent 都投影为 `inactive`。只有正在执行轮次时才是 `running`;Team 保留表示成员创建状态的 `provisioning` 与 `failed`,普通 subagent 的诊断行保留读取失败原因。中断结果按同一规则投影之前的状态。可用状态不报告任务结果或等待依赖。 -这些投影归工具所有。运行时加载、冷恢复、中断权限和邮箱投递保持现有行为。普通 subagent 继续使用 `agent_id`;稳定的 subagent 名称和历史数据处理暂缓。不修改持久化类型或 Session 格式。 +Team 服务为工具和 Web 统一提供 roster 与中断的可用状态。普通 subagent 的投影归其列表工具所有。运行时加载、冷恢复、中断权限和邮箱投递保持现有行为。普通 subagent 继续使用 `agent_id`;稳定的 subagent 名称和历史数据处理暂缓。不修改持久化类型或 Session 格式。 ## 考虑过的替代方案 **立即为普通 subagent 增加稳定名称。** 这需要持久化命名策略,以及对历史无名称子级的明确处理。它与移除误导性的 Team 身份和模型状态区别相互独立。 -**合并内部运行状态。** 驻留状态决定投递是启动已加载的 agent,还是从存储恢复。模型可以使用统一的可用状态,同时服务和 Web 保留该区别。 +**合并内部运行状态。** 驻留状态决定投递是启动已加载的 agent,还是从存储恢复。底层 Agent 状态与注册表中的存在性保留该区别;Team 视图仅暴露轮次可用状态。 ## 影响 模型可以把 Team 结果的 target 直接复制到适用操作。这些工具结果不再提供成员 UUID 和驻留细节;Team 寻址不需要它们。在另行决定命名方案之前,普通 subagent 与 Team 的寻址方式仍不同。 -定向工具测试覆盖所有状态投影、返回目标的操作、诊断行和在线与仅存储子级。录制的 headless Team 与 SDK subagent 场景固定组装后的模型输出和 schema。[Agent Teams 决策](../feature/2026-08-05-agent-teams.zh.md)继续拥有 roster 持久化、作用域和权限规则。 +定向服务与工具测试覆盖所有状态投影、返回目标的操作、诊断行和在线与仅存储子级。录制的 headless Team 与 SDK subagent 场景固定组装后的模型输出和 schema。[Agent Teams 决策](../feature/2026-08-05-agent-teams.zh.md)继续拥有 roster 持久化、作用域和权限规则。 diff --git a/apps/web/tests/snapshots/agent-team-panel/task.expected.md b/apps/web/tests/snapshots/agent-team-panel/task.expected.md index 691a99c8e4..447a1f6858 100644 --- a/apps/web/tests/snapshots/agent-team-panel/task.expected.md +++ b/apps/web/tests/snapshots/agent-team-panel/task.expected.md @@ -4,7 +4,7 @@ - button "Refresh Team" - button "Close" - heading "Members" [level=3] - - 'button "lead Idle · Model: deepseek-v4-flash" [disabled]' + - 'button "lead Inactive · Model: deepseek-v4-flash" [disabled]' - heading "Shared tasks" [level=3] - button "New task" - article: diff --git a/docs/subsystems/agent-team.i18n.yaml b/docs/subsystems/agent-team.i18n.yaml index 60b0f39970..d74cca98e8 100644 --- a/docs/subsystems/agent-team.i18n.yaml +++ b/docs/subsystems/agent-team.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/agent-team.md -agent-team.md: 6f20ee11cbaf66028a5a3123d1682a0e9a87b014 -agent-team.zh.md: 5f2cfae582f275f25380cbe996f5bad711c0f917 +agent-team.md: f2a410c33e28bc4fca81b4724454b6fcbf788cc4 +agent-team.zh.md: e9ef2d5dc824a81d1c666d0ea7a270c0cfe2c92e diff --git a/docs/subsystems/agent-team.md b/docs/subsystems/agent-team.md index 6f20ee11cb..f2a410c33e 100644 --- a/docs/subsystems/agent-team.md +++ b/docs/subsystems/agent-team.md @@ -21,7 +21,7 @@ interface TeamMemberSnapshot { } ``` -Every member starts in `provisioning` and reaches exactly one terminal roster phase, `active` or `failed`. Runtime `running`/`idle`/`inactive` status is derived separately and never rewrites this record. +Every member starts in `provisioning` and reaches exactly one terminal roster phase, `active` or `failed`. Roster `running`/`inactive` status is derived separately and never rewrites this record. ## Durable mailbox @@ -38,7 +38,7 @@ interface TeamMessageSnapshot { } ``` -Every message attempts Steer delivery. A running target receives it at the nearest step boundary, an idle target starts a turn, and an inactive teammate cold-resumes. Scheduling is not stored in the durable record because callers cannot select another mode. +Every message attempts Steer delivery. A running target receives it at the nearest step boundary, an inactive target starts a turn if loaded or cold-resumes otherwise. Scheduling is not stored in the durable record because callers cannot select another mode. The target Session keeps message identity and sender attribution on both the pending inbox item and the eventual user message. Folding that source across inbox and history is the target-side de-duplication key; the model-visible framing repeats the id and sender. @@ -168,7 +168,7 @@ async waitForChange(caller: Agent, timeoutMs: number, signal: AbortSignal): Prom * @param targetName - durable teammate name. * @returns the target status sampled before cancellation. */ -interrupt(caller: Agent, targetName: string): { previousStatus: 'running' | 'idle' | 'inactive' } +interrupt(caller: Agent, targetName: string): { previousStatus: 'running' | 'inactive' } /** * Resolve a caller without throwing, used by scoped-tool installation and observers. diff --git a/docs/subsystems/agent-team.zh.md b/docs/subsystems/agent-team.zh.md index 5f2cfae582..e9ef2d5dc8 100644 --- a/docs/subsystems/agent-team.zh.md +++ b/docs/subsystems/agent-team.zh.md @@ -21,7 +21,7 @@ interface TeamMemberSnapshot { } ``` -每个 member 都从 `provisioning` 开始,并且只到达一个终态 roster phase:`active` 或 `failed`。运行时 `running`/`idle`/`inactive` 状态单独派生,绝不会重写该记录。 +每个 member 都从 `provisioning` 开始,并且只到达一个终态 roster phase:`active` 或 `failed`。roster 的 `running`/`inactive` 状态单独派生,绝不会重写该记录。 ## 持久 mailbox @@ -38,7 +38,7 @@ interface TeamMessageSnapshot { } ``` -每条消息都会尝试 Steer 投递。running target 在最近的步骤边界收到消息,idle target 启动一个轮次,inactive teammate 则冷恢复。调用方不能选择其他模式,因此持久记录不存储调度方式。 +每条消息都会尝试 Steer 投递。running target 在最近的步骤边界收到消息,inactive target 在已加载时启动一个轮次,否则冷恢复。调用方不能选择其他模式,因此持久记录不存储调度方式。 target Session 会在 pending inbox 条目和最终用户消息上保留消息身份与发送者归因。跨 inbox 与历史折叠该 source 构成 target 侧去重键;模型可见的 framing 会重复 id 和发送者。 @@ -168,7 +168,7 @@ async waitForChange(caller: Agent, timeoutMs: number, signal: AbortSignal): Prom * @param targetName - durable teammate name. * @returns the target status sampled before cancellation. */ -interrupt(caller: Agent, targetName: string): { previousStatus: 'running' | 'idle' | 'inactive' } +interrupt(caller: Agent, targetName: string): { previousStatus: 'running' | 'inactive' } /** * Resolve a caller without throwing, used by scoped-tool installation and observers. diff --git a/packages/experimental/agent-team/README.i18n.yaml b/packages/experimental/agent-team/README.i18n.yaml index 56fefe2904..23fa1b5e5b 100644 --- a/packages/experimental/agent-team/README.i18n.yaml +++ b/packages/experimental/agent-team/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/experimental/agent-team/README.md -README.md: 1dba55aaef5c2f061de85de3b38cf5c644fa90b7 -README.zh.md: 749f5b9d27ed863639a6dabad80254c9a19727dd +README.md: 01e25b82d26507446b6607bcbad63c66617791a6 +README.zh.md: b931000b68124e24774c6e42114f4e81948665df diff --git a/packages/experimental/agent-team/README.md b/packages/experimental/agent-team/README.md index 1dba55aaef..01e25b82d2 100644 --- a/packages/experimental/agent-team/README.md +++ b/packages/experimental/agent-team/README.md @@ -60,7 +60,7 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a Ask the Lead to create a teammate: give it a unique lowercase name such as `reviewer` and describe its job. A teammate starts fresh with no memory of the Lead's conversation, or as a fork that inherits the Lead's completed turns; the creation request chooses which. Teammate names are permanent — even a teammate whose creation failed keeps its name, and no name is ever reused. -The roster shows every member with its role (`lead` or `teammate`) and current status: `running`, `idle`, `inactive` (a member that exists but is not loaded), `provisioning`, or `failed`. A member that is not loaded receives its messages when it wakes. +The roster shows every member with its role (`lead` or `teammate`) and current status: `running`, `inactive` (no turn is executing, whether loaded or stored), `provisioning`, or `failed`. A member that is not loaded receives its messages when it wakes. Only the Lead can create teammates or interrupt them. @@ -68,7 +68,7 @@ Only the Lead can create teammates or interrupt them. Any member can send a message to any other member or to the Lead. A live member receives it immediately; an offline member's messages queue and arrive when it resumes. Messages are never lost and never delivered twice. -Every message uses Steer: a running target receives it at the nearest step boundary, an idle target starts a turn, and an inactive teammate cold-resumes. The sender always sees the outcome — accepted by the target inbox, or retained as queued when delivery is temporarily unavailable. A queued message is already safely stored, so it must not be resent. +Every message uses Steer: a running target receives it at the nearest step boundary, an inactive target starts a turn if loaded or cold-resumes otherwise. The sender always sees the outcome — accepted by the target inbox, or retained as queued when delivery is temporarily unavailable. A queued message is already safely stored, so it must not be resent. ### Shared task board diff --git a/packages/experimental/agent-team/README.zh.md b/packages/experimental/agent-team/README.zh.md index 749f5b9d27..b931000b68 100644 --- a/packages/experimental/agent-team/README.zh.md +++ b/packages/experimental/agent-team/README.zh.md @@ -60,7 +60,7 @@ kind: "package-reference" 请 Lead 创建 teammate:给它一个唯一的小写名字(例如 `reviewer`)并描述其职责。teammate 可以 fresh 启动(不携带 Lead 对话的任何记忆),也可以作为 fork 启动(继承 Lead 已完成的轮次);创建请求决定用哪种。teammate 名字是永久的——即使创建失败的 teammate 也保留其名字,任何名字都不会被复用。 -roster 显示每个成员的职责(`lead` 或 `teammate`)与当前状态:`running`、`idle`、`inactive`(存在但未加载的成员)、`provisioning` 或 `failed`。未加载的成员会在唤醒后收到其消息。 +roster 显示每个成员的职责(`lead` 或 `teammate`)与当前状态:`running`、`inactive`(当前没有执行轮次,包括已加载和仅存储的成员)、`provisioning` 或 `failed`。未加载的成员会在唤醒后收到其消息。 只有 Lead 可以创建 teammate 或中断它们。 @@ -68,7 +68,7 @@ roster 显示每个成员的职责(`lead` 或 `teammate`)与当前状态:` 任何成员都可以向任何其他成员或 Lead 发送消息。live 成员会立即收到;离线成员的消息会排队,并在其恢复后到达。消息不会丢失,也不会重复投递。 -每条消息都使用 Steer:running target 在最近的步骤边界收到消息,idle target 启动一个轮次,inactive teammate 则冷恢复。发送方始终能看到结果——target inbox 已接受,或在投递暂时不可用时保留为 queued。排队的消息已经安全存储,因此绝不能重发。 +每条消息都使用 Steer:running target 在最近的步骤边界收到消息,inactive target 在已加载时启动一个轮次,否则冷恢复。发送方始终能看到结果——target inbox 已接受,或在投递暂时不可用时保留为 queued。排队的消息已经安全存储,因此绝不能重发。 ### 共享任务板 diff --git a/packages/experimental/agent-team/src/index.ts b/packages/experimental/agent-team/src/index.ts index 9066e185ca..abdedc6d63 100644 --- a/packages/experimental/agent-team/src/index.ts +++ b/packages/experimental/agent-team/src/index.ts @@ -221,7 +221,7 @@ export class TeamService extends TypertRemoteService { * @param targetName - durable teammate name. * @returns the target status sampled before cancellation. */ - interrupt(caller: Agent, targetName: string): { previousStatus: 'running' | 'idle' | 'inactive' } { + interrupt(caller: Agent, targetName: string): { previousStatus: 'running' | 'inactive' } { return this.roster.interrupt(caller, targetName) } diff --git a/packages/experimental/agent-team/src/roster.ts b/packages/experimental/agent-team/src/roster.ts index ae52c741c2..d58357e03f 100644 --- a/packages/experimental/agent-team/src/roster.ts +++ b/packages/experimental/agent-team/src/roster.ts @@ -133,7 +133,7 @@ export class TeamRoster { id: root.id, name: 'lead', role: 'lead', - status: root.status, + status: availability(root), ...root.options.model === undefined ? {} : { model: root.options.model }, diagnostics: [], }] @@ -148,7 +148,7 @@ export class TeamRoster { ? 'failed' : member.phase === 'provisioning' ? 'provisioning' - : live?.status ?? 'inactive', + : availability(live), description: member.description, provider: member.provider, context: member.context, @@ -201,7 +201,7 @@ export class TeamRoster { * @param targetName - durable teammate name. * @returns the target status sampled before cancellation. */ - interrupt(caller: Agent, targetName: string): { previousStatus: 'running' | 'idle' | 'inactive' } { + interrupt(caller: Agent, targetName: string): { previousStatus: 'running' | 'inactive' } { const membership = this.membership(caller) if (membership.role !== 'lead') throw new TeamError('only the Team Lead can interrupt teammates', 'TEAM_LEAD_REQUIRED') const state = this.journal.state(membership.root) @@ -209,7 +209,7 @@ export class TeamRoster { if (target.id === membership.root.id) throw new TeamError('the Team Lead cannot interrupt itself', 'TEAM_INVALID_TARGET') const live = this.ctx.agents.get(target.id) if (live === undefined) return { previousStatus: 'inactive' } - const previousStatus = live.status + const previousStatus = availability(live) this.ctx.subagents.interrupt(target.id, { kind: 'ancestor', agent: caller }) return { previousStatus } } @@ -440,7 +440,7 @@ export class TeamRoster { id: member.id, name: member.name, role: 'teammate', - status: live?.status ?? 'inactive', + status: availability(live), description: member.description, provider: member.provider, context: member.context, @@ -487,3 +487,8 @@ export class TeamRoster { return foldSubagentDescriptor(agent.session.snapshotEvents(agent.session.inheritedEventCount)) !== undefined } } + +/** Turn availability is independent of whether the Agent is loaded. */ +function availability(agent: Agent | undefined): 'running' | 'inactive' { + return agent?.status === 'running' ? 'running' : 'inactive' +} diff --git a/packages/experimental/agent-team/src/types.ts b/packages/experimental/agent-team/src/types.ts index f7b8ca414f..cf659ce9d0 100644 --- a/packages/experimental/agent-team/src/types.ts +++ b/packages/experimental/agent-team/src/types.ts @@ -59,7 +59,7 @@ export interface TeamMemberView { readonly id: SessionId readonly name: string readonly role: 'lead' | 'teammate' - readonly status: 'running' | 'idle' | 'inactive' | 'provisioning' | 'failed' + readonly status: 'running' | 'inactive' | 'provisioning' | 'failed' readonly description?: string readonly provider?: string readonly context?: 'fresh' | 'fork' diff --git a/packages/experimental/agent-team/tests/team.spec.ts b/packages/experimental/agent-team/tests/team.spec.ts index 79354a6427..416df1fd8f 100644 --- a/packages/experimental/agent-team/tests/team.spec.ts +++ b/packages/experimental/agent-team/tests/team.spec.ts @@ -178,7 +178,7 @@ describe('Team identity and provisioning', () => { expect(service.listMembers(lead)).toEqual([expect.objectContaining({ name: 'lead', - status: 'idle', + status: 'inactive', diagnostics: [], })]) const provisioning = { @@ -220,7 +220,7 @@ describe('Team identity and provisioning', () => { expect((await ctx.sessionPersistence.stat(forked.member.id))?.header.isSeeded).toBe(true) expect((await ctx.sessionPersistence.stat(fresh.member.id))?.header.isSeeded).toBe(false) expect(ctx.agentTeams.listMembers(lead).map(row => [row.name, row.context, row.status])).toEqual([ - ['lead', undefined, 'idle'], + ['lead', undefined, 'inactive'], ['fork-worker', 'fork', 'inactive'], ['fresh-worker', 'fresh', 'inactive'], ]) @@ -878,7 +878,7 @@ describe('Team Remote API', () => { const { ctx, lead } = await setup([]) expect(ctx.agentTeams.typertRemote).toMatchObject({ serviceKey: 'agentTeams', namespace: 'agentTeams' }) expect(ctx.agentTeams.remoteView(lead)).toEqual({ - members: [expect.objectContaining({ name: 'lead', role: 'lead', status: 'idle' })], + members: [expect.objectContaining({ name: 'lead', role: 'lead', status: 'inactive' })], tasks: [], }) diff --git a/packages/experimental/client-ui-agent-team/README.i18n.yaml b/packages/experimental/client-ui-agent-team/README.i18n.yaml index a4250bc7be..196e093462 100644 --- a/packages/experimental/client-ui-agent-team/README.i18n.yaml +++ b/packages/experimental/client-ui-agent-team/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/experimental/client-ui-agent-team/README.md -README.md: f9609eba7cc156151b7e62bc0a298730f6f258a8 -README.zh.md: 4e47e162008f567b08debf18adb2423f81d18486 +README.md: 34e89cfe92db9656cf2fc948e154627c0d5f9407 +README.zh.md: e01cbbb534282fb2c6731579fc753954bcde314b diff --git a/packages/experimental/client-ui-agent-team/README.md b/packages/experimental/client-ui-agent-team/README.md index f9609eba7c..34e89cfe92 100644 --- a/packages/experimental/client-ui-agent-team/README.md +++ b/packages/experimental/client-ui-agent-team/README.md @@ -29,7 +29,7 @@ Enable this package through [`@deepseek-ai/dsh-experimental-agent-team-profile`] ### Inspect and navigate the roster -Opening the panel calls `agentTeams/view`. Roster rows show durable names, runtime status, model, and diagnostics. Provisioning and running members use the shared ongoing loader, idle and inactive members use idle, and failed members use error. Selecting a healthy teammate refreshes the existing direct-child catalog and opens the ordinary `{ parentSessionId, childSessionId, mode: 'continuable' }` address. History and later human prompts continue through the stable addressed-subagent conversation path; this package adds no Team-specific address field. +Opening the panel calls `agentTeams/view`. Roster rows show durable names, turn availability, model, and diagnostics. Provisioning and running members use the shared ongoing loader, inactive members use idle, and failed members use error. Selecting a healthy teammate refreshes the existing direct-child catalog and opens the ordinary `{ parentSessionId, childSessionId, mode: 'continuable' }` address. History and later human prompts continue through the stable addressed-subagent conversation path; this package adds no Team-specific address field. ### Manage the task board diff --git a/packages/experimental/client-ui-agent-team/README.zh.md b/packages/experimental/client-ui-agent-team/README.zh.md index 4e47e16200..e01cbbb534 100644 --- a/packages/experimental/client-ui-agent-team/README.zh.md +++ b/packages/experimental/client-ui-agent-team/README.zh.md @@ -29,7 +29,7 @@ kind: "package-reference" ### 检查并导航 roster -打开 panel 会调用 `agentTeams/view`。Roster row 展示持久 name、运行时 status、model 与 diagnostics。provisioning 和 running 成员使用共享 ongoing loading,idle 和 inactive 成员使用 idle 灰点,failed 成员使用 error 红点。选择健康 teammate 时,系统刷新既有直接 child catalog,并打开普通的 `{ parentSessionId, childSessionId, mode: 'continuable' }` address。History 与后续人类提示词继续使用稳定 addressed-subagent 会话路径;本包不会添加 Team 专用 address 字段。 +打开 panel 会调用 `agentTeams/view`。Roster row 展示持久 name、轮次可用状态、model 与 diagnostics。provisioning 和 running 成员使用共享 ongoing loading,inactive 成员使用 idle 灰点,failed 成员使用 error 红点。选择健康 teammate 时,系统刷新既有直接 child catalog,并打开普通的 `{ parentSessionId, childSessionId, mode: 'continuable' }` address。History 与后续人类提示词继续使用稳定 addressed-subagent 会话路径;本包不会添加 Team 专用 address 字段。 ### 管理任务板 diff --git a/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx b/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx index 250caa9da3..f0a25a790e 100644 --- a/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx +++ b/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx @@ -89,7 +89,6 @@ function statusKey(status: TeamTask['status']): TeamKey { function memberStatusKey(status: TeamRosterMember['status']): TeamKey { switch (status) { case 'running': return 'memberStatus.running' - case 'idle': return 'memberStatus.idle' case 'inactive': return 'memberStatus.inactive' case 'provisioning': return 'memberStatus.provisioning' case 'failed': return 'memberStatus.failed' @@ -100,7 +99,6 @@ function memberDotState(status: TeamRosterMember['status']): StateDotState { switch (status) { case 'running': case 'provisioning': return 'ongoing' - case 'idle': case 'inactive': return 'idle' case 'failed': return 'error' } diff --git a/packages/experimental/client-ui-agent-team/src/client/locales.ts b/packages/experimental/client-ui-agent-team/src/client/locales.ts index 692df60d64..930446a9d7 100644 --- a/packages/experimental/client-ui-agent-team/src/client/locales.ts +++ b/packages/experimental/client-ui-agent-team/src/client/locales.ts @@ -33,7 +33,6 @@ export const zh = { blocked: '被依赖阻塞', conflict: '任务状态已变化,已重新加载;请检查后重试。', 'memberStatus.running': '运行中', - 'memberStatus.idle': '空闲', 'memberStatus.inactive': '未运行', 'memberStatus.provisioning': '准备中', 'memberStatus.failed': '失败', @@ -75,7 +74,6 @@ export const en = { blocked: 'Blocked by dependencies', conflict: 'Task state changed and was reloaded. Review it before retrying.', 'memberStatus.running': 'Running', - 'memberStatus.idle': 'Idle', 'memberStatus.inactive': 'Inactive', 'memberStatus.provisioning': 'Provisioning', 'memberStatus.failed': 'Failed', diff --git a/packages/experimental/client-ui-agent-team/tests/browser-plugin.client.spec.ts b/packages/experimental/client-ui-agent-team/tests/browser-plugin.client.spec.ts index b1e473d518..360839e3dc 100644 --- a/packages/experimental/client-ui-agent-team/tests/browser-plugin.client.spec.ts +++ b/packages/experimental/client-ui-agent-team/tests/browser-plugin.client.spec.ts @@ -56,7 +56,7 @@ async function bench(options: { } const view = { members: [{ - id: SESSION, name: 'lead', role: 'lead' as const, status: 'idle' as const, diagnostics: [], + id: SESSION, name: 'lead', role: 'lead' as const, status: 'inactive' as const, diagnostics: [], }], tasks: [task], } ctx.provide('remote.agentTeams', { @@ -179,7 +179,7 @@ describe('ui-team browser plugin', () => { id: SESSION, name: 'lead', role: 'lead', - status: 'idle', + status: 'inactive', diagnostics: [], }) expect(b.navigation).toEqual([]) diff --git a/packages/experimental/client-ui-agent-team/tests/team-action.client.spec.tsx b/packages/experimental/client-ui-agent-team/tests/team-action.client.spec.tsx index 7485435a35..52136ffc1e 100644 --- a/packages/experimental/client-ui-agent-team/tests/team-action.client.spec.tsx +++ b/packages/experimental/client-ui-agent-team/tests/team-action.client.spec.tsx @@ -33,7 +33,7 @@ const task: TeamTask = { } const view: TeamView = { members: [ - { id: SESSION, name: 'lead', role: 'lead', status: 'idle', model: 'model-a', diagnostics: [] }, + { id: SESSION, name: 'lead', role: 'lead', status: 'inactive', model: 'model-a', diagnostics: [] }, { id: 'worker-id' as SessionId, name: 'worker', @@ -95,7 +95,7 @@ describe('TeamAction', () => { const firstLoad = Promise.withResolvers<{ ok: true; value: TeamView }>() const nextView: TeamView = { ...view, - members: [{ id: nextSession, name: 'lead', role: 'lead', status: 'idle', diagnostics: [] }], + members: [{ id: nextSession, name: 'lead', role: 'lead', status: 'inactive', diagnostics: [] }], tasks: [{ ...task, id: 'task-next' as TeamTaskId, subject: 'Next session task' }], } const load = vi.fn((sessionId: SessionId) => sessionId === SESSION diff --git a/packages/experimental/tool-agent-team/src/index.ts b/packages/experimental/tool-agent-team/src/index.ts index cf5b0c0e02..3ab4b77235 100644 --- a/packages/experimental/tool-agent-team/src/index.ts +++ b/packages/experimental/tool-agent-team/src/index.ts @@ -59,10 +59,10 @@ const MEMBER_VIEW_SCHEMA = { }, } as const -/** Project member identity and availability without changing the service or Web view. */ +/** Expose the member name as its model-facing target. */ function modelMember(member: TeamMemberView): InferValue { - const { id: _id, name, status, ...details } = member - return { target: name, status: status === 'idle' ? 'inactive' : status, ...details } + const { id: _id, name, ...details } = member + return { target: name, ...details } } /** One shared task, matching the public `TeamTaskView`. */ @@ -274,11 +274,7 @@ function install(agent: Agent, ctx: Context, config: Required): () => vo }, output: jsonOutput(INTERRUPT_VALUE_SCHEMA), execute(args, exec) { - const { previousStatus } = ctx.agentTeams.interrupt( - callingAgent(exec.agent, 'interrupt_agent'), - args.target, - ) - return Promise.resolve({ previousStatus: previousStatus === 'running' ? 'running' : 'inactive' } as const) + return Promise.resolve(ctx.agentTeams.interrupt(callingAgent(exec.agent, 'interrupt_agent'), args.target)) }, }))) 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 08610a5704..09025f208a 100644 --- a/packages/experimental/tool-agent-team/tests/tool-team.spec.ts +++ b/packages/experimental/tool-agent-team/tests/tool-team.spec.ts @@ -128,7 +128,7 @@ async function waitNoAgent(ctx: Context, id: SessionId): Promise { } describe('dsh-tool-team', () => { - it.each(['running', 'idle', 'inactive', 'provisioning', 'failed'] as const)( + it.each(['running', 'inactive', 'provisioning', 'failed'] as const)( 'projects %s members consistently in creation, listing, and schemas', async (status) => { const { ctx, lead } = await setup([]) const member = { @@ -138,7 +138,7 @@ describe('dsh-tool-team', () => { vi.spyOn(ctx.agentTeams, 'spawnTeammate').mockResolvedValue({ member }) vi.spyOn(ctx.agentTeams, 'listMembers').mockReturnValue([member]) const expected = { - target: 'reviewer', role: 'teammate', status: status === 'idle' ? 'inactive' : status, + target: 'reviewer', role: 'teammate', status, description: 'review changes', diagnostics: [], } const spawned = await execute(ctx, lead, 'spawn_teammate', { @@ -162,11 +162,11 @@ describe('dsh-tool-team', () => { }, ) - it.each(['running', 'idle', 'inactive'] as const)('projects interrupted %s status', async (previousStatus) => { + it.each(['running', 'inactive'] as const)('returns interrupted %s status', async (previousStatus) => { const { ctx, lead } = await setup([]) const interrupt = vi.spyOn(ctx.agentTeams, 'interrupt').mockReturnValue({ previousStatus }) const result = await execute(ctx, lead, 'interrupt_agent', { target: 'reviewer' }) - expect(JSON.parse(text(result))).toEqual({ previousStatus: previousStatus === 'running' ? 'running' : 'inactive' }) + expect(JSON.parse(text(result))).toEqual({ previousStatus }) expect(interrupt).toHaveBeenCalledWith(lead, 'reviewer') }) @@ -196,9 +196,11 @@ describe('dsh-tool-team', () => { const interrupted = await execute(ctx, lead, 'interrupt_agent', { target: listed[1]!.target }) expect(interrupted.isError).toBe(false) await child.whenIdle() + expect(child.status).toBe('idle') + expect(ctx.agentTeams.interrupt(lead, member.target)).toEqual({ previousStatus: 'inactive' }) const stored = await execute(ctx, lead, 'list_agents', {}) expect(JSON.parse(text(stored))).toContainEqual(expect.objectContaining({ target: member.target, status: 'inactive' })) - expect(ctx.agentTeams.listMembers(lead)[1]).toMatchObject({ id: childId, name: member.target, status: 'idle' }) + expect(ctx.agentTeams.listMembers(lead)[1]).toMatchObject({ id: childId, name: member.target, status: 'inactive' }) }) it('installs the complete scoped schema and shared-checkout policy for roots and teammates', async () => { diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 5e6d2a4bcf..643ff6bed0 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -405,7 +405,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ returns: 'one observed change or a timeout result.', }, { - signature: 'interrupt(caller: Agent, targetName: string): { previousStatus: \'running\' | \'idle\' | \'inactive\' }', + signature: 'interrupt(caller: Agent, targetName: string): { previousStatus: \'running\' | \'inactive\' }', description: 'Interrupt one live teammate turn without clearing its pending inbox.', parameters: [{ name: 'caller', description: 'exact live Lead Agent.' }, { name: 'targetName', description: 'durable teammate name.' }], returns: 'the target status sampled before cancellation.', @@ -6542,7 +6542,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'TeamMemberView', - declaration: 'export interface TeamMemberView {\n readonly id: SessionId;\n readonly name: string;\n readonly role: \'lead\' | \'teammate\';\n readonly status: \'running\' | \'idle\' | \'inactive\' | \'provisioning\' | \'failed\';\n readonly description?: string;\n readonly provider?: string;\n readonly context?: \'fresh\' | \'fork\';\n readonly model?: string;\n readonly diagnostics: string[];\n}', + declaration: 'export interface TeamMemberView {\n readonly id: SessionId;\n readonly name: string;\n readonly role: \'lead\' | \'teammate\';\n readonly status: \'running\' | \'inactive\' | \'provisioning\' | \'failed\';\n readonly description?: string;\n readonly provider?: string;\n readonly context?: \'fresh\' | \'fork\';\n readonly model?: string;\n readonly diagnostics: string[];\n}', }, { name: 'TeamMessageId', diff --git a/snapshots/session/team-targets/session.v3.jsonl b/snapshots/session/team-targets/session.v3.jsonl index 805624dac8..1d3472a13b 100644 --- a/snapshots/session/team-targets/session.v3.jsonl +++ b/snapshots/session/team-targets/session.v3.jsonl @@ -14,12 +14,12 @@ {"type":"session/title","data":{"title":"Use Agent Teams list_agents to","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"team-list-1","name":"list_agents","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788778145981,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788778145981,"index":0,"dt":[],"id":"team-list-1","name":"list_agents","args":["{}"]},{"type":"chunk","time":1788778145981,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"team-list-1","name":"list_agents","arguments":"{}"}}},{"type":"chunk","time":1788778145981,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788778145981,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"team-list-1","name":"list_agents","arguments":"{}"}} -{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"team-list-1"},"content":[{"type":"tool-result","toolCallId":"team-list-1","content":[{"type":"text","text":"[{\"target\":\"lead\",\"status\":\"running\",\"role\":\"lead\",\"model\":\"deepseek-v4-flash\",\"diagnostics\":[]}]"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"team-list-1"},"content":[{"type":"tool-result","toolCallId":"team-list-1","content":[{"type":"text","text":"[{\"target\":\"lead\",\"role\":\"lead\",\"status\":\"running\",\"model\":\"deepseek-v4-flash\",\"diagnostics\":[]}]"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"team-list-2","name":"list_agents","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788778146017,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788778146017,"index":0,"dt":[],"id":"team-list-2","name":"list_agents","args":["{}"]},{"type":"chunk","time":1788778146017,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"team-list-2","name":"list_agents","arguments":"{}"}}},{"type":"chunk","time":1788778146017,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788778146017,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"team-list-2","name":"list_agents","arguments":"{}"}} -{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"team-list-2"},"content":[{"type":"tool-result","toolCallId":"team-list-2","content":[{"type":"text","text":"[{\"target\":\"lead\",\"status\":\"running\",\"role\":\"lead\",\"model\":\"deepseek-v4-flash\",\"diagnostics\":[]}]"}],"isError":false}],"role":"user","id":"{{message:7}}"}},"sourceEventSeqs":[19],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"team-list-2"},"content":[{"type":"tool-result","toolCallId":"team-list-2","content":[{"type":"text","text":"[{\"target\":\"lead\",\"role\":\"lead\",\"status\":\"running\",\"model\":\"deepseek-v4-flash\",\"diagnostics\":[]}]"}],"isError":false}],"role":"user","id":"{{message:7}}"}},"sourceEventSeqs":[19],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} {"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"TEAM_TARGETS_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788778146037,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788778146037,"index":0,"dt":[],"texts":["TEAM_TARGETS_OK"]},{"type":"chunk","time":1788778146037,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"TEAM_TARGETS_OK"}}},{"type":"chunk","time":1788778146037,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788778146037,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}