mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
Merge pull request #6098 from deepseek-harness/fsy/team-runtime-context
fix(agent-team): inject identity through required runtime context
This commit is contained in:
commit
ea5ae3a7ef
26 files changed
+205
-120
No files matched your search
+19
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-10-13-team-identity-runtime-context.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-13-team-identity-runtime-context.md
|
||||
/agent-note-team-identity-in-required-runtime-context:
|
||||
en: 8c88759320ae8481
|
||||
zh: 4068b18723907eb9
|
||||
/agent-note-team-identity-in-required-runtime-context/problem:
|
||||
en: c9a81cb731ddf7b2
|
||||
zh: 428c0e6f8937d7c5
|
||||
/agent-note-team-identity-in-required-runtime-context/decision:
|
||||
en: 6c73479973c1e378
|
||||
zh: 98677c77f77f8d81
|
||||
/agent-note-team-identity-in-required-runtime-context/alternatives-considered:
|
||||
en: 774566ffbf9e9db8
|
||||
zh: ea761d876af462f2
|
||||
/agent-note-team-identity-in-required-runtime-context/consequences:
|
||||
en: 18d1aa67936f1227
|
||||
zh: 7e06c1f51553ecdd
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: Team identity in required runtime context
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-13-team-identity-runtime-context.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A teammate identity embedded in its initial task depends on ordinary history retention. Compaction can remove it, and an ordinary fork from that teammate can retain the old role even though the fork is an independent Lead. Identity must reflect current membership while keeping shared system instructions uniform.
|
||||
|
||||
## Decision
|
||||
|
||||
`tool-agent-team` contributes `team:identity` through the Agent-scoped `systemPrompt.context()` registry with `required: true`. Its provider reads the existing Team membership and renders the member's role, name, and addressing instructions. Optional-context suppression cannot remove it. Fixed collaboration policy stays in the system prompt; roster and task state remain tool queries. Initial tasks contain only their supplied content.
|
||||
|
||||
The loop owns logging, retained-snapshot comparison, and re-admission after compaction. A changed role appends the current full runtime-context snapshot after inherited history; unchanged retained context adds no message. No Team-specific retention state, event type, or log migration is introduced. Existing recorded reminders remain historical messages.
|
||||
|
||||
This supersedes only the initial-task identity decision in [Agent Teams](../feature/2026-08-05-agent-teams.md#team-identity); its roster, mailbox, and task decisions remain active.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep identity in the initial task.** Preserves a uniform system prompt but cannot restore removed identity or correct a fork's inherited role without another mechanism.
|
||||
|
||||
**Put identity in the system prompt.** Makes system text differ across members and can rewrite the inherited request prefix on routes without in-history system updates.
|
||||
|
||||
**Add custom reminder retention and reinjection.** Duplicates the loop's existing runtime-context projection and recovery responsibilities.
|
||||
|
||||
## Consequences
|
||||
|
||||
Members pay for identity within the full runtime-context snapshot. Other context changes repeat it, while stable retained context adds no new message. Forks preserve inherited history and append current identity; actual provider cache hits remain best-effort. Existing Team authorization remains in tool execution.
|
||||
|
||||
Verification covers fresh and fork teammates, an independent fork from a teammate, optional-context suppression, compaction restoration, cold resume, and registration disposal. The recorded `team-targets` scenario pins Lead context; the keyless headless Team workflow pins teammate context through the shipped profile.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: 必需 runtime context 中的 Team 身份
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-13-team-identity-runtime-context.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
嵌入初始任务的 teammate 身份依赖普通历史保留。压缩可能移除它;从 teammate 创建的普通 fork 即使已成为独立 Lead,也可能保留旧角色。身份必须反映当前 membership,同时保持共享系统指令一致。
|
||||
|
||||
## Decision
|
||||
|
||||
`tool-agent-team` 通过 Agent 作用域的 `systemPrompt.context()` 注册 `team:identity`,并设置 `required: true`。提供方读取现有 Team membership,渲染成员角色、名字与寻址说明。关闭可选 context 不能移除它。固定协作策略保留在 system prompt;roster 和 task 状态仍通过工具查询。初始任务只包含传入的正文。
|
||||
|
||||
Loop 负责日志记录、保留快照比较和压缩后的重新接纳。角色变化会在继承历史之后追加当前完整 runtime-context 快照;保留的 context 未变化时不追加消息。不引入 Team 专属的保留状态、事件类型或日志迁移。已有身份提醒仍作为历史消息保留。
|
||||
|
||||
本决策仅取代 [Agent Teams](../feature/2026-08-05-agent-teams.zh.md#team-identity) 中的初始任务身份决策;其 roster、mailbox 和 task 决策仍然有效。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**身份留在初始任务中。** 保持 system prompt 一致,但恢复被移除的身份或纠正 fork 继承的角色需要额外机制。
|
||||
|
||||
**身份放进 system prompt。** 成员之间的 system 文本会不同;不支持历史内 system 更新的路由可能重写继承的请求前缀。
|
||||
|
||||
**添加自定义提醒保留与重新注入。** 重复了 loop 已有的 runtime-context 投影和恢复职责。
|
||||
|
||||
## Consequences
|
||||
|
||||
成员承担完整 runtime-context 快照中的身份 token 成本。其他 context 变化会重复身份,保留的 context 稳定时则不追加消息。Fork 保留继承历史并追加当前身份;提供方实际缓存命中仍为尽力而为。现有 Team 授权仍在工具执行时落实。
|
||||
|
||||
验证覆盖 fresh 和 fork teammate、从 teammate 创建的独立 fork、关闭可选 context、压缩后恢复、冷恢复和注册释放。录制的 `team-targets` 场景固定 Lead context;无密钥 headless Team 工作流通过内置 profile 固定 teammate context。
|
||||
@@ -12,11 +12,11 @@
|
||||
en: 95e7d723d6b00930
|
||||
zh: d18fdf9b2bf69bdb
|
||||
/agent-note-durable-agent-teams-over-continuable-children/profile-delegation:
|
||||
en: 3a66308576f74247
|
||||
zh: 3453f0bcce88033d
|
||||
en: 4835fb8c0c0d8a0d
|
||||
zh: 5f9438a0d58104c4
|
||||
/agent-note-durable-agent-teams-over-continuable-children/team-identity:
|
||||
en: 83e446c928a20c47
|
||||
zh: b0a2bd24419dd815
|
||||
en: f82812d6ac0ac78d
|
||||
zh: 7dd80a6ad13674ca
|
||||
/agent-note-durable-agent-teams-over-continuable-children/provisioning-and-recovery:
|
||||
en: 4345ac9a6ac7ad04
|
||||
zh: 1d19ecbb128a75a3
|
||||
|
||||
@@ -22,11 +22,11 @@ The Lead must wait for required work before its final answer. Process teardown r
|
||||
|
||||
## Profile delegation
|
||||
|
||||
The [Team profile](../../../../packages/experimental/agent-team-profile/README.md) disables `subagent` and `subagent_fork` together with the overlapping global controls. Direct model delegation uses `spawn_teammate` with fresh or fork context, keeping those children in the durable roster. Workflow remains available through the base profile’s fresh `spawn` provider for scripted orchestration; it cannot inherit a teammate’s conversation identity through the model tool. The Subagent services and providers remain shared infrastructure. Ordinary Session forks retain their history without identity correction. Provider-owned child tool visibility remains a [documented limitation](../../../../packages/experimental/tool-agent-team/README.md#known-limitations-and-deferred-work).
|
||||
The [Team profile](../../../../packages/experimental/agent-team-profile/README.md) disables `subagent` and `subagent_fork` together with the overlapping global controls. Direct model delegation uses `spawn_teammate` with fresh or fork context, keeping those children in the durable roster. Workflow remains available through the base profile’s fresh `spawn` provider for scripted orchestration; it cannot inherit a teammate’s conversation identity through the model tool. The Subagent services and providers remain shared infrastructure. Ordinary Session forks retain their history; [current identity handling](../architecture/2026-10-13-team-identity-runtime-context.md) supplies the fork’s own role.
|
||||
|
||||
## Team identity
|
||||
|
||||
The `spawn_teammate` tool prefixes the initial task with a user-role `<system-reminder>` stating `You are teammate "<name>".`. Identity and task enter the same durable inbox message. Shared system policy and all tool schemas stay uniform across members; execution owns role restrictions. Team tools resolve the caller’s Team and accept member names, so the model needs no Team id. Identity follows ordinary history through cold recovery and compaction; the plugin does not inspect reminder retention or add replacement messages. Forks inherit the recorded text without a Lead identity correction. Putting identity in the system prompt changes the prefix before inherited history; keeping it in the initial task preserves that prefix without per-step identity bookkeeping. Existing system-embedded identities may require a one-time prompt reconciliation; retained event generations are unchanged.
|
||||
Shared system policy and all tool schemas stay uniform across members; execution owns role restrictions. Team tools resolve the caller’s Team and accept member names, so the model needs no Team id. [Required runtime-context identity](../architecture/2026-10-13-team-identity-runtime-context.md) owns identity injection, retention, and fork handling.
|
||||
|
||||
## Provisioning and recovery
|
||||
|
||||
|
||||
@@ -22,11 +22,11 @@ Lead 必须等待所需工作后才能给出最终答案。进程 teardown 仍
|
||||
|
||||
## Profile delegation
|
||||
|
||||
[Team profile](../../../../packages/experimental/agent-team-profile/README.zh.md) 禁用 `subagent`、`subagent_fork` 以及名称重叠的全局控件。模型直接委派使用支持 fresh 或 fork 上下文的 `spawn_teammate`,使这些子代理进入持久 roster。Workflow 仍通过 base profile 的 fresh `spawn` 提供方执行脚本编排;经模型工具创建的 workflow 子代理不会继承 teammate 的对话身份。Subagent 服务和提供方仍是共享基础设施。普通 Session fork 保留历史,不纠正身份。提供方所拥有的子代理工具可见性仍是[已记录的限制](../../../../packages/experimental/tool-agent-team/README.zh.md#known-limitations-and-deferred-work)。
|
||||
[Team profile](../../../../packages/experimental/agent-team-profile/README.zh.md) 禁用 `subagent`、`subagent_fork` 以及名称重叠的全局控件。模型直接委派使用支持 fresh 或 fork 上下文的 `spawn_teammate`,使这些子代理进入持久 roster。Workflow 仍通过 base profile 的 fresh `spawn` 提供方执行脚本编排;经模型工具创建的 workflow 子代理不会继承 teammate 的对话身份。Subagent 服务和提供方仍是共享基础设施。普通 Session fork 保留历史;[当前身份处理](../architecture/2026-10-13-team-identity-runtime-context.zh.md)提供 fork 自己的角色。
|
||||
|
||||
## Team identity
|
||||
|
||||
工具 `spawn_teammate` 在初始任务前加上 user-role `<system-reminder>`,声明 `You are teammate "<name>".`。身份和任务进入同一条持久化收件箱消息。共享 system 策略和全部工具 schema 在成员间保持一致;执行时检查角色权限。Team 工具根据调用者确定 Team,并接受成员名字,因此模型不需要 Team id。身份随普通历史经历冷恢复和压缩;插件不检查提醒是否保留,也不添加替代消息。fork 继承已记录文本,不补发 Lead 身份修正。把身份放进 system prompt 会在继承历史之前改变前缀;把它留在初始任务中既保留此前缀,也无需每步维护身份提醒。已有的 system 内嵌身份可能需要一次提示词协调;保留的事件格式代际保持不变。
|
||||
共享 system 策略和全部工具 schema 在成员间保持一致;执行时检查角色权限。Team 工具根据调用者确定 Team,并接受成员名字,因此模型不需要 Team id。[必需 runtime-context 身份](../architecture/2026-10-13-team-identity-runtime-context.zh.md)负责身份注入、保留与 fork 处理。
|
||||
|
||||
## Provisioning and recovery
|
||||
|
||||
|
||||
@@ -183,17 +183,18 @@ class TeamFixtureAdapter extends LlmAdapter {
|
||||
throw new Error('Team profile exposes a direct subagent tool')
|
||||
}
|
||||
const initial = options.messages.findLast(message => message.role === 'user' && message.source.kind === 'user')
|
||||
const identity = initial?.content[0]?.text?.trimEnd()
|
||||
if (identity !== 'TEAM_WORKFLOW_CHILD' && (!tools.includes('spawn_teammate') || !tools.includes('workflow'))) {
|
||||
const workflowChild = initial?.content[0]?.text === 'TEAM_WORKFLOW_CHILD'
|
||||
const context = options.messages.findLast(message => message.source?.kind === 'runtime-context')
|
||||
const identity = context?.source.sections?.find(section => section.name === 'team:identity')?.text
|
||||
if (!workflowChild && (!tools.includes('spawn_teammate') || !tools.includes('workflow'))) {
|
||||
throw new Error('Team profile is missing teammate or workflow tools')
|
||||
}
|
||||
const chunks = identity === 'TEAM_WORKFLOW_CHILD'
|
||||
? textChunks('Fresh workflow child complete.')
|
||||
: identity === '<system-reminder>\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: "<teammate name>", message: "..." }).\n</system-reminder>'
|
||||
? researcher(options.messages)
|
||||
: identity === '<system-reminder>\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: "<teammate name>", message: "..." }).\n</system-reminder>'
|
||||
? implementer(options.messages)
|
||||
: lead(options.messages)
|
||||
let chunks
|
||||
if (workflowChild) chunks = textChunks('Fresh workflow child complete.')
|
||||
else if (identity?.startsWith('You are teammate "researcher".')) chunks = researcher(options.messages)
|
||||
else if (identity?.startsWith('You are teammate "implementer".')) chunks = implementer(options.messages)
|
||||
else if (identity?.startsWith('You are the Team Lead named "lead".')) chunks = lead(options.messages)
|
||||
else throw new Error('Team runtime context is missing a recognized identity')
|
||||
for (const chunk of chunks) {
|
||||
options.signal?.throwIfAborted()
|
||||
yield chunk
|
||||
|
||||
@@ -985,22 +985,26 @@ describe('headless stream-json snapshots', () => {
|
||||
if (data.name !== 'team_task_update' || typeof data.arguments !== 'string') return false
|
||||
return (JSON.parse(data.arguments) as JsonObject).action === 'complete'
|
||||
})
|
||||
const identityReminders = logs.flatMap((log) => {
|
||||
const identities = logs.flatMap((log) => {
|
||||
if (log === parent || log === workflowChild) return []
|
||||
const initial = parseJsonl(log.content).find(row => row.type === 'user/message'
|
||||
&& ((row.data as JsonObject).source as JsonObject).kind === 'user')
|
||||
if (initial === undefined) throw new Error('Teammate Session has no initial task')
|
||||
const content = (initial.data as JsonObject).content as JsonObject[]
|
||||
expect(content).toHaveLength(2)
|
||||
const identity = content[0]!.text
|
||||
if (typeof identity !== 'string') throw new Error('Teammate initial task has no identity text')
|
||||
return [identity.trimEnd()]
|
||||
expect(content).toHaveLength(1)
|
||||
const context = parseJsonl(log.content).find(row => row.type === 'user/message'
|
||||
&& ((row.data as JsonObject).source as JsonObject).kind === 'runtime-context')
|
||||
const source = (context?.data as JsonObject)?.source as JsonObject
|
||||
const sections = source?.sections as JsonObject[] | undefined
|
||||
const identity = sections?.find(section => section.name === 'team:identity')?.text
|
||||
if (typeof identity !== 'string') throw new Error('Teammate runtime context has no identity')
|
||||
return [identity]
|
||||
}).sort()
|
||||
projection = {
|
||||
sessions: logs.length,
|
||||
workflowStopReason: (rows.find(row => row.type === 'tool-workflow/run-end')?.data as JsonObject)?.stopReason,
|
||||
memberEdges: members.length,
|
||||
identityReminders,
|
||||
identities,
|
||||
activeMembers: members.filter(member => member.phase === 'active').map(member => member.name).sort(),
|
||||
tasks: latestTasks.map(task => ({
|
||||
subject: task.subject,
|
||||
@@ -1035,21 +1039,17 @@ describe('headless stream-json snapshots', () => {
|
||||
],
|
||||
"checkedRoster": true,
|
||||
"deliveredMessages": 0,
|
||||
"identityReminders": [
|
||||
"<system-reminder>
|
||||
You are teammate "implementer".
|
||||
"identities": [
|
||||
"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: "<teammate name>", message: "..." }).
|
||||
</system-reminder>",
|
||||
"<system-reminder>
|
||||
You are teammate "researcher".
|
||||
Use list_agents({}) to find your teammates and their names.
|
||||
To message another teammate, use send_message({ target: "<teammate name>", 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: "<teammate name>", message: "..." }).
|
||||
</system-reminder>",
|
||||
Use list_agents({}) to find your teammates and their names.
|
||||
To message another teammate, use send_message({ target: "<teammate name>", message: "..." }).",
|
||||
],
|
||||
"memberEdges": 4,
|
||||
"queuedMessages": 0,
|
||||
|
||||
@@ -3492,7 +3492,7 @@ export interface Config {
|
||||
|
||||
## `@deepseek-ai/dsh-system-prompt`
|
||||
|
||||
- `source`: [`packages/core/system-prompt/src/index.ts:257`](../packages/core/system-prompt/src/index.ts)
|
||||
- `source`: [`packages/core/system-prompt/src/index.ts:258`](../packages/core/system-prompt/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Plugin config: the deployment-authored fragment of the system prompt (see {@link Config.personaPrefix} for its contract). */
|
||||
|
||||
@@ -3494,7 +3494,7 @@ export interface Config {
|
||||
|
||||
## `@deepseek-ai/dsh-system-prompt`
|
||||
|
||||
- `source`: [`packages/core/system-prompt/src/index.ts:257`](../packages/core/system-prompt/src/index.ts)
|
||||
- `source`: [`packages/core/system-prompt/src/index.ts:258`](../packages/core/system-prompt/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Plugin config: the deployment-authored fragment of the system prompt (see {@link Config.personaPrefix} for its contract). */
|
||||
|
||||
@@ -172,6 +172,7 @@ const CONTEXT_ORDERS = {
|
||||
SANDBOX_POLICY: 110,
|
||||
APPROVAL_POLICY: 115,
|
||||
SUBAGENT_DELEGATION: 120,
|
||||
TEAM_IDENTITY: 125,
|
||||
// Process-specific paths and endpoints stay out of the cached system prompt.
|
||||
HARNESS_SOURCE: 200,
|
||||
WEB_SURFACE: 210,
|
||||
|
||||
@@ -25,7 +25,7 @@ const SECTION_ORDER_NAMES = [
|
||||
] as const satisfies readonly PromptSectionOrderName[]
|
||||
const CONTEXT_ORDER_NAMES = [
|
||||
'WORKING_DIRECTORY',
|
||||
'SANDBOX_POLICY', 'APPROVAL_POLICY', 'SUBAGENT_DELEGATION',
|
||||
'SANDBOX_POLICY', 'APPROVAL_POLICY', 'SUBAGENT_DELEGATION', 'TEAM_IDENTITY',
|
||||
'HARNESS_SOURCE', 'WEB_SURFACE',
|
||||
] as const satisfies readonly PromptContextOrderName[]
|
||||
function contributed(assembly: PromptAssembly): PromptAssembly['sections'] {
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
en: da8c178179ba50ba
|
||||
zh: c53d5863bc14be79
|
||||
/deepseek-ai-dsh-experimental-agent-team-profile/model-experience/team-policy-and-tools/kv-cache-effect:
|
||||
en: e331337b094aacde
|
||||
zh: bb2e5df5ebeb9305
|
||||
en: ac4c8d0ba492fa6a
|
||||
zh: 16d4a05f2e719651
|
||||
/deepseek-ai-dsh-experimental-agent-team-profile/known-limitations-and-deferred-work:
|
||||
en: 0413821c2fc20bea
|
||||
zh: 53e135425dd82e12
|
||||
|
||||
@@ -95,7 +95,7 @@ The bundle adds the Team policy and tool schemas described by `@deepseek-ai/dsh-
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
The bundle's composition is prefix-stable while its patch, Team identity, and configured tool schemas remain unchanged.
|
||||
The shared system prompt and tool schemas remain prefix-stable while the bundle patch and configured tool schemas remain unchanged. Team identity is appended through runtime-context snapshots; [Team tools](../tool-agent-team/README.md#model-experience) define snapshot reuse.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ Team 策略与 schema 由 [`@deepseek-ai/dsh-experimental-tool-agent-team`](../t
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
只要 bundle patch、Team identity 与配置的工具 schema 不变,本 bundle 的 composition 就保持前缀稳定。
|
||||
只要 bundle patch 与配置的工具 schema 不变,共享 system prompt 和工具 schema 就保持前缀稳定。Team 身份通过 runtime-context 快照追加;[Team 工具](../tool-agent-team/README.zh.md#model-experience)定义快照复用方式。
|
||||
|
||||
## 已知限制与延期工作
|
||||
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
en: 6a18034f1ccc247a
|
||||
zh: e08b0eb0ccb41ea8
|
||||
/deepseek-ai-dsh-experimental-agent-team/understand-the-implementation/design-philosophy:
|
||||
en: 4b154ca76747dd92
|
||||
zh: 8fcd239fcf1c460b
|
||||
en: 9034f3b96f68dee4
|
||||
zh: 4ea926474d3a8c24
|
||||
/deepseek-ai-dsh-experimental-agent-team/understand-the-implementation/source-map:
|
||||
en: d96ba512b8327751
|
||||
zh: 21481c29cf5af19a
|
||||
@@ -66,8 +66,8 @@
|
||||
en: a12fb358183cc6d8
|
||||
zh: 7fdb0434d3c407fe
|
||||
/deepseek-ai-dsh-experimental-agent-team/further-exploration:
|
||||
en: 020d07f96ef2077d
|
||||
zh: 9e771d38a02048e1
|
||||
en: 067ed50203315774
|
||||
zh: 7b4dfd41eea2208b
|
||||
/deepseek-ai-dsh-experimental-agent-team/further-exploration/browser-projection:
|
||||
en: 4b3033806e6db528
|
||||
zh: 53e0dfef7309840e
|
||||
|
||||
@@ -106,7 +106,7 @@ The service is built on one separation and three commitments:
|
||||
- **Explicit authority.** Every service method takes the exact live calling `Agent`; only the Lead spawns, reassigns, or interrupts.
|
||||
- **Bounds that fail loud.** Every limit is a validated deployment value, and exhaustion reports a typed error instead of reusing an id or name.
|
||||
|
||||
The [Agent Teams Agent Note](../../../.agents/notes/implemented/feature/2026-08-05-agent-teams.md) owns the identity, mailbox, task, and shared-checkout decisions.
|
||||
The [Agent Teams Agent Note](../../../.agents/notes/implemented/feature/2026-08-05-agent-teams.md) owns the roster, mailbox, task, and shared-checkout decisions. The [runtime-context identity decision](../../../.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.md) owns model-facing identity.
|
||||
|
||||
### Source map
|
||||
|
||||
@@ -162,7 +162,7 @@ Read these pages when the package-level contract is not enough. They move from t
|
||||
|
||||
- [Agent Teams subsystem](../../../docs/subsystems/agent-team.md) — durable Team types and the `ctx.agentTeams` service API.
|
||||
- [tool-agent-team package](../tool-agent-team/README.md) — the tools that let the model create, message, and coordinate teammates.
|
||||
- [Agent Teams Agent Note](../../../.agents/notes/implemented/feature/2026-08-05-agent-teams.md) — identity, mailbox, task, and shared-checkout decisions.
|
||||
- [Agent Teams Agent Note](../../../.agents/notes/implemented/feature/2026-08-05-agent-teams.md) — roster, mailbox, task, and shared-checkout decisions.
|
||||
- [Experimental package reference](../AGENTS.md) — placement, publication, and dependency isolation.
|
||||
|
||||
-----
|
||||
|
||||
@@ -106,7 +106,7 @@ Lead 可以停止 teammate 的当前轮次,而不会删除其排队的消息
|
||||
- **显式权限。** 每个服务方法都接收确切的实时调用方 `Agent`;只有 Lead 可以 spawn、reassign 或 interrupt。
|
||||
- **超出上限时明确失败。** 每个限制都是经过校验的部署值,耗尽时报告类型化错误,而不是复用 id 或名字。
|
||||
|
||||
[Agent Teams Agent Note](../../../.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md)负责身份、mailbox、任务与共享 checkout 决策。
|
||||
[Agent Teams Agent Note](../../../.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md)负责 roster、mailbox、任务与共享 checkout 决策。[runtime-context 身份决策](../../../.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.zh.md)负责面向模型的身份。
|
||||
|
||||
### 源码地图
|
||||
|
||||
@@ -162,7 +162,7 @@ Mailbox 投影与 checkpoint 准入保留本地声明的校验器之外获准内
|
||||
|
||||
- [Agent Teams 子系统](../../../docs/subsystems/agent-team.zh.md)——持久 Team 类型与 `ctx.agentTeams` 服务 API。
|
||||
- [tool-agent-team 包](../tool-agent-team/README.zh.md)——让模型创建 teammate、向其发送消息并进行协调的工具。
|
||||
- [Agent Teams Agent Note](../../../.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md)——身份、mailbox、任务与共享 checkout 决策。
|
||||
- [Agent Teams Agent Note](../../../.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md)——roster、mailbox、任务与共享 checkout 决策。
|
||||
- [实验包参考](../AGENTS.md)——位置、公开发布与依赖隔离。
|
||||
|
||||
-----
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
en: d152484eb41ac6b4
|
||||
zh: 09388d293f9be9cb
|
||||
/deepseek-ai-dsh-experimental-tool-agent-team/use-this-package:
|
||||
en: 1e928b05f250c945
|
||||
zh: 0281800e08a58fce
|
||||
en: 5a56781cb7bf4d3b
|
||||
zh: 74df21efd6321afd
|
||||
/deepseek-ai-dsh-experimental-tool-agent-team/use-this-package/when-to-choose-it:
|
||||
en: 5261677934ea6e71
|
||||
zh: a8bafd94d2b293ec
|
||||
@@ -54,17 +54,17 @@
|
||||
en: 8e1e365ff3b0dfb2
|
||||
zh: c6c91fbf1e1df934
|
||||
/deepseek-ai-dsh-experimental-tool-agent-team/model-experience/team-policy-and-tools/what-the-model-sees:
|
||||
en: 601ef7bfff117b60
|
||||
zh: 50435d70524c12e2
|
||||
en: 8887cc2b9eab8738
|
||||
zh: 892c17ae0613000a
|
||||
/deepseek-ai-dsh-experimental-tool-agent-team/model-experience/team-policy-and-tools/token-effect:
|
||||
en: fe314af98a2130b8
|
||||
zh: ea89cb40dd11d57c
|
||||
en: 3f49674f937a8ca3
|
||||
zh: 79d34d605732abb1
|
||||
/deepseek-ai-dsh-experimental-tool-agent-team/model-experience/team-policy-and-tools/kv-cache-effect:
|
||||
en: cba54e491b29b664
|
||||
zh: 288015d4794998f9
|
||||
en: 2ba143e89873e768
|
||||
zh: 4f09446c0fb13cdb
|
||||
/deepseek-ai-dsh-experimental-tool-agent-team/known-limitations-and-deferred-work:
|
||||
en: c459b2e8236e6a6c
|
||||
zh: 7cd2af569e9b751a
|
||||
en: 0fb9ccc715a43f3a
|
||||
zh: baf04d4b1cf2a7f6
|
||||
/deepseek-ai-dsh-experimental-tool-agent-team/known-limitations-and-deferred-work/dev-note:
|
||||
en: e67f633dc6fe7773
|
||||
zh: 3bb0c20c93f16f98
|
||||
@@ -25,7 +25,7 @@ This package lets the model create named teammates, send them messages, inspect
|
||||
<a id="use-this-package"></a>
|
||||
## Use this package
|
||||
|
||||
Add this package on top of `@deepseek-ai/dsh-experimental-agent-team` when the model should run a team through tools. Once mounted, every team member — the Lead and each teammate — gets the same nine tools plus the same coordination policy. `spawn_teammate` prefixes the initial task with the teammate’s role and name.
|
||||
Add this package on top of `@deepseek-ai/dsh-experimental-agent-team` when the model should run a team through tools. Once mounted, every team member — the Lead and each teammate — gets the same nine tools plus the same coordination policy. Each member receives its current role and name through required runtime context; `spawn_teammate` sends the initial task unchanged.
|
||||
|
||||
### When to choose it
|
||||
|
||||
@@ -124,22 +124,20 @@ Read these pages when the package-level contract is not enough. They move from t
|
||||
|
||||
#### What the model sees
|
||||
|
||||
One shared system policy covers explicit delegation, shared-cwd safety, task coordination, Steer delivery, inbox acceptance, and the Lead’s duty to wait. All nine schemas are identical for Leads and teammates; execution enforces Lead-only operations. `spawn_teammate` prefixes the initial task with a user-role reminder of the teammate name, Lead name, and messaging tools. The reminder follows ordinary history through recovery and compaction.
|
||||
One shared system policy covers explicit delegation, shared-cwd safety, task coordination, Steer delivery, inbox acceptance, and the Lead’s duty to wait. All nine schemas are identical for Leads and teammates; execution enforces Lead-only operations. Required runtime context states `You are the Team Lead named "lead".` or `You are teammate "<name>".` with `Your Team Lead is named "lead".`, plus roster and messaging instructions. Identity comes from current Team membership, survives optional-context suppression, and is restored when compaction removes its snapshot. An ordinary fork from a teammate receives its current Lead identity.
|
||||
|
||||
#### Token effect
|
||||
|
||||
Fixed policy and schema cost on every Team member request. The initial identity text follows ordinary history through later steps, cold recovery, and compaction; the plugin neither scans for it nor reinserts it. Tool calls add compact JSON roster, task, wait, or receipt results. Peer content is retained by the Team domain in the target's history.
|
||||
Fixed policy and schema cost on every Team member request. Identity is part of the complete runtime-context snapshot. An unchanged retained snapshot adds no message; changed or removed context causes the loop to append a complete snapshot, including identity. Tool calls add compact JSON roster, task, wait, or receipt results. Peer content is retained by the Team domain in the target's history.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
With the same provider/model, shared system policy, and tool schemas, a fork retains the parent request prefix and appends the initial task with its identity prefix. Tool results and peer messages append after the reusable request prefix. Sessions recorded with identity inside the system prompt can change that prefix on their first request under this layout; actual provider cache hits remain best-effort.
|
||||
With the same provider/model, shared system policy, and tool schemas, a fork retains the parent request prefix and appends its task and current runtime context. Tool results and peer messages append after the reusable request prefix. Sessions recorded with identity inside the system prompt can change that prefix on their first request under this layout; actual provider cache hits remain best-effort.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
- **One-shot child tool visibility** — in-process one-shot children receive their subagent descriptor after publication. Team installation can therefore mistake them for Leads and expose Team policy and tools. Calls are rejected once the descriptor identifies them as non-members. Correcting installation timing is deferred.
|
||||
|
||||
These limits describe what the policy and tools cannot guarantee for a team. They are current package constraints, not a comparison with other collaboration surfaces.
|
||||
|
||||
- **Prompt policy is coordination, not confinement** — it cannot stop Bash or external processes from writing overlapping files.
|
||||
|
||||
@@ -25,7 +25,7 @@ kind: "package-reference"
|
||||
<a id="use-this-package"></a>
|
||||
## 使用本包
|
||||
|
||||
当模型应该通过工具运行一支团队时,在 `@deepseek-ai/dsh-experimental-agent-team` 之上挂载本包。挂载后,每个团队成员——Lead 与每个 teammate——都会获得相同的九个工具,以及相同的协作策略。`spawn_teammate` 在初始任务前加上 teammate 的角色和名字。
|
||||
当模型应该通过工具运行一支团队时,在 `@deepseek-ai/dsh-experimental-agent-team` 之上挂载本包。挂载后,每个团队成员——Lead 与每个 teammate——都会获得相同的九个工具,以及相同的协作策略。`spawn_teammate` 原样发送初始任务;每个成员通过必需的 runtime context 接收当前角色和名字。
|
||||
|
||||
### 何时选择
|
||||
|
||||
@@ -124,22 +124,20 @@ member scope 上的一个 `team:policy` 段落说明共享的协作规则;固
|
||||
|
||||
#### 模型看到什么
|
||||
|
||||
共享系统 policy 覆盖显式委派、共享 cwd 安全、任务协调、Steer 投递、inbox 接收和 Lead 等待责任。Lead 与 teammate 使用相同的九个 schema;执行时落实 Lead-only 操作。`spawn_teammate` 在初始任务前加入用户角色提醒,说明 teammate 名称、Lead 名称与消息工具。提醒随普通历史经历恢复与压缩。
|
||||
共享系统 policy 覆盖显式委派、共享 cwd 安全、任务协调、Steer 投递、inbox 接收和 Lead 等待责任。Lead 与 teammate 使用相同的九个 schema;执行时落实 Lead-only 操作。必需的 runtime context 声明 `You are the Team Lead named "lead".`,或 `You are teammate "<name>".` 与 `Your Team Lead is named "lead".`,并提供 roster 和消息操作说明。身份来自当前 Team membership,关闭可选 context 时仍保留,压缩移除快照后会恢复。从 teammate 创建的普通 fork 会收到当前 Lead 身份。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
每次 Team member 请求都有固定策略与 schema 成本。初始身份文本随普通历史经历后续步骤、冷恢复和压缩;插件不扫描或重新插入它。工具调用会增加紧凑 JSON roster、task、wait 或 receipt 结果。Peer 内容由 Team 领域保留在 target 历史中。
|
||||
每次 Team member 请求都有固定策略与 schema 成本。身份是完整 runtime-context 快照的一部分。保留的快照未变化时不追加消息;context 变化或被移除时,loop 追加包含身份的完整快照。工具调用会增加紧凑 JSON roster、task、wait 或 receipt 结果。Peer 内容由 Team 领域保留在 target 历史中。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
provider/model、共享 system 策略和工具 schema 相同时,fork 保留父请求前缀并追加带身份前缀的初始任务。工具结果与 peer 消息追加在可复用请求前缀之后。原先在 system prompt 中记录身份的 Session,首次使用此布局请求时可能改变该前缀;提供方实际缓存命中仍为尽力而为。
|
||||
provider/model、共享 system 策略和工具 schema 相同时,fork 保留父请求前缀并追加任务与当前 runtime context。工具结果与 peer 消息追加在可复用请求前缀之后。原先在 system prompt 中记录身份的 Session,首次使用此布局请求时可能改变该前缀;提供方实际缓存命中仍为尽力而为。
|
||||
|
||||
## 已知限制与延期工作
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
- **一次性子代理工具可见性**——进程内一次性子代理在发布后才获得 subagent descriptor。Team 安装因此可能将它们误认作 Lead 并暴露 Team 策略和工具。descriptor 将它们识别为非成员后,调用会被拒绝。安装时序修复留待以后处理。
|
||||
|
||||
这些限制说明策略与工具无法为一支团队保证什么。它们是当前包约束,不是与其他协作方式的对比。
|
||||
|
||||
- **提示词策略只负责协调,不负责 confinement**——它无法阻止 Bash 或外部进程写入重叠文件。
|
||||
|
||||
@@ -175,6 +175,19 @@ function install(agent: Agent, ctx: Context, config: Required<Config>): () => vo
|
||||
order: scoped.systemPrompt.getSectionOrder('TEAM_POLICY'),
|
||||
text: teamPolicy(config.interrupt),
|
||||
}))
|
||||
register(scoped.systemPrompt.context({
|
||||
name: 'team:identity',
|
||||
order: scoped.systemPrompt.getContextOrder('TEAM_IDENTITY'),
|
||||
required: true,
|
||||
text: () => {
|
||||
const { role, name } = ctx.agentTeams.membership(agent)
|
||||
return `${role === 'lead'
|
||||
? 'You are the Team Lead named "lead".'
|
||||
: `You are teammate "${name}".\nYour Team Lead is named "lead".\nTo message your Team Lead, use send_message({ target: "lead", message: "..." }).`}
|
||||
Use list_agents({}) to find your teammates and their names.
|
||||
To message another teammate, use send_message({ target: "<teammate name>", message: "..." }).`
|
||||
},
|
||||
}))
|
||||
|
||||
register(scoped.tools.register(defineTool({
|
||||
name: 'spawn_teammate',
|
||||
@@ -196,18 +209,7 @@ function install(agent: Agent, ctx: Context, config: Required<Config>): () => vo
|
||||
const result = await ctx.agentTeams.spawnTeammate(agent, {
|
||||
name: args.name,
|
||||
description: args.description,
|
||||
prompt: [
|
||||
{ type: 'text', text: `<system-reminder>
|
||||
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: "<teammate name>", message: "..." }).
|
||||
</system-reminder>
|
||||
|
||||
` },
|
||||
{ type: 'text', text: args.prompt },
|
||||
],
|
||||
prompt: [{ type: 'text', text: args.prompt }],
|
||||
context,
|
||||
provider: context === 'fork' ? config.forkProvider : config.freshProvider,
|
||||
signal: exec.signal,
|
||||
|
||||
@@ -30,6 +30,11 @@ function serializeRequest(request: GenerateOptions) {
|
||||
return serialize(request, connection, request.messages, new Map(), () => undefined)
|
||||
}
|
||||
|
||||
function runtimeContextText(request: GenerateOptions) {
|
||||
const context = request.messages.findLast(message => message.source?.kind === 'runtime-context')
|
||||
return context?.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('\n')
|
||||
}
|
||||
|
||||
const SIGNAL = new AbortController().signal
|
||||
const TOOL_NAMES = [
|
||||
'spawn_teammate',
|
||||
@@ -250,7 +255,7 @@ describe('dsh-tool-team', () => {
|
||||
expect(leadPrompt).toContain('Task readiness never starts an owner')
|
||||
expect(leadPrompt).toContain('returns noProgress immediately')
|
||||
expect(leadPrompt).not.toContain('Your Team role')
|
||||
expect(renderContextSnapshot(leadAssembly)).toBe('')
|
||||
expect(renderContextSnapshot(leadAssembly)).toContain('You are the Team Lead named "lead".')
|
||||
|
||||
const spawned = await execute(ctx, lead, 'spawn_teammate', {
|
||||
name: 'tool-worker',
|
||||
@@ -264,14 +269,13 @@ describe('dsh-tool-team', () => {
|
||||
expect(childAssembly.tools.map(schema => schema.name).filter(name => TOOL_NAMES.includes(name)).sort())
|
||||
.toEqual(TOOL_NAMES)
|
||||
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 === '<system-reminder>\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: "<teammate name>", message: "..." }).\n</system-reminder>\n\n'))).toBe(true)
|
||||
expect(renderContextSnapshot(childAssembly)).toContain('You are teammate "tool-worker".')
|
||||
expect(childAssembly.contexts).toContainEqual(expect.objectContaining({ name: 'team:identity' }))
|
||||
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(['<system-reminder>\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: "<teammate name>", message: "..." }).\n</system-reminder>\n\n', 'stay available'])
|
||||
: []).toEqual(['stay available'])
|
||||
|
||||
const denied = await execute(ctx, child, 'spawn_teammate', {
|
||||
name: 'nested', description: 'not allowed', prompt: 'no',
|
||||
@@ -306,20 +310,17 @@ describe('dsh-tool-team', () => {
|
||||
} else {
|
||||
expect(JSON.stringify(childRequest.messages)).not.toContain('Parent task')
|
||||
}
|
||||
expect(childRequest.messages.at(-1)?.content.slice(0, 2)).toEqual([
|
||||
{ type: 'text', text: '<system-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: "<teammate name>", message: "..." }).\n</system-reminder>\n\n' },
|
||||
{ type: 'text', text: 'Review the work' },
|
||||
])
|
||||
expect(runtimeContextText(adapter.requests[1]!)).toContain('You are teammate "reviewer".')
|
||||
expect(childRequest.messages.at(-1)?.content).toContainEqual({ 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: '<system-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: "<teammate name>", message: "..." }).\n</system-reminder>\n\n' },
|
||||
{ type: 'text', text: 'Review the work' },
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps an ordinary Lead fork free of identity reminders without replacing the inherited prefix', async () => {
|
||||
it('reuses unchanged Lead identity in an ordinary Lead fork', async () => {
|
||||
const { ctx, lead, adapter } = await setup([textResponse('parent answer'), textResponse('fork answer')])
|
||||
await runTurn(lead, 'Parent task')
|
||||
const seed = lead.session.snapshotEvents()
|
||||
@@ -344,7 +345,7 @@ describe('dsh-tool-team', () => {
|
||||
await handle.dispose()
|
||||
})
|
||||
|
||||
it.each([undefined, 'in-history'] as const)('keeps a teammate fork prefix with update mode %s without a Lead correction', async (systemPromptUpdate) => {
|
||||
it.each([undefined, 'in-history'] as const)('appends current Lead identity to a teammate fork with update mode %s', async (systemPromptUpdate) => {
|
||||
const { ctx, lead, adapter } = await setup([textResponse('review done'), textResponse('lead notified'), textResponse('fork answer')])
|
||||
if (systemPromptUpdate !== undefined) adapter.systemPromptUpdate = systemPromptUpdate
|
||||
const spawned = await execute(ctx, lead, 'spawn_teammate', {
|
||||
@@ -372,14 +373,14 @@ describe('dsh-tool-team', () => {
|
||||
expect(fork.messages.slice(0, original.messages.length)).toEqual(original.messages)
|
||||
expect(forkRequest.messages.slice(0, inheritedHistory.length)).toEqual(inheritedHistory)
|
||||
expect(fork.messages.at(-1)?.content).toContainEqual({ type: 'text', text: 'Continue independently' })
|
||||
expect(JSON.stringify(fork.messages)).not.toContain('You are the Team Lead')
|
||||
expect(runtimeContextText(forkRequest)).toContain('You are the Team Lead named "lead".')
|
||||
expect(handle.agent.session.snapshotEvents().slice(0, seed.length)).toEqual(seed)
|
||||
} finally {
|
||||
await handle.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('leaves initial identity to ordinary history compaction without reinserting it', async () => {
|
||||
it('restores compacted identity and does not repeat an unchanged retained snapshot', async () => {
|
||||
const { ctx, lead, adapter } = await setup([
|
||||
toolCallResponse('first-list', 'list_agents', {}),
|
||||
toolCallResponse('second-list', 'list_agents', {}),
|
||||
@@ -388,8 +389,8 @@ describe('dsh-tool-team', () => {
|
||||
ctx.on('agent/pre-step', async ({ agent, step }, next) => {
|
||||
if (step === 3) {
|
||||
const identity = agent.session.snapshotEvents().find(event => event.type === 'user/message'
|
||||
&& event.data.source.kind === 'user')
|
||||
if (identity === undefined) throw new Error('expected initial teammate reminder')
|
||||
&& event.data.source.kind === 'runtime-context')
|
||||
if (identity === undefined) throw new Error('expected teammate runtime context')
|
||||
agent.session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'Compacted earlier context.' }],
|
||||
source: { kind: 'test-compaction' },
|
||||
@@ -405,16 +406,15 @@ describe('dsh-tool-team', () => {
|
||||
})
|
||||
const childId = spawnedChildId(ctx, lead, spawned)
|
||||
await waitNoAgent(ctx, childId)
|
||||
const reminder = '<system-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: "<teammate name>", message: "..." }).\n</system-reminder>\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 =>
|
||||
block.type === 'text' && block.text === reminder))).toHaveLength(1)
|
||||
expect(JSON.stringify(serializeRequest(adapter.requests[2]!).messages)).not.toContain('You are teammate')
|
||||
for (const request of adapter.requests.filter(request => request.sessionId === childId)) {
|
||||
const snapshots = request.messages.filter(message => message.source?.kind === 'runtime-context')
|
||||
expect(snapshots).toHaveLength(1)
|
||||
expect(runtimeContextText(request)).toContain('You are teammate "reviewer".')
|
||||
}
|
||||
await using persisted = await ctx.sessionPersistence.open(childId, 'read')
|
||||
const { events } = await persisted.read()
|
||||
expect(events.filter(event => event.type === 'user/message'
|
||||
&& (event.data.source as { readonly kind?: unknown }).kind === toolTeam.name)).toHaveLength(0)
|
||||
&& event.data.source.kind === 'runtime-context')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it.each(['reject', 'empty', 'abort'] as const)('does not revive a teammate step after %s', async (mode) => {
|
||||
@@ -438,7 +438,7 @@ describe('dsh-tool-team', () => {
|
||||
&& event.data.source.kind === 'user')).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps teammate reminders when runtime context is suppressed', async () => {
|
||||
it('keeps required Team identities when optional runtime context is suppressed', async () => {
|
||||
const { ctx, lead, adapter } = await setup([textResponse('worker done')])
|
||||
ctx.systemPrompt.suppressRuntimeContext()
|
||||
const spawned = await execute(ctx, lead, 'spawn_teammate', {
|
||||
@@ -446,10 +446,8 @@ 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: '<system-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: "<teammate name>", message: "..." }).\n</system-reminder>\n\n' },
|
||||
{ type: 'text', text: 'Review the work' },
|
||||
])
|
||||
expect(renderContextSnapshot(await assembly(ctx, lead))).toContain('You are the Team Lead named "lead".')
|
||||
expect(runtimeContextText(adapter.requests[0]!)).toContain('You are teammate "reviewer".')
|
||||
})
|
||||
|
||||
it('returns actionable no-progress output and renders structured wait cancellation', async () => {
|
||||
@@ -646,6 +644,9 @@ describe('dsh-tool-team', () => {
|
||||
expect((await assembly(ctx, lead)).tools.map(schema => schema.name).some(name => TOOL_NAMES.includes(name))).toBe(false)
|
||||
expect((await assembly(ctx, child)).tools.map(schema => schema.name).some(name => TOOL_NAMES.includes(name))).toBe(false)
|
||||
expect(ctx.agents.get(childId)).toBe(child)
|
||||
for (const agent of [lead, child]) {
|
||||
expect((await assembly(ctx, agent)).contexts.some(context => context.name === 'team:identity')).toBe(false)
|
||||
}
|
||||
|
||||
const disabled = await ctx.plugin(toolTeam, { interrupt: false })
|
||||
for (const agent of [lead, child]) {
|
||||
@@ -692,11 +693,11 @@ describe('dsh-tool-team', () => {
|
||||
const assembled = await assembly(ctx, lead)
|
||||
expect(assembled.tools.filter(schema => TOOL_NAMES.includes(schema.name)).map(schema => schema.name))
|
||||
.toEqual(['spawn_teammate'])
|
||||
expect(renderContextSnapshot(assembled)).not.toContain('Your Team role is lead')
|
||||
expect(assembled.contexts.some(context => context.name === 'team:identity')).toBe(false)
|
||||
})
|
||||
|
||||
it('resolves direct-apply defaults without Loader schema normalization', async () => {
|
||||
const { ctx, lead, fiber } = await setup([textResponse('ordinary child')])
|
||||
const { ctx, lead, fiber, adapter } = await setup([textResponse('ordinary child')])
|
||||
await fiber.dispose()
|
||||
toolTeam.apply(ctx, {})
|
||||
expect((await assembly(ctx, lead)).tools.map(schema => schema.name).filter(name => TOOL_NAMES.includes(name)).sort())
|
||||
@@ -709,6 +710,9 @@ describe('dsh-tool-team', () => {
|
||||
signal: SIGNAL,
|
||||
})
|
||||
await vi.waitFor(() => { expect(ctx.agents.get(ordinary.childId)).toBeUndefined() }, { timeout: 5_000 })
|
||||
const request = adapter.requests.find(request => request.sessionId === ordinary.childId)
|
||||
expect(request).toBeDefined()
|
||||
expect(JSON.stringify(request?.messages)).not.toContain('You are the Team Lead')
|
||||
})
|
||||
|
||||
it.each([true, false])('reinstalls Team scope with interrupt=%s before a cold-resumed request', async (interrupt) => {
|
||||
@@ -739,7 +743,7 @@ describe('dsh-tool-team', () => {
|
||||
expect((await assembly(ctx, resumed)).tools.map(schema => schema.name)
|
||||
.filter(name => TOOL_NAMES.includes(name)).sort()).toEqual(TOOL_NAMES.filter(name => interrupt || name !== 'interrupt_agent'))
|
||||
expect(renderPrompt(await assembly(ctx, resumed)).includes('interrupt_agent')).toBe(interrupt)
|
||||
expect(renderContextSnapshot(await assembly(ctx, resumed))).not.toContain('You are teammate')
|
||||
expect(renderContextSnapshot(await assembly(ctx, resumed))).toContain('You are teammate "cold-worker".')
|
||||
const childRequests = () => adapter.requests.filter(request => request.messages.some(message => message.role === 'user'
|
||||
&& message.content.some(block => block.type === 'text' && block.text.includes('You are teammate "cold-worker".'))))
|
||||
await vi.waitFor(() => { expect(childRequests()).toHaveLength(2) })
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
{"type":"sandbox/mode","data":{"mode":"danger-full-access","source":"delegation"}}
|
||||
{"type":"approval/policy","data":{"policy":"never","source":"delegation"}}
|
||||
{"type":"permission/preset","data":{"preset":"danger-full-access"}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"<system-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: \"<teammate name>\", message: \"...\" }).\n</system-reminder>\n\n"},{"type":"text","text":"Create the empty file team-ready, then wait in Bash until interrupted."}],"source":{"kind":"user"},"role":"user","id":"{{message:13}}"}]}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Create the empty file team-ready, then wait in Bash until interrupted."}],"source":{"kind":"user"},"role":"user","id":"{{message:13}}"}]}}
|
||||
{"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":"{{message:14}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"<system-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: \"<teammate name>\", message: \"...\" }).\n</system-reminder>\n\n"},{"type":"text","text":"Create the empty file team-ready, then wait in Bash until interrupted."}],"source":{"kind":"user"},"role":"user","id":"{{message:13}}"},"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: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\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\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: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"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":"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":"{{message:15}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Create the empty file team-ready, then wait in Bash until interrupted."}],"source":{"kind":"user"},"role":"user","id":"{{message:13}}"},"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: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\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\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.\n\nYou are teammate \"reviewer\".\nYour Team Lead is named \"lead\".\nTo message your Team Lead, use send_message({ target: \"lead\", message: \"...\" }).\nUse list_agents({}) to find your teammates and their names.\nTo message another teammate, use send_message({ target: \"<teammate name>\", message: \"...\" })."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"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":"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."},{"name":"team:identity","text":"You are teammate \"reviewer\".\nYour Team Lead is named \"lead\".\nTo message your Team Lead, use send_message({ target: \"lead\", message: \"...\" }).\nUse list_agents({}) to find your teammates and their names.\nTo message another teammate, use send_message({ target: \"<teammate name>\", message: \"...\" })."}]},"role":"user","id":"{{message:15}}"},"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"}}
|
||||
{"type":"session/title","data":{"title":"<system-reminder> You are teammate \"revi","messageSeqs":[9],"source":{"kind":"fallback"}}}
|
||||
{"type":"session/title","data":{"title":"Create the empty file team-ready,","messageSeqs":[9],"source":{"kind":"fallback"}}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"direct-1","name":"bash","arguments":"{\"command\":\"touch team-ready; while :; do sleep 1; done\",\"description\":\"Signal readiness and wait for interruption\",\"timeoutMs\":60000}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:16}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1791450215117,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791450215117,"index":0,"dt":[],"id":"direct-1","name":"bash","args":["{\"command\":\"touch team-ready; while :; do sleep 1; done\",\"description\":\"Signal readiness and wait for interruption\",\"timeoutMs\":60000}"]},{"type":"chunk","time":1791450215117,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"direct-1","name":"bash","arguments":"{\"command\":\"touch team-ready; while :; do sleep 1; done\",\"description\":\"Signal readiness and wait for interruption\",\"timeoutMs\":60000}"}}},{"type":"chunk","time":1791450215117,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1791450215117,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":1,"callId":"direct-1","name":"bash","arguments":"{\"command\":\"touch team-ready; while :; do sleep 1; done\",\"description\":\"Signal readiness and wait for interruption\",\"timeoutMs\":60000}"}}
|
||||
{"type":"job/registered","data":{"jobId":"{{id:2}}","owner":"{{session:2}}","kind":"bash","label":"touch team-ready; while :; do sleep 1; done","startedAt":1791450215128,"toolCall":{"callId":"direct-1","rootCallId":"direct-1"}}}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{"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":"{{message:2}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Spawn teammate reviewer, wait for its team-ready file, send it the message Review the direct inbox delivery., interrupt its waiting turn, then reply TEAM_DIRECT_INBOX_OK."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"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: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\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`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"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`)."}]},"role":"user","id":"{{message:3}}"},"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: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\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\nYou are the Team Lead named \"lead\".\nUse list_agents({}) to find your teammates and their names.\nTo message another teammate, use send_message({ target: \"<teammate name>\", message: \"...\" })."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"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":"team:identity","text":"You are the Team Lead named \"lead\".\nUse list_agents({}) to find your teammates and their names.\nTo message another teammate, use send_message({ target: \"<teammate name>\", message: \"...\" })."}]},"role":"user","id":"{{message:3}}"},"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"}}
|
||||
{"type":"session/title","data":{"title":"Spawn teammate reviewer, wait for","messageSeqs":[8],"source":{"kind":"fallback"}}}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{"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":"{{message:2}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Use Agent Teams list_agents to inspect the roster, attempt to send yourself a message and observe its rejection, then reply TEAM_TARGETS_OK."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"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: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\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`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"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`)."}]},"role":"user","id":"{{message:3}}"},"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: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\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\nYou are the Team Lead named \"lead\".\nUse list_agents({}) to find your teammates and their names.\nTo message another teammate, use send_message({ target: \"<teammate name>\", message: \"...\" })."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"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":"team:identity","text":"You are the Team Lead named \"lead\".\nUse list_agents({}) to find your teammates and their names.\nTo message another teammate, use send_message({ target: \"<teammate name>\", message: \"...\" })."}]},"role":"user","id":"{{message:3}}"},"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"}}
|
||||
{"type":"session/title","data":{"title":"Use Agent Teams list_agents to","messageSeqs":[8],"source":{"kind":"fallback"}}}
|
||||
|
||||
Reference in new issue
Block a user