Add tree-wide names for subagent collaboration

This commit is contained in:
Dudu-0223 committed 2026-10-16 12:33:15 +08:00
1 parent 0518302384
commit 7fd0dba80d
279 files changed
+5299 -2098

No files matched your search

@@ -3,8 +3,8 @@
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md
/agent-note-agent-teams-teammates-are-subagents:
en: 067bfa44266e764e
zh: dcdeff6c10e492be
en: 3a3c59709e08af73
zh: 3ea1dd59c0f9866b
/agent-note-agent-teams-teammates-are-subagents/problem:
en: ca2848e3bbc3a009
zh: 0551e32ff6d187d0
@@ -4,6 +4,8 @@ Status: implemented
English | [中文](2026-10-15-agent-teams-as-subagents.zh.md)
The name-free model interface below is superseded by [tree-wide agent names](2026-10-16-tree-wide-agent-names.md). Session identity, lineage authority, and Team reuse remain current.
## Problem
Teammates already use the subagent runtime for creation, Session identity, catalog membership, capacity, cold resume, settlement, and interruption. A separate Team roster duplicates identity, provisioning recovery, and teardown. Team-scoped messaging and control tools shadow the standard subagent tools, forcing profiles to disable ordinary delegation and making fork tool schemas diverge from their parent.
@@ -4,6 +4,8 @@ Status: implemented
[English](2026-10-15-agent-teams-as-subagents.md) | 中文
下述不使用名字的模型接口已被[树内唯一 agent 名字](2026-10-16-tree-wide-agent-names.zh.md)取代。Session 身份、谱系权限和 Team 复用决策仍然有效。
## Problem
队友已经使用子代理运行时完成创建、Session 身份、目录成员关系、容量、冷恢复、结算和中断。独立 Team 成员列表重复了身份、创建恢复和释放职责。Team 作用域内的消息及控制工具覆盖标准子代理工具,迫使 profile 禁用普通委派,并使分叉工具模式偏离父级。
@@ -0,0 +1,22 @@
# Bilingual-pair consistency record for 2026-10-16-tree-wide-agent-names.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-names.md
/agent-note-tree-wide-agent-names:
en: e976b1e450bda2b9
zh: e7a35fec64a52cb7
/agent-note-tree-wide-agent-names/problem:
en: 45ad48d0442bbd54
zh: 46837f64f46568b8
/agent-note-tree-wide-agent-names/decision:
en: 53822735b4ce3e4e
zh: 316cb613b22a3e80
/agent-note-tree-wide-agent-names/alternatives-considered:
en: 93964747050c9265
zh: 2acb9708d145894b
/agent-note-tree-wide-agent-names/consequences:
en: 7810ae426c76bfcd
zh: 4b954c0f0672c534
/agent-note-tree-wide-agent-names/testing:
en: 4210801f0de47c2c
zh: fe4d154a15796478
@@ -0,0 +1,31 @@
# Agent Note: Tree-wide agent names
Status: implemented
English | [中文](2026-10-16-tree-wide-agent-names.zh.md)
## Problem
Session ids identify executions durably but make model coordination difficult to read. Descriptions can repeat and cannot identify recipients. The user requires explicit, unique names for subagents and teammates, with `lead` reserved for the root.
## Decision
Keep Session ids for persistence and authorization. Model tools accept immutable names unique throughout one agent tree. A root-owned `subagent/named` event records each successful assignment; the projection excludes fork-inherited facts. The manager reserves names before asynchronous preparation and releases failed unpublished reservations. Committed names remain taken after settlement so delayed messages cannot reach a different agent.
This supersedes the name-free interface in [Agent Teams as subagents](2026-10-15-agent-teams-as-subagents.md). Its lineage authority and shared subagent lifecycle remain unchanged. Resolution never grants messaging, interruption, or task-assignment authority. Older unnamed descendants receive stable `legacy:<session-id>` addresses; Team assignment also accepts such addresses for active historical members.
## Alternatives considered
**Parent-local names.** Descendant discovery would require path-qualified names. One tree-wide namespace gives every caller the same address.
**Recyclable names.** Reuse could redirect queued or delayed work after the original agent settles. Retaining assignments keeps recipient identity stable.
**A second committed registry.** The root projection already reconstructs assignments after restart. A second cache would require synchronization and recovery; only unpublished reservations need separate state.
## Consequences
Creation requires a name and rejects duplicates before dispatch. Independent roots can reuse names. Tool results, initial child guidance, message prefixes, and settlement notices use names. Internal Session and task-owner ids stay stable. The [upgrade guide](../../../../docs/upgrade-guide/v0.2.1-alpha.4/agent-team-subagents/guide.md) lists changed tool arguments and results.
## Testing
Name tests exercise concurrent duplicate rejection, failed-start release, nested-tree uniqueness, independent roots, retained projection state, invalid durable assignments, and legacy lookup. Recorded Headless and SDK sessions cover creation, messages, listing, and completion notices; Team tests preserve historical assignment without granting message authority.
@@ -0,0 +1,31 @@
# Agent Note: 树内唯一 agent 名字
Status: implemented
[English](2026-10-16-tree-wide-agent-names.md) | 中文
## 问题
Session id 提供持久执行身份,但模型协作时难以阅读。描述可以重复,不能标识接收方。用户要求 subagent 和 teammate 显式使用唯一名字,并为根保留 `lead`。
## 决策
持久化与授权保留 Session id。模型工具接受整棵 agent 树内唯一且不可变的名字。根拥有的 `subagent/named` 事件记录每次成功分配;投影排除分叉继承的事实。管理器在异步准备前预留名字,释放失败且未发布的预留。执行结束后已提交名字仍被占用,避免延迟消息到达其他 agent。
本决策取代 [Agent Teams 作为 subagent](2026-10-15-agent-teams-as-subagents.zh.md) 中不使用名字的接口。谱系权限与共享子代理生命周期保持不变。名字解析不授予消息、中断或任务分配权限。旧的无名后代获得稳定的 `legacy:<session-id>` 地址;Team 分配任务也接受活跃历史成员的此类地址。
## 考虑过的替代方案
**父级内唯一的名字。** 发现后代时需要限定路径。整棵树共享一个命名空间,使每个调用方使用相同地址。
**可回收的名字。** 原 agent 结束后复用名字,可能改变排队或延迟工作的接收方。保留分配使接收方身份稳定。
**第二份已提交注册表。** 根投影已经可以在重启后重建分配。第二份缓存需要同步与恢复;只有未发布的预留需要独立状态。
## 影响
创建必须提供名字,分发前拒绝重名。独立根可以复用名字。工具结果、子级初始指引、消息前缀与结束通知使用名字。内部 Session 和任务负责人 id 保持稳定。[升级指南](../../../../docs/upgrade-guide/v0.2.1-alpha.4/agent-team-subagents/guide.zh.md)列出变化的工具参数与结果。
## 测试
命名测试覆盖并发重名拒绝、启动失败释放、嵌套树唯一性、独立根、保留的投影状态、无效持久化分配与旧地址查找。Headless 和 SDK 录制会话覆盖创建、消息、列表与完成通知;Team 测试保留历史任务分配,同时拒绝消息权限。
@@ -81,13 +81,13 @@ function researcher(messages) {
if (!names.includes('send_message')) {
return toolChunks([
{ name: 'team_task_update', args: { task_id: 'task-1', expected_revision: 2, action: 'complete' } },
{ name: 'send_message', args: { agent_id: sibling, message: 'Research complete: use the deterministic finding.' } },
{ name: 'send_message', args: { name: sibling, message: 'Research complete: use the deterministic finding.' } },
])
}
return textChunks('Research teammate complete.')
}
function implementer(messages, parentId) {
function implementer(messages, parentName) {
const names = calls(messages)
const last = latestAssistantCalls(messages)
const text = latestToolText(messages)
@@ -128,7 +128,7 @@ function implementer(messages, parentId) {
if (!names.includes('send_message')) {
return toolChunks([
{ name: 'team_task_update', args: { task_id: 'task-2', expected_revision: 2, action: 'complete' } },
{ name: 'send_message', args: { agent_id: parentId, message: 'Implementation complete and verified.' } },
{ name: 'send_message', args: { name: parentName, message: 'Implementation complete and verified.' } },
])
}
return textChunks('Implementation teammate complete.')
@@ -148,6 +148,7 @@ function lead(messages) {
return toolChunks([{
name: 'subagent',
args: {
name: 'implementer',
description: 'Own deterministic implementation.',
prompt: 'IMPLEMENTER_MARK: wait for research, complete dependent task 2, report to lead.',
},
@@ -157,6 +158,7 @@ function lead(messages) {
return toolChunks([{
name: 'subagent',
args: {
name: 'researcher',
description: 'Own deterministic research.',
prompt: 'RESEARCHER_MARK: complete research task 1, message implementer, then finish.',
},
@@ -186,7 +188,7 @@ class TeamFixtureAdapter extends LlmAdapter {
const context = options.messages.findLast(message => message.source?.kind === 'runtime-context')
const section = name => context?.source.sections?.find(candidate => candidate.name === name)?.text
const role = section('team:identity')
const parentId = role?.match(/led by agent "([^"]+)"/u)?.[1]
const parentName = role?.match(/led by "([^"]+)"/u)?.[1]
const task = initial?.content.filter(block => block.type === 'text').map(block => block.text).join('\n')
if (!workflowChild && (!tools.includes('subagent') || !tools.includes('workflow') || !tools.includes('team_task_create'))) {
throw new Error('Team profile is missing delegation, workflow, or task tools')
@@ -194,7 +196,7 @@ class TeamFixtureAdapter extends LlmAdapter {
let chunks
if (workflowChild) chunks = textChunks('Fresh workflow child complete.')
else if (task?.startsWith('RESEARCHER_MARK:')) chunks = researcher(options.messages)
else if (task?.startsWith('IMPLEMENTER_MARK:')) chunks = implementer(options.messages, parentId)
else if (task?.startsWith('IMPLEMENTER_MARK:')) chunks = implementer(options.messages, parentName)
else if (role?.startsWith('You are the Team Lead.')) chunks = lead(options.messages)
else throw new Error('Team runtime context is missing a recognized identity')
for (const chunk of chunks) {
@@ -1041,8 +1041,8 @@ describe('headless stream-json snapshots', () => {
"checkedRoster": true,
"deliveredMessages": 0,
"identities": [
"You are a teammate in the Team led by agent "<lead>".",
"You are a teammate in the Team led by agent "<lead>".",
"Your name is "implementer". You are a teammate in the Team led by "lead".",
"Your name is "researcher". You are a teammate in the Team led by "lead".",
],
"queuedMessages": 0,
"sessions": 4,
@@ -156,7 +156,7 @@ describe.skipIf(MODE === 'record')('web snapshot: Official native subagent bundl
if (notice?.data.source.kind !== 'subagent-settled') throw new Error('Native completion was not delivered to the parent')
expect(notice.data.source.senderSessionId).toBe(catalog[0]?.childId)
expect(notice.data.content).toContainEqual({ type: 'text', text: RESPONSE })
expect(dispatches[0]?.content).toEqual([{ type: 'text', text: `started subagent ${notice.data.source.senderSessionId}` }])
expect(dispatches[0]?.content).toEqual([{ type: 'text', text: 'started subagent native-worker' }])
expect(events.filter(event => event.type === 'turn/end')).toHaveLength(2)
expect(server.requests).toHaveLength(1)
expect(server.requests[0]?.body).toMatchObject({ model: 'fixture-model' })
@@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
import { mkdir } from 'node:fs/promises'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
@@ -96,7 +97,7 @@ describe('web e2e: sidebar subagent activity', () => {
source: { kind: 'user' },
}))
await parentHandle.agent.whenIdle()
const started = await scaffold.ctx.subagents.startActivation({
const started = await scaffold.ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
delivery: 'parent',
provider: 'spawn',
label: 'sidebar activity child',
+2 -1
View File
@@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { fileURLToPath } from 'node:url'
@@ -144,7 +145,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
await parentInput.press('Enter')
expect(await parentSettled).toBe(parent.id)
const started = await scaffold.ctx.subagents.startActivation({
const started = await scaffold.ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
delivery: 'parent',
provider: 'spawn',
label: LABEL,
@@ -45,7 +45,7 @@ it.skipIf(MODE === 'record')('replays inherited schedule tools and refusals in a
const childPrompt = parseSessionLog(expected[1]!).findLast(event => event.type === 'user/message'
&& event.data.source.kind === 'user')
if (childPrompt?.type !== 'user/message') throw new Error('child fixture has no user prompt')
const activation = await scaffold.ctx.subagents.startActivation({
const activation = await scaffold.ctx.subagents.startActivation({ name: 'scheduled-child',
provider: 'fork', label: 'Check child reminder refusal', delivery: 'caller',
signal: new AbortController().signal,
request: { parent, prompt: [...childPrompt.data.content] },
+3 -2
View File
@@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
// Web e2e scenario: the composer's independent Stop interrupts a running
// continuable child. The child holds its model turn open through a replay
// hang entry; the browser proves Send and Stop coexist, the parent-offline
@@ -148,7 +149,7 @@ describe.skipIf(MODE === 'record')('web e2e: composer interrupt for a running co
if (root === undefined) throw new Error('fresh workspace did not publish its parent Agent')
parent = root
// The child's first model call claims the primary override and holds.
const started = await scaffold.ctx.subagents.startActivation({
const started = await scaffold.ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
delivery: 'parent',
provider: 'spawn',
label: LABEL,
@@ -378,7 +379,7 @@ describe.skipIf(MODE === 'record')('web e2e: composer interrupt for a running co
? event.data.content.flatMap(block => block.type === 'text' ? [block.text] : [])
: [])
expect(userTexts[0]).toBe(INITIAL)
expect(userTexts[1]).toMatch(/^Your parent agent id is .+send_message\(\{ agent_id: /)
expect(userTexts[1]).toMatch(/^Your name is .+Your parent agent name is .+send_message\(\{ name: /)
expect(userTexts.slice(2)).toEqual([REARM, REARM_WAKE, EDITED_FOLLOWUP, WAKING])
const imageMessage = events.find(event => event.type === 'user/message'
&& event.data.content.some(block => block.type === 'image'))
+2 -2
View File
@@ -108,7 +108,7 @@ describe.skipIf(MODE === 'record')('web e2e: subagents/interruptByParent over th
const parent = scaffold.ctx.agents.get(parentId)
if (parent === undefined) throw new Error('created parent session did not publish a live Agent')
const started = await scaffold.ctx.subagents.startActivation({
const started = await scaffold.ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
delivery: 'parent',
provider: 'spawn',
label: 'event-sourcing researcher',
@@ -189,7 +189,7 @@ describe.skipIf(MODE === 'record')('web e2e: subagents/interruptByParent over th
? event.data.content.flatMap(block => block.type === 'text' ? [block.text] : [])
: [])
expect(userTexts[0]).toBe(INITIAL)
expect(userTexts[1]).toMatch(/^Your parent agent id is .+send_message\(\{ agent_id: /)
expect(userTexts[1]).toMatch(/^Your name is .+Your parent agent name is .+send_message\(\{ name: /)
expect(userTexts.slice(2)).toEqual([FOLLOWUP, WAKING])
const turnEndKinds = events
.filter(event => event.type === 'turn/end')
+4 -4
View File
@@ -1260,8 +1260,8 @@ export interface Config {
## `@deepseek-ai/dsh-experimental-tool-agent-team`
- `inject`: `agents` · `agentTeams` · `tools` · `systemPrompt`
- `source`: [`packages/experimental/tool-agent-team/src/index.ts:17`](../packages/experimental/tool-agent-team/src/index.ts)
- `inject`: `agents` · `agentTeams` · `subagents` · `tools` · `systemPrompt`
- `source`: [`packages/experimental/tool-agent-team/src/index.ts:18`](../packages/experimental/tool-agent-team/src/index.ts)
```ts config-catalog
/** The Team tool plugin takes no configuration. */
@@ -2039,7 +2039,7 @@ export type PiAiThinkingTokenBudgetField = NonNullable<OpenAICompletionsCompat['
- `inject`: `llm`
- `refs`: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts) · [`ToolUpdate`](../packages/llm/llm/src/index.ts)
- `source`: [`packages/test-support/llm-replay/src/index.ts:1129`](../packages/test-support/llm-replay/src/index.ts)
- `source`: [`packages/test-support/llm-replay/src/index.ts:1109`](../packages/test-support/llm-replay/src/index.ts)
```ts config-catalog
/** Plugin config: the {@link ReplayConfig} inputs, each defaulting to its `DSH_SNAPSHOT_*` env var in `apply`. */
@@ -3312,7 +3312,7 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
- `inject`: `workingDirectory`
- `refs`: `Volatile` (`@deepseek-ai/cordis`)
- `source`: [`packages/subagent/subagent/src/index.ts:187`](../packages/subagent/subagent/src/index.ts)
- `source`: [`packages/subagent/subagent/src/index.ts:188`](../packages/subagent/subagent/src/index.ts)
```ts config-catalog
/** Host configuration for continuable subagent capacity. */
+4 -4
View File
@@ -1262,8 +1262,8 @@ export interface Config {
## `@deepseek-ai/dsh-experimental-tool-agent-team`
- `inject`: `agents` · `agentTeams` · `tools` · `systemPrompt`
- `source`: [`packages/experimental/tool-agent-team/src/index.ts:17`](../packages/experimental/tool-agent-team/src/index.ts)
- `inject`: `agents` · `agentTeams` · `subagents` · `tools` · `systemPrompt`
- `source`: [`packages/experimental/tool-agent-team/src/index.ts:18`](../packages/experimental/tool-agent-team/src/index.ts)
```ts config-catalog
/** The Team tool plugin takes no configuration. */
@@ -2041,7 +2041,7 @@ export type PiAiThinkingTokenBudgetField = NonNullable<OpenAICompletionsCompat['
- `inject`: `llm`
- `refs`: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts) · [`ToolUpdate`](../packages/llm/llm/src/index.ts)
- `source`: [`packages/test-support/llm-replay/src/index.ts:1129`](../packages/test-support/llm-replay/src/index.ts)
- `source`: [`packages/test-support/llm-replay/src/index.ts:1109`](../packages/test-support/llm-replay/src/index.ts)
```ts config-catalog
/** Plugin config: the {@link ReplayConfig} inputs, each defaulting to its `DSH_SNAPSHOT_*` env var in `apply`. */
@@ -3314,7 +3314,7 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
- `inject`: `workingDirectory`
- `refs`: `Volatile` (`@deepseek-ai/cordis`)
- `source`: [`packages/subagent/subagent/src/index.ts:187`](../packages/subagent/subagent/src/index.ts)
- `source`: [`packages/subagent/subagent/src/index.ts:188`](../packages/subagent/subagent/src/index.ts)
```ts config-catalog
/** Host configuration for continuable subagent capacity. */
+2 -2
View File
@@ -45,5 +45,5 @@
en: 85b38f78d87fc88f
zh: 00d5eb625e7dfb61
/development-guide/contributor-reference/documenting-types-verbatim-ts-type-equiv:
en: 9d578b6797be921b
zh: 823cbfcfd9a75c17
en: f622a56c33cd62ec
zh: f30408572094bb74
+1 -1
View File
@@ -211,4 +211,4 @@ The [subsystems](subsystems/README.md) pages paste source-equivalent declaration
{ "doc": "docs/subsystems/session.md", "symbol": "SessionEvent", "source": "packages/core/session/src/types.ts" }
```
`pnpm run verify-type-equiv` (part of `doc-sync`) then extracts that symbol's declaration and attached JSDoc from source via the TypeScript parser and asserts the block matches both. For a class whose implementation bodies do not belong in the catalog, use ` ```ts public-api ` and set `"projection": "public-api"`; the checked projection retains the public fields, constructor, accessors, methods, and original class/member JSDoc while omitting bodies and private or protected members. Comparison ignores whitespace and non-JSDoc comments but requires every original JSDoc comment, including member documentation, so readers see the source contract beside the exact type definition. The gate enforces a 1:1 correspondence by document, symbol, and projection between primary blocks and manifest entries; a paired `.zh.md` block reuses its unsuffixed sibling's entry only when the whole tracked fence sequence is byte-identical and ordered identically. `doc-typecheck` applies the same derivative rule to compilable fences, while skipping both source-equivalence fence kinds from compilation and its opt-out ratio. When you change a documented declaration or its JSDoc, the gate fails until you update the paste; when you add or remove a primary block, update the manifest in the same change.
`pnpm run verify-type-equiv` (part of `doc-sync`) then extracts that symbol's declaration and attached JSDoc from source via the TypeScript parser and asserts the block matches both. For a class whose implementation bodies do not belong in the catalog, use ` ```ts public-api ` and set `"projection": "public-api"`; the checked projection retains the public fields, constructor, accessors, methods, and original class/member JSDoc while omitting bodies and private or protected members. Comparison ignores whitespace and non-JSDoc comments but requires every original JSDoc comment, including member documentation, so readers see the source contract beside the exact type definition. The gate enforces a 1:1 correspondence by document, symbol, and projection between primary blocks and manifest entries; a paired `.zh.md` block reuses its unsuffixed sibling's entry only when the whole tracked fence sequence is byte-identical and ordered identically. `doc-typecheck` applies the same derivative rule to compilable fences, while skipping both source-equivalence fence kinds from compilation and its opt-out ratio. When you change a documented declaration or its JSDoc, the gate fails until you update the paste; when you add or remove a primary block, update the manifest in the same change. Frozen upgrade guides for earlier root package versions are excluded from current-API compilation; the editable current-version guides remain checked.
+1 -1
View File
@@ -218,4 +218,4 @@ Web 命令后面的参数会传给 `dsh web`,例如 `pnpm run dev:web --no-ope
{ "doc": "docs/subsystems/session.md", "symbol": "SessionEvent", "source": "packages/core/session/src/types.ts" }
```
`pnpm run verify-type-equiv`(`doc-sync` 的一环)随后通过 TypeScript 解析器从源码提取该符号的声明及其附带的 JSDoc,并断言代码块同时匹配两者。对于不应把实现体写进目录的类,请使用 ` ```ts public-api ` 并设置 `"projection": "public-api"`;门禁检查的投影会保留公共字段、构造函数、访问器、方法以及类和成员的原始 JSDoc,同时省略实现体和私有或受保护成员。比对会忽略空白和非 JSDoc 注释,但要求保留每条原始 JSDoc(包括成员文档),让读者同时看到源码约定和确切类型定义。该门禁按文档、符号和投影,在主块与 manifest 条目之间强制 1:1 对应;只有当配对 `.zh.md` 块的完整受跟踪围栏序列与其无后缀兄弟文件按字节一致且顺序相同时,才会复用后者的条目。`doc-typecheck` 对可编译围栏应用同一派生规则,同时跳过两种源码等价围栏的编译,并将其排除在 opt-out 比例的计算之外。当你改动一个已记录的类型声明或其 JSDoc 时,门禁会失败直到你更新粘贴内容;当你增删一个主块时,请在同一个变更里更新 manifest。
`pnpm run verify-type-equiv`(`doc-sync` 的一环)随后通过 TypeScript 解析器从源码提取该符号的声明及其附带的 JSDoc,并断言代码块同时匹配两者。对于不应把实现体写进目录的类,请使用 ` ```ts public-api ` 并设置 `"projection": "public-api"`;门禁检查的投影会保留公共字段、构造函数、访问器、方法以及类和成员的原始 JSDoc,同时省略实现体和私有或受保护成员。比对会忽略空白和非 JSDoc 注释,但要求保留每条原始 JSDoc(包括成员文档),让读者同时看到源码约定和确切类型定义。该门禁按文档、符号和投影,在主块与 manifest 条目之间强制 1:1 对应;只有当配对 `.zh.md` 块的完整受跟踪围栏序列与其无后缀兄弟文件按字节一致且顺序相同时,才会复用后者的条目。`doc-typecheck` 对可编译围栏应用同一派生规则,同时跳过两种源码等价围栏的编译,并将其排除在 opt-out 比例的计算之外。当你改动一个已记录的类型声明或其 JSDoc 时,门禁会失败直到你更新粘贴内容;当你增删一个主块时,请在同一个变更里更新 manifest。 早于根 package 当前版本的冻结升级指南不参与当前 API 编译;仍可编辑的当前版本指南继续检查。
+4 -4
View File
@@ -68,10 +68,10 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:82`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) |
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:75`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`emit`) | `remotes` |
| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:167`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), `server` |
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:143`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:149`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:159`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) |
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:168`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), `server` |
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:144`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:150`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:160`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) |
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference), [`working-directory`](../packages/session/working-directory) |
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:209`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) |
+4 -4
View File
@@ -70,10 +70,10 @@
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:82`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) |
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:75`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`emit`) | `remotes` |
| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:167`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), `server` |
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:143`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:149`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:159`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) |
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:168`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), `server` |
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:144`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:150`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:160`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) |
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference), [`working-directory`](../packages/session/working-directory) |
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:209`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) |
+22 -10
View File
@@ -9,8 +9,8 @@
en: 1387c4505dde2faf
zh: 48570fa1c13d6910
/session-persistence-event-catalog/persistence-type-fingerprints:
en: a696c43a89d5fbe8
zh: 6855eb0239b8dcb3
en: 703191ea7dd30cc8
zh: c85ffc80d296778d
/session-persistence-event-catalog/event-envelope:
en: 689f781d95689e20
zh: 602f6a7f0835d698
@@ -213,8 +213,11 @@
en: f5d53639dc5afdfc
zh: 8c553e7397d08832
/session-persistence-event-catalog/events/subagent/subagent-model-selection-policy-log-only:
en: cdf085d18fae747a
zh: 83cd5cf56fef4050
en: 68c666987d376151
zh: 7c56ceb033c53de7
/session-persistence-event-catalog/events/subagent/subagent-named-log-only:
en: 6255823943d4f0da
zh: 14a3a543a2e0e887
/session-persistence-event-catalog/events/system:
en: 8cac41d2fb575975
zh: 8cac41d2fb575975
@@ -780,8 +783,11 @@
en: e932f06c949f5f6c
zh: e932f06c949f5f6c
/session-persistence-event-catalog/resolved-persistence-types/subagent-model-selection-policy:
en: 25ccd80df944ac00
zh: 25ccd80df944ac00
en: 257216410047d2eb
zh: 257216410047d2eb
/session-persistence-event-catalog/resolved-persistence-types/subagent-named:
en: bcfc29bf1001dc32
zh: bcfc29bf1001dc32
/session-persistence-event-catalog/resolved-persistence-types/success:
en: 801923015f7dc7e4
zh: 801923015f7dc7e4
@@ -1227,8 +1233,11 @@
en: 4da876322ab7a590
zh: 67992071915ab925
/session-persistence-event-catalog/resolved-persistence-types/subagentdescriptordata:
en: c56821e57987ae9e
zh: 0364f59a472d51da
en: 648c373bb4d7017b
zh: c7b729e5435d3b48
/session-persistence-event-catalog/resolved-persistence-types/subagentnamedata:
en: 12db98d0b0b894f3
zh: 1d0fd64e438dde78
/session-persistence-event-catalog/resolved-persistence-types/subagentsettledmessagesource:
en: d34acab659e74957
zh: 25cabc6c9fb44a7e
@@ -2058,8 +2067,11 @@
en: 2adab4361a5b558d
zh: 7f2a8da669f9e117
/session-persistence-event-catalog/resolved-persistence-types/type-subagent-model-selection-policy:
en: 9dd6539f4a5d9d16
zh: d886630b70a4bce8
en: ba25b6c68d971788
zh: 4c5ca166b4739cce
/session-persistence-event-catalog/resolved-persistence-types/type-subagent-named:
en: 4b4ecf7269d7daf5
zh: 78ce0a540ad8141a
/session-persistence-event-catalog/resolved-persistence-types/type-system-message:
en: 7fb45f52932f268b
zh: 5019109b99274589
+56
View File
@@ -64,6 +64,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover
| `event:subagent/catalog` | event | `53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac) |
| `event:subagent/descriptor` | event | `b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5` | [`{ type: "subagent/descriptor" }`](#persistence-type-sha256-b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5) |
| `event:subagent/model-selection-policy` | event | `a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc` | [`{ type: "subagent/model-selection-policy" }`](#persistence-type-sha256-a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc) |
| `event:subagent/named` | event | `273f07aea05d56ffbb8b43508bcfb4a20cce230819feadb3fe740bceb8b12ca6` | [`{ type: "subagent/named" }`](#persistence-type-sha256-273f07aea05d56ffbb8b43508bcfb4a20cce230819feadb3fe740bceb8b12ca6) |
| `event:system/message` | event | `69081694be231d56fd9580ba14645fd5e35373202605d5c5c841a9435b5fa3b1` | [`{ type: "system/message" }`](#persistence-type-sha256-69081694be231d56fd9580ba14645fd5e35373202605d5c5c841a9435b5fa3b1) |
| `event:team/member` | event | `4fb59762612c3e3ac3a3bd4f84c9c148d3c3893bd422ba2b201cc039fabd49bc` | [`{ type: "team/member" }`](#persistence-type-sha256-4fb59762612c3e3ac3a3bd4f84c9c148d3c3893bd422ba2b201cc039fabd49bc) |
| `event:team/message/delivered` | event | `48f9c19417a1abbedfa59f4667bba36b93ac2db407adf5e84cb3ba0de30942cb` | [`{ type: "team/message/delivered" }`](#persistence-type-sha256-48f9c19417a1abbedfa59f4667bba36b93ac2db407adf5e84cb3ba0de30942cb) |
@@ -976,6 +977,20 @@ Source: [`packages/subagent/subagent/src/descriptor.ts:38`](../packages/subagent
Source: [`packages/subagent/tool-subagent/src/model-selection-state.ts:17`](../packages/subagent/tool-subagent/src/model-selection-state.ts)
<a id="subagentnamed--log-only"></a>
#### `subagent/named` — log-only
```ts persistence-catalog
/**
* A tree-wide agent name, retained after its execution ends.
* @param data - the name and durable child identity.
*/
'subagent/named': SubagentNameData
```
Source: [`packages/subagent/subagent/src/names.ts:26`](../packages/subagent/subagent/src/names.ts)
### `system/*`
<a id="systemmessage--surface"></a>
@@ -2609,6 +2624,14 @@ SHA-256: `0a8a062bc9c3c0d97fd97742262f2ea53dd32297f834e81114870d38ac3dc6da`
`"subagent/model-selection-policy"`
<a id="persistence-type-sha256-fdca5e24e7a0bb1af1f3536b8c85787ca1dbca2802f632bf4a5347a2ba62fdb1"></a>
### `"subagent/named"`
SHA-256: `fdca5e24e7a0bb1af1f3536b8c85787ca1dbca2802f632bf4a5347a2ba62fdb1`
`"subagent/named"`
<a id="persistence-type-sha256-511801b0a6470d98566e8385d3dffddb2b67f79fcf73ed6056f46c3cc0a245a7"></a>
### `"success"`
@@ -4848,6 +4871,23 @@ One of:
- [`ContinuableSubagentDescriptorData`](#persistence-type-sha256-6487b10ba7542f3c447b1e41a1c6a590b7bfafe0b9b66b49f34028da85bb970f)
- [`OneShotSubagentDescriptorData`](#persistence-type-sha256-5f758f3de5f00c18968a7c6d347083aac5c9ae13f6a79c78f6611b23c4973564)
<a id="persistence-type-sha256-b0b7b7f4d127101c49bfd0128e6f37c4627ebbc8baddf5b2e9edff1dac8c84b8"></a>
<a id="persistence-type-packagessubagentsubagentsrcnamestssubagentnamedata"></a>
<a id="persistence-type-subagentnamedata"></a>
### `SubagentNameData`
SHA-256: `b0b7b7f4d127101c49bfd0128e6f37c4627ebbc8baddf5b2e9edff1dac8c84b8`
Sources: [`packages/subagent/subagent/src/names.ts:15`](../packages/subagent/subagent/src/names.ts)
| Property | Presence | Type |
|---|---|---|
| `id` | required | `string` |
| `name` | required | `string` |
<a id="persistence-type-sha256-4e97c3d85c0fc817ee58873c38c25a2474abaaa0af4839311b50b68db3b8cf1a"></a>
<a id="persistence-type-packagessubagentsubagentsrccontinuation-messagestssubagentsettledmessagesource"></a>
@@ -8679,6 +8719,22 @@ SHA-256: `a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc`
| `time` | required | `number` |
| `type` | required | `"subagent/model-selection-policy"` |
<a id="persistence-type-sha256-273f07aea05d56ffbb8b43508bcfb4a20cce230819feadb3fe740bceb8b12ca6"></a>
<a id="persistence-type-eventsubagentnamed"></a>
### `{ type: "subagent/named" }`
SHA-256: `273f07aea05d56ffbb8b43508bcfb4a20cce230819feadb3fe740bceb8b12ca6`
| Property | Presence | Type |
|---|---|---|
| `data` | required | [`SubagentNameData`](#persistence-type-sha256-b0b7b7f4d127101c49bfd0128e6f37c4627ebbc8baddf5b2e9edff1dac8c84b8) |
| `ignorable` | optional | `true` |
| `seq` | required | `number` |
| `time` | required | `number` |
| `type` | required | `"subagent/named"` |
<a id="persistence-type-sha256-69081694be231d56fd9580ba14645fd5e35373202605d5c5c841a9435b5fa3b1"></a>
<a id="persistence-type-eventsystemmessage"></a>
+56
View File
@@ -66,6 +66,7 @@
| `event:subagent/catalog` | event | `53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac) |
| `event:subagent/descriptor` | event | `b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5` | [`{ type: "subagent/descriptor" }`](#persistence-type-sha256-b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5) |
| `event:subagent/model-selection-policy` | event | `a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc` | [`{ type: "subagent/model-selection-policy" }`](#persistence-type-sha256-a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc) |
| `event:subagent/named` | event | `273f07aea05d56ffbb8b43508bcfb4a20cce230819feadb3fe740bceb8b12ca6` | [`{ type: "subagent/named" }`](#persistence-type-sha256-273f07aea05d56ffbb8b43508bcfb4a20cce230819feadb3fe740bceb8b12ca6) |
| `event:system/message` | event | `69081694be231d56fd9580ba14645fd5e35373202605d5c5c841a9435b5fa3b1` | [`{ type: "system/message" }`](#persistence-type-sha256-69081694be231d56fd9580ba14645fd5e35373202605d5c5c841a9435b5fa3b1) |
| `event:team/member` | event | `4fb59762612c3e3ac3a3bd4f84c9c148d3c3893bd422ba2b201cc039fabd49bc` | [`{ type: "team/member" }`](#persistence-type-sha256-4fb59762612c3e3ac3a3bd4f84c9c148d3c3893bd422ba2b201cc039fabd49bc) |
| `event:team/message/delivered` | event | `48f9c19417a1abbedfa59f4667bba36b93ac2db407adf5e84cb3ba0de30942cb` | [`{ type: "team/message/delivered" }`](#persistence-type-sha256-48f9c19417a1abbedfa59f4667bba36b93ac2db407adf5e84cb3ba0de30942cb) |
@@ -978,6 +979,20 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
来源:[`packages/subagent/tool-subagent/src/model-selection-state.ts:17`](../packages/subagent/tool-subagent/src/model-selection-state.ts)
<a id="subagentnamed--log-only"></a>
#### `subagent/named` — log-only
```ts persistence-catalog
/**
* A tree-wide agent name, retained after its execution ends.
* @param data - the name and durable child identity.
*/
'subagent/named': SubagentNameData
```
来源:[`packages/subagent/subagent/src/names.ts:26`](../packages/subagent/subagent/src/names.ts)
### `system/*`
<a id="systemmessage--surface"></a>
@@ -2611,6 +2626,14 @@ SHA-256: `0a8a062bc9c3c0d97fd97742262f2ea53dd32297f834e81114870d38ac3dc6da`
`"subagent/model-selection-policy"`
<a id="persistence-type-sha256-fdca5e24e7a0bb1af1f3536b8c85787ca1dbca2802f632bf4a5347a2ba62fdb1"></a>
### `"subagent/named"`
SHA-256: `fdca5e24e7a0bb1af1f3536b8c85787ca1dbca2802f632bf4a5347a2ba62fdb1`
`"subagent/named"`
<a id="persistence-type-sha256-511801b0a6470d98566e8385d3dffddb2b67f79fcf73ed6056f46c3cc0a245a7"></a>
### `"success"`
@@ -4850,6 +4873,23 @@ SHA-256: `86d1d0e9cfebb240421187e3dc37fb71cc38a1501885cf32f8f9adef7b0a2bde`
- [`ContinuableSubagentDescriptorData`](#persistence-type-sha256-6487b10ba7542f3c447b1e41a1c6a590b7bfafe0b9b66b49f34028da85bb970f)
- [`OneShotSubagentDescriptorData`](#persistence-type-sha256-5f758f3de5f00c18968a7c6d347083aac5c9ae13f6a79c78f6611b23c4973564)
<a id="persistence-type-sha256-b0b7b7f4d127101c49bfd0128e6f37c4627ebbc8baddf5b2e9edff1dac8c84b8"></a>
<a id="persistence-type-packagessubagentsubagentsrcnamestssubagentnamedata"></a>
<a id="persistence-type-subagentnamedata"></a>
### `SubagentNameData`
SHA-256: `b0b7b7f4d127101c49bfd0128e6f37c4627ebbc8baddf5b2e9edff1dac8c84b8`
来源:[`packages/subagent/subagent/src/names.ts:15`](../packages/subagent/subagent/src/names.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
| `id` | 必需 | `string` |
| `name` | 必需 | `string` |
<a id="persistence-type-sha256-4e97c3d85c0fc817ee58873c38c25a2474abaaa0af4839311b50b68db3b8cf1a"></a>
<a id="persistence-type-packagessubagentsubagentsrccontinuation-messagestssubagentsettledmessagesource"></a>
@@ -8681,6 +8721,22 @@ SHA-256: `a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc`
| `time` | 必需 | `number` |
| `type` | 必需 | `"subagent/model-selection-policy"` |
<a id="persistence-type-sha256-273f07aea05d56ffbb8b43508bcfb4a20cce230819feadb3fe740bceb8b12ca6"></a>
<a id="persistence-type-eventsubagentnamed"></a>
### `{ type: "subagent/named" }`
SHA-256: `273f07aea05d56ffbb8b43508bcfb4a20cce230819feadb3fe740bceb8b12ca6`
| 属性 | 存在性 | 类型 |
|---|---|---|
| `data` | 必需 | [`SubagentNameData`](#persistence-type-sha256-b0b7b7f4d127101c49bfd0128e6f37c4627ebbc8baddf5b2e9edff1dac8c84b8) |
| `ignorable` | 可选 | `true` |
| `seq` | 必需 | `number` |
| `time` | 必需 | `number` |
| `type` | 必需 | `"subagent/named"` |
<a id="persistence-type-sha256-69081694be231d56fd9580ba14645fd5e35373202605d5c5c841a9435b5fa3b1"></a>
<a id="persistence-type-eventsystemmessage"></a>
@@ -0,0 +1,28 @@
# Bilingual-pair consistency record for 2026-10-16-tree-wide-agent-names.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/persistence-changes/2026-10-16-tree-wide-agent-names.md
/:
en: 3d5a424ae7d7e46b
zh: c22a8e4a62d8cc1a
/2026-10-16-tree-wide-agent-names:
en: a5bae46eec7e5c5b
zh: 190923c7b9b397d9
/2026-10-16-tree-wide-agent-names/summary:
en: 3f16c8de27431ce8
zh: d3db668518da7e37
/2026-10-16-tree-wide-agent-names/table-of-contents:
en: 6cf314a069ceb451
zh: 9adff678e9ecab3b
/2026-10-16-tree-wide-agent-names/declaration:
en: 6907136f28e48c90
zh: 02f8298485f3d8e7
/2026-10-16-tree-wide-agent-names/compatibility:
en: fbdcc4f1b829bb9c
zh: 2454c11be18019c7
/2026-10-16-tree-wide-agent-names/verification:
en: 1b417eb5cdfa4ce6
zh: ec3f2dfcddd16b25
/2026-10-16-tree-wide-agent-names/dev-note:
en: b75c8458928b72b9
zh: 003ba4e17b4b5269
@@ -0,0 +1,48 @@
---
description: "Records a persistence type transition and its compatibility acknowledgement."
kind: persistence-change
---
# 2026-10-16-tree-wide-agent-names
English | [中文](2026-10-16-tree-wide-agent-names.zh.md)
## Summary
Records immutable tree-wide agent names in the root Session.
## Table of Contents
- [Declaration](#declaration)
- [Compatibility](#compatibility)
- [Verification](#verification)
- [Dev Note](#dev-note)
<a id="declaration"></a>
## Declaration
```yaml persistence-change
schemaVersion: 1
id: 2026-10-16-tree-wide-agent-names
baseline: false
changes:
- root: "event:subagent/named"
previous: null
after: "273f07aea05d56ffbb8b43508bcfb4a20cce230819feadb3fe740bceb8b12ca6"
decision: same-version
```
<a id="compatibility"></a>
## Compatibility
New readers accept older Sessions without assignments and expose stable legacy addresses. Older readers reject the new non-ignorable subagent/named discriminator instead of silently losing identity; existing catalog records and Session ids are unchanged. No writer format bump is needed.
<a id="verification"></a>
## Verification
The names.spec.ts and tool-team.spec.ts tests passed (33 cases); names.ts reached 100% statement, branch, function, and line coverage. Headless and TypeScript SDK replay exercised named creation, messages, discovery, and completion.
<a id="dev-note"></a>
## Dev Note
None.
@@ -0,0 +1,81 @@
{
"formatVersion": 2,
"roots": [
{
"key": "event:subagent/named",
"kind": "event",
"event": "subagent/named",
"surface": false,
"digest": "273f07aea05d56ffbb8b43508bcfb4a20cce230819feadb3fe740bceb8b12ca6",
"schema": {
"root": 0,
"nodes": [
{
"kind": "object",
"properties": [
{
"name": "data",
"type": 1,
"optional": false
},
{
"name": "ignorable",
"type": 3,
"optional": true
},
{
"name": "seq",
"type": 4,
"optional": false
},
{
"name": "time",
"type": 4,
"optional": false
},
{
"name": "type",
"type": 5,
"optional": false
}
],
"indices": []
},
{
"kind": "object",
"properties": [
{
"name": "id",
"type": 2,
"optional": false
},
{
"name": "name",
"type": 2,
"optional": false
}
],
"indices": []
},
{
"kind": "primitive",
"type": "string"
},
{
"kind": "literal",
"value": true
},
{
"kind": "primitive",
"type": "number"
},
{
"kind": "literal",
"value": "subagent/named"
}
]
}
}
],
"types": []
}
@@ -0,0 +1,48 @@
---
description: "记录持久化类型更改及其兼容性确认。"
kind: persistence-change
---
# 2026-10-16-tree-wide-agent-names
[English](2026-10-16-tree-wide-agent-names.md) | 中文
## 概述
在根 Session 记录整棵树内唯一且不可变的 agent 名字。
## 目录
- [声明](#declaration)
- [兼容性](#compatibility)
- [验证](#verification)
- [开发备注](#dev-note)
<a id="declaration"></a>
## 声明
```yaml persistence-change
schemaVersion: 1
id: 2026-10-16-tree-wide-agent-names
baseline: false
changes:
- root: "event:subagent/named"
previous: null
after: "273f07aea05d56ffbb8b43508bcfb4a20cce230819feadb3fe740bceb8b12ca6"
decision: same-version
```
<a id="compatibility"></a>
## 兼容性
新读取方接受没有名字分配的旧 Session,并公开稳定的旧地址。旧读取方拒绝新的非可忽略 subagent/named 判别值,避免静默丢失身份;现有目录记录与 Session id 不变。无需提升写入格式版本。
<a id="verification"></a>
## 验证
names.spec.ts 与 tool-team.spec.ts 的 33 个用例通过;names.ts 的语句、分支、函数与行覆盖率均为 100%。Headless 与 TypeScript SDK 回放覆盖按名字创建、消息、发现和完成通知。
<a id="dev-note"></a>
## 开发备注
无。
@@ -15,8 +15,8 @@
en: 14c57f30e676f6e3
zh: b1a0ccf26fab3727
/session-persistence-formats/format-references:
en: 43a4a6600fa37a1a
zh: 72174fab8355c344
en: 57e3fe7dd9e36b70
zh: 2dac8a29964d982f
/session-persistence-formats/scope-and-evidence:
en: b220062139ddac1e
zh: 7c4c0efe05dafa9c
@@ -32,7 +32,7 @@ The index is generated from validated snapshots and the current writer constant.
| 1 | PR #3349 | [V1](v1.md) | [JSON](v1.schema.json) | 54 / 415 |
| 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.md) | [JSON](v2.schema.json) | 56 / 435 |
| 3 | PR #4320 | [V3](v3.md) | [JSON](v3.schema.json) | 60 / 467 |
| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 65 / 612 |
| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 66 / 615 |
<!-- persistence-format-index:end -->
@@ -32,7 +32,7 @@ description: "查找从 V0 到工作区写入器版本的每个 Session 格式
| 1 | PR #3349 | [V1](v1.zh.md) | [JSON](v1.schema.json) | 54 / 415 |
| 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.zh.md) | [JSON](v2.schema.json) | 56 / 435 |
| 3 | PR #4320 | [V3](v3.zh.md) | [JSON](v3.schema.json) | 60 / 467 |
| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 65 / 612 |
| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 66 / 615 |
<!-- persistence-format-index:end -->
+94
View File
@@ -15907,6 +15907,81 @@
]
}
},
{
"key": "event:subagent/named",
"kind": "event",
"event": "subagent/named",
"surface": false,
"digest": "273f07aea05d56ffbb8b43508bcfb4a20cce230819feadb3fe740bceb8b12ca6",
"schema": {
"root": 0,
"nodes": [
{
"kind": "object",
"properties": [
{
"name": "data",
"type": 1,
"optional": false
},
{
"name": "ignorable",
"type": 3,
"optional": true
},
{
"name": "seq",
"type": 4,
"optional": false
},
{
"name": "time",
"type": 4,
"optional": false
},
{
"name": "type",
"type": 5,
"optional": false
}
],
"indices": []
},
{
"kind": "object",
"properties": [
{
"name": "id",
"type": 2,
"optional": false
},
{
"name": "name",
"type": 2,
"optional": false
}
],
"indices": []
},
{
"kind": "primitive",
"type": "string"
},
{
"kind": "literal",
"value": true
},
{
"kind": "primitive",
"type": "number"
},
{
"kind": "literal",
"value": "subagent/named"
}
]
}
},
{
"key": "event:system/message",
"kind": "event",
@@ -22996,6 +23071,11 @@
"names": [],
"sources": []
},
{
"digest": "273f07aea05d56ffbb8b43508bcfb4a20cce230819feadb3fe740bceb8b12ca6",
"names": [],
"sources": []
},
{
"digest": "2765e9119edf5816ac1ab4c95c5ee32c90403f3742962eff11544f7752d59d2e",
"names": [],
@@ -25071,6 +25151,15 @@
"packages/attachment/attachment/src/types.ts:11"
]
},
{
"digest": "b0b7b7f4d127101c49bfd0128e6f37c4627ebbc8baddf5b2e9edff1dac8c84b8",
"names": [
"packages/subagent/subagent/src/names.ts#SubagentNameData"
],
"sources": [
"packages/subagent/subagent/src/names.ts:15"
]
},
{
"digest": "b158f3ff6923714807c0d73882f0b05dc87f8fa85c9e90814852d48bbc866081",
"names": [
@@ -26117,6 +26206,11 @@
"names": [],
"sources": []
},
{
"digest": "fdca5e24e7a0bb1af1f3536b8c85787ca1dbca2802f632bf4a5347a2ba62fdb1",
"names": [],
"sources": []
},
{
"digest": "fe1a7acdeed4d0f5ab483396eaa5b3dc97263b7c10eb4da2f0028cb01728bfc9",
"names": [],
+3
View File
@@ -26,3 +26,6 @@
/subagents/durable-enumeration-listchildren-listdescendants-and-their-entries:
en: d8ce7969b65ad86e
zh: d39dac6592681bf2
/subagents/agent-names:
en: 934e8a775e67a697
zh: df4c9f5d0f5c0171
+42
View File
@@ -29,6 +29,8 @@ interface SubagentCapabilities {
```ts type-equiv
/** One managed child execution and its result delivery policy. */
interface SubagentActivationSpec {
/** Immutable name, unique across the parent's whole subagent tree. */
readonly name: string
/** Registered backend to use. */
readonly provider: string
/** Short task label retained in cataloged child membership. */
@@ -47,6 +49,8 @@ interface SubagentActivationSpec {
```ts type-equiv
/** A managed execution; disposal addresses this exact activation, never a later resume. */
interface SubagentActivation {
/** Immutable name used by model-facing collaboration tools. */
readonly name: string
/** Stable identity of the child. */
readonly childId: SessionId
/** Accepted inbox message, when the backend has a local inbox. */
@@ -392,6 +396,26 @@ type SubagentDescendantListEntry = SubagentListEntry & {
## Agent names
Names are unique in one live subagent tree; the root is `lead`. The root Session owns immutable assignments and the projection excludes inherited facts. See the [naming rules](../../packages/subagent/subagent/README.md#local-and-external-children).
```ts type-equiv
/** One immutable name assigned in the root Session before child publication. */
interface SubagentNameData {
readonly name: string
readonly id: SessionId
}
```
```ts type-equiv
/** Root-owned name assignments, excluding fork-inherited events. */
interface SubagentNamesState {
readonly inheritedEventCount: SessionLogOffset
readonly entries: readonly SubagentNameData[]
}
```
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
@@ -444,6 +468,24 @@ startActivation(spec: SubagentActivationSpec & { readonly childId: SessionId }):
*/
startActivation(spec: SubagentActivationSpec): Promise<SubagentActivation>
/**
* Read a model-facing name in the caller's tree, retaining legacy addresses for older unnamed children.
* @param caller - exact live Agent selecting the tree.
* @param id - target identity; omission selects the caller.
* @returns lead, the immutable assigned name, or a legacy address.
*/
nameOf(caller: Agent, id: SessionId = caller.id): string
/**
* Resolve a tree-wide name without granting authority to act on its Agent.
* @param caller - exact live Agent selecting the tree.
* @param name - a name returned by creation or discovery.
* @param signal - cancellation during historical discovery.
* @returns the durable Session identity for the consuming operation to authorize.
* @throws when the name is absent or the caller's live tree is unavailable.
*/
resolveName(caller: Agent, name: string, signal?: AbortSignal): Promise<SessionId>
/**
* Join progressing descendants without cancelling them. Idle descendants whose
* inboxes require a later wake stay resident and do not delay host completion.
+42
View File
@@ -29,6 +29,8 @@ interface SubagentCapabilities {
```ts type-equiv
/** One managed child execution and its result delivery policy. */
interface SubagentActivationSpec {
/** Immutable name, unique across the parent's whole subagent tree. */
readonly name: string
/** Registered backend to use. */
readonly provider: string
/** Short task label retained in cataloged child membership. */
@@ -47,6 +49,8 @@ interface SubagentActivationSpec {
```ts type-equiv
/** A managed execution; disposal addresses this exact activation, never a later resume. */
interface SubagentActivation {
/** Immutable name used by model-facing collaboration tools. */
readonly name: string
/** Stable identity of the child. */
readonly childId: SessionId
/** Accepted inbox message, when the backend has a local inbox. */
@@ -394,6 +398,26 @@ type SubagentDescendantListEntry = SubagentListEntry & {
<a id="the-terminal-result-subagentresult"></a>
## Agent 名字
名字在一棵在线子代理树内唯一;根名为 `lead`。根 Session 拥有不可变的名字分配,投影排除继承的事实。详见[命名规则](../../packages/subagent/subagent/README.zh.md#local-and-external-children)。
```ts type-equiv
/** One immutable name assigned in the root Session before child publication. */
interface SubagentNameData {
readonly name: string
readonly id: SessionId
}
```
```ts type-equiv
/** Root-owned name assignments, excluding fork-inherited events. */
interface SubagentNamesState {
readonly inheritedEventCount: SessionLogOffset
readonly entries: readonly SubagentNameData[]
}
```
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
@@ -446,6 +470,24 @@ startActivation(spec: SubagentActivationSpec & { readonly childId: SessionId }):
*/
startActivation(spec: SubagentActivationSpec): Promise<SubagentActivation>
/**
* Read a model-facing name in the caller's tree, retaining legacy addresses for older unnamed children.
* @param caller - exact live Agent selecting the tree.
* @param id - target identity; omission selects the caller.
* @returns lead, the immutable assigned name, or a legacy address.
*/
nameOf(caller: Agent, id: SessionId = caller.id): string
/**
* Resolve a tree-wide name without granting authority to act on its Agent.
* @param caller - exact live Agent selecting the tree.
* @param name - a name returned by creation or discovery.
* @param signal - cancellation during historical discovery.
* @returns the durable Session identity for the consuming operation to authorize.
* @throws when the name is absent or the caller's live tree is unavailable.
*/
resolveName(caller: Agent, name: string, signal?: AbortSignal): Promise<SessionId>
/**
* Join progressing descendants without cancelling them. Idle descendants whose
* inboxes require a later wake stay resident and do not delay host completion.
+7 -7
View File
@@ -9,8 +9,8 @@
en: 6cc2cd9ced70bb09
zh: 1c0e6cb4ba2ed637
/tool-schema-catalog/tool-package-map:
en: aff10b6cad96bfcb
zh: 800df4738667d1cc
en: b5a48ad99a91a27a
zh: b14ccf7ed0b503be
/tool-schema-catalog/deepseek-ai-dsh-plugin-manager:
en: e0143916bb634762
zh: e0143916bb634762
@@ -234,17 +234,17 @@
en: c89084b31ffccbce
zh: d7b3b7000f2d2be9
/tool-schema-catalog/deepseek-ai-dsh-tool-subagent/subagent:
en: 9270966e03a2a62b
zh: 0544ac614aed3144
en: c53129ca536b0d17
zh: bbadc2251304388b
/tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control:
en: ab82e130c3413f04
zh: ab82e130c3413f04
/tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control/interrupt-agent:
en: 9190a32df5e10ddd
zh: 48e1b05956d63e26
zh: 6ece9cbd62eb4d8e
/tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control/list-agents:
en: b68f6c31d72980d1
zh: 38be7bfea793043c
en: 6caa1abcb5ac1aaf
zh: f99f93111177da5f
/tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control/send-message:
en: 69c1c0a80c24e3a4
zh: 8d3e8b8b1137e5c5
+18 -13
View File
@@ -38,10 +38,10 @@ This table connects model-visible tool names to the plugin package and service s
| `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`, `ctx.workflowEngine`, `ctx.subagents`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents every fresh round)` | `tool/call`, `tool/result`, `workflow and child session events during execution` | - | A fixed foreground workflow starts one fresh structured child per round; the model selects only the immutable objective and an optional round cap. |
| `@deepseek-ai/dsh-tool-skill` | `skill` | `ctx.workingDirectory`, `ctx.tools`, `ctx.agents`, `ctx.skills` | `tool/call`, `tool/result`, `user/message replacement catalogs via agent.inject()` | - | - |
| `@deepseek-ai/dsh-tool-session-query` | `session_event_read`, `session_event_search`, `session_event_trace`, `session_search`, `session_trace` | `ctx.tools`, `ctx.systemPrompt`, `ctx.sessionQuery`, `a calling Agent for workspace authority` | `tool/call`, `tool/result` | - | The five read-only tools hide provider cursors and authorize every result from the immutable calling agent session. The package is opt-in; compositions that need enforced deadlines or bounded inline output also mount the generic timeout or spill policies. |
| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`, `subagent` | `ctx.tools`, `ctx.subagents`, `ctx.systemPrompt`, `ctx.llm for model discovery and selected-route validation` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion available in an enabled Session. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` remains fixed-route. Each instance selects its backend through `provider` and reads model-selection settings through `modelSelectionSettings`. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically. |
| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`, `subagent` | `ctx.tools`, `ctx.subagents`, `ctx.systemPrompt`, `ctx.llm for model discovery and selected-route validation` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion available in an enabled Session. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` remains fixed-route. Each instance selects its backend through `provider` and reads model-selection settings through `modelSelectionSettings`. Every delegation returns a unique agent name promptly; local children accept follow-ups, while external backends report their single execution automatically. |
| `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`, `list_agents`, `send_message` | `ctx.tools`, `ctx.subagents`, `ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`, `tool/result`, `child session events through ctx.subagents` | - | The globally named discovery, interruption, and local-message tools over managed subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` and `interrupt_agent` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows use the sessionProjections and live Agent registries). |
| `@deepseek-ai/dsh-tool-jobs` | `job_kill`, `job_list`, `job_output` | `ctx.tools`, `ctx.jobs`, `ctx.systemPrompt` | `tool/call`, `tool/result`, `user/message via agent.inject() for background completion notices` | - | The kind-agnostic background-job controller: background bash commands and PTY sends are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers' `ctx.jobs.start()`. |
| `@deepseek-ai/dsh-experimental-tool-agent-team` | `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update`, `wait_agent` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `an exact live Team member Agent` | `tool/call`, `team/task`, `tool/result` | - | Five task-board and wait tools are scoped to the Lead and its live subagents. The optional Agent Teams profile adds this package while retaining the standard subagent delegation and control tools. |
| `@deepseek-ai/dsh-experimental-tool-agent-team` | `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update`, `wait_agent` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `ctx.subagents`, `an exact live Team member Agent` | `tool/call`, `team/task`, `tool/result` | - | Five task-board and wait tools are scoped to the Lead and its live subagents. The optional Agent Teams profile adds this package while retaining the standard subagent delegation and control tools. |
| `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`, `owning Agent session` | `tool/call`, `todo/write`, `tool/result` | - | todo_write is session-owned state; UIs render the latest todo/write event as a checklist. `allowParallelInProgress` is required with no default, so the catalog states its choice: `true`, whose description invites several `in_progress` items. A deployment choosing `false` receives the same tool with a description asking for exactly one active task. |
| `@deepseek-ai/dsh-tool-working-directory` | `working_directory` | `ctx.tools`, `ctx.workingDirectory` | `tool/call`, `working-directory/change`, `user context for directory changes`, `tool/result` | - | Reads or changes the calling Session directory. Existing shells and processes retain their own directories; origin metadata and permission roots stay fixed. |
| `@deepseek-ai/dsh-experimental-tool-worktree` | `create_worktree` | `ctx.tools`, `ctx.worktrees`, `a calling Agent` | `tool/call`, `Git branch and checkout`, `working-directory/change`, `tool/result` | - | Explicit experimental composition only. Creates a new branch and checkout from a pinned local commit under existing write permissions, then changes the Session directory. Leaving retains the branch and checkout. |
@@ -2090,12 +2090,16 @@ Source: [`packages/subagent/tool-subagent/src/list-models.ts`](../packages/subag
### `subagent`
Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.
Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its name. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.
```json
{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Unique name across this agent tree. Start with a lower-case letter; use only lower-case letters, digits, underscores, or hyphens. lead and unowned are reserved. Names remain taken after agents finish."
},
"cwd": {
"type": "string",
"description": "Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent."
@@ -2110,6 +2114,7 @@ Delegate a self-contained task to a subagent (a separate agent that works in its
}
},
"required": [
"name",
"description",
"prompt"
]
@@ -2118,7 +2123,7 @@ Delegate a self-contained task to a subagent (a separate agent that works in its
Source: [`packages/subagent/tool-subagent/src/index.ts`](../packages/subagent/tool-subagent/src/index.ts)
The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion available in an enabled Session. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` remains fixed-route. Each instance selects its backend through `provider` and reads model-selection settings through `modelSelectionSettings`. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically.
The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion available in an enabled Session. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` remains fixed-route. Each instance selects its backend through `provider` and reads model-selection settings through `modelSelectionSettings`. Every delegation returns a unique agent name promptly; local children accept follow-ups, while external backends report their single execution automatically.
<a id="deepseek-aidsh-tool-subagent-control"></a>
@@ -2132,13 +2137,13 @@ Ask a subagent to stop its current work. This call returns without waiting for i
{
"type": "object",
"properties": {
"agent_id": {
"name": {
"type": "string",
"description": "The id of an agent created under you: your direct child or a deeper descendant."
"description": "The name of an agent created under you: your direct child or a deeper descendant."
}
},
"required": [
"agent_id"
"name"
]
}
```
@@ -2147,7 +2152,7 @@ Source: [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/sub
### `list_agents`
List all your reachable continuable descendants and, when you are a continuable subagent, your parent and continuable siblings. Rows carry ids, descriptions, and status; descendant rows also carry parent ids and depth. You can message your parent, continuable siblings, and direct children. Deeper descendants accept only interrupt_agent. running means it is working; inactive means it is not currently working. You will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation.
List all your reachable continuable descendants and, when you are a continuable subagent, your parent and continuable siblings. Rows carry names, descriptions, and status; descendant rows also carry parent names and depth. You can message your parent, continuable siblings, and direct children. Deeper descendants accept only interrupt_agent. running means it is working; inactive means it is not currently working. You will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation.
```json
{
@@ -2166,9 +2171,9 @@ Send a message to an agent. A working agent receives it at its next step; an idl
{
"type": "object",
"properties": {
"agent_id": {
"name": {
"type": "string",
"description": "The agent id of your direct continuable child, or your direct parent or continuable sibling when you are a resident continuable child."
"description": "The name of your direct continuable child, or your direct parent or continuable sibling when you are a resident continuable child."
},
"message": {
"type": "string",
@@ -2176,7 +2181,7 @@ Send a message to an agent. A working agent receives it at its next step; an idl
}
},
"required": [
"agent_id",
"name",
"message"
]
}
@@ -2343,7 +2348,7 @@ List shared tasks, including readiness, owner, revision, blockers, and write-sco
},
"owner": {
"type": "string",
"description": "Optional exact ownerId filter; use unowned for tasks without an owner."
"description": "Optional exact ownerName filter; use unowned for tasks without an owner."
},
"ready": {
"type": "boolean",
@@ -2417,7 +2422,7 @@ Compare-and-set a shared task action using the latest revision from team_task_ge
},
"owner": {
"type": "string",
"description": "Agent id of the Lead or one of its direct continuable children for Lead-only reassign; omit to unassign."
"description": "Agent name of the Lead or one of its direct continuable children for Lead-only reassign; omit to unassign."
}
},
"required": [
+19 -14
View File
@@ -42,10 +42,10 @@
| `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`、`ctx.workflowEngine`、`ctx.subagents`、`ctx.systemPrompt`、`a calling Agent (exec.agent parents every fresh round)` | `tool/call`、`tool/result`、`workflow and child session events during execution` | - | 固定的前台工作流会在每个 Round 启动一个全新的结构化子级;模型只能选择不可变目标和可选的 Round 上限。 |
| `@deepseek-ai/dsh-tool-skill` | `skill` | `ctx.workingDirectory`、`ctx.tools`、`ctx.agents`、`ctx.skills` | `tool/call`、`tool/result`、`user/message replacement catalogs via agent.inject()` | - | - |
| `@deepseek-ai/dsh-tool-session-query` | `session_event_read`、`session_event_search`、`session_event_trace`、`session_search`、`session_trace` | `ctx.tools`、`ctx.systemPrompt`、`ctx.sessionQuery`、`a calling Agent for workspace authority` | `tool/call`、`tool/result` | - | 这 5 个只读工具会隐藏提供方游标,并根据不可变的调用 agent 会话为每个结果授权。该包需要选择启用;需要强制截止时间或限制行内输出的组合还会挂载通用超时或 spill 策略。 |
| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述默认 schema 关闭模型选择,而发现 schema 则展示为已启用 Session 中可用的固定配套工具。Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;`subagent_fork` 始终使用固定路由。每个实例通过 `provider` 选择后端,通过 `modelSelectionSettings` 读取模型选择设置。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 |
| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述默认 schema 关闭模型选择,而发现 schema 则展示为已启用 Session 中可用的固定配套工具。Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;`subagent_fork` 始终使用固定路由。每个实例通过 `provider` 选择后端,通过 `modelSelectionSettings` 读取模型选择设置。每次委派都会立即返回唯一 agent 名字;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 |
| `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`、`list_agents`、`send_message` | `ctx.tools`、`ctx.subagents`、`ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`、`tool/result`、`child session events through ctx.subagents` | - | 这些是用于发现、中断托管 subagent 和向本地子代理发送消息的全局命名工具:绑定提供方的 `tool-subagent` 实例注册不同的委派工具;本包注册一次 `send_message` 和 `interrupt_agent`,另由 `list_agents` 通过单独加载的 `/list-agents` 插件提供,其目录行使用 sessionProjections 和实时 Agent 注册表。 |
| `@deepseek-ai/dsh-tool-jobs` | `job_kill`、`job_list`、`job_output` | `ctx.tools`、`ctx.jobs`、`ctx.systemPrompt` | `tool/call`、`tool/result`、`user/message via agent.inject() for background completion notices` | - | 与任务种类无关的后台任务控制器:后台 bash 命令与 PTY 发送 都通过相同的 3 个工具读取、列出和终止。加载该插件会挂接控制器,从而启用生产方的 `ctx.jobs.start()`。 |
| `@deepseek-ai/dsh-experimental-tool-agent-team` | `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update`, `wait_agent` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `an exact live Team member Agent` | `tool/call`, `team/task`, `tool/result` | - | 五个任务板和等待工具作用于 Lead 及其在线子代理。可选 Agent Teams profile 增加本包,同时保留标准子代理委派和控制工具。 |
| `@deepseek-ai/dsh-experimental-tool-agent-team` | `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update`, `wait_agent` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `ctx.subagents`, `an exact live Team member Agent` | `tool/call`, `team/task`, `tool/result` | - | 五个任务板和等待工具作用于 Lead 及其在线子代理。可选 Agent Teams profile 增加本包,同时保留标准子代理委派和控制工具。 |
| `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`、`owning Agent session` | `tool/call`、`todo/write`、`tool/result` | - | todo_write 是会话所有的状态;UI 将最新的 todo/write 事件渲染为检查清单。`allowParallelInProgress` 是没有默认值的必填项,因此本目录明确选择 `true`,对应描述允许同时存在多个 `in_progress` 项。选择 `false` 的部署会获得同一工具,但描述会要求只能有 1 个活动任务。 |
| `@deepseek-ai/dsh-tool-working-directory` | `working_directory` | `ctx.tools`, `ctx.workingDirectory` | `tool/call`, `working-directory/change`, `user context for directory changes`, `tool/result` | - | 读取或改变调用方 Session 的目录。已有 shell 和进程保留自己的目录;原始元数据和权限根目录保持不变。 |
| `@deepseek-ai/dsh-experimental-tool-worktree` | `create_worktree` | `ctx.tools`, `ctx.worktrees`, `a calling Agent` | `tool/call`, `Git branch and checkout`, `working-directory/change`, `tool/result` | - | 仅通过显式实验组合加载。在现有写权限下从固定的本地提交创建新分支与检出目录,再改变 Session 工作目录。离开时保留分支与检出目录。 |
@@ -2096,12 +2096,16 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
### `subagent`
将一项自包含任务委派给 subagent(在自身上下文中工作的独立 agent),用它卸载聚焦且独立的工作,例如研究、限定范围的实现或分析,以免消耗当前对话的上下文。subagent 会返回结果,但不会返回中间步骤。请提供完整、独立的提示词,因为它看不到当前对话。此工具启动独立管理的 subagent 并立即返回其 id。运行时会在任务结束时通知你。子代理通过 `send_message` 汇报结果;你可以使用 `send_message` 在运行时引导它,或在结束后继续对话。
将一项自包含任务委派给 subagent(在自身上下文中工作的独立 agent),用它卸载聚焦且独立的工作,例如研究、限定范围的实现或分析,以免消耗当前对话的上下文。subagent 会返回结果,但不会返回中间步骤。请提供完整、独立的提示词,因为它看不到当前对话。此工具启动独立管理的 subagent 并立即返回其名字。运行时会在任务结束时通知你。子代理通过 `send_message` 汇报结果;你可以使用 `send_message` 在运行时引导它,或在结束后继续对话。
```json
{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Unique name across this agent tree. Start with a lower-case letter; use only lower-case letters, digits, underscores, or hyphens. lead and unowned are reserved. Names remain taken after agents finish."
},
"cwd": {
"type": "string",
"description": "Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent."
@@ -2116,6 +2120,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
}
},
"required": [
"name",
"description",
"prompt"
]
@@ -2124,7 +2129,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
来源:[`packages/subagent/tool-subagent/src/index.ts`](../packages/subagent/tool-subagent/src/index.ts)
注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述默认 schema 关闭模型选择,而发现 schema 则展示为已启用 Session 中可用的固定配套工具。Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;`subagent_fork` 始终使用固定路由。每个实例通过 `provider` 选择后端,通过 `modelSelectionSettings` 读取模型选择设置。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。
注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述默认 schema 关闭模型选择,而发现 schema 则展示为已启用 Session 中可用的固定配套工具。Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;`subagent_fork` 始终使用固定路由。每个实例通过 `provider` 选择后端,通过 `modelSelectionSettings` 读取模型选择设置。每次委派都会立即返回唯一 agent 名字;本地子代理接受后续消息,外部后端自动汇报单次执行结果。
<a id="deepseek-aidsh-tool-subagent-control"></a>
@@ -2132,19 +2137,19 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
### `interrupt_agent`
根据 agent id 请求取消后台 agent 的当前轮次。目标可以是你的直接子级,也可以是在你下方创建的更深层 agent。对于本地 agent,只有当前轮次会停止:已经排队发给该 agent 的消息会一直搁置到后续的 send_message;它启动的 agent 会继续运行;该 agent 本身仍可接受后续操作。外部执行会永久停止,无法接收后续消息。停止请求被接受后,此调用立即返回,因此目标可能还会短暂运行;中断一个已经完成的 agent 是可接受的空操作。
根据 agent 名字请求取消后台 agent 的当前轮次。目标可以是你的直接子级,也可以是在你下方创建的更深层 agent。对于本地 agent,只有当前轮次会停止:已经排队发给该 agent 的消息会一直搁置到后续的 send_message;它启动的 agent 会继续运行;该 agent 本身仍可接受后续操作。外部执行会永久停止,无法接收后续消息。停止请求被接受后,此调用立即返回,因此目标可能还会短暂运行;中断一个已经完成的 agent 是可接受的空操作。
```json
{
"type": "object",
"properties": {
"agent_id": {
"name": {
"type": "string",
"description": "The id of an agent created under you: your direct child or a deeper descendant."
"description": "The name of an agent created under you: your direct child or a deeper descendant."
}
},
"required": [
"agent_id"
"name"
]
}
```
@@ -2153,7 +2158,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
### `list_agents`
列出全部可达的可继续后代;如果调用方是可继续子代理,还列出父代理和可继续同级。行包含 id、描述和状态;后代行还包含父级 id 和深度。可以向父代理、可继续同级和直接子级发送消息,更深后代只接受 interrupt_agent。running 表示正在工作;inactive 表示当前没有工作。直属子代理结束时会收到通知,无需反复查询其状态。使用 send_message 继续对话。
列出全部可达的可继续后代;如果调用方是可继续子代理,还列出父代理和可继续同级。行包含名字、描述和状态;后代行还包含父级名字和深度。可以向父代理、可继续同级和直接子级发送消息,更深后代只接受 interrupt_agent。running 表示正在工作;inactive 表示当前没有工作。直属子代理结束时会收到通知,无需反复查询其状态。使用 send_message 继续对话。
```json
{
@@ -2172,9 +2177,9 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
{
"type": "object",
"properties": {
"agent_id": {
"name": {
"type": "string",
"description": "The agent id of your direct continuable child, or your direct parent or continuable sibling when you are a resident continuable child."
"description": "The name of your direct continuable child, or your direct parent or continuable sibling when you are a resident continuable child."
},
"message": {
"type": "string",
@@ -2182,7 +2187,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
}
},
"required": [
"agent_id",
"name",
"message"
]
}
@@ -2349,7 +2354,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
},
"owner": {
"type": "string",
"description": "Optional exact ownerId filter; use unowned for tasks without an owner."
"description": "Optional exact ownerName filter; use unowned for tasks without an owner."
},
"ready": {
"type": "boolean",
@@ -2423,7 +2428,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
},
"owner": {
"type": "string",
"description": "Agent id of the Lead or one of its direct continuable children for Lead-only reassign; omit to unassign."
"description": "Agent name of the Lead or one of its direct continuable children for Lead-only reassign; omit to unassign."
}
},
"required": [
@@ -3,14 +3,14 @@
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.4/agent-team-subagents/guide.md
/:
en: 1f073189776760d8
zh: cfbb0403aea96f84
/agent-teams-uses-subagents-and-session-ids:
en: a6aba40e910d4ecb
zh: 11881b6e9d8228d9
/agent-teams-uses-subagents-and-session-ids/change:
en: 208add38ac8dc327
zh: c34fce9b2a978e5f
/agent-teams-uses-subagents-and-session-ids/migration:
en: 0ed5e22412074192
zh: 60deae7d46a21c75
en: 9a266a4f19673f89
zh: 0b716c060f3e568d
/agent-teams-uses-subagents-and-unique-agent-names:
en: e2fd55138832d6e1
zh: 5fc33b77673c6ea0
/agent-teams-uses-subagents-and-unique-agent-names/change:
en: 955ec629b091e254
zh: 219985a87010acca
/agent-teams-uses-subagents-and-unique-agent-names/migration:
en: b971e8801c9b5590
zh: a3656d78aad1a5c1
@@ -1,27 +1,27 @@
---
kind: upgrade-guide
description: "Agent Teams uses the standard subagent tools and Session ids; Team-specific creation, messaging, listing, interruption, and their configuration keys are removed."
description: "Agent Teams uses the standard subagent tools and unique agent names; Team-specific creation, messaging, listing, interruption, and their configuration keys are removed."
---
# Agent Teams uses subagents and Session ids
# Agent Teams uses subagents and unique agent names
English | [中文](guide.zh.md)
## Change
The experimental Agent Teams plugins share agent creation, discovery, messaging, and interruption with subagents. Start a teammate with `subagent` or `subagent_fork`, describe its responsibility with `description`, and use its returned agent id for messages and task assignment. Descriptions may repeat and never identify a recipient.
The experimental Agent Teams plugins share agent creation, discovery, messaging, and interruption with subagents. Start a teammate with `subagent` or `subagent_fork`, give it a tree-wide unique `name`, describe its responsibility with `description`, and use its returned name for messages and task assignment. Descriptions may repeat and never identify a recipient.
- `spawn_teammate` and the Team-scoped `send_message`, `list_agents`, and `interrupt_agent` are removed. The standard control tools use `agent_id`, not `target`. Continuable children of one live parent can message each other by id.
- `spawn_teammate` and the Team-scoped `send_message`, `list_agents`, and `interrupt_agent` are removed. The standard control tools use `name`, not `agent_id` or `target`. Continuable children of one live parent can message each other by name.
- `list_agents` takes no arguments and always lists all reachable continuable descendants, plus a continuable caller’s live parent and siblings. Remove the `scope` argument from scripted calls. Listing deeper descendants does not grant messaging or task-assignment authority.
- The Team profile no longer disables the subagent tools. Team adds only task tools, `wait_agent`, and collaboration guidance.
- `agent-team` rejects `maxMembers`, `maxMessageBytes`, and `disposalTimeoutMs`; `tool-agent-team` rejects `interrupt`, `freshProvider`, and `forkProvider`.
- `ctx.agentTeams` removes `spawnTeammate()`, `sendMessage()`, `interrupt()`, and `listMembers()`. Task views expose `ownerId` instead of `ownerName`; task ownership uses Session ids, including the Lead's id.
- Historical Team roster and mailbox records remain readable. Names in those records are historical data and do not resolve message recipients or task owners. Subagent catalog payloads are unchanged.
- `ctx.agentTeams` removes `spawnTeammate()`, `sendMessage()`, `interrupt()`, and `listMembers()`. Host task views retain `ownerId` Session identities; model-facing task results expose `ownerName`, and task `owner` arguments use names. The root agent is always `lead`.
- Historical Team roster and mailbox records remain readable. Names in those records are historical data and do not resolve message recipients or task owners. Subagent catalog payloads are unchanged. New names are recorded in the root Session and remain reserved after settlement; older unnamed children receive `legacy:<session-id>` addresses during discovery.
## Migration
1. Remove the obsolete configuration keys and any patch rows that disable subagent tools solely for Team support.
2. Replace `spawn_teammate({ name, description, prompt, context })` with `subagent({ description, prompt })`, or `subagent_fork` for inherited conversation history. Save the returned agent id.
3. Use `send_message({ agent_id, message })` and `interrupt_agent({ agent_id })`. Discover parent and sibling ids with `list_agents`.
4. Pass member Session ids to task `owner` fields and read `ownerId` in results. Use the Lead's Session id instead of the alias `lead`.
5. Replace removed service calls with `ctx.subagents.startActivation()`, `sendMessage()`, `interrupt()`, and `listChildren()`. Start a Team-enabled profile and verify that a child's description appears in the Team panel and that an id-addressed message reaches it.
2. Replace `spawn_teammate({ name, description, prompt, context })` with `subagent({ name, description, prompt })`, or `subagent_fork` for inherited conversation history. Choose a unique lower-case name with letters, digits, underscores, or hyphens; start with a letter. `lead` and `unowned` are reserved. Duplicate names fail before creation.
3. Use `send_message({ name, message })` and `interrupt_agent({ name })`. Discover parent and sibling names with `list_agents`.
4. Pass member names to task `owner` fields and read `ownerName` in tool results. Use `lead` for the root agent.
5. Replace removed service calls with `ctx.subagents.startActivation()`, `sendMessage()`, `interrupt()`, and `listChildren()`. Supply `name` to `startActivation()`; host control APIs still use Session ids. Start a Team-enabled profile and verify that a child's description appears in the Team panel and that a name-addressed message reaches it.
@@ -1,27 +1,27 @@
---
kind: upgrade-guide
description: "Agent Teams 使用标准 subagent 工具和 Session id;移除 Team 专属创建、消息、列举、中断操作及其配置键。"
description: "Agent Teams 使用标准 subagent 工具与唯一 agent 名字;Team 专用创建、消息、列表、中断工具及其配置键被移除。"
---
# Agent Teams 使用子代理和 Session id
# Agent Teams 使用 subagent 与唯一 agent 名字
[English](guide.md) | 中文
## 变更
实验性 Agent Teams 插件与 subagent 共用代理创建、发现、消息和中断能力。使用 `subagent` 或 `subagent_fork` 创建队友,以 `description` 描述职责,并用返回的 agent id 发送消息和分配任务。描述可以重复,不能作为收件人标识。
实验性 Agent Teams 插件与 subagent 共享创建、发现、消息与中断。使用 `subagent` 或 `subagent_fork` 启动队友,为其指定树内唯一的 `name`,用 `description` 描述职责,并以返回的名字发送消息和分配任务。描述可以重复,不标识接收方。
- 移除 `spawn_teammate` 以及 Team 作用域的 `send_message`、`list_agents` 和 `interrupt_agent`。标准控制工具使用 `agent_id` 而非 `target`。同一个在线父代理的可继续子代理可以通过 id 互发消息。
- `list_agents` 不接收参数,始终列出全部可达的可继续后代,以及可继续调用方的在线父级和同级。从脚本调用中移除 `scope` 参数。列出更深后代不会授予消息或任务指派权限。
- Team profile 不再禁用 subagent 工具。Team 只增加任务工具、`wait_agent` 和协作提示。
- `agent-team` 拒绝 `maxMembers`、`maxMessageBytes` 和 `disposalTimeoutMs`;`tool-agent-team` 拒绝 `interrupt`、`freshProvider` 和 `forkProvider`。
- `ctx.agentTeams` 移除 `spawnTeammate()`、`sendMessage()`、`interrupt()` 和 `listMembers()`。任务视图以 `ownerId` 取代 `ownerName`;包括 Lead 在内,任务所有者均使用 Session id。
- 历史 Team 名册和邮箱记录仍可读取。其中的名字仅作为历史数据,不用于解析消息收件人或任务所有者。Subagent catalog 载荷不变。
- `spawn_teammate` 及 Team 作用域的 `send_message`、`list_agents`、`interrupt_agent` 被移除。标准控制工具使用 `name`,而非 `agent_id` 或 `target`。同一在线父级的可继续子级可以按名字互发消息。
- `list_agents` 不接受参数,始终列出所有可达的可继续后代,以及可继续调用方的在线父级和同级。脚本调用需移除 `scope` 参数。列出更深后代不授予消息或任务分配权限。
- Team profile 不再禁用 subagent 工具。Team 只增加任务工具、`wait_agent` 和协作指引。
- `agent-team` 拒绝 `maxMembers`、`maxMessageBytes` 与 `disposalTimeoutMs`;`tool-agent-team` 拒绝 `interrupt`、`freshProvider` 与 `forkProvider`。
- `ctx.agentTeams` 移除 `spawnTeammate()`、`sendMessage()`、`interrupt()` 与 `listMembers()`。宿主任务视图保留 `ownerId` Session 身份;面向模型的任务结果公开 `ownerName`,任务 `owner` 参数使用名字。根 agent 始终名为 `lead`。
- 历史 Team 成员列表和邮箱记录仍可读取。其中的名字属于历史数据,不解析消息接收方或任务负责人。subagent 目录载荷不变。新名字记录在根 Session 中,执行结束后仍被占用;旧的无名子级在发现时获得 `legacy:<session-id>` 地址。
## 迁移
1. 删除废弃配置键,以及仅为支持 Team 而禁用 subagent 工具的 patch 行。
2. 将 `spawn_teammate({ name, description, prompt, context })` 替换为 `subagent({ description, prompt })`;继承对话历史时使用 `subagent_fork`。保存返回的 agent id。
3. 使用 `send_message({ agent_id, message })` 和 `interrupt_agent({ agent_id })`。通过 `list_agents` 查询父代理和兄弟代理的 id。
4. 任务的 `owner` 字段传入成员 Session id,结果读取 `ownerId`。以 Lead 的 Session id 取代 `lead` 别名。
5. 将已移除的服务调用替换为 `ctx.subagents.startActivation()`、`sendMessage()`、`interrupt()` 和 `listChildren()`。启动启用 Team 的 profile,确认面板显示子代理描述,且通过 id 发送的消息到达该子代理。
1. 移除废弃配置键,以及仅为 Team 支持而禁用 subagent 工具的 patch 行。
2. 将 `spawn_teammate({ name, description, prompt, context })` 改为 `subagent({ name, description, prompt })`,需要继承会话历史时使用 `subagent_fork`。选择唯一的小写名字,只含字母、数字、下划线或连字符,并以字母开头。`lead` 和 `unowned` 为保留名。重名在创建前失败。
3. 使用 `send_message({ name, message })` 与 `interrupt_agent({ name })`。通过 `list_agents` 发现父级和同级名字。
4. 向任务 `owner` 字段传成员名字,读取工具结果中的 `ownerName`。根 agent 使用 `lead`。
5. 将移除的服务调用改为 `ctx.subagents.startActivation()`、`sendMessage()`、`interrupt()` 与 `listChildren()`。向 `startActivation()` 提供 `name`;宿主控制 API 仍使用 Session id。启动启用 Team 的 profile,确认子级描述出现在 Team 面板,并且按名字发送的消息能够到达。
+2 -1
View File
@@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
import { mountWorkingDirectoryFixture } from '../../../subagent/subagent/tests/working-directory-fixture.ts'
import { createUserMessage, type StreamChunk } from '@deepseek-ai/dsh-llm'
import type { ContextFormed } from '@deepseek-ai/dsh-llm'
@@ -47,7 +48,7 @@ async function installDelegationTool(ctx: BridgeHarness['ctx']): Promise<() => b
name: 'delegate_again', description: 'Run a later child.', parameters: {},
execute: async (_args, exec) => {
if (exec.agent === undefined) throw new Error('delegation fixture requires an Agent')
const child = await ctx.subagents.startActivation({
const child = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
provider: 'local-test', label: 'later child', delivery: 'caller', signal: exec.signal,
request: { parent: exec.agent, prompt: [{ type: 'text', text: 'later child task' }] },
})
@@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
/** Direct one-shot Agent driving, exact Session adoption, machine-readable projection, and exit mapping. */
import { Readable } from 'node:stream'
@@ -242,7 +243,7 @@ describe('headless runner', () => {
async afterPrompt(session, message, parent) {
turns += 1
if (turns === 1) {
await test.ctx.subagents.startActivation({
await test.ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
provider: 'external', label: 'work', delivery: 'parent', signal: new AbortController().signal,
request: { parent, prompt: [] },
})
+2 -2
View File
@@ -15,8 +15,8 @@
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
/deepseek-ai-dsh-client-ui-tool/use-this-package:
en: c6d4e666191521b2
zh: 2498020e7080e172
en: 34321c376896ba9c
zh: b847f7a33458caf0
/deepseek-ai-dsh-client-ui-tool/use-this-package/registering-a-business-tool-view:
en: 2e3c306b13d3bc25
zh: 77127f01763b3976
+1 -1
View File
@@ -25,7 +25,7 @@ English | [中文](README.zh.md)
<a id="use-this-package"></a>
## Use this package
Tool calls appear in the conversation as cards: a root call tree with its nested subcalls, each atomic call rendered by its owning view. Every lifecycle state retains the tool's ordinary business glyph; failure and interruption remain explicit through the frozen call/result state, accessible status text, and failure summary. Users can open files or inspect calls through the Host callbacks. A collapsed `web_fetch` row links its http(s) URL, which opens in a new browser tab.
Tool calls appear in the conversation as cards: a root call tree with its nested subcalls, each atomic call rendered by its owning view. Every lifecycle state retains the tool's ordinary business glyph; failure and interruption remain explicit through the frozen call/result state, accessible status text, and failure summary. Users can open files or inspect calls through the Host callbacks. A collapsed `web_fetch` row links its http(s) URL, which opens in a new browser tab. Control receipts display agent `name` arguments; historical `agent_id` recordings remain readable.
Settled foreground commands with a nonzero exit code, a terminating signal, or an explicit missing exit code keep their description in the ordinary gray text and show a neutral capsule with a red dot immediately after it. The capsule displays only the recorded code, signal, or localized missing-code status and stays on the same line; the description truncates with an ellipsis to reserve its space. Running, successful, and background calls omit it. Generic execution errors and stopped summaries retain their error and warning colors, including on hover.
+1 -1
View File
@@ -25,7 +25,7 @@ kind: "package-reference"
<a id="use-this-package"></a>
## 使用本包
工具调用在对话中显示为卡片:一个根调用树带其嵌套子调用,每个原子调用由所属视图渲染。所有生命周期状态都保留工具的普通业务图标;失败与中断仍通过冻结调用/结果状态、无障碍状态文本和失败摘要明确表达。用户可通过宿主回调打开文件或检查调用。折叠的 `web_fetch` 行把其 http(s) URL 显示为链接,在新浏览器标签页中打开。
工具调用在对话中显示为卡片:一个根调用树带其嵌套子调用,每个原子调用由所属视图渲染。所有生命周期状态都保留工具的普通业务图标;失败与中断仍通过冻结调用/结果状态、无障碍状态文本和失败摘要明确表达。用户可通过宿主回调打开文件或检查调用。折叠的 `web_fetch` 行把其 http(s) URL 显示为链接,在新浏览器标签页中打开。 控制工具回执显示 agent 的 `name` 参数;历史 `agent_id` 记录仍可读取。
已结束的前台命令若返回非零退出码、终止信号,或明确记录为无退出码,描述保持普通灰色,后面紧接带红点的中性胶囊。胶囊仅显示记录的退出码、信号或本地化的“未正常退出”状态,并保持在同一行;描述超出可用宽度时显示省略号,为胶囊预留空间。运行中、成功和后台调用不显示胶囊。通用执行错误与停止摘要仍保留错误色和警告色,悬停时也不改变。
@@ -109,7 +109,7 @@ function lspDetails(args: Record<string, unknown>, text: string, t: DetailTransl
export function controlDetails(
name: string, args: Record<string, unknown>, text: string, json: unknown, t: DetailTranslate,
): ToolDetailsModel | null {
const target = arg(args, 'target') || arg(args, 'agent_id') || arg(args, 'sessionId') || arg(args, 'job_id')
const target = arg(args, 'name') || arg(args, 'target') || arg(args, 'agent_id') || arg(args, 'sessionId') || arg(args, 'job_id')
switch (name) {
case 'list_agents': return agentList(text, json, t)
case 'job_list': return jobList(text, t)
@@ -89,7 +89,7 @@ describe('control detail adapters', () => {
expect(details('team_task_create', '{"id":"task-1","subject":"Review","status":"pending"}')?.summary).toBe('Review')
expect(details('team_task_list', '{"tasks":[{"id":"task-1","subject":"Review"}],"nextCursor":3}')?.caption).toBe('More tasks available; next cursor is 3')
expect(details('send_message', '{"status":"queued"}', { agent_id: 'a1', message: 'hello' })?.items[0]?.badge?.label).toBe('Queued')
expect(details('send_message', 'message delivered to agent a1', { agent_id: 'a1', message: 'hello' })?.summary).toBe('a1 · Message delivered')
expect(details('send_message', 'message delivered to agent reviewer', { name: 'reviewer', message: 'hello' })?.summary).toBe('reviewer · Message delivered')
expect(details('interrupt_agent', '{"previousStatus":"running"}', { agent_id: 'a1' })?.items[0]?.fields).toContainEqual({ label: 'Previous status', value: 'Running' })
expect(details('wait_agent', '{"timedOut":true}')?.summary).toBe('Subagent activity · Wait timed out')
expect(details('wait_agent', '{"timedOut":false,"noProgress":{"message":"No peer"}}')?.items[0]?.description).toBe('No peer')
@@ -110,7 +110,7 @@ describe('control detail adapters', () => {
expect(details('unknown', 'x')).toBeNull()
expect(details('team_task_list', '{"tasks":[],"nextCursor":"bad"}')).toBeNull()
expect(details('terminal_read', 'bad', { sessionId: 'pty-1' })).toBeNull()
expect(details('interrupt_agent', 'interrupt requested for agent a1', { agent_id: 'a1' })?.summary).toContain('Interrupt')
expect(details('interrupt_agent', 'interrupt requested for agent reviewer', { name: 'reviewer' })?.summary).toContain('Interrupt')
expect(details('interrupt_agent', 'unexpected', { agent_id: 'a1' })).toBeNull()
expect(details('job_kill', 'unexpected', { job_id: 'j1' })).toBeNull()
expect(details('terminal_close', 'unexpected', { sessionId: 'pty-1' })).toBeNull()
@@ -62,6 +62,7 @@ export const KNOWN_SESSION_EVENT_TYPES: ReadonlySet<string> = new Set([
'subagent/catalog',
'subagent/descriptor',
'subagent/model-selection-policy',
'subagent/named',
'system/message',
'team/member',
'team/message/delivered',
@@ -10,7 +10,7 @@
zh: 59e3d49c3d63baf9
/deepseek-ai-dsh-experimental-agent-team/summary:
en: 829b0c91f9659037
zh: 58031bda875765e5
zh: 18971a134a874bce
/deepseek-ai-dsh-experimental-agent-team/table-of-contents:
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
@@ -24,14 +24,14 @@
en: 0eab809f5f1af167
zh: 21a5960927603bf2
/deepseek-ai-dsh-experimental-agent-team/use-this-package/teammates:
en: 6946c12e1482c65c
zh: 9320041928c49519
en: 77cbb59eee00c536
zh: 1cc55a924392b4dd
/deepseek-ai-dsh-experimental-agent-team/use-this-package/messages-between-teammates:
en: 3bcd3a00b3d9de9b
zh: 00a404f4cf033a16
en: 301a1b1d29dd7323
zh: 6936fb87e017d701
/deepseek-ai-dsh-experimental-agent-team/use-this-package/shared-task-board:
en: 21623c0bb323b8bd
zh: 13012bad307d26f3
en: a365f36c0aa710a5
zh: af7b2b71add4319b
/deepseek-ai-dsh-experimental-agent-team/use-this-package/waiting-and-interruption:
en: 494f4d031fc69dec
zh: ebaf87dbca8c4346
@@ -81,8 +81,8 @@
en: 8b29cb821f80db40
zh: 43e402736a553ce5
/deepseek-ai-dsh-experimental-agent-team/model-experience/task-views-and-errors/what-the-model-sees:
en: 2353fe861b9fa710
zh: 34a92639c01f57f5
en: 8aabdf12a2b74f7e
zh: bf786d251f9702f1
/deepseek-ai-dsh-experimental-agent-team/model-experience/task-views-and-errors/token-effect:
en: d5f77a18bfca1051
zh: b988301e31491408
+4 -4
View File
@@ -53,19 +53,19 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
### Teammates
Ask the Lead to start a subagent with a `description` of its work: `subagent` starts fresh, while `subagent_fork` inherits completed turns. The returned Session id addresses the teammate; descriptions are display text and may repeat.
Ask the Lead to start a subagent with a unique `name` and a `description` of its work: `subagent` starts fresh, while `subagent_fork` inherits completed turns. The returned name addresses the teammate; descriptions may repeat.
Every root agent leads its own Team, and every live subagent below it, at any depth, receives Team tools. The roster lists the Lead, its direct continuable children, and historical `team/member` records. Deeper descendants work on the same board under their Session ids but have no roster row.
### Messages between teammates
Members use standard `send_message` with Session ids: the Lead reaches direct children, and resident continuable children reach their parent and continuable siblings. Steer delivers into the target inbox and can cold-resume children or siblings. Team stores no messages; accepted messages follow ordinary Agent persistence.
Members use standard `send_message` with agent names: the Lead reaches direct children, and resident continuable children reach their parent and continuable siblings. Steer delivers into the target inbox and can cold-resume children or siblings. Team stores no messages; accepted messages follow ordinary Agent persistence.
### Shared task board
Any member can add a task with a title, details, optional dependencies on other tasks, and optional hints about which files it will touch. A task is claimable only when everything it depends on is complete.
A member claims a task to start work, completes it, releases it, or reopens it. The Lead can reassign a task by Session id to itself, a direct continuable child, or an active historical member. Other members claim tasks themselves. Every change is compare-and-set, so stale updates cannot overwrite newer work.
A member claims a task to start work, completes it, releases it, or reopens it. The Lead can reassign a task by agent name to itself, a direct continuable child, or an active historical member. Other members claim tasks themselves. Every change is compare-and-set, so stale updates cannot overwrite newer work.
File hints produce warnings when two in-progress tasks plan to touch overlapping paths — they never block anything. Deleted tasks remain in history but disappear from the active list.
@@ -173,7 +173,7 @@ Read these pages when the package-level contract is not enough. They move from t
#### What the model sees
The model sees Team state through task-tool results; coordination events never enter the conversation. Task views identify their owner through `ownerId`. Failures surface as tool errors with the `TeamError` message, including the current revision for stale updates.
The model sees Team state through task-tool results; coordination events never enter the conversation. Model-facing task views identify their owner through `ownerName`; host views retain `ownerId`. Failures surface as tool errors with the `TeamError` message, including the current revision for stale updates.
#### Token effect
@@ -9,7 +9,7 @@ kind: "package-reference"
## 概述
`dsh-experimental-agent-team` 将代理及其子代理组成共享持久任务板的 Team。标准 subagent 工具负责创建队友并按 id 发消息。Team 增加比较并设置的任务所有权、进度等待和 Web 面板成员列表。模型工具由 `dsh-experimental-tool-agent-team` 提供。该包为实验性功能,成员共享同一工作树。
`dsh-experimental-agent-team` 将代理及其子代理组成共享持久任务板的 Team。标准 subagent 工具负责创建队友并按名字发消息。Team 增加比较并设置的任务所有权、进度等待和 Web 面板成员列表。模型工具由 `dsh-experimental-tool-agent-team` 提供。该包为实验性功能,成员共享同一工作树。
## 目录
@@ -53,19 +53,19 @@ kind: "package-reference"
### Teammate
让 Lead 使用 `description` 描述工作并启动子代理:`subagent` 从全新上下文开始,`subagent_fork` 继承已完成轮次。返回的 Session id 用于寻址队友;描述用于展示,可以重复。
让 Lead 使用唯一的 `name` 和工作 `description` 启动子代理:`subagent` 从全新上下文开始,`subagent_fork` 继承已完成轮次。返回的名字用于寻址队友;描述可以重复。
每个根代理领导自己的 Team,其下任意深度的在线子代理都获得 Team 工具。成员列表包含 Lead、其直接可继续子级以及历史 `team/member` 记录。更深的后代按 Session id 使用同一任务板,但不显示为成员行。
### teammate 之间的消息
成员使用标准 `send_message` 按 Session id 发消息:Lead 联系直接子级,驻留的可继续子级联系父级和可继续同级。Steer 投递到目标收件箱,并可冷恢复子级或同级。Team 不存储消息;已接受消息遵循普通 Agent 持久化。
成员使用标准 `send_message` 按 agent 名字 发消息:Lead 联系直接子级,驻留的可继续子级联系父级和可继续同级。Steer 投递到目标收件箱,并可冷恢复子级或同级。Team 不存储消息;已接受消息遵循普通 Agent 持久化。
### 共享任务板
任何成员都可以添加任务,包含标题、详情、对其他任务的可选依赖,以及可选的文件触及提示。只有其全部依赖完成后,任务才可 claim。
成员认领任务后开始工作,可以完成、释放或重新打开任务。Lead 可按 Session id 将任务重新分配给自身、直接可继续子级或活跃的历史成员。其他成员自行认领任务。每次变更都使用比较并设置,防止过期更新覆盖新工作。
成员认领任务后开始工作,可以完成、释放或重新打开任务。Lead 可按 agent 名字 将任务重新分配给自身、直接可继续子级或活跃的历史成员。其他成员自行认领任务。每次变更都使用比较并设置,防止过期更新覆盖新工作。
当两个 in-progress 任务计划触及重叠路径时,文件提示会产生警告——它们绝不阻止任何操作。已删除任务保留在历史中,但从活动列表中消失。
@@ -173,7 +173,7 @@ Mailbox 投影与 checkpoint 准入保留本地声明的校验器之外获准内
#### 模型看到什么
模型通过任务工具结果查看 Team 状态;协调事件不进入对话。任务视图通过 `ownerId` 标识负责人。失败以携带 `TeamError` 消息的工具错误显示,过期更新错误包含当前修订。
模型通过任务工具结果查看 Team 状态;协调事件不进入对话。面向模型的任务视图通过 `ownerName` 标识负责人;宿主视图保留 `ownerId`。失败以携带 `TeamError` 消息的工具错误显示,过期更新错误包含当前修订。
#### Token 影响
@@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { mkdtempSync } from 'node:fs'
import { rm } from 'node:fs/promises'
@@ -117,7 +118,7 @@ for (const backend of backends) {
const first = await stack(backend, storageRoot, [textResponse('initial teammate answer')])
const firstLead = await first.ctx.agentLoop.create(rootId, { provider: 'mock', model: 'mock' })
const started = await first.ctx.subagents.startActivation({
const started = await first.ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
provider: 'spawn',
label: 'restart worker',
request: { prompt: [{ type: 'text', text: 'finish before restart' }], parent: firstLead },
@@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
@@ -79,7 +80,7 @@ function teamInternals(ctx: Context): TeamServiceInternals {
/** Start one teammate: a continuable child of the Lead. */
async function spawn(ctx: Context, lead: Agent, description: string | undefined, provider = 'spawn'): Promise<SessionId> {
const started = await ctx.subagents.startActivation({
const started = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
provider,
label: `${description ?? 'shared'} responsibility`,
request: { prompt: content(`${description ?? 'shared'} initial`), parent: lead },
@@ -130,7 +131,7 @@ describe('Team identity and membership', () => {
const { ctx, lead } = await setup(['hang', 'hang', 'hang'])
const named = await waitRunning(ctx, await spawn(ctx, lead, 'alpha'))
const unnamed = await waitRunning(ctx, await spawn(ctx, lead, undefined))
const grandchildId = (await ctx.subagents.startActivation({
const grandchildId = (await ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
provider: 'spawn', label: 'nested', delivery: 'caller', signal: SIGNAL,
request: { prompt: content('nested'), parent: named },
})).childId
@@ -24,8 +24,8 @@
en: 8dca5e3fa72a3bb7
zh: 51cdf2eb834dd746
/deepseek-ai-dsh-experimental-tool-agent-team/use-this-package/what-the-model-can-do:
en: de03950e2e41ba1f
zh: eeb52afda9bd2249
en: 624b472c31463fbc
zh: a066b1d9c577461e
/deepseek-ai-dsh-experimental-tool-agent-team/use-this-package/what-success-and-failure-look-like:
en: 8b6cd7c8bd1b8abc
zh: d281898bdd1db783
@@ -45,8 +45,8 @@
en: c2b29f27a74c02c0
zh: 026f2ef93dd604d8
/deepseek-ai-dsh-experimental-tool-agent-team/further-exploration:
en: c0b484e56b3a3990
zh: becaf4dc193f00d5
en: e557874da4eba7ff
zh: 2f580bea15598603
/deepseek-ai-dsh-experimental-tool-agent-team/model-experience:
en: 215e7ba838619b7b
zh: a311da3843709f90
@@ -69,8 +69,8 @@
en: ed8642015d306d6d
zh: 09d1970fb8e02ff3
/deepseek-ai-dsh-experimental-tool-agent-team/model-experience/team-identity-runtime-context/what-the-model-sees:
en: 4d17511e1d5fb4f2
zh: c98734115d753a57
en: a71a80314cfe8922
zh: 6cf10566bc0f4e4a
/deepseek-ai-dsh-experimental-tool-agent-team/model-experience/team-identity-runtime-context/token-effect:
en: f0dc7b201c8b3cf0
zh: b5f16ef4a133257c
@@ -52,7 +52,7 @@ The five tools cover two capabilities; the standard subagent tools cover the thi
- **Wait for teammates** — `wait_agent` waits for the next task, member status, or inbox change after the call starts.
- **Create and reach teammates** — `subagent`, `subagent_fork`, `send_message`, `list_agents`, and `interrupt_agent` come from the subagent tool packages and are unchanged by this package.
Task `owner` arguments and `ownerId` results use Session ids. Descriptions are display text and may repeat. Any member can use the board; only the Lead reassigns tasks. Updates enforce ownership and revision checks, rejecting stale edits.
Task `owner` arguments and `ownerName` results use agent names; the root is `lead`. Descriptions may repeat. Any member can use the board; only the Lead reassigns tasks. Updates enforce ownership and revision checks, rejecting stale edits.
### What success and failure look like
@@ -104,7 +104,7 @@ Read these pages when the package-level contract is not enough. They move from t
- [agent-team package](../agent-team/README.md) — the `ctx.agentTeams` domain service behind these tools.
- [Agent Teams subsystem](../../../docs/subsystems/agent-team.md) — durable Team types and service API.
- [Subagent control tools](../../subagent/tool-subagent-control/README.md) — messaging, listing, and interruption by Session id.
- [Subagent control tools](../../subagent/tool-subagent-control/README.md) — messaging, listing, and interruption by agent name.
- [Generated tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-experimental-tool-agent-team) — every tool schema the model receives.
- [Subagent teammates Agent Note](../../../.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md) — model-facing, scoping, and tool-family decisions.
@@ -122,13 +122,13 @@ Every Team member's system prompt carries one `team:policy` section; the Lead an
##### Team policy
```markdown
Agent Teams is available in this session. Teammates are subagents. Use agent ids with send_message to reach your direct continuable children or, as a continuable subagent, your parent and continuable siblings. Descriptions explain responsibilities and may repeat.
Agent Teams is available in this session. Teammates are subagents. Use agent names with send_message to reach your direct continuable children or, as a continuable subagent, your parent and continuable siblings. Descriptions explain responsibilities and may repeat.
The Team Lead and all teammates share the same working directory and filesystem. Edits are immediately visible to every member. Split write work into disjoint scopes, record expected write scopes on shared tasks, and use task dependencies when work must be ordered. Write-scope overlap is advisory, not a lock.
Prefer read/edit/write for file changes. If a file operation returns FS_STALE_VERSION, read the current file, rebase your intended change onto the new content, and retry. Bash, formatters, code generators, and scripts are not fully protected by the filesystem version guard; coordinate them explicitly and have the Lead review the final diff and run tests.
Use agent ids as task owners. The Lead can reassign to itself, its direct continuable children, or historical members; other descendants claim tasks themselves. list_agents shows direct contacts and all reachable continuable descendants; discovery does not grant message or assignment authority. inactive means no turn is executing; it does not describe task completion, success, failure, or waiting for other agents. A successful send does not mean the recipient has processed it. Shared-task workflow is list, get, claim with the current revision, perform the work, then complete. Task readiness never starts an owner. Before wait_agent, make sure another required member is running; use send_message first for an inactive direct contact, or coordinate through its parent for a deeper member. wait_agent observes only changes after that call starts, never wakes a member, and returns noProgress immediately when no other member is running. Re-list after wakeup or timeout. The Lead must wait for required teammates before giving the final answer.
Use agent names as task owners; the root agent is named lead. The Lead can reassign to itself, its direct continuable children, or historical members; other descendants claim tasks themselves. list_agents shows direct contacts and all reachable continuable descendants; discovery does not grant message or assignment authority. inactive means no turn is executing; it does not describe task completion, success, failure, or waiting for other agents. A successful send does not mean the recipient has processed it. Shared-task workflow is list, get, claim with the current revision, perform the work, then complete. Task readiness never starts an owner. Before wait_agent, make sure another required member is running; use send_message first for an inactive direct contact, or coordinate through its parent for a deeper member. wait_agent observes only changes after that call starts, never wakes a member, and returns noProgress immediately when no other member is running. Re-list after wakeup or timeout. The Lead must wait for required teammates before giving the final answer.
```
#### Token effect
@@ -143,7 +143,7 @@ The text is identical for the Lead and every descendant and does not change whil
#### What the model sees
Required `team:identity` runtime context states `You are the Team Lead. Your agent id is "<lead id>".` for a root and `You are a teammate in the Team led by agent "<lead id>".` for a subagent. It follows current membership, survives optional-context suppression, and is restored after compaction. An ordinary fork of a teammate is a new root and receives the Lead statement.
Required `team:identity` runtime context states `You are the Team Lead. Your name is lead.` for a root and `Your name is "<name>". You are a teammate in the Team led by "lead".` for a subagent. It follows current membership, survives optional-context suppression, and is restored after compaction. An ordinary fork of a teammate is a new root and receives the Lead statement.
#### Token effect
@@ -52,7 +52,7 @@ kind: "package-reference"
- **等待 teammate**——`wait_agent` 等待调用开始后的下一次任务、成员状态或 inbox 变化。
- **创建和联系队友**——`subagent`、`subagent_fork`、`send_message`、`list_agents` 和 `interrupt_agent` 来自子代理工具包,本包不修改它们。
任务 `owner` 参数和 `ownerId` 结果使用 Session id。描述用于展示,可以重复。任何成员均可使用任务板;只有 Lead 能重新分配任务。更新校验所有权和修订,拒绝过期编辑。
任务 `owner` 参数与 `ownerName` 结果使用 agent 名字;根名为 `lead`。描述可以重复。任何成员均可使用任务板;只有 Lead 能重新分配任务。更新校验所有权和修订,拒绝过期编辑。
### 成功与失败的表现
@@ -104,7 +104,7 @@ member scope 上的一个 `team:policy` 段落和一个必需的 `team:identity`
- [agent-team 包](../agent-team/README.zh.md)——这些工具背后的 `ctx.agentTeams` 领域服务。
- [Agent Teams 子系统](../../../docs/subsystems/agent-team.zh.md)——持久 Team 类型与服务 API。
- [子代理控制工具](../../subagent/tool-subagent-control/README.zh.md)——按 Session id 发消息、列举和中断。
- [子代理控制工具](../../subagent/tool-subagent-control/README.zh.md)——按 agent 名字 发消息、列举和中断。
- [生成的工具目录](../../../docs/tool-catalog.zh.md#deepseek-aidsh-experimental-tool-agent-team)——模型接收的每个工具 schema。
- [subagent teammate Agent Note](../../../.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.zh.md)——模型侧、scoping 与工具族决策。
@@ -122,13 +122,13 @@ member scope 上的一个 `team:policy` 段落和一个必需的 `team:identity`
##### Team 策略
```markdown
Agent Teams is available in this session. Teammates are subagents. Use agent ids with send_message to reach your direct continuable children or, as a continuable subagent, your parent and continuable siblings. Descriptions explain responsibilities and may repeat.
Agent Teams is available in this session. Teammates are subagents. Use agent names with send_message to reach your direct continuable children or, as a continuable subagent, your parent and continuable siblings. Descriptions explain responsibilities and may repeat.
The Team Lead and all teammates share the same working directory and filesystem. Edits are immediately visible to every member. Split write work into disjoint scopes, record expected write scopes on shared tasks, and use task dependencies when work must be ordered. Write-scope overlap is advisory, not a lock.
Prefer read/edit/write for file changes. If a file operation returns FS_STALE_VERSION, read the current file, rebase your intended change onto the new content, and retry. Bash, formatters, code generators, and scripts are not fully protected by the filesystem version guard; coordinate them explicitly and have the Lead review the final diff and run tests.
Use agent ids as task owners. The Lead can reassign to itself, its direct continuable children, or historical members; other descendants claim tasks themselves. list_agents shows direct contacts and all reachable continuable descendants; discovery does not grant message or assignment authority. inactive means no turn is executing; it does not describe task completion, success, failure, or waiting for other agents. A successful send does not mean the recipient has processed it. Shared-task workflow is list, get, claim with the current revision, perform the work, then complete. Task readiness never starts an owner. Before wait_agent, make sure another required member is running; use send_message first for an inactive direct contact, or coordinate through its parent for a deeper member. wait_agent observes only changes after that call starts, never wakes a member, and returns noProgress immediately when no other member is running. Re-list after wakeup or timeout. The Lead must wait for required teammates before giving the final answer.
Use agent names as task owners; the root agent is named lead. The Lead can reassign to itself, its direct continuable children, or historical members; other descendants claim tasks themselves. list_agents shows direct contacts and all reachable continuable descendants; discovery does not grant message or assignment authority. inactive means no turn is executing; it does not describe task completion, success, failure, or waiting for other agents. A successful send does not mean the recipient has processed it. Shared-task workflow is list, get, claim with the current revision, perform the work, then complete. Task readiness never starts an owner. Before wait_agent, make sure another required member is running; use send_message first for an inactive direct contact, or coordinate through its parent for a deeper member. wait_agent observes only changes after that call starts, never wakes a member, and returns noProgress immediately when no other member is running. Re-list after wakeup or timeout. The Lead must wait for required teammates before giving the final answer.
```
#### Token 影响
@@ -143,7 +143,7 @@ Use agent ids as task owners. The Lead can reassign to itself, its direct contin
#### 模型看到什么
必需的 `team:identity` 运行时上下文对根显示 `You are the Team Lead. Your agent id is "<lead id>".`,对子代理显示 `You are a teammate in the Team led by agent "<lead id>".`。它跟随当前成员身份,不受可选上下文关闭影响,并在压缩后恢复。队友的普通分叉是新的根,接收 Lead 声明。
必需的 `team:identity` runtime context 对根声明 `You are the Team Lead. Your name is lead.`,对子代理声明 `Your name is "<name>". You are a teammate in the Team led by "lead".`。它跟随当前成员关系,不受可选上下文抑制影响,压缩后会恢复。队友的普通分叉是新的根,收到 Lead 声明。
#### Token 影响
@@ -2,16 +2,17 @@
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { SessionId } from '@deepseek-ai/dsh-session'
import { TeamTaskId } from '@deepseek-ai/dsh-experimental-agent-team'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { TeamTaskId, type TeamTaskView } from '@deepseek-ai/dsh-experimental-agent-team'
import type {} from '@deepseek-ai/dsh-subagent'
import { defineTool } from '@deepseek-ai/dsh-tools'
import type { InferValue, ValueSchemaSpec } from '@deepseek-ai/dsh-tools'
/** Cordis plugin name. */
export const name = 'tool-agent-team'
/** Services required by the Team tool plugin. */
export const inject = ['agents', 'agentTeams', 'tools', 'systemPrompt']
export const inject = ['agents', 'agentTeams', 'subagents', 'tools', 'systemPrompt']
/** The Team tool plugin takes no configuration. */
export type Config = Record<never, never>
@@ -23,13 +24,13 @@ export const Config: z<Config> = z.object({})
const REMOVED_CONFIG_KEYS = ['interrupt', 'freshProvider', 'forkProvider'] as const
/** Model-facing collaboration guidance shared by Lead and teammates. */
const TEAM_POLICY = `Agent Teams is available in this session. Teammates are subagents. Use agent ids with send_message to reach your direct continuable children or, as a continuable subagent, your parent and continuable siblings. Descriptions explain responsibilities and may repeat.
const TEAM_POLICY = `Agent Teams is available in this session. Teammates are subagents. Use agent names with send_message to reach your direct continuable children or, as a continuable subagent, your parent and continuable siblings. Descriptions explain responsibilities and may repeat.
The Team Lead and all teammates share the same working directory and filesystem. Edits are immediately visible to every member. Split write work into disjoint scopes, record expected write scopes on shared tasks, and use task dependencies when work must be ordered. Write-scope overlap is advisory, not a lock.
Prefer read/edit/write for file changes. If a file operation returns FS_STALE_VERSION, read the current file, rebase your intended change onto the new content, and retry. Bash, formatters, code generators, and scripts are not fully protected by the filesystem version guard; coordinate them explicitly and have the Lead review the final diff and run tests.
Use agent ids as task owners. The Lead can reassign to itself, its direct continuable children, or historical members; other descendants claim tasks themselves. list_agents shows direct contacts and all reachable continuable descendants; discovery does not grant message or assignment authority. inactive means no turn is executing; it does not describe task completion, success, failure, or waiting for other agents. A successful send does not mean the recipient has processed it. Shared-task workflow is list, get, claim with the current revision, perform the work, then complete. Task readiness never starts an owner. Before wait_agent, make sure another required member is running; use send_message first for an inactive direct contact, or coordinate through its parent for a deeper member. wait_agent observes only changes after that call starts, never wakes a member, and returns noProgress immediately when no other member is running. Re-list after wakeup or timeout. The Lead must wait for required teammates before giving the final answer.`
Use agent names as task owners; the root agent is named lead. The Lead can reassign to itself, its direct continuable children, or historical members; other descendants claim tasks themselves. list_agents shows direct contacts and all reachable continuable descendants; discovery does not grant message or assignment authority. inactive means no turn is executing; it does not describe task completion, success, failure, or waiting for other agents. A successful send does not mean the recipient has processed it. Shared-task workflow is list, get, claim with the current revision, perform the work, then complete. Task readiness never starts an owner. Before wait_agent, make sure another required member is running; use send_message first for an inactive direct contact, or coordinate through its parent for a deeper member. wait_agent observes only changes after that call starts, never wakes a member, and returns noProgress immediately when no other member is running. Re-list after wakeup or timeout. The Lead must wait for required teammates before giving the final answer.`
const NO_ACTIVE_PEER_MESSAGE = 'No other Team member is running. wait_agent cannot make progress or wake inactive teammates. Re-list with list_agents and team_task_list, then use send_message to wake a required inactive direct contact before waiting again; coordinate deeper members through their parent.'
@@ -43,7 +44,7 @@ const TASK_VIEW_SCHEMA = {
subject: { type: 'string', required: true },
description: { type: 'string', required: true },
status: { type: 'string', required: true, enum: ['pending', 'in_progress', 'completed', 'deleted'] },
ownerId: { type: 'string' },
ownerName: { type: 'string' },
blockedBy: { type: 'array', required: true, items: { type: 'string' } },
writeScopes: { type: 'array', required: true, items: { type: 'string' } },
ready: { type: 'boolean', required: true },
@@ -94,6 +95,12 @@ function jsonOutput<const S extends ValueSchemaSpec>(schema: S): {
}
}
/** Project durable task ownership to the names used by collaboration tools. */
function namedTask(ctx: Context, caller: Agent, task: TeamTaskView) {
const { ownerId, ...view } = task
return { ...view, ...ownerId === undefined ? {} : { ownerName: ctx.subagents.nameOf(caller, ownerId) } }
}
/** Recover the exact caller guaranteed by Agent-scoped tool discovery. */
function callingAgent(agent: Agent | undefined, toolName: string): Agent {
/* v8 ignore next 2 -- Team tools are registered only in an exact Agent scope, so discovery supplies this carrier. */
@@ -120,8 +127,8 @@ function install(agent: Agent, ctx: Context): () => void {
text: () => {
const { role, root } = ctx.agentTeams.membership(agent)
return role === 'lead'
? `You are the Team Lead. Your agent id is ${JSON.stringify(agent.id)}.`
: `You are a teammate in the Team led by agent ${JSON.stringify(root.id)}.`
? 'You are the Team Lead. Your name is lead.'
: `Your name is ${JSON.stringify(ctx.subagents.nameOf(agent))}. You are a teammate in the Team led by ${JSON.stringify(ctx.subagents.nameOf(agent, root.id))}.`
},
}))
@@ -173,12 +180,13 @@ function install(agent: Agent, ctx: Context): () => void {
},
output: jsonOutput(TASK_VIEW_SCHEMA),
async execute(args, exec) {
return await ctx.agentTeams.createTask(callingAgent(exec.agent, 'team_task_create'), {
const caller = callingAgent(exec.agent, 'team_task_create')
return namedTask(ctx, caller, await ctx.agentTeams.createTask(caller, {
subject: args.subject,
description: args.description,
...args.blocked_by === undefined ? {} : { blockedBy: args.blocked_by.map(TeamTaskId) },
...args.write_scopes === undefined ? {} : { writeScopes: args.write_scopes },
})
}))
},
})))
@@ -191,7 +199,7 @@ function install(agent: Agent, ctx: Context): () => void {
enum: ['pending', 'in_progress', 'completed'],
description: 'Optional exact status filter.',
},
owner: { type: 'string', description: 'Optional exact ownerId filter; use unowned for tasks without an owner.' },
owner: { type: 'string', description: 'Optional exact ownerName filter; use unowned for tasks without an owner.' },
ready: { type: 'boolean', description: 'Optional readiness filter.' },
cursor: { type: 'integer', description: 'Zero-based result offset. Defaults to 0.' },
limit: { type: 'integer', description: 'Number of rows, 1 through 100. Defaults to 50.' },
@@ -199,9 +207,10 @@ function install(agent: Agent, ctx: Context): () => void {
output: jsonOutput(TASK_LIST_VALUE_SCHEMA),
execute(args, exec) {
const status = args.status
const filtered = ctx.agentTeams.listTasks(callingAgent(exec.agent, 'team_task_list')).filter(task =>
const caller = callingAgent(exec.agent, 'team_task_list')
const filtered = ctx.agentTeams.listTasks(caller).map(task => namedTask(ctx, caller, task)).filter(task =>
(status === undefined || task.status === status)
&& (args.owner === undefined || (args.owner === 'unowned' ? task.ownerId === undefined : task.ownerId === args.owner))
&& (args.owner === undefined || (args.owner === 'unowned' ? task.ownerName === undefined : task.ownerName === args.owner))
&& (args.ready === undefined || task.ready === args.ready))
const cursor = args.cursor ?? 0
const limit = args.limit ?? 50
@@ -221,11 +230,9 @@ function install(agent: Agent, ctx: Context): () => void {
task_id: { type: 'string', required: true, description: 'Shared task id.' },
},
output: jsonOutput(TASK_VIEW_SCHEMA),
async execute(args, exec) {
return Promise.resolve(ctx.agentTeams.getTask(
callingAgent(exec.agent, 'team_task_get'),
TeamTaskId(args.task_id),
))
execute(args, exec) {
const caller = callingAgent(exec.agent, 'team_task_get')
return Promise.resolve(namedTask(ctx, caller, ctx.agentTeams.getTask(caller, TeamTaskId(args.task_id))))
},
})))
@@ -245,11 +252,17 @@ function install(agent: Agent, ctx: Context): () => void {
description: { type: 'string', description: 'Replacement details for edit.' },
blocked_by: { type: 'array', items: { type: 'string' }, description: 'Complete blocker list for set_dependencies.' },
write_scopes: { type: 'array', items: { type: 'string' }, description: 'Replacement advisory write scopes for edit.' },
owner: { type: 'string', description: 'Agent id of the Lead or one of its direct continuable children for Lead-only reassign; omit to unassign.' },
owner: { type: 'string', description: 'Agent name of the Lead or one of its direct continuable children for Lead-only reassign; omit to unassign.' },
},
output: jsonOutput(TASK_VIEW_SCHEMA),
async execute(args, exec) {
return await ctx.agentTeams.updateTask(callingAgent(exec.agent, 'team_task_update'), {
const caller = callingAgent(exec.agent, 'team_task_update')
// Team authorizes historical members that predate subagent catalogs.
const legacyId = args.owner?.startsWith('legacy:') ? SessionId(args.owner.slice('legacy:'.length)) : undefined
const owner = args.owner === undefined ? undefined
: legacyId !== undefined && ctx.subagents.nameOf(caller, legacyId) === args.owner ? legacyId
: await ctx.subagents.resolveName(caller, args.owner, exec.signal)
return namedTask(ctx, caller, await ctx.agentTeams.updateTask(caller, {
taskId: TeamTaskId(args.task_id),
expectedRevision: args.expected_revision,
action: args.action,
@@ -257,8 +270,8 @@ function install(agent: Agent, ctx: Context): () => void {
...args.description === undefined ? {} : { description: args.description },
...args.blocked_by === undefined ? {} : { blockedBy: args.blocked_by.map(TeamTaskId) },
...args.write_scopes === undefined ? {} : { writeScopes: args.write_scopes },
...args.owner === undefined ? {} : { owner: SessionId(args.owner) },
})
...owner === undefined ? {} : { owner },
}))
},
})))
} catch (error: unknown) {
@@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
@@ -23,7 +24,7 @@ import { serialize } from '@deepseek-ai/dsh-llm-deepseek/src/serialize.ts'
import type { GenerateOptions } from '@deepseek-ai/dsh-llm'
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import { mountWorkingDirectoryFixture } from '../../../subagent/subagent/tests/working-directory-fixture.ts'
import TeamService from '../../agent-team/src/index.ts'
import TeamService, { TeamId } from '../../agent-team/src/index.ts'
import * as toolTeam from '../src/index.ts'
function serializeRequest(request: GenerateOptions) {
@@ -44,7 +45,7 @@ const TEAM_TOOLS = [
'team_task_update',
'wait_agent',
]
const leadIdentity = (agent: Agent): string => `You are the Team Lead. Your agent id is ${JSON.stringify(agent.id)}.`
const leadIdentity = (_agent: Agent): string => 'You are the Team Lead. Your name is lead.'
const roots: string[] = []
const contexts = new Set<Context>()
@@ -112,7 +113,7 @@ function text(result: Awaited<ReturnType<typeof execute>>): string {
/** Start a continuable child of the Lead with a task description. */
async function spawn(ctx: Context, lead: Agent, description: string | undefined, prompt: string, provider = 'spawn'): Promise<SessionId> {
const started = await ctx.subagents.startActivation({
const started = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
provider,
label: `${description ?? 'shared'} task`,
request: { prompt: [{ type: 'text', text: prompt }], parent: lead },
@@ -132,8 +133,8 @@ function teamTools(assembled: Awaited<ReturnType<typeof assembly>>): string[] {
return assembled.tools.map(schema => schema.name).filter(name => TEAM_TOOLS.includes(name)).sort()
}
function teammateIdentity(lead: Agent): string {
return `You are a teammate in the Team led by agent ${JSON.stringify(lead.id)}.`
function teammateIdentity(_lead: Agent): string {
return 'You are a teammate in the Team led by \"lead\".'
}
async function runTurn(agent: Agent, text: string): Promise<void> {
@@ -367,7 +368,7 @@ describe('dsh-tool-team', () => {
expected_revision: task.revision,
action: 'claim',
})
expect(JSON.parse(text(claimed))).toMatchObject({ status: 'in_progress', ownerId: childId })
expect(JSON.parse(text(claimed))).toMatchObject({ status: 'in_progress', ownerName: ctx.subagents.nameOf(lead, childId) })
const stale = await execute(ctx, lead, 'team_task_update', {
task_id: task.id,
expected_revision: task.revision,
@@ -412,7 +413,7 @@ describe('dsh-tool-team', () => {
const claim = JSON.parse(text(claimed)) as { revision: number }
expect(JSON.parse(text(await execute(ctx, lead, 'team_task_list', {
status: 'in_progress', owner: lead.id, cursor: 0, limit: 1,
status: 'in_progress', owner: 'lead', cursor: 0, limit: 1,
})))).toMatchObject({ tasks: [{ id: first.id }] })
expect(JSON.parse(text(await execute(ctx, lead, 'team_task_list', {
owner: 'unowned', limit: 1,
@@ -447,7 +448,7 @@ describe('dsh-tool-team', () => {
task_id: first.id,
expected_revision: dependency.revision,
action: 'reassign',
owner: childId,
owner: ctx.subagents.nameOf(lead, childId),
})).isError).toBe(true)
const wait = execute(ctx, lead, 'wait_agent', {})
@@ -465,6 +466,23 @@ describe('dsh-tool-team', () => {
await waitNoAgent(ctx, childId)
})
it('reassigns by a historical member address without granting message authority', async () => {
const { ctx, lead } = await setup([])
const id = SessionId('historical-worker')
for (const phase of ['provisioning', 'active'] as const) lead.session.append('team/member', {
version: 2, teamId: TeamId(lead.id),
member: { id, name: 'old-name', description: 'historical member', provider: 'spawn', context: 'fresh', phase },
})
const created = await ctx.agentTeams.createTask(lead, { subject: 'Review', description: 'Review history' })
expect(JSON.parse(text(await execute(ctx, lead, 'team_task_update', {
task_id: created.id, expected_revision: created.revision, action: 'reassign', owner: 'legacy:historical-worker',
})))).toMatchObject({ ownerName: 'legacy:historical-worker' })
expect((await execute(ctx, lead, 'send_message', { name: 'legacy:historical-worker', message: 'hello' })).isError).toBe(true)
expect((await execute(ctx, lead, 'team_task_update', {
task_id: created.id, expected_revision: 2, action: 'reassign', owner: 'legacy:missing',
})).isError).toBe(true)
})
it('removes and reinstalls every scoped registration across plugin HMR without stopping the child', async () => {
const { ctx, lead, fiber } = await setup(['hang'])
const childId = await spawn(ctx, lead, 'hmr-worker', 'wait')
@@ -557,6 +575,6 @@ describe('dsh-tool-team', () => {
expect(text(result)).toContain('unknown tool "wait_agent"')
expect('default' in toolTeam).toBe(false)
expect(toolTeam.name).toBe('tool-agent-team')
expect(toolTeam.inject).toEqual(['agents', 'agentTeams', 'tools', 'systemPrompt'])
expect(toolTeam.inject).toEqual(['agents', 'agentTeams', 'subagents', 'tools', 'systemPrompt'])
})
})
@@ -3017,6 +3017,19 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
parameters: [{ name: 'spec', description: 'task, backend, and result recipient.' }],
returns: 'activation with a message id only for local children.',
},
{
signature: 'nameOf(caller: Agent, id: SessionId = caller.id): string',
description: 'Read a model-facing name in the caller\'s tree, retaining legacy addresses for older unnamed children.',
parameters: [{ name: 'caller', description: 'exact live Agent selecting the tree.' }, { name: 'id', description: 'target identity; omission selects the caller.' }],
returns: 'lead, the immutable assigned name, or a legacy address.',
},
{
signature: 'resolveName(caller: Agent, name: string, signal?: AbortSignal): Promise<SessionId>',
description: 'Resolve a tree-wide name without granting authority to act on its Agent.',
parameters: [{ name: 'caller', description: 'exact live Agent selecting the tree.' }, { name: 'name', description: 'a name returned by creation or discovery.' }, { name: 'signal', description: 'cancellation during historical discovery.' }],
returns: 'the durable Session identity for the consuming operation to authorize.',
throws: ['when the name is absent or the caller\'s live tree is unavailable.'],
},
{
signature: 'async waitForChildren(parent: Agent): Promise<boolean>',
description: 'Join progressing descendants without cancelling them. Idle descendants whose inboxes require a later wake stay resident and do not delay host completion.',
@@ -7785,11 +7798,11 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'SubagentActivation',
declaration: 'export interface SubagentActivation {\n readonly childId: SessionId;\n readonly messageId?: MessageId;\n readonly result: Promise<SubagentResult>;\n dispose(): Promise<void>;\n}',
declaration: 'export interface SubagentActivation {\n readonly name: string;\n readonly childId: SessionId;\n readonly messageId?: MessageId;\n readonly result: Promise<SubagentResult>;\n dispose(): Promise<void>;\n}',
},
{
name: 'SubagentActivationSpec',
declaration: 'export interface SubagentActivationSpec {\n readonly provider: string;\n readonly label: string;\n readonly childId?: SessionId;\n readonly request: Omit<SubagentStartRequest, \'label\' | \'signal\'>;\n readonly signal: AbortSignal;\n readonly delivery: \'parent\' | \'caller\';\n}',
declaration: 'export interface SubagentActivationSpec {\n readonly name: string;\n readonly provider: string;\n readonly label: string;\n readonly childId?: SessionId;\n readonly request: Omit<SubagentStartRequest, \'label\' | \'signal\'>;\n readonly signal: AbortSignal;\n readonly delivery: \'parent\' | \'caller\';\n}',
},
{
name: 'SubagentCapabilities',
@@ -7857,7 +7870,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'SubagentRuntime',
declaration: 'export class SubagentRuntime extends TypertRemoteService {\n static Config;\n static inject;\n constructor(ctx: Context, private config: Config);\n resolveMaxDepth(configured?: number | \'provider-managed\'): number | undefined;\n startActivation(spec: SubagentActivationSpec & {\n readonly childId: SessionId;\n }): Promise<SubagentActivation & {\n readonly messageId: MessageId;\n }>;\n startActivation(spec: SubagentActivationSpec): Promise<SubagentActivation>;\n async startActivation(spec: SubagentActivationSpec): Promise<SubagentActivation>;\n async waitForChildren(parent: Agent): Promise<boolean>;\n async sendMessage(sender: Agent, targetId: SessionId, content: ContentBlock[], options: SubagentSendMessageOptions): Promise<MessageId>;\n interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void;\n async drainDescendants(parents: readonly Agent[]): Promise<void>;\n async drainChildren(parent: Agent, childIds: readonly SessionId[]): Promise<void>;\n listChildren(parentSessionId: SessionId, signal?: AbortSignal): Promise<SubagentCatalogEntry[]>;\n listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise<SubagentDescendantListEntry[]>;\n @Remote(\'prompt\')\n async prompt(request: SubagentPromptRequest, signal: AbortSignal): Promise<SubagentPromptReceipt>;\n @Remote(\'interruptByParent\')\n interruptByParent(childSessionId: SessionId, parentSessionId: SessionId, mode: \'continuable\'): SubagentIn /* …truncated — full shape in source */',
declaration: 'export class SubagentRuntime extends TypertRemoteService {\n static Config;\n static inject;\n constructor(ctx: Context, private config: Config);\n resolveMaxDepth(configured?: number | \'provider-managed\'): number | undefined;\n startActivation(spec: SubagentActivationSpec & {\n readonly childId: SessionId;\n }): Promise<SubagentActivation & {\n readonly messageId: MessageId;\n }>;\n startActivation(spec: SubagentActivationSpec): Promise<SubagentActivation>;\n async startActivation(spec: SubagentActivationSpec): Promise<SubagentActivation>;\n nameOf(caller: Agent, id: SessionId = caller.id): string;\n resolveName(caller: Agent, name: string, signal?: AbortSignal): Promise<SessionId>;\n async waitForChildren(parent: Agent): Promise<boolean>;\n async sendMessage(sender: Agent, targetId: SessionId, content: ContentBlock[], options: SubagentSendMessageOptions): Promise<MessageId>;\n interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void;\n async drainDescendants(parents: readonly Agent[]): Promise<void>;\n async drainChildren(parent: Agent, childIds: readonly SessionId[]): Promise<void>;\n listChildren(parentSessionId: SessionId, signal?: AbortSignal): Promise<SubagentCatalogEntry[]>;\n listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise<SubagentDescendantListEntry[]>;\n @Remote(\'prompt\')\n async prompt(request: SubagentPromptRequest, signal: AbortSignal): Promise<SubagentPromptRe /* …truncated — full shape in source */',
},
{
name: 'SubagentSendMessageOptions',
+2 -1
View File
@@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
import { mountWorkingDirectoryFixture } from '../../../subagent/subagent/tests/working-directory-fixture.ts'
/** SDK shutdown releases managed descendants before their root Agents leave the registry. */
@@ -49,7 +50,7 @@ describe('SDK descendant shutdown', () => {
const start = async (parent: Agent, id: SessionId): Promise<Agent> => {
const gate = { entered: Promise.withResolvers<undefined>(), release: Promise.withResolvers<undefined>() }
gates.set(id, gate)
const receipt = await ctx.subagents.startActivation({
const receipt = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
provider: 'local-test', childId: id, label: 'parked child', delivery: 'caller', signal: new AbortController().signal,
request: { parent, prompt: [{ type: 'text', text: 'work' }] },
})
+3 -2
View File
@@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
import { mountWorkingDirectoryFixture } from '../../../subagent/subagent/tests/working-directory-fixture.ts'
/** SDK task completion retains live failures that have no durable terminal outcome. */
import { Context } from '@deepseek-ai/cordis'
@@ -64,7 +65,7 @@ describe('SDK session/wait with parked child input', () => {
let owner = parent
let child = parent
for (let level = 0; level < depth; level++) {
const started = await ctx.subagents.startActivation({
const started = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
provider: 'local-test', label: 'interruptible child', delivery: 'caller', signal: new AbortController().signal,
request: { parent: owner, prompt: [{ type: 'text', text: 'work' }] },
})
@@ -116,7 +117,7 @@ describe('SDK session/wait failures', () => {
prepareContinuable: () => Promise.resolve({}),
})
const warnings = vi.spyOn(ctx.logger, 'warn')
const child = await ctx.subagents.startActivation({
const child = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
provider: 'local-test', label: 'child failure', delivery: 'caller', signal: new AbortController().signal,
request: { parent, prompt: [{ type: 'text', text: 'delegated task' }] },
})
@@ -14,7 +14,7 @@ class MockDelegatingAdapter extends LlmAdapter {
: ''
if (!options.messages.some(message => message.role === 'tool')) {
const args = JSON.stringify({ description: 'cwd probe', prompt: 'report your workspace' })
const args = JSON.stringify({ name: 'loader-child', description: 'cwd probe', prompt: 'report your workspace' })
yield { type: 'block-start', index: 0, blockType: 'tool-call' }
yield { type: 'tool-call-delta', index: 0, id: ToolCallId('call-delegate'), name: 'subagent', argumentsDelta: args }
yield { type: 'block-end', index: 0, block: { type: 'tool-call', id: ToolCallId('call-delegate'), name: 'subagent', arguments: args } }
@@ -43,7 +43,7 @@ function settlementText(events: SessionEvent[]): string[] {
const notice = notices[0]!
if (notice.type !== 'user/message' || notice.data.source.kind !== 'subagent-settled') throw new Error('missing completion notice')
expect(results[0]!.data.message.content).toEqual([
{ type: 'text', text: `started subagent ${notice.data.source.senderSessionId}` },
{ type: 'text', text: 'started subagent loader-child' },
])
return notice.data.content.slice(1).filter(block => block.type === 'text').map(block => block.text)
}
@@ -32,7 +32,7 @@ class MockDelegatingAdapter extends LlmAdapter {
const selectedRoute = process.env.DSH_TEST_CHILD_DEFAULT_ROUTE === '1'
? { reasoning_effort: 'max' }
: { provider: 'mock', model: 'mock-routed', reasoning_effort: 'max' }
const args = JSON.stringify({
const args = JSON.stringify({ name: 'loader-child',
description: 'route probe',
prompt: 'report your route and workspace',
...selectedRoute,
@@ -20,7 +20,7 @@ class NestedAdapter extends LlmAdapter {
}
answer = 'nested child result'
} else if (!options.messages.some(message => message.role === 'tool')) {
const args = JSON.stringify({ description: 'nested fork', prompt: 'nested-child-task' })
const args = JSON.stringify({ name: 'loader-child', description: 'nested fork', prompt: 'nested-child-task' })
const id = ToolCallId('nested-fork')
yield { type: 'block-start', index: 0, blockType: 'tool-call' }
yield { type: 'tool-call-delta', index: 0, id, name: 'subagent_fork', argumentsDelta: args }
@@ -47,7 +47,7 @@ function settlementText(events: SessionEvent[]): string[] {
const notice = notices[0]!
if (notice.type !== 'user/message' || notice.data.source.kind !== 'subagent-settled') throw new Error('missing completion notice')
expect(results[0]!.data.message.content).toEqual([
{ type: 'text', text: `started subagent ${notice.data.source.senderSessionId}` },
{ type: 'text', text: 'started subagent loader-child' },
])
return notice.data.content.slice(1).filter(block => block.type === 'text').map(block => block.text)
}
+6 -6
View File
@@ -18,17 +18,17 @@
en: 924b8c15f9529028
zh: 9fb0c399b5a79e3e
/deepseek-ai-dsh-subagent/use-this-package/enabling-delegation:
en: 388ae5427c869173
zh: aff830f4986cf1b3
en: ae565bb3cf491799
zh: 6c71c30e5e284a31
/deepseek-ai-dsh-subagent/use-this-package/delegation-settings:
en: 86c982223fab1a3a
zh: 3d220d8ed84eab9a
/deepseek-ai-dsh-subagent/use-this-package/activation-capacity:
en: 7c9548b9d02efe1f
zh: e23bd30b4c9cdabd
en: c888e23af70b5635
zh: f9be27ef7833c4fc
/deepseek-ai-dsh-subagent/use-this-package/local-and-external-children:
en: 013644290e74493e
zh: 05014c3c3773edeb
en: bd9389d8daac973d
zh: 942a10b183cacc47
/deepseek-ai-dsh-subagent/use-this-package/child-presets:
en: a740e5e9bdd661d9
zh: 712a5458fc8b3615
+5 -2
View File
@@ -42,7 +42,7 @@ Mount the service with a provider and the delegation tool. The provider register
toolName: subagent
```
The tool returns a child session id immediately. The parent receives a completion notice when the background activation settles. Mounting the service alone enables no delegation.
The tool requires a unique agent name and returns it immediately. The parent receives a completion notice when the background activation settles. Mounting the service alone enables no delegation.
### Delegation settings
@@ -56,9 +56,12 @@ The current `maxActiveSubagents` value is sampled before every new or cold-resum
At capacity, creation or cold resume rejects with `ACTIVATION_LIMIT_REACHED` (browser prompts receive `subagent/delivery-unavailable`): wait for a child to finish or continue using the existing agents. Admission does not queue, because a parent waiting for descendants must not wait for its own occupied slot. Slots are process-local and do not constrain cumulative Session history or token usage.
<a id="local-and-external-children"></a>
### Local and external children
`startActivation({ provider, label, request, signal, delivery })` returns `{ childId, messageId?, result, dispose }`. Supplying `childId` selects a local backend and guarantees `messageId`. Local children accept later messages and can cold-resume; external children expose one execution and reject continuation. `request.agentOptions` overrides supported child model settings. `delivery: 'parent'` supplies the model-facing completion notice; `delivery: 'caller'` lets workflows await the result without a parent notice or initial return guidance. Local parent delivery requires persistence; caller delivery can use an ephemeral local session. Cold resume requires persistence and Session query.
`startActivation({ name, provider, label, request, signal, delivery })` returns `{ childId, name, messageId?, result, dispose }`. Supplying `childId` selects a local backend and guarantees `messageId`. Local children accept later messages and can cold-resume; external children expose one execution and reject continuation. `request.agentOptions` overrides supported child model settings. `delivery: 'parent'` supplies the model-facing completion notice; `delivery: 'caller'` lets workflows await the result without a parent notice or initial return guidance. Local parent delivery requires persistence; caller delivery can use an ephemeral local session. Cold resume requires persistence and Session query.
Agent names are immutable and unique throughout one agent tree; its root is `lead`. Creation requires a lower-case letter followed by lower-case letters, digits, underscores, or hyphens; `lead` and `unowned` are reserved. `SubagentNames` reserves before asynchronous preparation, releases failed unpublished starts, and records successful assignments as `subagent/named` in the root Session. The `subagentNames` projection retains assignments after settlement and restart and excludes fork-inherited events. `nameOf()` and `resolveName()` translate between names and stable Session ids without granting operation authority. Older unnamed descendants use `legacy:<session-id>` addresses.
### Child presets
+5 -2
View File
@@ -42,7 +42,7 @@ kind: "package-reference"
toolName: subagent
```
工具立即返回子会话 ID,后台 activation 结算后父级收到完成通知。仅挂载服务本身不会启用委派。
工具要求唯一的 agent 名字,并立即返回该名字。后台 activation 结束时,父级收到完成通知。只挂载服务不会启用委派。
### 委派设置
@@ -56,9 +56,12 @@ kind: "package-reference"
容量耗尽时,新建或冷恢复以 `ACTIVATION_LIMIT_REACHED` 拒绝(浏览器消息返回 `subagent/delivery-unavailable`):等待子代理完成,或继续使用现有代理。接纳不会排队,避免等待后代的父代理又等待自己占用的名额。名额仅存在于当前进程,不限制累计 Session 历史或 token 用量。
<a id="local-and-external-children"></a>
### 本地与外部子级
`startActivation({ provider, label, request, signal, delivery })` 返回 `{ childId, messageId?, result, dispose }`。提供 `childId` 会选择本地后端,并保证返回 `messageId`。本地子级接受后续消息并支持冷恢复;外部子级只暴露一次执行,拒绝继续输入。`request.agentOptions` 覆盖后端支持的子级模型设置。`delivery: 'parent'` 提供面向模型的完成通知;`delivery: 'caller'` 让工作流等待结果,不发送父级通知,也不附加初始返回指导。本地父级投递需要持久化,调用方投递可以使用临时本地会话;冷恢复需要持久化与 Session 查询服务。
`startActivation({ name, provider, label, request, signal, delivery })` 返回 `{ childId, name, messageId?, result, dispose }`。提供 `childId` 会选择本地后端,并保证返回 `messageId`。本地子级接受后续消息,支持冷恢复;外部子级只执行一次,拒绝继续执行。`request.agentOptions` 覆盖受支持的子级模型设置。`delivery: 'parent'` 提供面向模型的完成通知;`delivery: 'caller'` 允许 workflow 等待结果,不发送父级通知或初始返回指引。本地 parent 交付要求持久化;caller 交付可使用临时本地会话。冷恢复要求持久化和 Session query。
agent 名字不可变,在整棵 agent 树内唯一;根名为 `lead`。创建时必须以小写字母开头,后续只能包含小写字母、数字、下划线或连字符;`lead` 和 `unowned` 为保留名。`SubagentNames` 在异步准备前预留名字,释放失败且未发布的启动,并将成功分配作为 `subagent/named` 记录在根 Session 中。`subagentNames` 投影在结束和重启后保留分配,排除分叉继承的事件。`nameOf()` 和 `resolveName()` 在名字与稳定 Session id 之间转换,不授予操作权限。旧的无名后代使用 `legacy:<session-id>` 地址。
### 子级 preset
@@ -40,6 +40,8 @@ export class ActivationPool {
* The manager owns admission, execution handles, and resource release.
*/
interface ActivationBase {
/** Immutable model-facing name retained through teardown. */
readonly name: string
/** Shared capacity for this Activation and all its managed descendants. */
readonly pool: ActivationPool
/** Return this epoch's slot after its handle has finished disposal. */
@@ -56,15 +56,17 @@ function agentMessageSource(sender: Agent): AgentMessageSource {
* Build the model-visible and durable representation of one adjacent-Agent message.
* @param sender - exact live Agent that authored the message.
* @param content - model-visible message blocks supplied by the sender.
* @param senderName - immutable tree-wide name shown to the recipient.
* @returns the durable user-message representation delivered to the recipient.
*/
export function createAgentMessage(
sender: Agent,
content: ContentBlock[],
senderName: string,
): ReturnType<typeof createUserMessage> {
return createUserMessage({
content: [
{ type: 'text' as const, text: `Agent ${sender.id} sent a message: ` },
{ type: 'text' as const, text: `Agent ${senderName} sent a message: ` },
...content,
],
source: agentMessageSource(sender),
@@ -73,21 +75,23 @@ export function createAgentMessage(
/**
* Append adjacent-Agent return guidance to a continuable child's initial task.
* @param parentId - durable parent session id named in the guidance.
* @param parentName - name of the direct parent.
* @param childName - name assigned to this child.
* @param prompt - initial model-visible task blocks.
* @returns task blocks followed by the continuable return guidance.
*/
export function withContinuableReturnGuidance(
parentId: SessionId,
parentName: string,
childName: string,
prompt: ContentBlock[],
): ContentBlock[] {
const encodedParentId = JSON.stringify(parentId)
const encodedParentName = JSON.stringify(parentName)
return [
...prompt,
{
type: 'text',
text: `Your parent agent id is ${encodedParentId}. Before you finish, send your result to that agent with `
+ `send_message({ agent_id: ${encodedParentId}, message: "<self-contained result>" }). The parent shares `
text: `Your name is ${JSON.stringify(childName)}. Your parent agent name is ${encodedParentName}. Before you finish, send your result to that agent with `
+ `send_message({ name: ${encodedParentName}, message: "<self-contained result>" }). The parent shares `
+ 'your workspace but does not automatically receive your transcript, tool output, or reasoning. Send '
+ 'earlier messages as well when a finding changes what the parent should do next; sending a message '
+ 'does not end your turn.',
@@ -98,13 +102,13 @@ export function withContinuableReturnGuidance(
/**
* One line telling a parent that a background child is finished and why, in
* the parent's own task vocabulary.
* @param childId - the durable child the parent knows by id.
* @param childName - the immutable name the parent uses for the child.
* @param stopReason - how the child's last ordinary turn ended.
* @param continuable - whether the child accepts a later message.
* @returns the model-facing opening line of the settlement notice.
*/
function settlementSummary(childId: SessionId, stopReason: SubagentResult['stopReason'], continuable: boolean): string {
const subject = `Background subagent ${childId}`
function settlementSummary(childName: string, stopReason: SubagentResult['stopReason'], continuable: boolean): string {
const subject = `Background subagent ${childName}`
switch (stopReason) {
case 'completed':
return continuable
@@ -130,17 +134,19 @@ function settlementSummary(childId: SessionId, stopReason: SubagentResult['stopR
/**
* Build the runtime-owned settlement notice from the child's nonempty closing text.
* @param childId - durable child session id named in the notice.
* @param childId - durable child identity retained in message attribution.
* @param childName - immutable name shown in the notice.
* @param terminal - recorded terminal state for the settled Activation.
* @param continuable - whether the child can accept another task.
* @returns the durable user-message representation delivered to the parent.
*/
export function createSettlementMessage(
childId: SessionId,
childName: string,
terminal: SubagentResult,
continuable = true,
): ReturnType<typeof createUserMessage> {
const summary = settlementSummary(childId, terminal.stopReason, continuable)
const summary = settlementSummary(childName, terminal.stopReason, continuable)
// Parent providers receive this notice as a user message and may reject
// nontext assistant blocks. Keep this conversion local so SDK/UI consumers
// retain the complete child output.
+24
View File
@@ -75,6 +75,7 @@ import { installSubagentArchiveAdmission } from './archive-admission.ts'
import type { SubagentCatalogEntry } from './projection-types.ts'
import { subagentIdentityProjectionDefinition, subagentTimingProjectionDefinition } from './projection.ts'
import { subagentCatalogProjectionDefinition } from './catalog.ts'
import { subagentNamesProjectionDefinition } from './names.ts'
import { deliverSubagentPrompt } from './internal.ts'
export type {} from './catalog.ts'
@@ -227,6 +228,7 @@ export class SubagentRuntime extends TypertRemoteService {
})
ctx.inject(['sessionProjections'], (projectionCtx) => {
projectionCtx.sessionProjections.register(subagentCatalogProjectionDefinition)
projectionCtx.sessionProjections.register(subagentNamesProjectionDefinition)
projectionCtx.sessionProjections.register(subagentTimingProjectionDefinition)
projectionCtx.sessionProjections.register(subagentIdentityProjectionDefinition)
})
@@ -284,6 +286,28 @@ export class SubagentRuntime extends TypertRemoteService {
: manager.startLocal(spec)
}
/**
* Read a model-facing name in the caller's tree, retaining legacy addresses for older unnamed children.
* @param caller - exact live Agent selecting the tree.
* @param id - target identity; omission selects the caller.
* @returns lead, the immutable assigned name, or a legacy address.
*/
nameOf(caller: Agent, id: SessionId = caller.id): string {
return this.requireManager().names.nameOf(caller, id)
}
/**
* Resolve a tree-wide name without granting authority to act on its Agent.
* @param caller - exact live Agent selecting the tree.
* @param name - a name returned by creation or discovery.
* @param signal - cancellation during historical discovery.
* @returns the durable Session identity for the consuming operation to authorize.
* @throws when the name is absent or the caller's live tree is unavailable.
*/
resolveName(caller: Agent, name: string, signal?: AbortSignal): Promise<SessionId> {
return this.requireManager().names.resolve(caller, name, signal)
}
/**
* Join progressing descendants without cancelling them. Idle descendants whose
* inboxes require a later wake stay resident and do not delay host completion.
+19 -5
View File
@@ -33,6 +33,7 @@ import type { SubagentDescriptorData } from './descriptor.ts'
import { establishCatalogChild, establishExternalCatalogChild, liveCatalogEntries } from './catalog.ts'
import type { SubagentCatalogEntry } from './projection-types.ts'
import { SubagentError } from './error.ts'
import { SubagentNames } from './names.ts'
import { isAdjacentAgentSendMessageTool } from './internal.ts'
import type { ActivationObserver } from './lifecycle.ts'
import type {
@@ -70,6 +71,7 @@ interface SubagentHost {
/** Inputs shared by fresh and resumed Activation materialization. */
interface MaterializeBase {
name: string
childId: SessionId
provider: string
parent: Agent
@@ -131,6 +133,8 @@ function failureMessage(error: unknown): string {
/** Own subagent requests and the resources of every live activation. */
export class SubagentManager {
/** Tree-wide durable names and pending creation reservations. */
readonly names: SubagentNames
/** Child session id → its live Activation. Process-local, never durable. */
private readonly resident = new Map<SessionId, Activation>()
/** Root identities retain their pool across child settlement without retaining dead roots. */
@@ -167,6 +171,7 @@ export class SubagentManager {
// the drain before releasing the scope; a cleanup effect on the same scope
// as the Agent handles would let structural handle disposal bypass
// child-first ordering.
this.names = new SubagentNames(ctx)
const scope = ctx.plugin(function activationOwner() {})
this.ownerCtx = scope.ctx
ctx.on('agent/disposed', ({ agent }) => {
@@ -387,6 +392,7 @@ export class SubagentManager {
assertSubagentMaxDepth(request.maxDepth)
const childId = spec.childId ?? brandString<SessionId>(randomUUID())
this.assertChildIdAvailable(childId)
using reservation = this.names.reserve(parent, spec.name)
const childDepth = resolveChildDepth(parent, request.maxDepth)
// Snapshot before any await: invalid descriptor JSON rejects the call
// before a child exists, and the detached value is what reaches the log.
@@ -439,6 +445,7 @@ export class SubagentManager {
}
const activation = await this.materialize({
kind: 'local',
name: spec.name,
childId,
provider: spec.provider,
parent,
@@ -462,12 +469,13 @@ export class SubagentManager {
return await this.submitMaterialized(
activation,
spec.delivery !== 'caller' && isAdjacentAgentSendMessageTool(this.ctx.get('tools')?.get('send_message', child))
? withContinuableReturnGuidance(parent.id, request.prompt)
? withContinuableReturnGuidance(this.names.nameOf(parent), spec.name, request.prompt)
: request.prompt,
{ source: { kind: 'user' }, signal: spec.signal, delivery: 'queue' },
parent,
() => {
establishCatalogChild(parent.session, childHeader, descriptor)
reservation.commit(childId)
},
)
})
@@ -495,6 +503,7 @@ export class SubagentManager {
async startExternal(spec: SubagentActivationSpec): Promise<SubagentActivation> {
const parent = spec.request.parent
this.assertAdmitting(parent)
using reservation = this.names.reserve(parent, spec.name)
const pendingId = brandString<SessionId>(randomUUID())
const releaseHold = this.holdOwnership(parent, pendingId)
let activation: Activation | undefined
@@ -502,6 +511,7 @@ export class SubagentManager {
const cwd = await this.resolveDirectory(spec.request, spec.signal)
activation = await this.materialize({
kind: 'external',
name: spec.name,
childId: pendingId,
provider: spec.provider,
parent,
@@ -517,6 +527,7 @@ export class SubagentManager {
if (spec.delivery === 'parent') {
establishExternalCatalogChild(parent.session, activation.childId, spec.label)
}
reservation.commit(activation.childId)
this.announce(activation)
return this.receipt(activation)
} catch (error: unknown) {
@@ -576,7 +587,7 @@ export class SubagentManager {
}
if (resident !== undefined && targetId !== sender.id
&& this.catalogOf(resident.parent.session).some(child => child.id === targetId && child.mode === 'continuable')) {
const message = createAgentMessage(sender, content)
const message = createAgentMessage(sender, content, this.names.nameOf(sender))
return this.deliverToChild(resident.parent, targetId, [...message.content], {
signal: options.signal,
delivery: 'steer',
@@ -942,6 +953,7 @@ export class SubagentManager {
const observer = this.host.observeActivation(provider, childId, parent)
const activation: Activation = {
name: inputs.name,
pool,
releaseSlot,
childId,
@@ -1206,7 +1218,7 @@ export class SubagentManager {
const parent = this.ctx.agents.get(activation.parent.id)
if (parent !== activation.parent) return
const message = createSettlementMessage(
activation.childId, terminal, activationAgent(activation) !== undefined,
activation.childId, activation.name, terminal, activationAgent(activation) !== undefined,
)
if (this.closingTeardownFor(parent) !== undefined) {
parent.inject(message)
@@ -1237,6 +1249,7 @@ export class SubagentManager {
private receipt(activation: Activation): SubagentActivation {
return {
childId: activation.childId,
name: activation.name,
result: activation.result.promise,
dispose: () => this.dispose(activation),
}
@@ -1317,7 +1330,7 @@ export class SubagentManager {
'PARENT_UNAVAILABLE',
)
}
const message = createAgentMessage(sender, content)
const message = createAgentMessage(sender, content, this.names.nameOf(sender))
this.sendAgentMessage(parent, message)
return message.id
}
@@ -1378,6 +1391,7 @@ export class SubagentManager {
}
activation = await this.materialize({
kind: 'local',
name: this.names.nameOf(parent, childId),
childId,
provider: descriptor.provider,
parent,
@@ -1442,7 +1456,7 @@ export class SubagentManager {
parent: Agent,
): MessageId {
const message = options.source === undefined
? createAgentMessage(parent, content)
? createAgentMessage(parent, content, this.names.nameOf(parent))
: createUserMessage({ content, source: options.source })
options.signal.throwIfAborted()
this.assertAdmitting(parent)
+164
View File
@@ -0,0 +1,164 @@
/** Durable tree-wide agent names and unpublished creation reservations.
* @module @deepseek-ai/dsh-subagent/names
*/
import type { Context } from '@deepseek-ai/cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session'
import type { Session, SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
import { z } from 'zod'
import { SubagentError } from './error.ts'
import { listDescendants } from './list-children.ts'
/** One immutable name assigned in the root Session before child publication. */
export interface SubagentNameData {
readonly name: string
readonly id: SessionId
}
declare module '@deepseek-ai/dsh-session/types' {
interface SessionEventMap {
/**
* A tree-wide agent name, retained after its execution ends.
* @param data - the name and durable child identity.
*/
'subagent/named': SubagentNameData
}
}
/** Root-owned name assignments, excluding fork-inherited events. */
export interface SubagentNamesState {
readonly inheritedEventCount: SessionLogOffset
readonly entries: readonly SubagentNameData[]
}
declare module '@deepseek-ai/dsh-session-projection/types' {
interface SessionProjectionStateMap {
subagentNames: SubagentNamesState
}
}
const nameSchema = z.string().regex(/^[a-z][a-z0-9_-]*$/).refine(value => value !== 'lead' && value !== 'unowned')
const entrySchema = z.object({ name: nameSchema, id: z.string().transform(SessionId) }).strict()
const stateSchema: z.ZodType<SubagentNamesState> = z.object({
inheritedEventCount: z.number().int().nonnegative().transform(SessionLogOffset),
entries: z.array(entrySchema),
}).strict().refine(state => new Set(state.entries.map(entry => entry.name)).size === state.entries.length
&& new Set(state.entries.map(entry => entry.id)).size === state.entries.length, 'duplicate persisted agent name or identity')
/** Host-only name projection; corrupt duplicate assignments reject restoration. */
export const subagentNamesProjectionDefinition = {
key: 'subagentNames',
stateVersion: 0,
stateSchema,
init: (_header: SessionHeader, inheritedEventCount: SessionLogOffset): SubagentNamesState => ({ inheritedEventCount, entries: [] }),
apply: (state, event: SessionEvent) => {
if (event.type !== 'subagent/named' || event.seq < state.inheritedEventCount) return state
const entry = entrySchema.parse(event.data)
if (state.entries.some(existing => existing.name === entry.name || existing.id === entry.id)) {
throw new SubagentError('duplicate persisted agent name or identity; restore a valid Session log', 'CORRUPT_AGENT_NAMES')
}
return { ...state, entries: [...state.entries, entry] }
},
} satisfies ProjectionDefinition<'subagentNames', SubagentNamesState>
/** Owns creation reservations; committed assignments come only from the root projection. */
export class SubagentNames {
private readonly pending = new WeakMap<Session, Set<string>>()
/** @param ctx - subagent service context with live Agents and Session projections. */
constructor(private readonly ctx: Context) {}
/**
* Reserve a unique name before asynchronous child preparation.
* @param parent - exact live spawning Agent.
* @param name - explicit lower-case name; lead and unowned are reserved.
* @returns a publication operation and an idempotent unpublished-reservation release.
* @throws when the name is invalid, taken, or the live tree is unavailable.
*/
reserve(parent: Agent, name: string): { commit(id: SessionId): void; [Symbol.dispose](): void } {
if (!nameSchema.safeParse(name).success) {
throw new SubagentError('agent name must start with a lower-case letter and contain only lower-case letters, digits, underscores, or hyphens; lead and unowned are reserved', 'INVALID_AGENT_NAME')
}
const root = this.rootOf(parent)
let pending = this.pending.get(root.session)
if (pending === undefined) {
pending = new Set()
this.pending.set(root.session, pending)
}
if (pending.has(name) || this.entries(root.session).some(entry => entry.name === name)) {
throw new SubagentError(`agent name "${name}" is already in use in this tree; choose another name`, 'DUPLICATE_AGENT_NAME')
}
pending.add(name)
return {
commit: (id) => {
if (this.rootOf(parent) !== root) throw new SubagentError('agent tree changed during creation', 'AGENT_TREE_UNAVAILABLE')
root.session.append('subagent/named', { name, id })
},
[Symbol.dispose]: () => { pending.delete(name) },
}
}
/**
* Read an agent's model-facing name in the caller's tree.
* @param caller - exact live Agent selecting the tree.
* @param id - target identity; defaults to the caller.
* @returns lead, an assigned name, or a stable legacy address for an older unnamed identity.
*/
nameOf(caller: Agent, id: SessionId = caller.id): string {
const root = this.rootOf(caller)
if (id === root.id) return 'lead'
return this.entries(root.session).find(entry => entry.id === id)?.name ?? `legacy:${id}`
}
/**
* Resolve a name without granting message, interrupt, or assignment authority.
* @param caller - exact live Agent selecting the tree.
* @param name - assigned name or a legacy address returned by discovery.
* @param signal - cancellation during legacy catalog discovery.
* @returns the durable identity; the consuming operation still authorizes access.
* @throws when the name is absent from the caller's tree.
*/
async resolve(caller: Agent, name: string, signal?: AbortSignal): Promise<SessionId> {
signal?.throwIfAborted()
const root = this.rootOf(caller)
if (name === 'lead') return root.id
const entries = this.entries(root.session)
const entry = entries.find(candidate => candidate.name === name)
if (entry !== undefined) return entry.id
if (name.startsWith('legacy:')) {
const id = SessionId(name.slice('legacy:'.length))
if (!entries.some(candidate => candidate.id === id)
&& (await listDescendants(this.ctx, root.id, signal)).some(candidate => candidate.id === id)) return id
}
throw new SubagentError(`agent name "${name}" was not found; use list_agents to discover available names`, 'AGENT_NAME_NOT_FOUND')
}
/** Resolve only uninterrupted live subagent ancestry, never a fork's inherited parent. */
private rootOf(caller: Agent): Agent {
if (this.ctx.agents.get(caller.id) !== caller) {
throw new SubagentError('agent naming requires the exact live caller', 'UNAUTHORIZED')
}
let root = caller
const seen = new Set<SessionId>()
while (root.session.header.origin === 'subagent') {
const parentId = root.session.header.parentSession
const parent = parentId === undefined ? undefined : this.ctx.agents.get(parentId)
seen.add(root.id)
if (parent === undefined || seen.has(parent.id)) {
throw new SubagentError('agent naming requires the live parent tree', 'AGENT_TREE_UNAVAILABLE')
}
root = parent
}
return root
}
/** Read the authoritative root projection without keeping a second name index. */
private entries(root: Session): readonly SubagentNameData[] {
const state = this.ctx.get('sessionProjections')?.stateOf(root, 'subagentNames')
if (state === undefined) throw new SubagentError('agent names require the subagentNames projection', 'CONTINUATION_UNAVAILABLE')
return state.entries
}
}
+4
View File
@@ -29,6 +29,8 @@ export function SubagentRunId(id: string): SubagentRunId {
/** One managed child execution and its result delivery policy. */
export interface SubagentActivationSpec {
/** Immutable name, unique across the parent's whole subagent tree. */
readonly name: string
/** Registered backend to use. */
readonly provider: string
/** Short task label retained in cataloged child membership. */
@@ -45,6 +47,8 @@ export interface SubagentActivationSpec {
/** A managed execution; disposal addresses this exact activation, never a later resume. */
export interface SubagentActivation {
/** Immutable name used by model-facing collaboration tools. */
readonly name: string
/** Stable identity of the child. */
readonly childId: SessionId
/** Accepted inbox message, when the backend has a local inbox. */
@@ -194,7 +194,7 @@ describe('in-process policy inheritance', () => {
.join('\n')
expect(systemText).not.toContain('Approval prompts are disabled')
expect(systemText).not.toContain('You are a delegated subagent')
expect(parent.session.snapshotEvents().filter(event => event.type !== 'subagent/catalog')).toHaveLength(parentLogLength)
expect(parent.session.snapshotEvents().filter(event => event.type !== 'subagent/catalog' && event.type !== 'subagent/named')).toHaveLength(parentLogLength)
} finally {
await run.dispose()
}
@@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts'
/**
* Continuable-child delegation policy: a fresh continuable start seeds the
@@ -98,7 +99,7 @@ describe('continuable policy inheritance', () => {
const current = vi.fn((session: Session) => session === parent.session ? preset : 'custom')
ctx.provide('permissionPresets', { current } as never)
const started = await ctx.subagents.startActivation({ ...startSpec(parent), delivery: 'parent' })
const started = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`, ...startSpec(parent), delivery: 'parent' })
await waitNoActivation(ctx, started.childId)
const loaded = await loadStoredSession(ctx.sessionPersistence, started.childId)
@@ -145,7 +146,7 @@ describe('continuable policy inheritance', () => {
current: (session: Session) => session === parent.session ? currentPreset : 'custom',
} as never)
const starting = ctx.subagents.startActivation({ ...startSpec(parent, 'fork'), delivery: 'parent' })
const starting = ctx.subagents.startActivation({ name: `child-${randomUUID()}`, ...startSpec(parent, 'fork'), delivery: 'parent' })
currentPreset = seedPreset
parent.session.append('permission/preset', { preset: seedPreset })
const started = await starting
@@ -167,7 +168,7 @@ describe('continuable policy inheritance', () => {
if (agent !== parent) child = agent
})
const started = await ctx.subagents.startActivation({ ...startSpec(parent), delivery: 'parent' })
const started = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`, ...startSpec(parent), delivery: 'parent' })
// The delegation events are appended in the creation window, so they are
// already the child's effective policy at inbox acceptance.
if (child === undefined) throw new Error('expected the continuable child to be created')
@@ -198,7 +199,7 @@ describe('continuable policy inheritance', () => {
const { ctx, parent } = await setup([textResponse('child done')])
setSandboxMode(parent.session, 'read-only')
const starting = ctx.subagents.startActivation({ ...startSpec(parent), delivery: 'parent' })
const starting = ctx.subagents.startActivation({ name: `child-${randomUUID()}`, ...startSpec(parent), delivery: 'parent' })
// A parent switch after the synchronous capture belongs to the parent's
// future, not to this child.
setSandboxMode(parent.session, 'danger-full-access')
@@ -213,7 +214,7 @@ describe('continuable policy inheritance', () => {
it('leaves an unswitched sandbox on the deployment default while still pinning approval', { timeout: 20_000 }, async () => {
const { ctx, parent } = await setup([textResponse('child done')])
const started = await ctx.subagents.startActivation({ ...startSpec(parent), delivery: 'parent' })
const started = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`, ...startSpec(parent), delivery: 'parent' })
await waitNoActivation(ctx, started.childId)
const loaded = await loadStoredSession(ctx.sessionPersistence, started.childId)
@@ -231,7 +232,7 @@ describe('continuable policy inheritance', () => {
}))
await parent.whenIdle()
const started = await ctx.subagents.startActivation({ ...startSpec(parent, 'fork'), delivery: 'parent' })
const started = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`, ...startSpec(parent, 'fork'), delivery: 'parent' })
await waitNoActivation(ctx, started.childId)
const loaded = await loadStoredSession(ctx.sessionPersistence, started.childId)
@@ -250,7 +251,7 @@ describe('continuable policy inheritance', () => {
if (agent !== parent) child = agent
})
const started = await ctx.subagents.startActivation({ ...startSpec(parent), delivery: 'parent' })
const started = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`, ...startSpec(parent), delivery: 'parent' })
if (child === undefined) throw new Error('expected the continuable child to be created')
expect(ctx.sandboxPolicy.overrideOf(child.session)).toBe('danger-full-access')
// Last event wins: the child's own runtime switch beats the seeded snapshot.
@@ -265,7 +266,7 @@ describe('continuable policy inheritance', () => {
it('cold-resumes on the persisted snapshot without re-capturing the parent', { timeout: 20_000 }, async () => {
const { ctx, parent } = await setup([textResponse('first'), textResponse('after resume')])
setSandboxMode(parent.session, 'read-only')
const started = await ctx.subagents.startActivation({ ...startSpec(parent), delivery: 'parent' })
const started = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`, ...startSpec(parent), delivery: 'parent' })
await waitNoActivation(ctx, started.childId)
// The parent widens AFTER the child was created; the resumed child keeps
@@ -303,7 +304,7 @@ describe('continuable policy inheritance', () => {
await parent.whenIdle()
setSandboxMode(parent.session, 'read-only')
const started = await ctx.subagents.startActivation({ ...startSpec(parent, 'fork'), delivery: 'parent' })
const started = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`, ...startSpec(parent, 'fork'), delivery: 'parent' })
await waitNoActivation(ctx, started.childId)
const loaded = await loadStoredSession(ctx.sessionPersistence, started.childId)
@@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
@@ -57,7 +58,7 @@ it('continues the parent through default Messages after a reasoning-bearing cont
settled.resolve(undefined)
})
const started = await ctx.subagents.startActivation({
const started = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
delivery: 'parent',
provider: 'spawn',
label: 'child task',
@@ -79,7 +80,7 @@ it('continues the parent through default Messages after a reasoning-bearing cont
expect(requests.map(request => request.path)).toEqual(['/anthropic/v1/messages', '/anthropic/v1/messages'])
const notice = parent.session.deriveMessages().find(message => message.source.kind === 'subagent-settled')
const closingContent = [
{ type: 'text', text: `Background subagent ${started.childId} finished and will do no further work unless you send it more.` },
{ type: 'text', text: `Background subagent ${started.name} finished and will do no further work unless you send it more.` },
{ type: 'text', text: 'Its closing message:' },
{ type: 'text', text: 'child answer' },
]
@@ -4,7 +4,7 @@ import { SessionId } from '@deepseek-ai/dsh-session'
import { createSettlementMessage } from '../src/continuation-messages.ts'
const childId = SessionId('settled-child')
const summary = { type: 'text', text: `Background subagent ${childId} finished and will do no further work unless you send it more.` }
const summary = { type: 'text', text: 'Background subagent reviewer finished and will do no further work unless you send it more.' }
const reasoning: ContentBlock = { type: 'reasoning', text: 'private child reasoning' }
const toolCall: ContentBlock = { type: 'tool-call', id: ToolCallId('child-call'), name: 'read', arguments: '{}' }
@@ -14,7 +14,7 @@ describe('continuable settlement content', () => {
['a tool call after the answer', [{ type: 'text', text: 'answer' }, toolCall]],
] satisfies [string, ContentBlock[]][])('reports only the closing text with %s', (_label, output) => {
const original = structuredClone(output)
const message = createSettlementMessage(childId, { stopReason: 'completed', output })
const message = createSettlementMessage(childId, 'reviewer', { stopReason: 'completed', output })
expect(message.role).toBe('user')
expect(message.content).toEqual([
@@ -30,7 +30,7 @@ describe('continuable settlement content', () => {
['reasoning-only output', [reasoning]],
['empty text', [{ type: 'text', text: '' }]],
] satisfies [string, ContentBlock[]][])('reports no closing message for %s', (_label, output) => {
const message = createSettlementMessage(childId, { stopReason: 'completed', output })
const message = createSettlementMessage(childId, 'reviewer', { stopReason: 'completed', output })
expect(message.content).toEqual([
summary,
@@ -41,7 +41,7 @@ describe('continuable settlement content', () => {
it('preserves text block order and bytes around omitted reasoning and tool calls', () => {
const first: ContentBlock = { type: 'text', text: ' first\n' }
const second: ContentBlock = { type: 'text', text: '\n第二段 ' }
const message = createSettlementMessage(childId, {
const message = createSettlementMessage(childId, 'reviewer', {
stopReason: 'completed',
output: [reasoning, first, toolCall, second],
})
@@ -56,7 +56,7 @@ describe('continuable settlement content', () => {
})
it('includes closing text alongside structured results and diagnostics', () => {
expect(createSettlementMessage(childId, {
expect(createSettlementMessage(childId, 'reviewer', {
stopReason: 'completed', output: [{ type: 'text', text: 'final answer' }],
structured: { answer: 42 }, diagnostic: 'safe diagnostic',
}).content).toEqual([
@@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
/** Independent spawn presets and compatibility of persisted child compositions. */
import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts'
@@ -80,6 +81,7 @@ function parkParent(ctx: Context, parent: Agent): void {
function startSpec(parent: Agent, agentPreset?: string, provider = 'separate') {
return {
provider,
name: `child-${randomUUID()}`,
label: 'preset work',
delivery: 'parent' as const,
request: {
File diff suppressed because it is too large. Load diff
@@ -66,7 +66,7 @@ export async function startExternalActivation(
registry.set(request.parent, parent)
}
const { label, signal, ...execution } = request
return ctx.subagents.startActivation({
return ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
provider,
label: label ?? 'External test task',
request: { ...execution, parent },
@@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts'
/** External execution ownership, result delivery, and process cleanup. */
@@ -26,7 +27,7 @@ async function setup(start: (request: SubagentStartRequest) => Promise<SubagentR
const controller = new AbortController()
return {
ctx, parent, controller,
start: (delivery: 'parent' | 'caller' = 'caller') => ctx.subagents.startActivation({
start: (delivery: 'parent' | 'caller' = 'caller') => ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
provider: 'external', label: 'External work', signal: controller.signal, delivery,
request: { parent, prompt: [{ type: 'text', text: 'Work independently' }] },
}),
@@ -102,7 +103,7 @@ describe('external subagent activations', () => {
}
})
try {
await expect(fixture.ctx.subagents.startActivation({
await expect(fixture.ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
provider: 'external', label: 'Unpublished work', signal: fixture.controller.signal, delivery: 'parent',
request: { parent, prompt: [{ type: 'text', text: 'Must not publish' }] },
})).rejects.toMatchObject({ code: change === 'drain' ? 'DRAINING' : 'UNAUTHORIZED' })
@@ -365,7 +366,7 @@ it('records external membership at creation without publishing settlement update
it('refuses a reserved Session id for an external provider before dispatch', async () => {
const start = vi.fn()
const fixture = await setup(start)
await expect(fixture.ctx.subagents.startActivation({
await expect(fixture.ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
provider: 'external', childId: SessionId('reserved'), label: 'External', delivery: 'parent',
signal: fixture.controller.signal, request: { parent: fixture.parent, prompt: [] },
})).rejects.toMatchObject({ code: 'UNSUPPORTED_CAPABILITY' })
@@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts'
/** External activation admission and resource ownership across parent lifetimes. */
@@ -25,7 +26,7 @@ async function setup(start: (request: SubagentStartRequest) => Promise<SubagentR
})
return {
ctx, parent,
start: (owner: Agent = parent) => ctx.subagents.startActivation({
start: (owner: Agent = parent) => ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
provider: 'external', label: 'External work', signal: new AbortController().signal, delivery,
request: { parent: owner, prompt: [{ type: 'text', text: 'work' }] },
}),
@@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
@@ -71,7 +72,7 @@ async function startChild(
parent: Agent,
label: string,
): Promise<SessionId> {
const started = await ctx.subagents.startActivation({ delivery: 'parent',
const started = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`, delivery: 'parent',
provider: 'spawn',
label,
request: { prompt: [{ type: 'text', text: `task: ${label}` }], parent },
@@ -36,7 +36,7 @@ export async function startTestActivation(
request: Omit<SubagentStartRequest, 'signal'> & { signal?: AbortSignal },
) {
const { signal = new AbortController().signal, label = provider, ...task } = request
const activation = await ctx.get('subagents')!.startActivation({ provider, label, request: task, signal, delivery: 'caller' })
const activation = await ctx.get('subagents')!.startActivation({ name: `child-${randomUUID()}`, provider, label, request: task, signal, delivery: 'caller' })
return { ...activation, id: activation.childId, localAgent: ctx.get('agents')!.get(activation.childId)! }
}
@@ -0,0 +1,158 @@
import { describe, expect, it, onTestFinished, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { SessionId, SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SubagentRuntime from '../src/index.ts'
import { SubagentNames, subagentNamesProjectionDefinition } from '../src/names.ts'
import { externalTestParent } from './external-activation-helpers.ts'
import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts'
import { TestSessionQuery } from './test-session-query.ts'
async function setup() {
const ctx = new Context()
const parent = await externalTestParent(ctx)
await ctx.plugin(SessionProjectionRegistry)
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(TestSessionQuery)
return { ctx, parent, names: new SubagentNames(ctx) }
}
describe('tree-wide agent names', () => {
it('reserves lead for each root and isolates independent trees', async () => {
const { ctx, parent, names } = await setup()
const other = await externalTestParent(ctx)
expect(names.nameOf(parent)).toBe('lead')
expect(await names.resolve(other, 'lead')).toBe(other.id)
using first = names.reserve(parent, 'reviewer')
first.commit(SessionId('first'))
using second = names.reserve(other, 'reviewer')
second.commit(SessionId('second'))
expect(await names.resolve(parent, 'reviewer')).toBe('first')
expect(await names.resolve(other, 'reviewer')).toBe('second')
})
it.each(['lead', 'unowned', '', 'Reviewer', 'has space', '1worker', 'legacy:old'])(
'rejects invalid or reserved creation name %j before writing a fact', async (name) => {
const { parent, names } = await setup()
const seq = parent.session.seq
expect(() => names.reserve(parent, name)).toThrow(expect.objectContaining({ code: 'INVALID_AGENT_NAME' }))
expect(parent.session.seq).toBe(seq)
},
)
it('rejects duplicates across nested parents and retains assignments after release', async () => {
const { ctx, parent, names } = await setup()
const session = ctx.sessions.create(SessionId('nested'), { meta: { origin: 'subagent', parentSession: parent.id } })
const nested = { ...parent, id: session.id, session }
onTestFinished(ctx.agents.enter(nested, undefined))
{
using reservation = names.reserve(parent, 'reviewer')
expect(() => names.reserve(nested, 'reviewer')).toThrow(expect.objectContaining({ code: 'DUPLICATE_AGENT_NAME' }))
reservation.commit(session.id)
}
expect(names.nameOf(nested)).toBe('reviewer')
expect(() => names.reserve(nested, 'reviewer')).toThrow(expect.objectContaining({ code: 'DUPLICATE_AGENT_NAME' }))
expect(await names.resolve(nested, 'lead')).toBe(parent.id)
const restoredManager = new SubagentNames(ctx)
expect(await restoredManager.resolve(parent, 'reviewer')).toBe(session.id)
expect(() => restoredManager.reserve(parent, 'reviewer')).toThrow(expect.objectContaining({ code: 'DUPLICATE_AGENT_NAME' }))
})
it('releases failed startup reservations and refuses a concurrent duplicate before provider dispatch', async () => {
const { ctx, parent } = await setup()
const entered = Promise.withResolvers<undefined>()
const gate = Promise.withResolvers<undefined>()
const start = vi.fn(async () => {
entered.resolve(undefined)
await gate.promise
throw new Error('provider failed')
})
ctx.subagents.registerProvider({
name: 'external', inheritsParentContext: false,
capabilities: { agentOptions: false, agentPreset: false, outputSchema: false, depthLimit: false },
start,
})
const spec = {
name: 'reviewer', provider: 'external', label: 'review', delivery: 'caller' as const,
request: { parent, prompt: [{ type: 'text' as const, text: 'Review' }] },
signal: new AbortController().signal,
}
const first = ctx.subagents.startActivation(spec)
const rejected = expect(first).rejects.toThrow('provider failed')
try {
await entered.promise
await expect(ctx.subagents.startActivation(spec)).rejects.toMatchObject({ code: 'DUPLICATE_AGENT_NAME' })
expect(start).toHaveBeenCalledTimes(1)
} finally {
gate.resolve(undefined)
await rejected
}
await expect(ctx.subagents.startActivation(spec)).rejects.toThrow('provider failed')
expect(start).toHaveBeenCalledTimes(2)
expect(parent.session.snapshotEvents().filter(event => event.type === 'subagent/named')).toEqual([])
})
it('resolves historical addresses only for unnamed descendants in the same tree', async () => {
const { ctx, parent, names } = await setup()
const child = ctx.sessions.create(SessionId('old-child'), { meta: { origin: 'subagent', parentSession: parent.id } })
parent.session.append('subagent/catalog', {
version: 0, childId: child.id, childCreatedAt: 0, mode: 'continuable', label: 'Old child',
})
expect(names.nameOf(parent, child.id)).toBe('legacy:old-child')
expect(await names.resolve(parent, 'legacy:old-child')).toBe(child.id)
await expect(names.resolve(parent, 'legacy:missing')).rejects.toMatchObject({ code: 'AGENT_NAME_NOT_FOUND' })
await expect(names.resolve(parent, 'missing')).rejects.toMatchObject({ code: 'AGENT_NAME_NOT_FOUND' })
using reservation = names.reserve(parent, 'reviewer')
reservation.commit(child.id)
await expect(names.resolve(parent, 'legacy:old-child')).rejects.toMatchObject({ code: 'AGENT_NAME_NOT_FOUND' })
const aborted = AbortSignal.abort(new Error('cancel lookup'))
await expect(names.resolve(parent, 'lead', aborted)).rejects.toThrow('cancel lookup')
})
it('rejects stale callers and a detached child instead of treating it as a new root', async () => {
const { ctx, parent, names } = await setup()
expect(() => names.nameOf({ ...parent })).toThrow(expect.objectContaining({ code: 'UNAUTHORIZED' }))
const session = ctx.sessions.create(SessionId('orphan'), {
meta: { origin: 'subagent', parentSession: SessionId('missing') },
})
const child = { ...parent, id: session.id, session }
onTestFinished(ctx.agents.enter(child, undefined))
expect(() => names.nameOf(child)).toThrow(expect.objectContaining({ code: 'AGENT_TREE_UNAVAILABLE' }))
})
it('refuses publication after ancestor replacement and missing naming state', async () => {
const { ctx, parent, names } = await setup()
const session = ctx.sessions.create(SessionId('nested'), { meta: { origin: 'subagent', parentSession: parent.id } })
const child = { ...parent, id: session.id, session }
onTestFinished(ctx.agents.enter(child, undefined))
using reservation = names.reserve(child, 'reviewer')
const get = ctx.agents.get.bind(ctx.agents)
const replacement = { ...parent }
const lookup = vi.spyOn(ctx.agents, 'get').mockImplementation(id => id === parent.id ? replacement : get(id))
expect(() => { reservation.commit(SessionId('new-child')) }).toThrow(expect.objectContaining({ code: 'AGENT_TREE_UNAVAILABLE' }))
lookup.mockRestore()
const state = vi.spyOn(ctx.sessionProjections, 'stateOf').mockReturnValue(undefined)
expect(() => names.reserve(parent, 'other')).toThrow(expect.objectContaining({ code: 'CONTINUATION_UNAVAILABLE' }))
state.mockRestore()
const orphan = ctx.sessions.create(SessionId('no-parent'), { meta: { origin: 'subagent' } })
const detached = { ...parent, id: orphan.id, session: orphan }
onTestFinished(ctx.agents.enter(detached, undefined))
expect(() => names.nameOf(detached)).toThrow(expect.objectContaining({ code: 'AGENT_TREE_UNAVAILABLE' }))
})
it('ignores fork-inherited names and refuses duplicate durable names or identities', async () => {
const { parent } = await setup()
const event = { type: 'subagent/named' as const, seq: SessionSeq(0), time: 0, data: { name: 'reviewer', id: SessionId('child') } }
const inherited = subagentNamesProjectionDefinition.init(parent.session.header, SessionLogOffset(1))
expect(subagentNamesProjectionDefinition.apply(inherited, event)).toBe(inherited)
const empty = subagentNamesProjectionDefinition.init(parent.session.header, SessionLogOffset(0))
const state = subagentNamesProjectionDefinition.apply(empty, event)
expect(subagentNamesProjectionDefinition.stateSchema.parse(JSON.parse(JSON.stringify(state)))).toEqual(state)
for (const data of [{ name: 'reviewer', id: SessionId('other') }, { name: 'writer', id: SessionId('child') }]) {
expect(subagentNamesProjectionDefinition.stateSchema.safeParse({ ...state, entries: [...state.entries, data] }).success).toBe(false)
expect(() => subagentNamesProjectionDefinition.apply(state, { ...event, data, seq: SessionSeq(1) }))
.toThrow(expect.objectContaining({ code: 'CORRUPT_AGENT_NAMES' }))
}
})
})
@@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts'
/** Parent result delivery independent of a local child's messaging tool. */
@@ -24,7 +25,7 @@ it.each(['absent', 'unused'] as const)('returns the closing answer when send_mes
const adapter = new MockAdapter([textResponse('CHILD_RESULT_42'), textResponse('Parent received the answer.')])
ctx.llm.registerAdapter(['mock'], adapter)
const parent = await ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
const activation = await ctx.subagents.startActivation({
const activation = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
provider: 'spawn', label: 'Compute the answer', delivery: 'parent', signal: new AbortController().signal,
request: {
parent, prompt: [{ type: 'text', text: 'Find the answer.' }],
@@ -58,7 +59,7 @@ it('delivers structured output to the parent when the child has send_message', a
])
ctx.llm.registerAdapter(['mock'], adapter)
const parent = await ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
const activation = await ctx.subagents.startActivation({
const activation = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
provider: 'spawn', label: 'Compute the answer', delivery: 'parent', signal: new AbortController().signal,
request: {
parent, prompt: [{ type: 'text', text: 'Find the answer.' }],
@@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts'
/** Host completion races around idle local children with preserved input. */
import { setImmediate } from 'node:timers/promises'
@@ -49,7 +50,7 @@ async function setup() {
}
function start(owner = parent, provider = 'local') {
const started = ctx.subagents.startActivation({
const started = ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
provider,
label: 'child task',
delivery: 'caller',
@@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts'
/**
* A preset-mounted Schedule tool reaches a delegated child, and the Host
@@ -93,7 +94,7 @@ function toolResultTexts(agent: Agent): string[] {
describe('a delegated child under a preset that mounts the Schedule tools', () => {
it('sees schedule_create and is refused with subagent_session without storing a task', async () => {
const { ctx, parent, dispatched } = await setupScheduleHost()
const run = await ctx.subagents.startActivation({
const run = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
provider: 'spawn',
label: 'child task',
delivery: 'caller',
@@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts'
import { describe, expect, expectTypeOf, it, onTestFinished, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
@@ -98,7 +99,7 @@ async function start(subagents: SubagentRuntime, provider: string, request: Suba
const parent = ctx.agents.get(request.parent.id)
?? (await ctx.agents.create({ sessionId: request.parent.id })).agent
const { label, signal, ...options } = request
return subagents.startActivation({
return subagents.startActivation({ name: `child-${randomUUID()}`,
provider, label: label ?? provider, request: { ...options, parent }, signal, delivery,
})
}
@@ -205,7 +206,7 @@ describe('SubagentRuntime', () => {
it('rejects continuable operations when their runtime services are absent', async () => {
const { subagents } = await service(false)
await expect(subagents.startActivation({
await expect(subagents.startActivation({ name: `child-${randomUUID()}`,
delivery: 'parent',
provider: 'unused',
label: 'unused child',
@@ -224,7 +225,7 @@ describe('SubagentRuntime', () => {
const { subagents } = await service(false)
const provider = new StubProvider('external')
subagents.registerProvider(provider)
await expect(subagents.startActivation({
await expect(subagents.startActivation({ name: `child-${randomUUID()}`,
provider: provider.name,
label: 'Unavailable runtime',
request: baseRequest(),
@@ -252,7 +253,7 @@ describe('SubagentRuntime', () => {
const provider = new StubProvider('external')
subagents.registerProvider(provider)
const { agent: parent } = await ctx.agents.create({ sessionId: SessionId('reserved-parent') })
await expect(subagents.startActivation({
await expect(subagents.startActivation({ name: `child-${randomUUID()}`,
provider: provider.name,
label: 'Reserved team child',
childId: SessionId('reserved-child'),
@@ -648,7 +649,7 @@ it('rejects a disposed parent before starting provider execution', async () => {
subagents.registerProvider(provider)
const handle = await ctx.agents.create({ sessionId: SessionId('disposed-parent') })
await handle.dispose()
await expect(subagents.startActivation({
await expect(subagents.startActivation({ name: `child-${randomUUID()}`,
provider: provider.name, label: 'work', delivery: 'caller', signal: new AbortController().signal,
request: { parent: handle.agent, prompt: [] },
})).rejects.toMatchObject({ code: 'UNAUTHORIZED' })
@@ -78,7 +78,7 @@ const signal = new AbortController().signal
/** Start one caller-delivery child whose history carries `TASK:<marker>`. */
async function start(ctx: Context, parent: Agent, marker: string, description = 'shared responsibility'): Promise<SessionId> {
const started = await ctx.subagents.startActivation({
const started = await ctx.subagents.startActivation({ name: marker,
provider: 'spawn',
label: description,
request: { prompt: [{ type: 'text', text: `TASK:${marker}` }], parent },
@@ -128,7 +128,7 @@ describe('subagent sibling messages', () => {
await ctx.subagents.sendMessage(sender, b, [{ type: 'text', text: 'hello' }], { signal })
const delivered = (await storedUserMessages(ctx, b)).find(event => event.data.source.kind === 'agent-message')!
expect(textOf(delivered)).toBe(`Agent ${a} sent a message: hello`)
expect(textOf(delivered)).toBe('Agent a sent a message: hello')
expect(delivered.data.source).toEqual({ kind: 'agent-message', form: 'relay', senderSessionId: a })
ctx.subagents.interrupt(a, { kind: 'ancestor', agent: parent })
await settled(ctx, a)
@@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts'
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
@@ -386,7 +387,7 @@ describe('in-process structured output', () => {
name: 'start_descendant', description: 'Start delegated work', parameters: {},
async execute(_args, exec): Promise<ContentBlock[]> {
if (exec.agent === undefined) throw new Error('expected the structured parent')
descendant = await ctx.subagents.startActivation({
descendant = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
provider: 'spawn', label: 'Nested work', signal: testToolSignal, delivery: 'parent',
request: {
parent: exec.agent, prompt: [{ type: 'text', text: 'Work until released' }],
@@ -450,7 +451,7 @@ describe('in-process structured output', () => {
name: 'delegate_answer', description: 'Delegate answer computation', parameters: {},
async execute(_args, exec): Promise<ContentBlock[]> {
if (exec.agent === undefined) throw new Error('expected the structured parent')
await ctx.subagents.startActivation({
await ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
provider: 'delayed-answer', label: 'Compute answer', signal: testToolSignal, delivery: 'parent',
request: { parent: exec.agent, prompt: [{ type: 'text', text: 'Compute the answer' }] },
})
@@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
import { startTestActivation } from './local-activation.ts'
import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts'
import { mkdtemp, mkdir, realpath, rm } from 'node:fs/promises'
@@ -68,7 +69,7 @@ describe('subagent working directories', () => {
it.each(['relative', 'absolute'])('retains an explicit %s directory when a continuable child cold-resumes', async (form) => {
const { ctx, parent, origin, first, second } = await fixture()
const selected = await realpath(second)
const started = await ctx.subagents.startActivation({
const started = await ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
delivery: 'caller',
provider: 'spawn', label: 'directory child',
request: { parent, cwd: form === 'absolute' ? second : 'second', prompt: [{ type: 'text', text: 'child' }] },
@@ -96,7 +97,7 @@ describe('subagent working directories', () => {
setup: (childCtx, child) => options.setup?.(childCtx.isolate('workingDirectory'), child),
}))
try {
await expect(ctx.subagents.startActivation({
await expect(ctx.subagents.startActivation({ name: `child-${randomUUID()}`,
delivery: 'caller',
provider: 'spawn', label: 'unavailable directory service', childId,
request: { parent, cwd: second, prompt: [{ type: 'text', text: 'child' }] },
@@ -21,23 +21,23 @@
en: 4a52c81317ec9001
zh: ebd9f1ba920da50a
/deepseek-ai-dsh-tool-subagent-control/use-this-package/send-message:
en: b160d6a41a73d64f
zh: 902a8e74db0c17a0
en: 91882e24297b19f2
zh: 4d7bd27e76e825e7
/deepseek-ai-dsh-tool-subagent-control/use-this-package/interrupt-agent:
en: 4ccd504f8886d8b0
zh: 480ab8cba905ac02
en: caf1c74171c10bf9
zh: 5031c99d8724e2b1
/deepseek-ai-dsh-tool-subagent-control/use-this-package/list-agents:
en: 22cd16d544be9f7f
zh: 52aa3655d8091ec4
en: 3b6b212b133f94ce
zh: 725d4875bda0ce22
/deepseek-ai-dsh-tool-subagent-control/understand-the-implementation:
en: c5cdf01a78c990f2
zh: 53c180d726ce4502
/deepseek-ai-dsh-tool-subagent-control/understand-the-implementation/design-concept:
en: 18102f0d2d227c32
zh: 0fa0350be29f9353
en: 8d317409d4a6029c
zh: 045538831dbaa0bb
/deepseek-ai-dsh-tool-subagent-control/understand-the-implementation/delivery-and-signal-ownership:
en: b596c53c18b3e7f5
zh: bd17f4216a3a6301
en: 6f47d91f63f8007c
zh: 1a3c63e18ece210b
/deepseek-ai-dsh-tool-subagent-control/understand-the-implementation/listing-projection:
en: 05a40d0cd5acd045
zh: 1914d078d4263753
@@ -54,8 +54,8 @@
en: e4c7376d4f29dbd5
zh: 6b7a9eaf69c9ea53
/deepseek-ai-dsh-tool-subagent-control/model-experience/tool-schema/what-the-model-sees:
en: b558efb37bc78acf
zh: 8534c438edd306cd
en: 8a397d9426162983
zh: cac68d77712bb321
/deepseek-ai-dsh-tool-subagent-control/model-experience/tool-schema/token-effect:
en: 282c65ac0eebfbdd
zh: e9610d49f72918b0
@@ -66,8 +66,8 @@
en: d64e936e83e89a2b
zh: b9905c33af6cca45
/deepseek-ai-dsh-tool-subagent-control/model-experience/interrupt-result/what-the-model-sees:
en: 89305619897680ba
zh: e610a844fc09c53f
en: f62d986ac7c1f6cc
zh: 047e2f72a7e34614
/deepseek-ai-dsh-tool-subagent-control/model-experience/interrupt-result/token-effect:
en: 5d1bcef28b5f18da
zh: 027d67d93d660e76
@@ -78,11 +78,11 @@
en: ab964e307bd37dca
zh: 03f975252f838a42
/deepseek-ai-dsh-tool-subagent-control/model-experience/delivery-result/what-the-model-sees:
en: c562b973653a1465
zh: 543e0877d9620114
en: cbce90fab7d1700a
zh: 0b68d13bb4d4e03d
/deepseek-ai-dsh-tool-subagent-control/model-experience/delivery-result/token-effect:
en: fba7eb5d4c53013a
zh: 4e481bbb133c8263
en: 9fcc3cae6b23815d
zh: fcc2a9fe0dcb2909
/deepseek-ai-dsh-tool-subagent-control/model-experience/delivery-result/kv-cache-effect:
en: f5d5f97a153f7c70
zh: 3d6bc05b9eadfbc2
@@ -45,15 +45,15 @@ This package takes no configuration: the root plugin provides `send_message` and
### send_message
Sends a message to the Session id in `agent_id`. Any exact live Agent may target its direct continuable child; a resident continuable child may also target its direct parent or continuable siblings. Sibling delivery uses the shared live parent’s authority and preserves the actual sender. A working target receives the message at its nearest step boundary through Steer; an inactive child or sibling starts or resumes a turn. Success returns inbox acceptance (`messageId`), never a reply. An unsupported target, unavailable parent, unknown child, missing descriptor, or rejected admission returns an error.
Sends a message to the agent `name`. Any exact live Agent may target its direct continuable child; a resident continuable child may also target its direct parent or continuable siblings. Sibling delivery uses the shared live parent’s authority and preserves the actual sender. A working target receives the message at its nearest step boundary through Steer; an inactive child or sibling starts or resumes a turn. Success returns inbox acceptance (`messageId`), never a reply. An unsupported target, unavailable parent, unknown name, missing descriptor, or rejected admission returns an error.
### interrupt_agent
`agent_id` identifies a descendant by Session id. External executions stop permanently and cannot receive follow-ups. Local targets stop only their current turn: queued messages stay parked until a later `send_message`, descendants keep running, and the child stays available for follow-ups. The call returns when the stop request is accepted, not when the target is quiet; interrupting an already-finished agent is an accepted no-op, and self, sibling, stale, and non-ancestor callers get errored results.
`name` identifies a descendant. External executions stop permanently and cannot receive follow-ups. Local targets stop only their current turn: queued messages stay parked until a later `send_message`, descendants keep running, and the child stays available for follow-ups. The call returns when the stop request is accepted, not when the target is quiet; interrupting an already-finished agent is an accepted no-op, and self, sibling, stale, and non-ancestor callers get errored results.
### list_agents
Lists all reachable continuable descendants in stable pre-order, with each row’s direct parent id and depth. A continuable subagent with a live parent first sees that parent and its continuable siblings. Live status is `running` or `inactive`. External entries are omitted without reading child Sessions; readable one-shot children are traversal nodes but not output rows. Unknown modes and unreadable child catalogs produce diagnostics. Ordinary Session forks are not catalog entries.
Lists all reachable continuable descendants in stable pre-order, with each row’s direct parent name and depth. A continuable subagent with a live parent first sees that parent and its continuable siblings. Live status is `running` or `inactive`. External entries are omitted without reading child Sessions; readable one-shot children are traversal nodes but not output rows. Unknown modes and unreadable child catalogs produce diagnostics. Ordinary Session forks are not catalog entries.
-----
@@ -67,11 +67,11 @@ This section explains what the tools delegate to the subagent service; the obser
### Design concept
Thin adapters over `ctx.subagents.sendMessage()`, `interrupt()`, and the list projections; residency, cold resume, and authorization belong to the service. Tools pass the exact live calling Agent (`exec.agent`) as sender and authority, and convert `agent_id` to the branded Session id.
Thin adapters resolve names through `ctx.subagents.resolveName()` before calling `sendMessage()` or `interrupt()`. Residency, cold resume, and authorization belong to the service. Tools pass the exact live calling Agent (`exec.agent`) as sender and authority.
### Delivery and signal ownership
The tool forwards its execution signal, which owns admission only until inbox acceptance. Accepted messages cannot be cancelled through this tool. Every message is framed as `Agent <sender-id> sent a message:` and recorded with `{ kind: 'agent-message', form: 'relay', senderSessionId: sender.id }`, including sibling delivery under the parent’s authority. Attribution never grants authority.
The tool forwards its execution signal, which owns admission only until inbox acceptance. Accepted messages cannot be cancelled through this tool. Every message is framed as `Agent <sender-name> sent a message:` and recorded with `{ kind: 'agent-message', form: 'relay', senderSessionId: sender.id }`, including sibling delivery under the parent’s authority. Attribution never grants authority.
### Listing projection
@@ -106,7 +106,7 @@ Read these pages when the package-level contract is not enough; they move from t
#### What the model sees
The generated [schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent-control): `send_message` takes an `agent_id` and `message`; `interrupt_agent` takes an `agent_id`; `list_agents` takes no arguments. All addresses are Session ids; descriptions are display text and may repeat.
The generated [schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent-control): `send_message` takes `name` and `message`; `interrupt_agent` takes `name`; `list_agents` takes no arguments. The root is `lead`; descriptions may repeat. Older unnamed agents have stable `legacy:<session-id>` addresses.
#### Token effect
@@ -120,7 +120,7 @@ Prefix-stable; the schemas do not change at runtime, and a parent and its contin
#### What the model sees
`interrupt requested for agent <agent_id>` on acceptance. An unauthorized caller — self, sibling, stale, or non-ancestor — is an errored result naming the rejection; an absent or settled target still renders the acceptance line.
`interrupt requested for agent <name>` on acceptance. An unknown name or unauthorized caller — self, sibling, stale, or non-ancestor — returns an error; a known settled target still renders the acceptance line.
#### Token effect
@@ -134,11 +134,11 @@ Append-only; each result follows the reusable request prefix.
#### What the model sees
`message delivered to agent <agent_id>` on acceptance; canonical output carries the accepted `messageId`. A target outside the caller’s supported parent, child, and sibling routes, or rejected admission, produces an errored result.
`message delivered to agent <name>` on acceptance; canonical output carries the accepted `messageId`. A target outside the caller’s supported parent, child, and sibling routes, or rejected admission, produces an errored result.
#### Token effect
One short acknowledgement per call; the target’s response never returns through this call. Children use parent and sibling ids from `list_agents` for follow-up messages.
One short acknowledgement per call; the target’s response never returns through this call. Children use parent and sibling names from `list_agents` for follow-up messages.
#### KV Cache effect
@@ -45,15 +45,15 @@ kind: "package-reference"
### send_message
向 `agent_id` 中的 Session id 发消息。确切在线的 Agent 可联系直接可继续子级;驻留的可继续子级还可联系直接父级或可继续同级。同级投递使用共同在线父级的权限,并保留实际发送方。工作中的目标通过 Steer 在最近步骤边界接收消息;不活跃的子级或同级会启动或恢复轮次。成功返回收件箱接受标识(`messageId`),而非回复。不支持的目标、不可用的父级、未知子级、缺失描述符或被拒绝的准入均返回错误。
向 agent 的 `name` 发消息。确切在线的 Agent 可联系直接可继续子级;驻留的可继续子级还可联系直接父级或可继续同级。同级投递使用共同在线父级的权限,并保留实际发送方。工作中的目标通过 Steer 在最近步骤边界接收消息;不活跃的子级或同级会启动或恢复轮次。成功返回收件箱接受标识(`messageId`),而非回复。不支持的目标、不可用的父级、未知名字、缺失描述符或被拒绝的准入均返回错误。
### interrupt_agent
`agent_id` 使用 Session id 指定后代。外部执行永久停止,不能接收后续消息。本地目标只停止当前轮次:排队消息保留到后续 `send_message`,后代继续运行,子级仍可接受后续任务。调用在停止请求被接受时返回,不等待目标静止;中断已结束代理是成功的空操作,自身、同级、过期或非祖先调用方会收到错误。
`name` 指定后代。外部执行永久停止,不能接收后续消息。本地目标只停止当前轮次:排队消息保留到后续 `send_message`,后代继续运行,子级仍可接受后续任务。调用在停止请求被接受时返回,不等待目标静止;中断已结束代理是成功的空操作,自身、同级、过期或非祖先调用方会收到错误。
### list_agents
按稳定前序列出全部可达的可继续后代,每行带直接父级 id 和深度。父级在线的可继续子代理先看到父级及可继续同级。在线状态为 `running` 或 `inactive`。外部条目无需读取子 Session 即被省略;可读的一次性子级用于遍历,但不输出行。未知模式和无法读取的子级目录产生诊断。普通 Session 分叉不是目录条目。
按稳定前序列出全部可达的可继续后代,每行带直接父级名字 和深度。父级在线的可继续子代理先看到父级及可继续同级。在线状态为 `running` 或 `inactive`。外部条目无需读取子 Session 即被省略;可读的一次性子级用于遍历,但不输出行。未知模式和无法读取的子级目录产生诊断。普通 Session 分叉不是目录条目。
-----
@@ -67,11 +67,11 @@ kind: "package-reference"
### 设计理念
这些工具是 `ctx.subagents.sendMessage()`、`interrupt()` 和列表投影的薄适配层;驻留、冷恢复和授权属于服务。工具使用确切在线的调用 Agent(`exec.agent`)作为发送方和权限凭据,并将 `agent_id` 转为品牌化 Session id。
工具先通过 `ctx.subagents.resolveName()` 解析名字,再调用 `sendMessage()` 或 `interrupt()`。驻留、冷恢复和授权属于服务。工具以确切在线的调用 Agent(`exec.agent`)作为发送方和权限凭据。
### 投递与信号所有权
工具转发执行信号,它只在收件箱接受前控制准入。已接受的消息不能通过此工具取消。每条消息以 `Agent <sender-id> sent a message:` 开头,并记录 `{ kind: 'agent-message', form: 'relay', senderSessionId: sender.id }`,使用父级权限的同级投递也一样。归属信息不授予权限。
工具转发执行信号,它只在收件箱接受前控制准入。已接受的消息不能通过此工具取消。每条消息以 `Agent <sender-name> sent a message:` 开头,并记录 `{ kind: 'agent-message', form: 'relay', senderSessionId: sender.id }`,使用父级权限的同级投递也一样。归属信息不授予权限。
### 列表投影
@@ -106,7 +106,7 @@ kind: "package-reference"
#### 模型看到什么
生成的[模式](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-subagent-control):`send_message` 接收 `agent_id` 和 `message`;`interrupt_agent` 接收 `agent_id`;`list_agents` 不接收参数。地址统一为 Session id;描述用于展示,可以重复。
生成的 [schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-subagent-control):`send_message` 接收 `name` 与 `message`;`interrupt_agent` 接收 `name`;`list_agents` 无参数。根名为 `lead`;描述可以重复。旧的无名 agent 使用稳定的 `legacy:<session-id>` 地址。
#### Token 影响
@@ -120,7 +120,7 @@ kind: "package-reference"
#### 模型看到什么
接受时返回 `interrupt requested for agent <agent_id>`。未授权的调用方——self、sibling、陈旧或非 ancestor——会成为指明拒绝原因的出错结果;目标不存在或已结算仍渲染接受行。
接受后返回 `interrupt requested for agent <name>`。未知名字或未授权调用方(自身、同级、过期或非祖先)返回错误;已知且已结束的目标仍返回接受提示。
#### Token 影响
@@ -134,11 +134,11 @@ kind: "package-reference"
#### 模型看到什么
接受后返回 `message delivered to agent <agent_id>`;规范输出携带已接受的 `messageId`。目标不在调用方支持的父级、子级或同级路由内,或准入被拒绝时,返回错误结果。
接受后返回 `message delivered to agent <name>`;规范输出携带已接受的 `messageId`。目标不在调用方支持的父级、子级或同级路由内,或准入被拒绝时,返回错误结果。
#### Token 影响
每次调用一条简短确认;目标回复不通过此调用返回。子级使用 `list_agents` 提供的父级和同级 id 发送后续消息。
每次调用一条简短确认;目标回复不通过此调用返回。子级使用 `list_agents` 提供的父级和同级名字 发送后续消息。
#### KV Cache 影响
@@ -10,10 +10,8 @@
*/
import type { Context } from '@deepseek-ai/cordis'
import { brandString } from '@deepseek-ai/dsh-brand'
import { defineTool } from '@deepseek-ai/dsh-tools'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { SessionId } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-subagent'
import { markAdjacentAgentSendMessageTool } from '@deepseek-ai/dsh-subagent/internal'
@@ -31,10 +29,10 @@ export function apply(ctx: Context): void {
'Send a message to an agent. A working agent receives it at its next step; an idle agent starts a new turn with it. '
+ 'Returns delivery confirmation, not the agent\'s answer.',
parameters: {
agent_id: {
name: {
type: 'string',
required: true,
description: 'The agent id of your direct continuable child, or your direct parent or continuable sibling when you are a resident continuable child.',
description: 'The name of your direct continuable child, or your direct parent or continuable sibling when you are a resident continuable child.',
},
message: {
type: 'string',
@@ -52,7 +50,7 @@ export function apply(ctx: Context): void {
},
render: (args, _value) => [{
type: 'text',
text: `message delivered to agent ${args.agent_id}`,
text: `message delivered to agent ${args.name}`,
}],
},
async execute(args, exec) {
@@ -63,7 +61,7 @@ export function apply(ctx: Context): void {
const message: ContentBlock[] = [{ type: 'text', text: args.message }]
const messageId = await ctx.subagents.sendMessage(
sender,
brandString<SessionId>(args.agent_id),
await ctx.subagents.resolveName(sender, args.name, exec.signal),
message,
{ signal: exec.signal },
)
@@ -78,10 +76,10 @@ export function apply(ctx: Context): void {
+ 'You can continue a local direct child\'s conversation later with send_message. '
+ 'External executions stop permanently and cannot receive follow-ups. Subagents it started will keep running.',
parameters: {
agent_id: {
name: {
type: 'string',
required: true,
description: 'The id of an agent created under you: your direct child or a deeper descendant.',
description: 'The name of an agent created under you: your direct child or a deeper descendant.',
},
},
output: {
@@ -94,10 +92,10 @@ export function apply(ctx: Context): void {
},
render: (args, _value) => [{
type: 'text',
text: `interrupt requested for agent ${args.agent_id}`,
text: `interrupt requested for agent ${args.name}`,
}],
},
execute(args, exec) {
async execute(args, exec) {
const caller = exec.agent
if (!caller) {
// Ancestor authority requires an exact live calling agent.
@@ -105,8 +103,8 @@ export function apply(ctx: Context): void {
}
// The service authorizes the exact live caller against the target's
// recorded lineage; the tool adds no authority of its own.
ctx.subagents.interrupt(brandString<SessionId>(args.agent_id), { kind: 'ancestor', agent: caller })
return Promise.resolve({ accepted: true })
ctx.subagents.interrupt(await ctx.subagents.resolveName(caller, args.name, exec.signal), { kind: 'ancestor', agent: caller })
return { accepted: true }
},
}))
}
@@ -19,22 +19,22 @@ export const inject = ['tools', 'subagents', 'agents']
type ListAgentsEntry =
| {
readonly kind: 'parent'
readonly id: SessionId
readonly name: string
readonly status: 'running' | 'inactive'
}
| {
readonly kind: 'child' | 'sibling'
readonly id: SessionId
readonly name: string
readonly label: string
readonly status: 'running' | 'inactive'
readonly parent?: SessionId
readonly parent?: string
readonly depth?: number
}
| {
readonly kind: 'diagnostic'
readonly id: SessionId
readonly name: string
readonly reason: 'corrupt' | 'unsupported' | 'unavailable'
readonly parent?: SessionId
readonly parent?: string
readonly depth?: number
}
@@ -47,17 +47,18 @@ function statusOf(agents: { get(id: SessionId): Agent | undefined }, id: Session
function project(
agents: { get(id: SessionId): Agent | undefined },
entry: SubagentDescendantListEntry,
nameOf: (id: SessionId) => string,
): ListAgentsEntry | undefined {
const at = { parent: entry.parentId, depth: entry.depth }
const at = { parent: nameOf(entry.parentId), depth: entry.depth }
if (entry.kind === 'diagnostic') {
return { kind: 'diagnostic', id: entry.id, reason: entry.reason, ...at }
return { kind: 'diagnostic', name: nameOf(entry.id), reason: entry.reason, ...at }
}
// One-shot children cannot be continued by send_message, so the model
// never selects them; discovery still traversed them for descendants.
if (entry.mode !== 'continuable') return undefined
return {
kind: 'child',
id: entry.id,
name: nameOf(entry.id),
label: entry.label,
status: statusOf(agents, entry.id),
...at,
@@ -77,13 +78,13 @@ async function family(ctx: Context, caller: Agent, signal: AbortSignal): Promise
? ctx.agents.get(header.parentSession)
: undefined
if (parent === undefined) return []
const rows: ListAgentsEntry[] = [{ kind: 'parent', id: parent.id, status: statusOf(ctx.agents, parent.id) }]
const rows: ListAgentsEntry[] = [{ kind: 'parent', name: ctx.subagents.nameOf(caller, parent.id), status: statusOf(ctx.agents, parent.id) }]
const children = await ctx.subagents.listChildren(parent.id, signal)
const own = children.find(entry => entry.id === caller.id)
if (own?.mode !== 'continuable') return []
for (const entry of children) {
if (entry.id === caller.id || entry.mode !== 'continuable') continue
rows.push({ kind: 'sibling', id: entry.id, label: entry.label, status: statusOf(ctx.agents, entry.id) })
rows.push({ kind: 'sibling', name: ctx.subagents.nameOf(caller, entry.id), label: entry.label, status: statusOf(ctx.agents, entry.id) })
}
return rows
}
@@ -97,7 +98,7 @@ export function apply(ctx: Context): void {
name: 'list_agents',
description:
'List all your reachable continuable descendants and, when you are a continuable subagent, your parent and continuable siblings. '
+ 'Rows carry ids, descriptions, and status; descendant rows also carry parent ids and depth. '
+ 'Rows carry names, descriptions, and status; descendant rows also carry parent names and depth. '
+ 'You can message your parent, continuable siblings, and direct children. Deeper descendants accept only interrupt_agent. '
+ 'running means it is working; inactive means it is not currently working. '
+ 'You will be notified when a direct child finishes; there is no need to keep checking its status. '
@@ -113,7 +114,7 @@ export function apply(ctx: Context): void {
additionalProperties: false,
properties: {
kind: { type: 'string', required: true, enum: ['parent'] },
id: { type: 'string', required: true },
name: { type: 'string', required: true },
status: { type: 'string', required: true, enum: ['running', 'inactive'] },
},
},
@@ -122,7 +123,7 @@ export function apply(ctx: Context): void {
additionalProperties: false,
properties: {
kind: { type: 'string', required: true, enum: ['child', 'sibling'] },
id: { type: 'string', required: true },
name: { type: 'string', required: true },
label: { type: 'string', required: true },
status: { type: 'string', required: true, enum: ['running', 'inactive'] },
parent: { type: 'string' },
@@ -134,7 +135,7 @@ export function apply(ctx: Context): void {
additionalProperties: false,
properties: {
kind: { type: 'string', required: true, enum: ['diagnostic'] },
id: { type: 'string', required: true },
name: { type: 'string', required: true },
reason: { type: 'string', required: true, enum: ['corrupt', 'unsupported', 'unavailable'] },
parent: { type: 'string' },
depth: { type: 'number' },
@@ -149,12 +150,12 @@ export function apply(ctx: Context): void {
text: entries.length === 0
? '(no subagents)'
: entries.map((entry) => {
if (entry.kind === 'parent') return `${entry.id} [${entry.status}] — your parent`
if (entry.kind === 'sibling') return `${entry.id} [${entry.status}] — sibling: ${entry.label}`
if (entry.kind === 'parent') return `${entry.name} [${entry.status}] — your parent`
if (entry.kind === 'sibling') return `${entry.name} [${entry.status}] — sibling: ${entry.label}`
const at = ` parent=${String(entry.parent)} depth=${String(entry.depth)}`
return entry.kind === 'diagnostic'
? `${entry.id} [diagnostic: ${entry.reason}]${at}`
: `${entry.id} [${entry.status}]${at} — ${entry.label}`
? `${entry.name} [diagnostic: ${entry.reason}]${at}`
: `${entry.name} [${entry.status}]${at} — ${entry.label}`
}).join('\n'),
}]
},
@@ -170,7 +171,7 @@ export function apply(ctx: Context): void {
return [
...adjacent,
...entries
.map(entry => project(ctx.agents, entry))
.map(entry => project(ctx.agents, entry, id => ctx.subagents.nameOf(parent, id)))
.filter(entry => entry !== undefined),
]
},
Loaded 100 of 279 files, more files were not shown because too many files have changed in this diff. Show more