From 6ec97fa124a753b73d6b7f48365e6d3e4bac64c9 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Tue, 22 Sep 2026 17:02:25 +0800 Subject: [PATCH 1/3] fix(agent-team): tell teammates how to message the lead --- .../profiles/headless/tests/fixtures/team-llm.mjs | 4 ++-- .../profiles/headless/tests/headless.expected.e2e.ts | 2 ++ .../experimental/tool-agent-team/README.i18n.yaml | 4 ++-- packages/experimental/tool-agent-team/README.md | 2 +- packages/experimental/tool-agent-team/README.zh.md | 2 +- packages/experimental/tool-agent-team/src/index.ts | 2 +- .../tool-agent-team/tests/tool-team.spec.ts | 12 ++++++------ 7 files changed, 15 insertions(+), 13 deletions(-) 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 3e2da6553f..6d83668519 100644 --- a/apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs +++ b/apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs @@ -189,9 +189,9 @@ class TeamFixtureAdapter extends LlmAdapter { } const chunks = identity === 'TEAM_WORKFLOW_CHILD' ? textChunks('Fresh workflow child complete.') - : identity === '\nYou are teammate "researcher".\n' + : identity === '\nYou are teammate "researcher".\nTo send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }).\n' ? researcher(options.messages) - : identity === '\nYou are teammate "implementer".\n' + : identity === '\nYou are teammate "implementer".\nTo send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }).\n' ? implementer(options.messages) : lead(options.messages) 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 93e6e27ad4..2329499e10 100644 --- a/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts +++ b/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts @@ -862,9 +862,11 @@ describe('headless stream-json snapshots', () => { "identityReminders": [ " You are teammate "implementer". + To send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }). ", " You are teammate "researcher". + To send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }). ", ], "memberEdges": 4, diff --git a/packages/experimental/tool-agent-team/README.i18n.yaml b/packages/experimental/tool-agent-team/README.i18n.yaml index 2b30aa79fc..6a462e06b3 100644 --- a/packages/experimental/tool-agent-team/README.i18n.yaml +++ b/packages/experimental/tool-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/tool-agent-team/README.md -README.md: bfa3fc7f466d4b7f405231de3de4cf648fc690de -README.zh.md: 990e786fd69877af17cef339a696bb5e93d5925c +README.md: 80030b26710be64bb8657c8252796643d26e8d00 +README.zh.md: fb7efe8d1e4420059b5e1352307780b68c0d471e diff --git a/packages/experimental/tool-agent-team/README.md b/packages/experimental/tool-agent-team/README.md index bfa3fc7f46..80030b2671 100644 --- a/packages/experimental/tool-agent-team/README.md +++ b/packages/experimental/tool-agent-team/README.md @@ -125,7 +125,7 @@ Read these pages when the package-level contract is not enough. They move from t #### What the model sees -One shared system policy states the explicit-delegation requirement, shared-cwd behavior, filesystem stale-version recovery, Bash/formatter/codegen risk, task and write-scope coordination, Steer delivery, the no-retry mailbox rule, and the Lead's duty to wait before answering. All nine Team schemas are identical for Leads and teammates; execution enforces Lead-only operations. `spawn_teammate` prefixes its initial user message with `\nYou are teammate "".\n`, followed by a blank line and the task. The prefix contains no Team id and works when runtime context is disabled. Forks inherit history without an additional Lead identity message. +One shared system policy states the explicit-delegation requirement, shared-cwd behavior, filesystem stale-version recovery, Bash/formatter/codegen risk, task and write-scope coordination, Steer delivery, the no-retry mailbox rule, and the Lead's duty to wait before answering. All nine Team schemas are identical for Leads and teammates; execution enforces Lead-only operations. `spawn_teammate` prefixes its initial user message with `\nYou are teammate "".\nTo send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }).\n`, followed by a blank line and the task. The prefix contains no Team id and works when runtime context is disabled. Forks inherit history without an additional Lead identity message. #### Token effect diff --git a/packages/experimental/tool-agent-team/README.zh.md b/packages/experimental/tool-agent-team/README.zh.md index 990e786fd6..fb7efe8d1e 100644 --- a/packages/experimental/tool-agent-team/README.zh.md +++ b/packages/experimental/tool-agent-team/README.zh.md @@ -125,7 +125,7 @@ member scope 上的一个 `team:policy` 段落说明共享的协作规则;固 #### 模型看到什么 -一段共享 system 策略会说明显式 delegation 要求、共享 cwd 行为、文件陈旧版本恢复、Bash/formatter/codegen 风险、task/write-scope 协调、Steer 投递、mailbox 不重试规则,以及 Lead 必须在回答前等待。Lead 与 teammate 的全部九个 Team schema 相同;执行时检查仅限 Lead 的操作权限。`spawn_teammate` 在初始 user 消息前加上 `\nYou are teammate "".\n`,接着是一个空行和任务。该前缀不含 Team id,禁用运行时上下文时也能生效。fork 继承历史,不额外添加 Lead 身份消息。 +一段共享 system 策略会说明显式 delegation 要求、共享 cwd 行为、文件陈旧版本恢复、Bash/formatter/codegen 风险、task/write-scope 协调、Steer 投递、mailbox 不重试规则,以及 Lead 必须在回答前等待。Lead 与 teammate 的全部九个 Team schema 相同;执行时检查仅限 Lead 的操作权限。`spawn_teammate` 在初始 user 消息前加上 `\nYou are teammate "".\nTo send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }).\n`,接着是一个空行和任务。该前缀不含 Team id,禁用运行时上下文时也能生效。fork 继承历史,不额外添加 Lead 身份消息。 #### Token 影响 diff --git a/packages/experimental/tool-agent-team/src/index.ts b/packages/experimental/tool-agent-team/src/index.ts index 3ab4b77235..106a0ee8b1 100644 --- a/packages/experimental/tool-agent-team/src/index.ts +++ b/packages/experimental/tool-agent-team/src/index.ts @@ -193,7 +193,7 @@ function install(agent: Agent, ctx: Context, config: Required): () => vo name: args.name, description: args.description, prompt: [ - { type: 'text', text: `\nYou are teammate "${args.name.trim()}".\n\n\n` }, + { type: 'text', text: `\nYou are teammate "${args.name.trim()}".\nTo send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }).\n\n\n` }, { type: 'text', text: args.prompt }, ], context, 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 198e39f35f..f4dc0ff56d 100644 --- a/packages/experimental/tool-agent-team/tests/tool-team.spec.ts +++ b/packages/experimental/tool-agent-team/tests/tool-team.spec.ts @@ -238,12 +238,12 @@ describe('dsh-tool-team', () => { expect(renderPrompt(childAssembly)).toBe(leadPrompt) expect(renderContextSnapshot(childAssembly)).not.toContain('team:identity') expect(child.session.deriveMessages().some(message => message.content.some(block => - block.type === 'text' && block.text === '\nYou are teammate "tool-worker".\n\n\n'))).toBe(true) + block.type === 'text' && block.text === '\nYou are teammate "tool-worker".\nTo send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }).\n\n\n'))).toBe(true) const initialPrompt = child.session.snapshotEvents().find(event => event.type === 'user/message' && event.data.source.kind === 'user') expect(initialPrompt?.type === 'user/message' ? initialPrompt.data.content.flatMap(block => block.type === 'text' ? [block.text] : []) - : []).toEqual(['\nYou are teammate "tool-worker".\n\n\n', 'stay available']) + : []).toEqual(['\nYou are teammate "tool-worker".\nTo send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }).\n\n\n', 'stay available']) const denied = await execute(ctx, child, 'spawn_teammate', { name: 'nested', description: 'not allowed', prompt: 'no', @@ -279,14 +279,14 @@ describe('dsh-tool-team', () => { expect(JSON.stringify(childRequest.messages)).not.toContain('Parent task') } expect(childRequest.messages.at(-1)?.content.slice(0, 2)).toEqual([ - { type: 'text', text: '\nYou are teammate "reviewer".\n\n\n' }, + { type: 'text', text: '\nYou are teammate "reviewer".\nTo send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }).\n\n\n' }, { type: 'text', text: 'Review the work' }, ]) await using persisted = await ctx.sessionPersistence.open(childId, 'read') const { events } = await persisted.read() const initial = events.findLast(event => event.type === 'user/message' && event.data.source.kind === 'user') expect(initial?.type === 'user/message' ? initial.data.content : []).toEqual([ - { type: 'text', text: '\nYou are teammate "reviewer".\n\n\n' }, + { type: 'text', text: '\nYou are teammate "reviewer".\nTo send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }).\n\n\n' }, { type: 'text', text: 'Review the work' }, ]) }) @@ -377,7 +377,7 @@ describe('dsh-tool-team', () => { }) const childId = spawnedChildId(ctx, lead, spawned) await waitNoAgent(ctx, childId) - const reminder = '\nYou are teammate "reviewer".\n\n\n' + const reminder = '\nYou are teammate "reviewer".\nTo send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }).\n\n\n' const first = serializeRequest(adapter.requests[0]!).messages expect(first.at(-1)?.content.slice(0, 2)).toEqual([{ type: 'text', text: reminder }, { type: 'text', text: 'Review the work' }]) expect(adapter.requests[1]!.messages.filter(message => message.content.some(block => @@ -419,7 +419,7 @@ describe('dsh-tool-team', () => { expect(spawned.isError, text(spawned)).toBe(false) await waitNoAgent(ctx, spawnedChildId(ctx, lead, spawned)) expect(serializeRequest(adapter.requests[0]!).messages.at(-1)?.content).toEqual([ - { type: 'text', text: '\nYou are teammate "reviewer".\n\n\n' }, + { type: 'text', text: '\nYou are teammate "reviewer".\nTo send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }).\n\n\n' }, { type: 'text', text: 'Review the work' }, ]) }) From d257fa4af7ba6c460790f2ec32da8c9c59961fe0 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Tue, 22 Sep 2026 17:08:45 +0800 Subject: [PATCH 2/3] fix(agent-team): limit the reminder to the lead identity --- .../profiles/headless/tests/fixtures/team-llm.mjs | 4 ++-- .../profiles/headless/tests/headless.expected.e2e.ts | 4 ++-- .../experimental/tool-agent-team/README.i18n.yaml | 4 ++-- packages/experimental/tool-agent-team/README.md | 2 +- packages/experimental/tool-agent-team/README.zh.md | 2 +- packages/experimental/tool-agent-team/src/index.ts | 2 +- .../tool-agent-team/tests/tool-team.spec.ts | 12 ++++++------ 7 files changed, 15 insertions(+), 15 deletions(-) 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 6d83668519..638b595011 100644 --- a/apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs +++ b/apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs @@ -189,9 +189,9 @@ class TeamFixtureAdapter extends LlmAdapter { } const chunks = identity === 'TEAM_WORKFLOW_CHILD' ? textChunks('Fresh workflow child complete.') - : identity === '\nYou are teammate "researcher".\nTo send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }).\n' + : identity === '\nYou are teammate "researcher".\nYour Team Lead is named "lead".\n' ? researcher(options.messages) - : identity === '\nYou are teammate "implementer".\nTo send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }).\n' + : identity === '\nYou are teammate "implementer".\nYour Team Lead is named "lead".\n' ? implementer(options.messages) : lead(options.messages) 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 2329499e10..6ce54179d8 100644 --- a/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts +++ b/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts @@ -862,11 +862,11 @@ describe('headless stream-json snapshots', () => { "identityReminders": [ " You are teammate "implementer". - To send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }). + Your Team Lead is named "lead". ", " You are teammate "researcher". - To send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }). + Your Team Lead is named "lead". ", ], "memberEdges": 4, diff --git a/packages/experimental/tool-agent-team/README.i18n.yaml b/packages/experimental/tool-agent-team/README.i18n.yaml index 6a462e06b3..95e89a11ad 100644 --- a/packages/experimental/tool-agent-team/README.i18n.yaml +++ b/packages/experimental/tool-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/tool-agent-team/README.md -README.md: 80030b26710be64bb8657c8252796643d26e8d00 -README.zh.md: fb7efe8d1e4420059b5e1352307780b68c0d471e +README.md: 628f5924c57f0a207d032de8686b2b569ca90a26 +README.zh.md: cf554290733d6d2fb513c849ca64bb1b146d8842 diff --git a/packages/experimental/tool-agent-team/README.md b/packages/experimental/tool-agent-team/README.md index 80030b2671..628f5924c5 100644 --- a/packages/experimental/tool-agent-team/README.md +++ b/packages/experimental/tool-agent-team/README.md @@ -125,7 +125,7 @@ Read these pages when the package-level contract is not enough. They move from t #### What the model sees -One shared system policy states the explicit-delegation requirement, shared-cwd behavior, filesystem stale-version recovery, Bash/formatter/codegen risk, task and write-scope coordination, Steer delivery, the no-retry mailbox rule, and the Lead's duty to wait before answering. All nine Team schemas are identical for Leads and teammates; execution enforces Lead-only operations. `spawn_teammate` prefixes its initial user message with `\nYou are teammate "".\nTo send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }).\n`, followed by a blank line and the task. The prefix contains no Team id and works when runtime context is disabled. Forks inherit history without an additional Lead identity message. +One shared system policy states the explicit-delegation requirement, shared-cwd behavior, filesystem stale-version recovery, Bash/formatter/codegen risk, task and write-scope coordination, Steer delivery, the no-retry mailbox rule, and the Lead's duty to wait before answering. All nine Team schemas are identical for Leads and teammates; execution enforces Lead-only operations. `spawn_teammate` prefixes its initial user message with `\nYou are teammate "".\nYour Team Lead is named "lead".\n`, followed by a blank line and the task. The prefix contains no Team id and works when runtime context is disabled. Forks inherit history without an additional Lead identity message. #### Token effect diff --git a/packages/experimental/tool-agent-team/README.zh.md b/packages/experimental/tool-agent-team/README.zh.md index fb7efe8d1e..cf55429073 100644 --- a/packages/experimental/tool-agent-team/README.zh.md +++ b/packages/experimental/tool-agent-team/README.zh.md @@ -125,7 +125,7 @@ member scope 上的一个 `team:policy` 段落说明共享的协作规则;固 #### 模型看到什么 -一段共享 system 策略会说明显式 delegation 要求、共享 cwd 行为、文件陈旧版本恢复、Bash/formatter/codegen 风险、task/write-scope 协调、Steer 投递、mailbox 不重试规则,以及 Lead 必须在回答前等待。Lead 与 teammate 的全部九个 Team schema 相同;执行时检查仅限 Lead 的操作权限。`spawn_teammate` 在初始 user 消息前加上 `\nYou are teammate "".\nTo send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }).\n`,接着是一个空行和任务。该前缀不含 Team id,禁用运行时上下文时也能生效。fork 继承历史,不额外添加 Lead 身份消息。 +一段共享 system 策略会说明显式 delegation 要求、共享 cwd 行为、文件陈旧版本恢复、Bash/formatter/codegen 风险、task/write-scope 协调、Steer 投递、mailbox 不重试规则,以及 Lead 必须在回答前等待。Lead 与 teammate 的全部九个 Team schema 相同;执行时检查仅限 Lead 的操作权限。`spawn_teammate` 在初始 user 消息前加上 `\nYou are teammate "".\nYour Team Lead is named "lead".\n`,接着是一个空行和任务。该前缀不含 Team id,禁用运行时上下文时也能生效。fork 继承历史,不额外添加 Lead 身份消息。 #### Token 影响 diff --git a/packages/experimental/tool-agent-team/src/index.ts b/packages/experimental/tool-agent-team/src/index.ts index 106a0ee8b1..35a22eabdd 100644 --- a/packages/experimental/tool-agent-team/src/index.ts +++ b/packages/experimental/tool-agent-team/src/index.ts @@ -193,7 +193,7 @@ function install(agent: Agent, ctx: Context, config: Required): () => vo name: args.name, description: args.description, prompt: [ - { type: 'text', text: `\nYou are teammate "${args.name.trim()}".\nTo send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }).\n\n\n` }, + { type: 'text', text: `\nYou are teammate "${args.name.trim()}".\nYour Team Lead is named "lead".\n\n\n` }, { type: 'text', text: args.prompt }, ], context, 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 f4dc0ff56d..066c6130e7 100644 --- a/packages/experimental/tool-agent-team/tests/tool-team.spec.ts +++ b/packages/experimental/tool-agent-team/tests/tool-team.spec.ts @@ -238,12 +238,12 @@ describe('dsh-tool-team', () => { expect(renderPrompt(childAssembly)).toBe(leadPrompt) expect(renderContextSnapshot(childAssembly)).not.toContain('team:identity') expect(child.session.deriveMessages().some(message => message.content.some(block => - block.type === 'text' && block.text === '\nYou are teammate "tool-worker".\nTo send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }).\n\n\n'))).toBe(true) + block.type === 'text' && block.text === '\nYou are teammate "tool-worker".\nYour Team Lead is named "lead".\n\n\n'))).toBe(true) const initialPrompt = child.session.snapshotEvents().find(event => event.type === 'user/message' && event.data.source.kind === 'user') expect(initialPrompt?.type === 'user/message' ? initialPrompt.data.content.flatMap(block => block.type === 'text' ? [block.text] : []) - : []).toEqual(['\nYou are teammate "tool-worker".\nTo send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }).\n\n\n', 'stay available']) + : []).toEqual(['\nYou are teammate "tool-worker".\nYour Team Lead is named "lead".\n\n\n', 'stay available']) const denied = await execute(ctx, child, 'spawn_teammate', { name: 'nested', description: 'not allowed', prompt: 'no', @@ -279,14 +279,14 @@ describe('dsh-tool-team', () => { expect(JSON.stringify(childRequest.messages)).not.toContain('Parent task') } expect(childRequest.messages.at(-1)?.content.slice(0, 2)).toEqual([ - { type: 'text', text: '\nYou are teammate "reviewer".\nTo send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }).\n\n\n' }, + { type: 'text', text: '\nYou are teammate "reviewer".\nYour Team Lead is named "lead".\n\n\n' }, { type: 'text', text: 'Review the work' }, ]) await using persisted = await ctx.sessionPersistence.open(childId, 'read') const { events } = await persisted.read() const initial = events.findLast(event => event.type === 'user/message' && event.data.source.kind === 'user') expect(initial?.type === 'user/message' ? initial.data.content : []).toEqual([ - { type: 'text', text: '\nYou are teammate "reviewer".\nTo send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }).\n\n\n' }, + { type: 'text', text: '\nYou are teammate "reviewer".\nYour Team Lead is named "lead".\n\n\n' }, { type: 'text', text: 'Review the work' }, ]) }) @@ -377,7 +377,7 @@ describe('dsh-tool-team', () => { }) const childId = spawnedChildId(ctx, lead, spawned) await waitNoAgent(ctx, childId) - const reminder = '\nYou are teammate "reviewer".\nTo send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }).\n\n\n' + const reminder = '\nYou are teammate "reviewer".\nYour Team Lead is named "lead".\n\n\n' const first = serializeRequest(adapter.requests[0]!).messages expect(first.at(-1)?.content.slice(0, 2)).toEqual([{ type: 'text', text: reminder }, { type: 'text', text: 'Review the work' }]) expect(adapter.requests[1]!.messages.filter(message => message.content.some(block => @@ -419,7 +419,7 @@ describe('dsh-tool-team', () => { expect(spawned.isError, text(spawned)).toBe(false) await waitNoAgent(ctx, spawnedChildId(ctx, lead, spawned)) expect(serializeRequest(adapter.requests[0]!).messages.at(-1)?.content).toEqual([ - { type: 'text', text: '\nYou are teammate "reviewer".\nTo send progress updates, questions, or blockers to the Team Lead, use send_message({ target: "lead", message: "..." }).\n\n\n' }, + { type: 'text', text: '\nYou are teammate "reviewer".\nYour Team Lead is named "lead".\n\n\n' }, { type: 'text', text: 'Review the work' }, ]) }) From a79e3a2a5fe6838788d6b4e7ce0c4a68236de643 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Tue, 22 Sep 2026 17:18:08 +0800 Subject: [PATCH 3/3] fix(agent-team): explain teammate discovery and messaging --- .../profiles/headless/tests/fixtures/team-llm.mjs | 4 ++-- .../profiles/headless/tests/headless.expected.e2e.ts | 6 ++++++ .../experimental/tool-agent-team/README.i18n.yaml | 4 ++-- packages/experimental/tool-agent-team/README.md | 2 +- packages/experimental/tool-agent-team/README.zh.md | 2 +- packages/experimental/tool-agent-team/src/index.ts | 10 +++++++++- .../tool-agent-team/tests/tool-team.spec.ts | 12 ++++++------ 7 files changed, 27 insertions(+), 13 deletions(-) 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 638b595011..119380327b 100644 --- a/apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs +++ b/apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs @@ -189,9 +189,9 @@ class TeamFixtureAdapter extends LlmAdapter { } const chunks = identity === 'TEAM_WORKFLOW_CHILD' ? textChunks('Fresh workflow child complete.') - : identity === '\nYou are teammate "researcher".\nYour Team Lead is named "lead".\n' + : identity === '\nYou are teammate "researcher".\nYour Team Lead is named "lead".\nUse list_agents({}) to find your teammates and their names.\nTo message your Team Lead, use send_message({ target: "lead", message: "..." }).\nTo message another teammate, use send_message({ target: "", message: "..." }).\n' ? researcher(options.messages) - : identity === '\nYou are teammate "implementer".\nYour Team Lead is named "lead".\n' + : identity === '\nYou are teammate "implementer".\nYour Team Lead is named "lead".\nUse list_agents({}) to find your teammates and their names.\nTo message your Team Lead, use send_message({ target: "lead", message: "..." }).\nTo message another teammate, use send_message({ target: "", message: "..." }).\n' ? implementer(options.messages) : lead(options.messages) 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 6ce54179d8..33752b7812 100644 --- a/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts +++ b/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts @@ -863,10 +863,16 @@ describe('headless stream-json snapshots', () => { " You are teammate "implementer". Your Team Lead is named "lead". + Use list_agents({}) to find your teammates and their names. + To message your Team Lead, use send_message({ target: "lead", message: "..." }). + To message another teammate, use send_message({ target: "", message: "..." }). ", " You are teammate "researcher". Your Team Lead is named "lead". + Use list_agents({}) to find your teammates and their names. + To message your Team Lead, use send_message({ target: "lead", message: "..." }). + To message another teammate, use send_message({ target: "", message: "..." }). ", ], "memberEdges": 4, diff --git a/packages/experimental/tool-agent-team/README.i18n.yaml b/packages/experimental/tool-agent-team/README.i18n.yaml index 95e89a11ad..4e2ee828f7 100644 --- a/packages/experimental/tool-agent-team/README.i18n.yaml +++ b/packages/experimental/tool-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/tool-agent-team/README.md -README.md: 628f5924c57f0a207d032de8686b2b569ca90a26 -README.zh.md: cf554290733d6d2fb513c849ca64bb1b146d8842 +README.md: 0918686d28f7212558aafa988a4b968c18aa8664 +README.zh.md: 8243f8471a67fe55d5a5d368d98e20c5519f42f3 diff --git a/packages/experimental/tool-agent-team/README.md b/packages/experimental/tool-agent-team/README.md index 628f5924c5..0918686d28 100644 --- a/packages/experimental/tool-agent-team/README.md +++ b/packages/experimental/tool-agent-team/README.md @@ -125,7 +125,7 @@ Read these pages when the package-level contract is not enough. They move from t #### What the model sees -One shared system policy states the explicit-delegation requirement, shared-cwd behavior, filesystem stale-version recovery, Bash/formatter/codegen risk, task and write-scope coordination, Steer delivery, the no-retry mailbox rule, and the Lead's duty to wait before answering. All nine Team schemas are identical for Leads and teammates; execution enforces Lead-only operations. `spawn_teammate` prefixes its initial user message with `\nYou are teammate "".\nYour Team Lead is named "lead".\n`, followed by a blank line and the task. The prefix contains no Team id and works when runtime context is disabled. Forks inherit history without an additional Lead identity message. +One shared system policy states the explicit-delegation requirement, shared-cwd behavior, filesystem stale-version recovery, Bash/formatter/codegen risk, task and write-scope coordination, Steer delivery, the no-retry mailbox rule, and the Lead's duty to wait before answering. All nine Team schemas are identical for Leads and teammates; execution enforces Lead-only operations. `spawn_teammate` prefixes its initial user message with `\nYou are teammate "".\nYour Team Lead is named "lead".\nUse list_agents({}) to find your teammates and their names.\nTo message your Team Lead, use send_message({ target: "lead", message: "..." }).\nTo message another teammate, use send_message({ target: "", message: "..." }).\n`, followed by a blank line and the task. The prefix contains no Team id and works when runtime context is disabled. Forks inherit history without an additional Lead identity message. #### Token effect diff --git a/packages/experimental/tool-agent-team/README.zh.md b/packages/experimental/tool-agent-team/README.zh.md index cf55429073..8243f8471a 100644 --- a/packages/experimental/tool-agent-team/README.zh.md +++ b/packages/experimental/tool-agent-team/README.zh.md @@ -125,7 +125,7 @@ member scope 上的一个 `team:policy` 段落说明共享的协作规则;固 #### 模型看到什么 -一段共享 system 策略会说明显式 delegation 要求、共享 cwd 行为、文件陈旧版本恢复、Bash/formatter/codegen 风险、task/write-scope 协调、Steer 投递、mailbox 不重试规则,以及 Lead 必须在回答前等待。Lead 与 teammate 的全部九个 Team schema 相同;执行时检查仅限 Lead 的操作权限。`spawn_teammate` 在初始 user 消息前加上 `\nYou are teammate "".\nYour Team Lead is named "lead".\n`,接着是一个空行和任务。该前缀不含 Team id,禁用运行时上下文时也能生效。fork 继承历史,不额外添加 Lead 身份消息。 +一段共享 system 策略会说明显式 delegation 要求、共享 cwd 行为、文件陈旧版本恢复、Bash/formatter/codegen 风险、task/write-scope 协调、Steer 投递、mailbox 不重试规则,以及 Lead 必须在回答前等待。Lead 与 teammate 的全部九个 Team schema 相同;执行时检查仅限 Lead 的操作权限。`spawn_teammate` 在初始 user 消息前加上 `\nYou are teammate "".\nYour Team Lead is named "lead".\nUse list_agents({}) to find your teammates and their names.\nTo message your Team Lead, use send_message({ target: "lead", message: "..." }).\nTo message another teammate, use send_message({ target: "", message: "..." }).\n`,接着是一个空行和任务。该前缀不含 Team id,禁用运行时上下文时也能生效。fork 继承历史,不额外添加 Lead 身份消息。 #### Token 影响 diff --git a/packages/experimental/tool-agent-team/src/index.ts b/packages/experimental/tool-agent-team/src/index.ts index 35a22eabdd..75dee88c70 100644 --- a/packages/experimental/tool-agent-team/src/index.ts +++ b/packages/experimental/tool-agent-team/src/index.ts @@ -193,7 +193,15 @@ function install(agent: Agent, ctx: Context, config: Required): () => vo name: args.name, description: args.description, prompt: [ - { type: 'text', text: `\nYou are teammate "${args.name.trim()}".\nYour Team Lead is named "lead".\n\n\n` }, + { type: 'text', text: ` +You are teammate "${args.name.trim()}". +Your Team Lead is named "lead". +Use list_agents({}) to find your teammates and their names. +To message your Team Lead, use send_message({ target: "lead", message: "..." }). +To message another teammate, use send_message({ target: "", message: "..." }). + + +` }, { type: 'text', text: args.prompt }, ], context, 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 066c6130e7..c1c98c19e2 100644 --- a/packages/experimental/tool-agent-team/tests/tool-team.spec.ts +++ b/packages/experimental/tool-agent-team/tests/tool-team.spec.ts @@ -238,12 +238,12 @@ describe('dsh-tool-team', () => { expect(renderPrompt(childAssembly)).toBe(leadPrompt) expect(renderContextSnapshot(childAssembly)).not.toContain('team:identity') expect(child.session.deriveMessages().some(message => message.content.some(block => - block.type === 'text' && block.text === '\nYou are teammate "tool-worker".\nYour Team Lead is named "lead".\n\n\n'))).toBe(true) + block.type === 'text' && block.text === '\nYou are teammate "tool-worker".\nYour Team Lead is named "lead".\nUse list_agents({}) to find your teammates and their names.\nTo message your Team Lead, use send_message({ target: "lead", message: "..." }).\nTo message another teammate, use send_message({ target: "", message: "..." }).\n\n\n'))).toBe(true) const initialPrompt = child.session.snapshotEvents().find(event => event.type === 'user/message' && event.data.source.kind === 'user') expect(initialPrompt?.type === 'user/message' ? initialPrompt.data.content.flatMap(block => block.type === 'text' ? [block.text] : []) - : []).toEqual(['\nYou are teammate "tool-worker".\nYour Team Lead is named "lead".\n\n\n', 'stay available']) + : []).toEqual(['\nYou are teammate "tool-worker".\nYour Team Lead is named "lead".\nUse list_agents({}) to find your teammates and their names.\nTo message your Team Lead, use send_message({ target: "lead", message: "..." }).\nTo message another teammate, use send_message({ target: "", message: "..." }).\n\n\n', 'stay available']) const denied = await execute(ctx, child, 'spawn_teammate', { name: 'nested', description: 'not allowed', prompt: 'no', @@ -279,14 +279,14 @@ describe('dsh-tool-team', () => { expect(JSON.stringify(childRequest.messages)).not.toContain('Parent task') } expect(childRequest.messages.at(-1)?.content.slice(0, 2)).toEqual([ - { type: 'text', text: '\nYou are teammate "reviewer".\nYour Team Lead is named "lead".\n\n\n' }, + { type: 'text', text: '\nYou are teammate "reviewer".\nYour Team Lead is named "lead".\nUse list_agents({}) to find your teammates and their names.\nTo message your Team Lead, use send_message({ target: "lead", message: "..." }).\nTo message another teammate, use send_message({ target: "", message: "..." }).\n\n\n' }, { type: 'text', text: 'Review the work' }, ]) await using persisted = await ctx.sessionPersistence.open(childId, 'read') const { events } = await persisted.read() const initial = events.findLast(event => event.type === 'user/message' && event.data.source.kind === 'user') expect(initial?.type === 'user/message' ? initial.data.content : []).toEqual([ - { type: 'text', text: '\nYou are teammate "reviewer".\nYour Team Lead is named "lead".\n\n\n' }, + { type: 'text', text: '\nYou are teammate "reviewer".\nYour Team Lead is named "lead".\nUse list_agents({}) to find your teammates and their names.\nTo message your Team Lead, use send_message({ target: "lead", message: "..." }).\nTo message another teammate, use send_message({ target: "", message: "..." }).\n\n\n' }, { type: 'text', text: 'Review the work' }, ]) }) @@ -377,7 +377,7 @@ describe('dsh-tool-team', () => { }) const childId = spawnedChildId(ctx, lead, spawned) await waitNoAgent(ctx, childId) - const reminder = '\nYou are teammate "reviewer".\nYour Team Lead is named "lead".\n\n\n' + const reminder = '\nYou are teammate "reviewer".\nYour Team Lead is named "lead".\nUse list_agents({}) to find your teammates and their names.\nTo message your Team Lead, use send_message({ target: "lead", message: "..." }).\nTo message another teammate, use send_message({ target: "", message: "..." }).\n\n\n' const first = serializeRequest(adapter.requests[0]!).messages expect(first.at(-1)?.content.slice(0, 2)).toEqual([{ type: 'text', text: reminder }, { type: 'text', text: 'Review the work' }]) expect(adapter.requests[1]!.messages.filter(message => message.content.some(block => @@ -419,7 +419,7 @@ describe('dsh-tool-team', () => { expect(spawned.isError, text(spawned)).toBe(false) await waitNoAgent(ctx, spawnedChildId(ctx, lead, spawned)) expect(serializeRequest(adapter.requests[0]!).messages.at(-1)?.content).toEqual([ - { type: 'text', text: '\nYou are teammate "reviewer".\nYour Team Lead is named "lead".\n\n\n' }, + { type: 'text', text: '\nYou are teammate "reviewer".\nYour Team Lead is named "lead".\nUse list_agents({}) to find your teammates and their names.\nTo message your Team Lead, use send_message({ target: "lead", message: "..." }).\nTo message another teammate, use send_message({ target: "", message: "..." }).\n\n\n' }, { type: 'text', text: 'Review the work' }, ]) })