Merge ID-only Team base into agent teamwork UI polish

This commit is contained in:
Dudu-0223 committed 2026-10-30 14:32:55 +08:00
commit 5a052d6767
321 files changed
+1771 -4966

No files matched your search

+2 -2
View File
@@ -21,8 +21,8 @@
en: 0f971f7ac7c09139
zh: ceb866da681c709c
/deepseek-ai-dsh-base/use-this-package/what-you-get:
en: f89afd9f3b2a875e
zh: d184abde8d3d31a0
en: 94430c891006526d
zh: 94ba771960ec6f71
/deepseek-ai-dsh-base/use-this-package/shell-tools-per-platform:
en: 2dce12f82d6ee955
zh: d4599eb899a3cd88
+1 -1
View File
@@ -49,7 +49,7 @@ Run `dsh --profile my-profile "your task"` and you get a working agent with mode
Out of the box, every profile built on this core provides: a DeepSeek model connection (the provider and model are configurable, and you can enable extra providers from your settings), the full tool set — file editing, shell commands, web search, HTTP(S) fetch, subagents, task and goal tracking — durable sessions that survive restarts, and the default permission policy that confines file writes to your workspace and asks before risky actions. Web fetch runs without per-call approval and can reach loopback and private-network hosts. Feedback stays in the Session log. [OTel session upload](../../session/session-telemetry-otel/README.md) defaults to `FEEDBACK_ONLY` for all users, including `deepseek-official`: new text feedback, message ratings, edits, and withdrawals release the complete canonical prefix through that event, including context. Later records wait for the next explicit feedback; sending an authorized batch needs no further interaction or model call. `DISABLED` prevents OTel capture. The default-on [DeepSeek session-log contributor](../../session/session-log-deepseek/README.md) remains a separate request path.
Both `subagent` and `subagent_fork` create managed children and return a child id with its full agent path immediately. The bundle mounts [Subagent names](../../subagent/subagent-names/README.md) for optional child names and name-based control tools. Use `send_message` to steer active children or continue their conversations after completion. [Delegation-tool behavior](../../subagent/tool-subagent/README.md) describes result delivery and backend capabilities.
Both `subagent` and `subagent_fork` create managed children and return a child id immediately. Use `send_message` to steer active children or continue their conversations after completion. [Delegation-tool behavior](../../subagent/tool-subagent/README.md) describes result delivery and backend capabilities.
The bundle also mounts [Agent Teams](../../team/agent-team/README.md) and its task tools. Team mode defaults off per root Session; the root conversation’s **Use Agent Teams** menu switch enables collaboration for that tree without changing the profile.
+1 -1
View File
@@ -49,7 +49,7 @@ kind: "package-bundle"
开箱即用,基于本核心构建的每个 profile 都提供:DeepSeek 模型连接(提供方与模型可配置,你还可以在设置中启用额外提供方)、完整工具集——文件编辑、shell 命令、web 搜索、HTTP(S) 抓取、subagent、任务与目标跟踪——可跨重启存活的持久会话,以及默认权限策略:把文件写入限制在工作区内,危险操作前征询许可。Web 抓取无需逐次审批,并且可以访问 loopback 与内网主机。反馈保存在会话日志中。[OTel 会话上传](../../session/session-telemetry-otel/README.zh.md)对所有用户默认使用 `FEEDBACK_ONLY`,包括 `deepseek-official`:新的文本反馈、消息评分、编辑与撤回会释放截至该事件的完整规范会话日志前缀,包含上下文。后续记录等待下一次显式反馈;发送已授权批次无需进一步交互或模型调用。`DISABLED` 阻止 OTel 捕获。默认开启的 [DeepSeek 会话日志贡献器](../../session/session-log-deepseek/README.zh.md)仍是独立的请求路径。
`subagent` 与 `subagent_fork` 都创建受管理的子任务,并立即返回子任务 ID 及其完整的 Agent 路径。bundle 挂载 [Subagent names](../../subagent/subagent-names/README.zh.md),提供可选的子任务名称和按名称寻址的控制工具。使用 `send_message` 可引导活跃子任务,或在完成后继续其对话。[委派工具行为](../../subagent/tool-subagent/README.zh.md)说明了结果交付和后端能力。
`subagent` 与 `subagent_fork` 都创建受管理的子任务,并立即返回 child id。使用 `send_message` 可引导活跃子任务,或在完成后继续其对话。[委派工具行为](../../subagent/tool-subagent/README.zh.md)说明了结果交付和后端能力。
此 bundle 还挂载 [Agent Teams](../../team/agent-team/README.zh.md) 及其任务工具。每个根 Session 的 Team 模式默认关闭;根会话菜单中的**使用智能体团队**开关为当前树开启协作,无需修改 profile。
-2
View File
@@ -359,8 +359,6 @@
- id: subagent
name: '@deepseek-ai/dsh-subagent'
- id: subagent-names
name: '@deepseek-ai/dsh-subagent-names'
- id: agent-team
name: '@deepseek-ai/dsh-agent-team'
config:
-1
View File
@@ -100,7 +100,6 @@
"@deepseek-ai/dsh-storage-json": "workspace:*",
"@deepseek-ai/dsh-subagent": "workspace:*",
"@deepseek-ai/dsh-subagent-fork-in-process": "workspace:*",
"@deepseek-ai/dsh-subagent-names": "workspace:*",
"@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:*",
"@deepseek-ai/dsh-subprocess-local": "workspace:*",
"@deepseek-ai/dsh-system-prompt": "workspace:*",
+4 -4
View File
@@ -15,8 +15,8 @@
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
/deepseek-ai-dsh-client-ui-tool/use-this-package:
en: fd961d923feca636
zh: 5925ea0fbdfc207d
en: 2419beff9bcd8139
zh: 5aafe9b168e571a5
/deepseek-ai-dsh-client-ui-tool/use-this-package/registering-a-business-tool-view:
en: e458aff61e634190
zh: 650e417f450b3edf
@@ -30,8 +30,8 @@
en: 672e63b2f92e689b
zh: ba950ecb480325a6
/deepseek-ai-dsh-client-ui-tool/understand-the-implementation/cards:
en: f14a14ec529126b1
zh: 414cad04d6af8e5e
en: d2ac2f855da27aff
zh: 61d257d60971f259
/deepseek-ai-dsh-client-ui-tool/understand-the-implementation/declared-optional-capabilities:
en: c1926deb4e5f3734
zh: c5845f465b94a459
+2 -2
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. Control receipts display agent `name` arguments; historical `agent_id` recordings remain readable. `send_message` receipts omit the leading `lead/` from target labels while preserving nested paths and the original arguments for inspection. Text agent lists show the name or path above a single-line description, with compact spacing and recorded status. Click the description to expand or collapse its full text; its accessible name follows the selected locale.
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 the agent ID supplied by the recorded call. Text agent lists show the ID above a single-line description, with compact spacing and recorded status. Click the description to expand or collapse its full text; its accessible name follows the selected locale.
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.
@@ -93,7 +93,7 @@ Chat diff cards keep nine rows before folding, enough for a file header, one rem
An Auto denial takes precedence over keyed specialized views. Its generic row preserves the call identity, omits raw arguments, and normalizes the stored reason only for display: trim surrounding whitespace and collapse line separators to spaces, with localized fallback for an empty result. Session and SDK error details keep the original reason.
Recorded tool details cover goal and schedule tools, Cordis inspection, workflow and Ralph reports, Session event/search/trace queries, agent and teammate controls, background jobs, persistent terminals, and LSP navigation. These expanded bodies read successful logged results, preserve generic input/output for failures or unsupported data, and keep Inspect available. Dates include the viewer's time zone, and statuses reflect the call result rather than current session state. Agent control receipts display the recorded target name or historical identifier. `wait_agent` shows a single waiting, ended, or timed-out header without a repeated summary; historical `noProgress` results keep their recorded explanation. Session traces preserve descendant indentation. LSP results open filesystem paths through the Host callback and display other URIs as text. The browser adapter consumes recorded producer text and JSON; Host service objects and presenter callbacks do not cross into the Client.
Recorded tool details cover goal and schedule tools, Cordis inspection, workflow and Ralph reports, Session event/search/trace queries, agent and teammate controls, background jobs, persistent terminals, and LSP navigation. These expanded bodies read successful logged results, preserve generic input/output for failures or unsupported data, and keep Inspect available. Dates include the viewer's time zone, and statuses reflect the call result rather than current session state. Agent control receipts display the recorded target Session id. `wait_agent` shows a single waiting, ended, or timed-out header without a repeated summary; historical `noProgress` results keep their recorded explanation. Session traces preserve descendant indentation. LSP results open filesystem paths through the Host callback and display other URIs as text. The browser adapter consumes recorded producer text and JSON; Host service objects and presenter callbacks do not cross into the Client.
Expanded status dots and labels use static semantic colors. Receipt and job-output headers keep neutral text and omit the status while expanded. An interruption receipt confirms only that interruption was requested.
+2 -2
View File
@@ -25,7 +25,7 @@ kind: "package-reference"
<a id="use-this-package"></a>
## 使用本包
工具调用在对话中显示为卡片:一个根调用树带其嵌套子调用,每个原子调用由所属视图渲染。所有生命周期状态都保留工具的普通业务图标;失败与中断仍通过冻结调用/结果状态、无障碍状态文本和失败摘要明确表达。用户可通过宿主回调打开文件或检查调用。折叠的 `web_fetch` 行把其 http(s) URL 显示为链接,在新浏览器标签页中打开。控制工具回执显示 agent 的 `name` 参数;历史 `agent_id` 记录仍可读取。`send_message` 回执的目标标签省略开头的 `lead/`,保留嵌套路径,检查视图仍显示原始参数。文本形式的智能体列表以紧凑间距展示名称或路径、下方的单行描述及记录中的状态。点击描述可展开或收起全文;其无障碍名称使用当前语言。
工具调用在对话中显示为卡片:一个根调用树带其嵌套子调用,每个原子调用由所属视图渲染。所有生命周期状态都保留工具的普通业务图标;失败与中断仍通过冻结调用/结果状态、无障碍状态文本和失败摘要明确表达。用户可通过宿主回调打开文件或检查调用。折叠的 `web_fetch` 行把其 http(s) URL 显示为链接,在新浏览器标签页中打开。控制工具回执显示记录的调用所提供的智能体 ID。文本形式的智能体列表以紧凑间距展示 ID、下方的单行描述及记录中的状态。点击描述可展开或收起全文;其无障碍名称使用当前语言。
已结束的前台命令若返回非零退出码、终止信号,或明确记录为无退出码,描述保持普通灰色,后面紧接带红点的中性胶囊。胶囊仅显示记录的退出码、信号或本地化的“未正常退出”状态,并保持在同一行;描述超出可用宽度时显示省略号,为胶囊预留空间。运行中、成功和后台调用不显示胶囊。通用执行错误与停止摘要仍保留错误色和警告色,悬停时也不改变。
@@ -93,7 +93,7 @@ Chat diff 卡片在折叠前保留九行,足以容纳文件标题、一对删
Auto 拒绝优先于按工具名选择的专门视图。其通用行保留调用身份、省略原始参数,并且只在显示时归一化存储的理由:去除首尾空白,把行分隔符折叠为空格,结果为空时使用本地化通用理由。Session 与 SDK 错误详情保留原始理由。
记录结果的工具详情覆盖目标和定时任务工具、Cordis 检查、workflow 与 Ralph 报告、Session 事件/搜索/轨迹查询、Agent 与 teammate 控制、后台作业、持久终端以及 LSP 导航。展开内容读取成功的记录结果,为失败或不支持的数据保留通用输入/输出,并保留 Inspect。日期包含查看者的时区,状态反映调用结果而非当前会话状态。Agent 控制回执显示记录的目标名称或历史标识符。`wait_agent` 的行标题仅显示等待中、已结束或已超时,不重复追加摘要;历史 `noProgress` 结果保留记录中的说明。Session 轨迹保留后代的缩进。LSP 结果通过 Host 回调打开文件系统路径,其他 URI 则显示为文本。浏览器适配器消费已记录的 producer 文本与 JSON;Host service 对象和 presenter 回调不会进入 Client。
记录结果的工具详情覆盖目标和定时任务工具、Cordis 检查、workflow 与 Ralph 报告、Session 事件/搜索/轨迹查询、Agent 与 teammate 控制、后台作业、持久终端以及 LSP 导航。展开内容读取成功的记录结果,为失败或不支持的数据保留通用输入/输出,并保留 Inspect。日期包含查看者的时区,状态反映调用结果而非当前会话状态。Agent 控制回执显示记录的目标 Session id。`wait_agent` 的行标题仅显示等待中、已结束或已超时,不重复追加摘要;历史 `noProgress` 结果保留记录中的说明。Session 轨迹保留后代的缩进。LSP 结果通过 Host 回调打开文件系统路径,其他 URI 则显示为文本。浏览器适配器消费已记录的 producer 文本与 JSON;Host service 对象和 presenter 回调不会进入 Client。
展开后的状态圆点和文字使用静态语义色。操作回执和任务输出的标题保持中性色,展开时省略标题中的状态。中断回执仅确认已发出中断请求。
@@ -114,12 +114,12 @@ export function subagentDetails(
} else {
if (text === undefined) return null
// Historical results lack the structured receipt stored by current tools.
const started = /^started (background subagent job|subagent) (\S+)(?:\nname: (\S+))?$/u.exec(text)
const started = /^started (background subagent job|subagent) (\S+)$/u.exec(text)
if (started === null) {
return detailList([{ title: t('detail.agent.reply'), markdown: text, fields: [], groups: [{ label: t('detail.field.task'), items: [{ description: arg(args, 'prompt'), fields: [] }] }] }], t('detail.agent.reply'), t)
}
if (started[1] === 'background subagent job') fields.push({ label: t('detail.field.job'), value: started[2] ?? '' })
if (started[3] !== undefined) fields.push({ label: t('detail.field.agentName'), value: started[3] })
else fields.push({ label: t('detail.field.id'), value: started[2] ?? '' })
}
return detailList([{
title: t('detail.receipt.started'), fields,
@@ -139,7 +139,7 @@ export function subagentDetails(
export function controlDetails(
name: string, args: Record<string, unknown>, text: string, json: unknown, t: DetailTranslate,
): ToolDetailsModel | null {
const target = arg(args, 'name') || arg(args, 'target') || arg(args, 'agent_id') || arg(args, 'sessionId') || arg(args, 'job_id')
const target = 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)
@@ -161,12 +161,11 @@ export function controlDetails(
...(json.nextCursor === undefined ? {} : { caption: t('detail.tasks.nextPage', { cursor: String(json.nextCursor) }) }),
}
case 'send_message': {
const title = target.replace(/^lead\//u, '')
const status = detailRecord(json) ? json.status : undefined
if (status === 'accepted' || status === 'queued') return receipt(title, {
if (status === 'accepted' || status === 'queued') return receipt(target, {
label: t(status === 'queued' ? 'detail.status.queued' : 'detail.receipt.delivered'), tone: status === 'queued' ? 'warning' : 'success',
}, t, [], arg(args, 'message'))
return text === `message delivered to agent ${target}` ? receipt(title, { label: t('detail.receipt.delivered'), tone: 'success' }, t, [], arg(args, 'message')) : null
return text === `message delivered to agent ${target}` ? receipt(target, { label: t('detail.receipt.delivered'), tone: 'success' }, t, [], arg(args, 'message')) : null
}
case 'interrupt_agent':
if (detailRecord(json) && typeof json.previousStatus === 'string') return receipt(target, { label: t('detail.receipt.interrupt'), tone: 'warning' }, t, [{ label: t('detail.field.previousStatus'), value: detailBadge(json.previousStatus, t).label }])
@@ -66,7 +66,7 @@ export function detailBadge(status: string, t: DetailTranslate): NonNullable<Det
const FIELD_KEYS = {
id: 'detail.field.id', revision: 'detail.field.revision', platform: 'detail.field.platform',
provider: 'detail.field.provider', model: 'detail.field.model', role: 'detail.field.role',
context: 'detail.field.context', ownerName: 'detail.field.owner', ready: 'detail.field.ready',
context: 'detail.field.context', ownerId: 'detail.field.owner', ready: 'detail.field.ready',
blockedBy: 'detail.field.dependencies', writeScopes: 'detail.field.writeScopes',
writeScopeWarnings: 'detail.field.warnings', diagnostics: 'detail.field.diagnostics',
methods: 'detail.field.methods', inputSchema: 'detail.field.inputSchema', outputSchema: 'detail.field.outputSchema',
@@ -88,35 +88,21 @@ describe('control detail adapters', () => {
expect(details('lsp', 'hover text', { file_path: 'src/main.ts', line: 2, character: 3, operation: 'hover' })?.items[0]?.markdown).toBe('hover text')
})
it('shows named agent targets in recorded messaging and interruption receipts', () => {
expect(details('send_message', 'message delivered to agent lead/reviewer', {
name: 'lead/reviewer', message: 'Review the change.',
})).toMatchObject({
summary: 'reviewer · Message delivered', expandedSummary: 'reviewer',
items: [{ title: 'reviewer', description: 'Review the change.' }],
})
expect(details('interrupt_agent', '{"previousStatus":"running"}', {
name: 'lead/reviewer',
})?.items[0]).toMatchObject({
title: 'lead/reviewer', fields: [{ label: 'Previous status', value: 'Running' }],
})
})
it.each(['subagent', 'subagent_fork'])('%s recognizes named creation receipts and retains the complete task', (name) => {
it.each(['subagent', 'subagent_fork'])('%s recognizes ID creation receipts and retains the complete task', (name) => {
const prompt = 'Research CLI agent products.\nCompare business models and write a report.'
const model = details(name, 'started subagent child-1\nname: lead/researcher', { description: 'Research CLI agent products', prompt })
const model = details(name, 'started subagent child-1', { description: 'Research CLI agent products', prompt })
expect(model?.summary).toBe('Research CLI agent products')
expect(model?.items[0]).toEqual({
title: 'Started',
fields: [{ label: 'Agent task description', value: 'Research CLI agent products' }, { label: 'Agent name', value: 'lead/researcher' }],
fields: [{ label: 'Agent task description', value: 'Research CLI agent products' }, { label: 'ID', value: 'child-1' }],
groups: [{ label: 'Task', items: [{ description: prompt, fields: [] }] }],
})
expect(details(name, 'started subagent child-1', { prompt })?.items[0]?.title).toBe('Started')
expect(details(name, 'started subagent child-1\nname: lead/researcher\nAdditional reply', { prompt })?.items[0]?.title).toBe('Agent response')
expect(details(name, 'started subagent child-1\nAdditional reply', { prompt })?.items[0]?.title).toBe('Agent response')
})
it.each(['subagent', 'subagent_fork'])('%s renders persisted creation metadata independently of result wording', (name) => {
const args = { description: 'Review the changes', prompt: 'Read the diff and report defects.', name: 'requested-name' }
const args = { description: 'Review the changes', prompt: 'Read the diff and report defects.' }
const block = { ...output(name, 'Delegation accepted.', args), meta: { kind: 'activation', subagentId: 'child-1' } }
const model = detailsCardModel(block, t, 'en')
expect(model?.items[0]).toEqual({
@@ -132,7 +118,7 @@ describe('control detail adapters', () => {
it.each([null, [], {}, { kind: 'other', subagentId: 'child-1' }, { kind: 'activation' }, { kind: 'activation', subagentId: 1 }, { kind: 'activation', subagentId: ' ' }])(
'keeps malformed metadata on the generic result path (%j)', (meta) => {
const block = { ...output('subagent', 'started subagent child-1\nname: lead/reviewer'), meta }
const block = { ...output('subagent', 'started subagent child-1'), meta }
expect(detailsCardModel(block, t, 'en')).toBeNull()
},
)
@@ -142,7 +128,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 reviewer', { name: 'reviewer', message: 'hello' })?.summary).toBe('reviewer · Message delivered')
expect(details('send_message', 'message delivered to agent reviewer', { agent_id: 'reviewer', message: 'hello' })?.summary).toBe('reviewer · Message delivered')
expect(details('interrupt_agent', '{"previousStatus":"running"}', { agent_id: 'a1' })?.items[0]?.fields).toContainEqual({ label: 'Previous status', value: 'Running' })
expect(details('wait_agent', '{"timedOut":true}')?.summary).toBe('Wait timed out')
expect(details('wait_agent', '{"timedOut":false,"noProgress":{"message":"No peer"}}')?.items[0]?.description).toBe('No peer')
@@ -163,7 +149,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 reviewer', { name: 'reviewer' })?.summary).toContain('Interrupt')
expect(details('interrupt_agent', 'interrupt requested for agent reviewer', { agent_id: '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()
@@ -96,15 +96,15 @@ describe('tool preparation', () => {
const settled: ToolResultNode = {
kind: 'tool-result', seq: 3, time: 3, callId: 'call', callTime: 2, name,
args: started.args, call: { name, argsRaw }, isError: false, subCalls: [],
content: [{ type: 'text', text: 'started subagent child-1\nname: lead/researcher' }],
content: [{ type: 'text', text: 'started subagent child-1' }],
}
view.rerender(<DetailsRow {...props} phase="result" block={settled} />)
expect(view.getByText('Research CLI products')).toBeTruthy()
expect(view.queryByText('Agent response')).toBeNull()
fireEvent.click(view.getByRole('button', { expanded: false }))
expect(view.getByText('Research CLI products')).toBeTruthy()
expect(view.getAllByText('Research CLI products')).toHaveLength(2)
expect(view.getByText('Research products in detail.')).toBeTruthy()
expect(view.getByText('lead/researcher')).toBeTruthy()
expect(view.getByText('child-1')).toBeTruthy()
})
it('retains the file row through streamed arguments, dispatch, and result', () => {
+1 -2
View File
@@ -59,7 +59,6 @@
"@deepseek-ai/dsh-tool-executor-native": "workspace:*",
"@deepseek-ai/dsh-tool-executor-ptc": "workspace:*",
"@deepseek-ai/dsh-tool-testkit": "workspace:*",
"@deepseek-ai/dsh-tools": "workspace:*",
"@deepseek-ai/dsh-subagent-names": "workspace:*"
"@deepseek-ai/dsh-tools": "workspace:*"
}
}
@@ -1,4 +1,3 @@
import SubagentNames from '@deepseek-ai/dsh-subagent-names'
import { executeToolFixture } from '@deepseek-ai/dsh-tool-testkit'
import { setImmediate } from 'node:timers/promises'
import { mkdtempSync, rmSync } from 'node:fs'
@@ -91,7 +90,6 @@ it.each(['loop', 'root'] as const)('unloads %s while a background child is stopp
await ctx.plugin(JsonlSessionPersistence, { root })
const loop = await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
await ctx.plugin(LocalJobRegistry)
await ctx.plugin(SubagentSpawn)
await ctx.plugin(ToolSubagent, { provider: 'spawn' })
@@ -59,7 +59,6 @@ export const KNOWN_SESSION_EVENT_TYPES: ReadonlySet<string> = new Set([
'session/title-llm-request',
'step/end',
'step/start',
'subagent-name/bound',
'subagent/catalog',
'subagent/descriptor',
'subagent/model-selection-policy',
@@ -76,8 +76,7 @@
"@deepseek-ai/dsh-tools": "workspace:*",
"@deepseek-ai/dsh-user-approval": "workspace:*",
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/dsh-working-directory": "workspace:*",
"@deepseek-ai/dsh-subagent-names": "workspace:*"
"@deepseek-ai/dsh-working-directory": "workspace:*"
},
"publishConfig": {
"access": "public"
@@ -1,4 +1,3 @@
import SubagentNames from '@deepseek-ai/dsh-subagent-names'
import { executeToolFixture, executeModelToolFixture } from '@deepseek-ai/dsh-tool-testkit'
import * as NativeToolExecutor from '@deepseek-ai/dsh-tool-executor-native'
import { externalTestParent } from '../../../subagent/subagent/tests/external-activation-helpers.ts'
@@ -1372,7 +1371,6 @@ describe('out-of-process delegation boundary', () => {
scriptedDecision('allow', 'allow'),
])
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
let providerRequest: SubagentStartRequest | undefined
ctx.subagents.registerProvider({
name: 'remote-boundary',
@@ -9,26 +9,26 @@
en: 520d9dc51e2b0142
zh: 8f332776243107a9
/deepseek-ai-dsh-experimental-tool-subagent-oar/summary:
en: 2d63ab94ccdafa8f
zh: 6372823aea399513
en: 8adbaf2511d2c47f
zh: 1495f6bdb84a30ea
/deepseek-ai-dsh-experimental-tool-subagent-oar/table-of-contents:
en: d152484eb41ac6b4
zh: 8d1e20b826450ffe
/deepseek-ai-dsh-experimental-tool-subagent-oar/use-this-package:
en: f95f1df04a1d3231
zh: 46647a030250eefe
en: 045000a545e143bb
zh: db19a60804c8d561
/deepseek-ai-dsh-experimental-tool-subagent-oar/use-this-package/when-to-choose-it:
en: 368805e5bcffeb0a
zh: 9340ff5074ada9e1
/deepseek-ai-dsh-experimental-tool-subagent-oar/use-this-package/minimal-configuration:
en: 7777b50b62aa53c0
zh: ff2a53de5dcbf518
en: 5b2b33c1d0fae20a
zh: 26bff1e013e2bfcd
/deepseek-ai-dsh-experimental-tool-subagent-oar/understand-the-implementation:
en: e168a1ffb8497b1a
zh: e9085b004a2d2c55
en: 3e8a9fffa9c5afd0
zh: 932b6d0b682208ba
/deepseek-ai-dsh-experimental-tool-subagent-oar/further-exploration:
en: 8aeb6cf566b48838
zh: cc0f5bb71ee7a9fe
en: a796236202b695f5
zh: 8e76361f31517714
/deepseek-ai-dsh-experimental-tool-subagent-oar/model-experience:
en: 215e7ba838619b7b
zh: a311da3843709f90
@@ -36,8 +36,8 @@
en: 8febbb031aa0452d
zh: da3f9011631652da
/deepseek-ai-dsh-experimental-tool-subagent-oar/model-experience/external-delegation-tool/what-the-model-sees:
en: 4b2fc7303598e643
zh: efaa45f0c42befd2
en: 2e0f5274092f794b
zh: 83a14e5f7018717f
/deepseek-ai-dsh-experimental-tool-subagent-oar/model-experience/external-delegation-tool/token-effect:
en: 60d7ffec7f4dc858
zh: 09f286978df6b4ec
@@ -45,8 +45,8 @@
en: 7c1ac0b007b2f0ce
zh: 2332a07c6f8bcbdc
/deepseek-ai-dsh-experimental-tool-subagent-oar/known-limitations-and-deferred-work:
en: e0ed82ba366c0f17
zh: c7fd3d1b48bb3c11
en: 5157ca8c4aa1a865
zh: 47ad253d87a02838
/deepseek-ai-dsh-experimental-tool-subagent-oar/known-limitations-and-deferred-work/dev-note:
en: e67f633dc6fe7773
zh: 8ec3e73528ddb1a6
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
## Summary
Use one `subagent_oar` tool to choose a configured external agent for each standalone text task. Configuration fixes the available runtime aliases and their providers; the model selects an alias without changing native arguments or permissions. Each task uses independent context and returns its child id and full name, with a later completion notice delivered to the parent.
Use one `subagent_oar` tool to choose a configured external agent for each standalone text task. Configuration fixes the available runtime aliases and their providers; the model selects an alias without changing native arguments or permissions. Each task uses independent context and returns a child id, with a later completion notice delivered to the parent.
## Table of Contents
@@ -25,7 +25,7 @@ Use one `subagent_oar` tool to choose a configured external agent for each stand
<a id="use-this-package"></a>
## Use this package
Mount this tool with `@deepseek-ai/dsh-subagent-names` beside configured external providers, such as [OAR provider instances](../subagent-oar/README.md). Declare the naming, tool, and provider packages as dependencies of your composition.
Mount this tool beside configured external providers, such as [OAR provider instances](../subagent-oar/README.md). Declare the tool and provider packages as dependencies of your composition.
### When to choose it
@@ -33,10 +33,9 @@ Choose this tool when one model-facing entry should select among independent, on
### Minimal configuration
With the shared subagent services available and `oar-codex` and `oar-claude` already registered, mount the naming service once alongside this tool:
With `oar-codex` and `oar-claude` already registered, mount this row in the composition:
```yaml
- name: '@deepseek-ai/dsh-subagent-names'
- name: '@deepseek-ai/dsh-experimental-tool-subagent-oar'
config:
providers:
@@ -49,7 +48,7 @@ With the shared subagent services available and `oar-codex` and `oar-claude` alr
| `providers` | required | Nonempty map from model-visible runtime aliases to registered provider names |
| `toolName` | `subagent_oar` | Tool name; every mounted instance needs a distinct name |
The [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-experimental-tool-subagent-oar) owns accepted fields and validation. A call requires `runtime`, `description`, and `prompt`; optional `name` selects a sibling segment under the [naming service rules](../../subagent/subagent-names/README.md); optional `cwd` selects the child directory, with relative paths resolved from the parent's current directory. The prompt must contain the context the child needs. Native installation, authentication, model settings, and permissions belong to the selected provider.
The [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-experimental-tool-subagent-oar) owns accepted fields and validation. A call requires `runtime`, `description`, and `prompt`; optional `cwd` selects the child directory, with relative paths resolved from the parent's current directory. The prompt must contain the context the child needs. Native installation, authentication, model settings, and permissions belong to the selected provider.
-----
@@ -59,7 +58,7 @@ The [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-exper
<details>
<summary>Implementation internals — click to expand</summary>
[`src/index.ts`](src/index.ts) registers the configured runtime choices and calls `ctx.subagents.startActivation()` directly. The naming service reserves a sibling name and maps the accepted child id to its full name; the child's prompt is the submitted task. The subagent service owns capacity, parent lifetime, cancellation, terminal recording, and completion delivery. The tool's call card uses the task description and submitted arguments. Disposing the plugin removes its tool registration; the activation manager retains ownership of accepted children.
[`src/index.ts`](src/index.ts) builds the runtime enum from the configured aliases and registers one tool. Each call resolves its provider and starts an activation through the shared subagent service, which owns capacity, parent lifetime, cancellation, terminal recording, and completion delivery. The tool's call card uses the task description and submitted arguments. Disposing the plugin removes its tool registration; the activation manager retains ownership of accepted children.
</details>
@@ -69,7 +68,6 @@ The [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-exper
## Further Exploration
- [Subagent service](../../subagent/subagent/README.md) — provider registration and activation ownership.
- [Subagent names](../../subagent/subagent-names/README.md) — sibling names and full-name lookup.
- [OAR provider](../subagent-oar/README.md) — native runtime setup and permission configuration.
- [Subagent subsystem](../../../docs/subsystems/subagent.md) — parent completion notices and retained results.
@@ -82,7 +80,7 @@ The [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-exper
#### What the model sees
The [tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-experimental-tool-subagent-oar) owns the description and argument schema. The `runtime` enum contains the configured aliases. After acceptance, the result contains `kind: "activation"`, `subagentId`, and `name`. The model sees `started subagent <ID>` followed by `name: <full name>` on the next line. The shared subagent service later supplies the child's answer or failure notice, identifying the external execution by the same child id.
The [tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-experimental-tool-subagent-oar) owns the description and argument schema. The `runtime` enum contains the configured aliases. After acceptance, the tool returns `started subagent <id>`, where `<id>` identifies the child. The shared subagent service later supplies the child's answer or failure notice.
#### Token effect
@@ -100,7 +98,6 @@ The tool's choices and execution capabilities are fixed by its configured provid
- **One-shot providers only:** child context is independent, and the tool exposes no follow-up, resume, structured output, or per-call native permission override.
- **Provider availability:** a selected provider that is absent or unloaded fails at call time; its configured alias remains in the schema until this tool's configuration changes.
- **Name binding failure:** if the accepted child's name cannot be recorded, the error reports its id and leaves execution managed by the subagent service. Check that child's outcome before retrying; the reserved name is released.
- **Provider-owned work:** runtime setup, startup cancellation, process cleanup, and answer selection follow the selected provider's documented behavior. Removing this tool does not revoke accepted activations.
<a id="dev-note"></a>
@@ -9,7 +9,7 @@ kind: "package-reference"
## 概述
使用一个 `subagent_oar` 工具,为每个独立文本任务选择已配置的外部智能体。配置固定可用的运行时别名及其提供方;模型选择别名,不能更改原生参数或权限。每个任务使用独立上下文并返回子任务 id 和完整名称,完成通知稍后送达父智能体。
使用一个 `subagent_oar` 工具,为每个独立文本任务选择已配置的外部智能体。配置固定可用的运行时别名及其提供方;模型选择别名,不能更改原生参数或权限。每个任务使用独立上下文并返回子任务 id,完成通知稍后送达父智能体。
## 目录
@@ -25,7 +25,7 @@ kind: "package-reference"
<a id="use-this-package"></a>
## 使用本包
将此工具与 `@deepseek-ai/dsh-subagent-names` 一起挂载在已配置的外部提供方旁,例如 [OAR 提供方实例](../subagent-oar/README.zh.md)。在组合中将命名、工具和提供方包声明为依赖。
在已配置的外部提供方旁挂载此工具,例如 [OAR 提供方实例](../subagent-oar/README.zh.md)。在组合中将工具包和提供方包声明为依赖。
### 适用场景
@@ -33,10 +33,9 @@ kind: "package-reference"
### 最小配置
在共享 subagent 服务可用,且 `oar-codex` 和 `oar-claude` 已注册后,在此工具旁挂载一次命名服务:
在 `oar-codex` 和 `oar-claude` 已注册的情况下,在组合中挂载此行:
```yaml
- name: '@deepseek-ai/dsh-subagent-names'
- name: '@deepseek-ai/dsh-experimental-tool-subagent-oar'
config:
providers:
@@ -49,7 +48,7 @@ kind: "package-reference"
| `providers` | 必填 | 从模型可见运行时别名映射到已注册提供方名称的非空映射 |
| `toolName` | `subagent_oar` | 工具名称;每个挂载实例需要不同名称 |
[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-experimental-tool-subagent-oar)定义接受的字段和校验规则。调用要求提供 `runtime`、`description` 和 `prompt`;可选的 `name` 按[命名服务规则](../../subagent/subagent-names/README.zh.md)选择同级路径段;可选的 `cwd` 选择子任务目录,相对路径从父智能体的当前目录解析。提示必须包含子任务需要的上下文。原生安装、认证、模型设置和权限由所选提供方负责。
[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-experimental-tool-subagent-oar)定义接受的字段和校验规则。调用要求提供 `runtime`、`description` 和 `prompt`;可选的 `cwd` 选择子任务目录,相对路径从父智能体的当前目录解析。提示必须包含子任务需要的上下文。原生安装、认证、模型设置和权限由所选提供方负责。
-----
@@ -59,7 +58,7 @@ kind: "package-reference"
<details>
<summary>实现内部细节 — 点击展开</summary>
[`src/index.ts`](src/index.ts)注册配置的运行时选项,并直接调用 `ctx.subagents.startActivation()`。命名服务预留同级名称,并将已接受的子任务 id 映射到完整名称;子任务的提示就是提交的任务。subagent 服务负责容量、父智能体生命周期、取消、最终记录和完成通知交付。工具调用卡片使用任务描述和提交的参数。释放插件会移除其工具注册;激活管理器继续负责已接受的子任务。
[`src/index.ts`](src/index.ts)根据配置的别名构建运行时枚举,并注册一个工具。每次调用解析其提供方,并通过共享子智能体服务启动激活;该服务负责容量、父智能体生命周期、取消、最终记录和完成通知交付。工具调用卡片使用任务描述和提交的参数。释放插件会移除其工具注册;激活管理器继续负责已接受的子任务。
</details>
@@ -69,7 +68,6 @@ kind: "package-reference"
## 延伸阅读
- [子智能体服务](../../subagent/subagent/README.zh.md) — 提供方注册和激活所有权。
- [Subagent 命名](../../subagent/subagent-names/README.zh.md) — 同级名称和完整名称查询。
- [OAR 提供方](../subagent-oar/README.zh.md) — 原生运行时配置和权限配置。
- [子智能体子系统](../../../docs/subsystems/subagent.zh.md) — 父智能体完成通知和保留结果。
@@ -82,7 +80,7 @@ kind: "package-reference"
#### 模型看到什么
[工具目录](../../../docs/tool-catalog.zh.md#deepseek-aidsh-experimental-tool-subagent-oar)定义描述和参数 schema。`runtime` 枚举包含配置的别名。接受任务后,结果包含 `kind: "activation"`、`subagentId` 和 `name`。模型看到 `started subagent <ID>`,下一行是 `name: <full name>`。共享子智能体服务稍后提供子任务的答复或失败通知,以相同的子任务 id 标识外部执行。
[工具目录](../../../docs/tool-catalog.zh.md#deepseek-aidsh-experimental-tool-subagent-oar)定义描述和参数 schema。`runtime` 枚举包含配置的别名。接受任务后,工具返回 `started subagent <id>`,其中 `<id>` 标识子任务。共享子智能体服务稍后提供子任务的答复或失败通知。
#### Token 影响
@@ -100,7 +98,6 @@ kind: "package-reference"
- **仅支持单次提供方:** 子任务上下文独立,工具不提供后续输入、恢复、结构化输出或按调用覆盖原生权限的功能。
- **提供方可用性:** 选择的提供方不存在或已卸载时,调用会失败;其配置的别名会保留在 schema 中,直到此工具的配置改变。
- **名称绑定失败:** 如果已接受的子任务名称无法记录,错误会报告其 id,执行仍由 subagent 服务管理。重试前应检查该子任务的结果;预留的名称会被释放。
- **提供方负责的工作:** 运行时配置、启动取消、进程清理和答复选择遵循所选提供方的文档行为。移除此工具不会撤销已接受的激活。
<a id="dev-note"></a>
@@ -30,9 +30,7 @@
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/dsh-llm": "workspace:*",
"@deepseek-ai/dsh-subagent": "workspace:*",
"@deepseek-ai/dsh-subagent-names": "workspace:*",
"@deepseek-ai/dsh-tools": "workspace:*"
},
"dependencies": {
@@ -45,7 +43,6 @@
"@deepseek-ai/dsh-llm": "workspace:*",
"@deepseek-ai/dsh-session": "workspace:*",
"@deepseek-ai/dsh-subagent": "workspace:*",
"@deepseek-ai/dsh-subagent-names": "workspace:*",
"@deepseek-ai/dsh-tools": "workspace:*",
"@deepseek-ai/dsh-tool-testkit": "workspace:*"
}
@@ -2,13 +2,11 @@
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { HarnessError } from '@deepseek-ai/dsh-llm'
import { defineTool } from '@deepseek-ai/dsh-tools'
import type {} from '@deepseek-ai/dsh-subagent'
import type {} from '@deepseek-ai/dsh-subagent-names'
export const name = 'experimental-tool-subagent-oar'
export const inject = ['tools', 'subagents', 'subagentNames']
export const inject = ['tools', 'subagents']
/** Deployment-owned runtime choices exposed by the external delegation tool. */
export interface Config {
@@ -25,7 +23,7 @@ export const Config: z<Config> = z.object({
/**
* Register a tool that selects one configured external provider per task.
* @param ctx - shared tool registry, managed subagents, and hierarchical naming service.
* @param ctx - shared tool registry and managed subagent service.
* @param config - runtime choices and the model-facing tool name.
*/
export function apply(ctx: Context, config: Config): void {
@@ -38,14 +36,10 @@ export function apply(ctx: Context, config: Config): void {
name: toolName,
description: 'Delegate a self-contained task to a configured external agent. Choose its runtime for each call. '
+ 'The child works in its own context and does not see this conversation. '
+ 'This tool returns the child id and full agent path; a later completion notice includes its final answer. '
+ 'This tool returns a child id; a later completion notice includes its final answer. '
+ 'The child does not accept follow-up messages. Start independent tasks together and continue useful work while they run. '
+ 'When only waiting for their results remains, end your turn instead of polling.',
parameters: {
name: {
type: 'string',
description: 'Optional sibling-unique name; omission selects the first unused agent-N, starting at agent-1. Explicit names: start with a lower-case letter and use lower-case letters, digits, underscores, or hyphens.',
},
runtime: {
type: 'string',
required: true,
@@ -76,10 +70,9 @@ export function apply(ctx: Context, config: Config): void {
properties: {
kind: { type: 'string', required: true, const: 'activation' },
subagentId: { type: 'string', required: true },
name: { type: 'string', required: true },
},
},
render: (_args, value) => [{ type: 'text', text: `started subagent ${value.subagentId}\nname: ${value.name}` }],
render: (_args, value) => [{ type: 'text', text: `started subagent ${value.subagentId}` }],
},
/* jscpd:ignore-end */
isConcurrencySafe: () => true,
@@ -92,32 +85,16 @@ export function apply(ctx: Context, config: Config): void {
if (provider !== undefined && (provider.prepareContinuable !== undefined || provider.inheritsParentContext)) {
throw new Error('subagent_oar requires a one-shot provider with independent conversation context')
}
const names = ctx.subagentNames
const rootSession = ctx.subagents.rootOf(parent).session
const reservation = names.reserve(rootSession, parent.id, args.name)
try {
const activation = await ctx.subagents.startActivation({
provider: providerName,
label: args.description,
parent,
prompt: [{ type: 'text', text: args.prompt }],
...args.cwd === undefined ? {} : { cwd: args.cwd },
signal: exec.signal,
delivery: 'parent',
})
try {
names.bind(rootSession, activation.childId, reservation.name)
} catch (cause: unknown) {
throw new HarnessError(
`Subagent ${activation.childId} started, but its name ${reservation.name} could not be recorded. `
+ 'Its execution is still managed by the subagent service. Check this child before retrying.',
'SUBAGENT_NAME_BIND_FAILED', { cause },
)
}
return { kind: 'activation' as const, subagentId: activation.childId, name: reservation.name }
} finally {
reservation.dispose()
}
const activation = await ctx.subagents.startActivation({
provider: providerName,
label: args.description,
parent,
prompt: [{ type: 'text', text: args.prompt }],
...args.cwd === undefined ? {} : { cwd: args.cwd },
signal: exec.signal,
delivery: 'parent',
})
return { kind: 'activation' as const, subagentId: activation.childId }
},
presentCall: args => ({ card: 'generic', title: args.description, rawInput: args }),
}))
@@ -7,9 +7,8 @@ import { mountAgentLoopTestDependencies, mountAgentLoopTestHarness } from '@deep
import { ToolCallId } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import SubagentRuntime, { NO_START_CAPABILITIES } from '@deepseek-ai/dsh-subagent'
import SubagentNames from '@deepseek-ai/dsh-subagent-names'
import type { ResolvedSubagentStartRequest, SubagentResult, SubagentRun } from '@deepseek-ai/dsh-subagent'
import { expect, it, vi } from 'vitest'
import type { ResolvedSubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import { expect, it } from 'vitest'
import { mountWorkingDirectoryFixture } from '../../../subagent/subagent/tests/working-directory-fixture.ts'
import * as externalTool from '../src/index.ts'
@@ -19,16 +18,14 @@ async function setup() {
const harness = await mountAgentLoopTestHarness(ctx)
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
const calls: { id: SessionId; provider: string; request: ResolvedSubagentStartRequest }[] = []
const calls: { provider: string; request: ResolvedSubagentStartRequest }[] = []
for (const name of ['codex-provider', 'claude-provider', 'future-provider']) {
ctx.subagents.registerProvider({
name, capabilities: NO_START_CAPABILITIES, inheritsParentContext: false,
start: async (request) => {
const id = SessionId(randomUUID())
calls.push({ id, provider: name, request })
calls.push({ provider: name, request })
return {
id,
id: SessionId(randomUUID()),
result: Promise.resolve({ output: [{ type: 'text' as const, text: 'child result' }], stopReason: 'completed' as const }),
dispose: () => Promise.resolve(),
}
@@ -47,22 +44,22 @@ it('selects one configured provider per call and exposes only task inputs', asyn
})
const schema = ctx.tools.schemas().find(tool => tool.name === 'subagent_oar')
expect(schema?.parameters.required).toEqual(['runtime', 'description', 'prompt'])
expect(Object.keys(schema?.parameters.properties as Record<string, unknown>).sort()).toEqual(['cwd', 'description', 'name', 'prompt', 'runtime'])
expect(Object.keys(schema?.parameters.properties as Record<string, unknown>).sort()).toEqual(['cwd', 'description', 'prompt', 'runtime'])
expect(schema?.parameters.properties).toMatchObject({ runtime: { enum: ['codex', 'claude', 'future'] } })
for (const [index, runtime] of ['codex', 'claude', 'future'].entries()) {
for (const runtime of ['codex', 'claude', 'future']) {
const result = await executeToolFixture(ctx, {
agent: parent, name: 'subagent_oar', signal: test.signal,
callId: ToolCallId(randomUUID()),
arguments: { runtime, description: `Use ${runtime}`, prompt: 'independent task' },
})
expect(result.isError, JSON.stringify(result.content)).toBe(false)
expect(result.value).toEqual({ kind: 'activation', subagentId: calls[index]?.id, name: `lead/agent-${index + 1}` })
expect(result.content).toEqual([{ type: 'text', text: `started subagent ${calls[index]?.id}\nname: lead/agent-${index + 1}` }])
expect(result.content).toHaveLength(1)
const content = result.content[0]
expect(content?.type).toBe('text')
if (content?.type === 'text') expect(content.text).toMatch(/^started subagent /u)
}
expect(calls.map(call => call.provider)).toEqual(['codex-provider', 'claude-provider', 'future-provider'])
expect(calls[0]?.request.prompt).toEqual([
{ type: 'text', text: 'independent task' },
])
expect(calls[0]?.request.prompt).toEqual([{ type: 'text', text: 'independent task' }])
expect(calls[0]?.request.parent).toBe(parent)
expect(calls[0]?.request.cwd).toBe(process.cwd())
expect(ctx.tools.get('subagent_oar')?.isConcurrencySafe?.({
@@ -116,7 +113,7 @@ it('requires a caller and independently scoped one-shot providers', async (test)
start: async () => { throw new Error('must not run') },
})
await ctx.plugin(externalTool, { providers: { inherited: 'inherited' } })
const args = { name: 'inspector', runtime: 'inherited', description: 'task', prompt: 'task' }
const args = { runtime: 'inherited', description: 'task', prompt: 'task' }
const noParent = await executeToolFixture(ctx, {
name: 'subagent_oar', arguments: args, callId: ToolCallId(randomUUID()), signal: test.signal,
})
@@ -132,7 +129,7 @@ it('passes an explicit child directory and presents the task description', async
const { ctx, parent, calls } = await setup()
test.onTestFinished(() => ctx.fiber.dispose())
await ctx.plugin(externalTool, { providers: { codex: 'codex-provider' } })
const args = { name: 'inspector', runtime: 'codex', description: 'Inspect child', prompt: 'task', cwd: 'packages' }
const args = { runtime: 'codex', description: 'Inspect child', prompt: 'task', cwd: 'packages' }
expect(ctx.tools.get('subagent_oar')?.presentCall?.(args)).toEqual({
card: 'generic', title: 'Inspect child', rawInput: args,
})
@@ -140,8 +137,6 @@ it('passes an explicit child directory and presents the task description', async
agent: parent, name: 'subagent_oar', arguments: args, callId: ToolCallId(randomUUID()), signal: test.signal,
})
expect(result.isError).toBe(false)
expect(result.content).toEqual([{ type: 'text', text: `started subagent ${calls[0]?.id}\nname: lead/inspector` }])
expect(result.value).toEqual({ kind: 'activation', subagentId: calls[0]?.id, name: 'lead/inspector' })
expect(calls[0]?.request.cwd).toBe(join(process.cwd(), 'packages'))
})
@@ -151,69 +146,3 @@ it('supports direct composition with the declared optional defaults', async (tes
externalTool.apply(ctx, { providers: { codex: 'codex-provider' } })
expect(ctx.tools.schemas().some(tool => tool.name === 'subagent_oar')).toBe(true)
})
it('releases the requested name when provider startup fails', async (test) => {
const { ctx, parent } = await setup()
test.onTestFinished(() => ctx.fiber.dispose())
const childId = SessionId(randomUUID())
const start = vi.fn<() => Promise<SubagentRun>>()
.mockRejectedValueOnce(new Error('provider startup failed'))
.mockResolvedValueOnce({
id: childId,
result: Promise.resolve({ output: [], stopReason: 'completed' }),
dispose: async () => {},
})
ctx.subagents.registerProvider({
name: 'retry-provider', capabilities: NO_START_CAPABILITIES, inheritsParentContext: false, start,
})
await ctx.plugin(externalTool, { providers: { retry: 'retry-provider' } })
const args = { name: 'inspector', runtime: 'retry', description: 'task', prompt: 'task' }
const failed = await executeToolFixture(ctx, {
agent: parent, name: 'subagent_oar', arguments: args, callId: ToolCallId(randomUUID()), signal: test.signal,
})
expect(failed.isError).toBe(true)
expect(failed.error?.message).toContain('provider startup failed')
const retried = await executeToolFixture(ctx, {
agent: parent, name: 'subagent_oar', arguments: args, callId: ToolCallId(randomUUID()), signal: test.signal,
})
expect(retried.isError, JSON.stringify(retried.content)).toBe(false)
expect(retried.value).toEqual({ kind: 'activation', subagentId: childId, name: 'lead/inspector' })
expect(start).toHaveBeenCalledTimes(2)
})
it('reports the accepted child id without stopping it when name binding fails', async (test) => {
const { ctx, parent } = await setup()
const childId = SessionId(randomUUID())
const childResult = Promise.withResolvers<SubagentResult>()
let childRequest: ResolvedSubagentStartRequest | undefined
const dispose = vi.fn(async () => { childResult.resolve({ output: [], stopReason: 'aborted' }) })
const bind = vi.spyOn(ctx.subagentNames, 'bind').mockImplementationOnce(() => { throw new Error('session append failed') })
test.onTestFinished(async () => {
bind.mockRestore()
childResult.resolve({ output: [], stopReason: 'completed' })
await ctx.fiber.dispose()
})
ctx.subagents.registerProvider({
name: 'pending-provider', capabilities: NO_START_CAPABILITIES, inheritsParentContext: false,
start: async (request) => {
childRequest = request
return { id: childId, result: childResult.promise, dispose }
},
})
await ctx.plugin(externalTool, { providers: { pending: 'pending-provider' } })
const result = await executeToolFixture(ctx, {
agent: parent, name: 'subagent_oar', callId: ToolCallId(randomUUID()), signal: test.signal,
arguments: { name: 'inspector', runtime: 'pending', description: 'task', prompt: 'task' },
})
expect(result.isError).toBe(true)
expect(result.error?.info?.code).toBe('SUBAGENT_NAME_BIND_FAILED')
expect(result.error?.message).toContain(`Subagent ${childId} started, but its name lead/inspector could not be recorded.`)
expect(result.error?.message).toContain('Check this child before retrying.')
expect(dispose).not.toHaveBeenCalled()
expect(childRequest?.signal.aborted).toBe(false)
const reservation = ctx.subagentNames.reserve(parent.session, parent.id, 'inspector')
expect(reservation.name).toBe('lead/inspector')
reservation.dispose()
childResult.resolve({ output: [{ type: 'text', text: 'child finished' }], stopReason: 'completed' })
await expect.poll(() => dispose.mock.calls.length).toBe(1)
})
@@ -17,14 +17,8 @@
{
"path": "../../core/tools"
},
{
"path": "../../llm/llm"
},
{
"path": "../../subagent/subagent"
},
{
"path": "../../subagent/subagent-names"
}
]
}
@@ -3595,36 +3595,6 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
description: 'Host settings owner that records Session authorization before the first model request.',
methods: [],
},
{
key: 'subagentNames',
summary: 'Root-Session aliases shared by named creation, discovery, and control tools.',
description: 'Root-Session aliases shared by named creation, discovery, and control tools.',
methods: [
{
signature: 'reserve(rootSession: Session, parentId: SessionId, requested?: string): SubagentNameReservation',
description: 'Reserve a complete name without starting an agent or writing a Session event.',
parameters: [{ name: 'rootSession', description: 'root Session selected by the caller through the subagent service.' }, { name: 'parentId', description: 'parent identity whose name prefixes the child name.' }, { name: 'requested', description: 'optional sibling segment; omission selects the first free agent-N.' }],
returns: 'a reservation the caller must dispose after startup and binding.',
},
{
signature: 'bind(rootSession: Session, childId: SessionId, name: string): void',
description: 'Record an accepted child\'s full name in the caller\'s root Session. Binding failure does not cancel or dispose the child; callers report its id.',
parameters: [{ name: 'rootSession', description: 'root Session selected by the caller through the subagent service.' }, { name: 'childId', description: 'identity returned by the subagent service.' }, { name: 'name', description: 'complete name returned by reserve.' }],
},
{
signature: 'nameOf(rootSession: Session, targetId: SessionId): string',
description: 'Read a bound name from the root projection without loading ancestors.',
parameters: [{ name: 'rootSession', description: 'root Session selected by the caller through the subagent service.' }, { name: 'targetId', description: 'identity whose name is requested.' }],
returns: 'lead for the root, a bound full name, or the unchanged id.',
},
{
signature: 'resolveName(rootSession: Session, name: string): SessionId',
description: 'Resolve an alias or pass through an id for the subagent service to authorize.',
parameters: [{ name: 'rootSession', description: 'root Session selected by the caller through the subagent service.' }, { name: 'name', description: 'lead, a bound full name, or a raw Session id.' }],
returns: 'the target identity; an unknown lead/... name throws.',
},
],
},
{
key: 'subagents',
summary: 'Named provider registry with managed activations, durable discovery, and local child messaging.',
@@ -3663,7 +3633,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
},
{
signature: 'async sendMessage( sender: Agent, targetId: SessionId, content: ContentBlock[], options: SubagentSendMessageOptions, ): Promise<MessageId>',
description: 'Steer one model-authored message to another member of the same subagent tree. A running target receives it at its next step; an idle target starts a turn. A cold target resumes under its runtime subagent tree root without restoring its creation ancestors. Sender identity is rechecked before inbox acceptance; cancellation stops only pre-acceptance work and never retracts an accepted message. Display attribution uses the sender\'s bound name when the naming service is mounted, otherwise its Session id. Name lookup failures reject before inbox acceptance.',
description: 'Steer one model-authored message to another member of the same subagent tree. A running target receives it at its next step; an idle target starts a turn. A cold target resumes under its runtime subagent tree root without restoring its creation ancestors. Sender identity is rechecked before inbox acceptance; cancellation stops only pre-acceptance work and never retracts an accepted message.',
parameters: [{ name: 'sender', description: 'exact live root or resident continuable Agent originating the message.' }, { name: 'targetId', description: 'another root or continuable member\'s durable Session id.' }, { name: 'content', description: 'model-authored content to deliver.' }, { name: 'options', description: 'caller cancellation before inbox acceptance.' }],
returns: 'the accepted message\'s inbox id.',
throws: ['when the target is outside the tree, cannot resume, or rejects input.'],
@@ -9095,10 +9065,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [
name: 'SubagentListEntry',
declaration: 'export type SubagentListEntry = SubagentCatalogRow & {\n readonly kind: \'child\';\n readonly hasChildren: boolean;\n} | {\n readonly kind: \'diagnostic\';\n readonly id: SessionId;\n readonly reason: \'corrupt\' | \'unsupported\' | \'unavailable\';\n};',
},
{
name: 'SubagentNameReservation',
declaration: 'export interface SubagentNameReservation {\n readonly name: string;\n dispose(): void;\n}',
},
{
name: 'SubagentPromptReceipt',
declaration: 'export interface SubagentPromptReceipt {\n readonly messageId: MessageId;\n}',
@@ -466,7 +466,6 @@ Every package below exports a Cordis plugin that a bundle patch can name in a Lo
| `@deepseek-ai/dsh-subagent-codex` | yes | One-shot Codex subagent provider over the official app-server protocol |
| `@deepseek-ai/dsh-subagent-dsh-sdk` | yes | Out-of-process SDK subagent backend: drives a child DeepSeek Harness runtime subprocess over stdio JSON-RPC through the TypeScript SDK client |
| `@deepseek-ai/dsh-subagent-fork-in-process` | yes | In-process fork subagent backend: runs a child agent seeded with a prefix of the parent's log |
| `@deepseek-ai/dsh-subagent-names` | no | Durable name mappings for subagent tools |
| `@deepseek-ai/dsh-subagent-spawn-in-process` | yes | In-process spawn subagent backend: runs a fresh child agent on ctx.agents |
| `@deepseek-ai/dsh-tool-subagent` | yes | Model-facing subagent delegation tool over the ctx.subagents seam |
| `@deepseek-ai/dsh-tool-subagent-control` | yes | Shared messaging, interruption, and child discovery tools for subagents |
+2 -2
View File
@@ -15,8 +15,8 @@
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
/deepseek-ai-dsh-sdk-jsonrpc-server/use-this-package:
en: 364c47155e5687be
zh: 7322c098fc8f23c5
en: 1a8598a9ca7ec995
zh: dbbd0dd3aecb7d24
/deepseek-ai-dsh-sdk-jsonrpc-server/use-this-package/wiring:
en: 47a33e9397e36e15
zh: e6d173820f9ea6e5
+1 -1
View File
@@ -27,7 +27,7 @@ English | [中文](README.zh.md)
Mount this plugin when a runtime must serve SDK clients: add it to a `cordis.yml` that composes the agent service, boot the runtime, and clients connect over stdio. The common path is explicit — the plugin needs the `agents` service; every other capability comes from the surrounding tree.
Set `initialize.approvalRequests` to `true` to send internal approvals as server-to-client `approval/request` calls. The bridge handles SDK-owned Agents and their live in-process descendants, resolving descendants through `subagents.rootOf()` and matching the exact SDK-owned root instance. Persisted `parentSession` lineage does not establish approval ownership. Unrelated Agents and clients that omit the flag continue through the composed answerer chain. The approval service still applies `never` policy before dispatch and records the ask/decision audit pair. The bridge accepts only `allowed-once`, `rejected`, or `cancelled`; an invalid response, client error, or transport loss yields the service's fail-closed `unavailable` outcome. Request cancellation abandons the pending wire waiter; shutdown abandons all such waiters before draining Agents. Late replies cannot grant an abandoned operation. See the [protocol](../protocol/README.md) for the payload and the [Python example](../../../python/sdk/README.md#client-approvals) for client handling.
Set `initialize.approvalRequests` to `true` to send internal approvals as server-to-client `approval/request` calls. The bridge handles SDK-owned Agents and their live in-process descendants, resolving descendants through `subagents.rootOf()` and matching the exact SDK-owned root instance. Persisted `parentSession` lineage does not establish approval ownership. A root lookup error with `name: SubagentError` and `code: UNAUTHORIZED` delegates to the next answerer regardless of its constructor; other lookup failures yield `unavailable` without delegation. Unrelated Agents and clients that omit the flag continue through the composed answerer chain. The approval service still applies `never` policy before dispatch and records the ask/decision audit pair. The bridge accepts only `allowed-once`, `rejected`, or `cancelled`; an invalid response, client error, or transport loss yields the service's fail-closed `unavailable` outcome. Request cancellation abandons the pending wire waiter; shutdown abandons all such waiters before draining Agents. Late replies cannot grant an abandoned operation. See the [protocol](../protocol/README.md) for the payload and the [Python example](../../../python/sdk/README.md#client-approvals) for client handling.
`session/resume` accepts a persisted `{ sessionId }` after initialization and returns `{}` once its Agent is ready. It loads history through `agents.resume()`, repairs an interrupted final turn, and preserves the stored origin and effective directory while recording the initialized model route as the Session selection. An omitted effort preserves an explicit saved effort only for that same route. It sends no new input. Subagent sessions reject before loading or repairing history; resume their root session instead. Missing, unreadable, incompatible, or occupied sessions reject without creating a replacement. Prompts for an identity currently being restored wait for that restoration; shutdown drains pending restoration and releases its Agent. Historical events are retained in the log rather than streamed again as new notifications. A model selection recorded during resume emits a new `session.event` notification before the resume response.
+1 -1
View File
@@ -27,7 +27,7 @@ kind: "package-reference"
当运行时必须服务 SDK 客户端时挂载本插件:把它加入组合了 agent 服务的 `cordis.yml`,启动运行时,客户端即可通过 stdio 连接。常用路径是显式的——插件需要 `agents` 服务;其余每个能力都来自外围插件树。
将 `initialize.approvalRequests` 设为 `true`,即可把内部审批作为服务端到客户端的 `approval/request` 请求发送。桥接处理 SDK 拥有的 Agent 及其仍在运行的进程内后代,通过 `subagents.rootOf()` 解析后代的根,并与 SDK 拥有的确切根实例匹配,持久化的 `parentSession` 历史关系不决定审批归属;其他 Agent,以及未提供该标志的客户端,继续使用已组合的应答链。审批服务仍在分发前执行 `never` 策略,并记录询问与决定的审计事件。桥接仅接受 `allowed-once`、`rejected` 或 `cancelled`;无效响应、客户端错误或传输断开会产生审批服务的拒绝放行结果 `unavailable`。请求取消时释放待处理的协议等待项;shutdown 在等待 Agent 结束前释放所有此类等待项。迟到响应不能授权已放弃的操作。载荷见[协议](../protocol/README.zh.md),客户端处理见 [Python 示例](../../../python/sdk/README.zh.md#client-approvals)。
将 `initialize.approvalRequests` 设为 `true`,即可把内部审批作为服务端到客户端的 `approval/request` 请求发送。桥接处理 SDK 拥有的 Agent 及其仍在运行的进程内后代,通过 `subagents.rootOf()` 解析后代的根,并与 SDK 拥有的确切根实例匹配,持久化的 `parentSession` 历史关系不决定审批归属。根查找错误的 `name: SubagentError` 与 `code: UNAUTHORIZED` 同时匹配时,无论其构造器是什么,均委派给下一个应答方;其他查找失败返回 `unavailable`,不继续委派。其他 Agent,以及未提供该标志的客户端,继续使用已组合的应答链。审批服务仍在分发前执行 `never` 策略,并记录询问与决定的审计事件。桥接仅接受 `allowed-once`、`rejected` 或 `cancelled`;无效响应、客户端错误或传输断开会产生审批服务的拒绝放行结果 `unavailable`。请求取消时释放待处理的协议等待项;shutdown 在等待 Agent 结束前释放所有此类等待项。迟到响应不能授权已放弃的操作。载荷见[协议](../protocol/README.zh.md),客户端处理见 [Python 示例](../../../python/sdk/README.zh.md#client-approvals)。
`session/resume` 在初始化后接受持久化的 `{ sessionId }`,待其 Agent 就绪后返回 `{}`。它通过 `agents.resume()` 加载历史、修复最后一个被中断的轮次,并保留存储的起始目录和当前有效目录,同时将初始化时选定的模型路由记录为 Session 选择。省略推理强度时,仅在路由相同的情况下保留已保存的显式强度。它不发送新输入。子代理会话在加载或修复历史前被拒绝;应恢复其根会话。会话缺失、不可读、不兼容或被占用时,请求失败且不创建替代会话。对正在恢复的标识发送提示词时,会等待恢复完成;shutdown 会等待进行中的恢复并释放其 Agent。历史事件保留在日志中,不会作为新通知重新发送。恢复期间记录的模型选择会在恢复响应前发出新的 `session.event` 通知。
+9 -2
View File
@@ -21,7 +21,7 @@ import type {} from '@deepseek-ai/dsh-working-directory'
import type {} from '@deepseek-ai/dsh-user-approval'
import type {} from '@deepseek-ai/dsh-schedule'
import { SubagentError, type SubagentRunEndInfo } from '@deepseek-ai/dsh-subagent'
import type { SubagentRunEndInfo } from '@deepseek-ai/dsh-subagent'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek-api-key'
import type {
ApprovalRequestParams,
@@ -382,6 +382,11 @@ export class HarnessSdkJsonRpcServer {
}
}
/**
* Resolves live SDK ownership; root lookup failures other than SubagentError/UNAUTHORIZED propagate.
* @param agent - Agent whose requests need routing.
* @returns Whether this server owns the Agent or its live subagent tree root.
*/
private ownsAgent(agent: Agent): boolean {
const agents = this.ctx.agents
if (agents.get(agent.id) !== agent) return false
@@ -392,7 +397,9 @@ export class HarnessSdkJsonRpcServer {
try {
root = subagents.rootOf(agent)
} catch (error: unknown) {
if (error instanceof SubagentError && error.code === 'UNAUTHORIZED') return false
if (typeof error === 'object' && error !== null
&& 'name' in error && error.name === 'SubagentError'
&& 'code' in error && error.code === 'UNAUTHORIZED') return false
throw error
}
return this.sessions.get(root.session.id)?.handle.agent === root
@@ -237,8 +237,30 @@ describe('SDK approval bridge', () => {
expect(clientDecision).not.toHaveBeenCalled()
})
it.each([
{ name: 'SubagentError', code: 'UNAUTHORIZED' },
Object.assign(new Error('not a managed subagent'), { name: 'SubagentError', code: 'UNAUTHORIZED' }),
])('delegates approval for an unauthorized ownership lookup by error fields: %s', async (error) => {
const h = await setup(true, true)
const foreign = await h.ctx.agents.create({ sessionId: SessionId('foreign') })
onTestFinished(() => foreign.dispose())
foreign.agent.session.append('turn/start', { turn: 1 })
vi.spyOn(h.ctx.subagents, 'rootOf').mockImplementationOnce(() => { throw error })
const clientDecision = vi.fn(async () => ({ outcome: 'allowed-once' }))
h.client.onRequest(clientDecision)
const fallback = vi.fn(() => Promise.resolve('rejected' as const))
h.ctx.on('approval/request', fallback)
await expect(h.ctx.approval.request({ agent: foreign.agent, toolName: 'bash' })).resolves.toBe('rejected')
expect(fallback).toHaveBeenCalledOnce()
expect(clientDecision).not.toHaveBeenCalled()
})
it.each([
new Error('root lookup failed'), new SubagentError('root lookup failed', 'NOT_RESUMABLE'),
null, undefined, 'root lookup failed', {},
{ name: 'SubagentError' }, { code: 'UNAUTHORIZED' },
{ name: 'OtherError', code: 'UNAUTHORIZED' },
{ name: 'SubagentError', code: 'NOT_RESUMABLE' },
])('does not pass a failed ownership lookup to another approval answerer: %s', async (error) => {
const h = await setup(true, true)
const foreign = await h.ctx.agents.create({ sessionId: SessionId('foreign') })
+2 -2
View File
@@ -15,8 +15,8 @@
en: 1eb35b015a6c423f
zh: a8a3dceee2c9e943
/subagent-subagent-capability-family/packages:
en: 65ac5ca8f6108b9a
zh: 35a61d1049a5b5d5
en: 6e34528cfd8a3b2c
zh: 88655c4fbe96c861
/subagent-subagent-capability-family/related-documentation:
en: e525958323eebabe
zh: d32725ee5393ad33
-1
View File
@@ -25,7 +25,6 @@ The subagent package family lets an agent delegate a task to a child, continue t
| Package | Role | ctx key |
|---|---|---|
| [`subagent/`](subagent/README.md) | Defines the delegation service: provider registry, one-shot runs, continuable children, and discovery | `ctx.subagents` |
| [`subagent-names/`](subagent-names/README.md) | Adds optional names to delegation and control tools without changing the execution service | `ctx.subagentNames` |
| [`subagent-spawn-in-process/`](subagent-spawn-in-process/README.md) | Runs a fresh in-process child | registers on `ctx.subagents` |
| [`subagent-fork-in-process/`](subagent-fork-in-process/README.md) | Runs an in-process child seeded from the parent's completed history | registers on `ctx.subagents` |
| [`subagent-acp/`](subagent-acp/README.md) | Runs an out-of-process child over the Agent Client Protocol | registers on `ctx.subagents` |
-1
View File
@@ -25,7 +25,6 @@ subagent 包家族让 agent(智能体)将任务委派给子 agent、继续
| 包 | 职责 | ctx 键 |
|---|---|---|
| [`subagent/`](subagent/README.zh.md) | 定义委派服务:提供方注册表、一次性运行、可继续子级与发现 | `ctx.subagents` |
| [`subagent-names/`](subagent-names/README.zh.md) | 为委派和控制工具添加可选名字,保持执行服务不变 | `ctx.subagentNames` |
| [`subagent-spawn-in-process/`](subagent-spawn-in-process/README.zh.md) | 运行全新的进程内子 agent | 注册到 `ctx.subagents` |
| [`subagent-fork-in-process/`](subagent-fork-in-process/README.zh.md) | 运行从父级已完成历史派生的进程内子 agent | 注册到 `ctx.subagents` |
| [`subagent-acp/`](subagent-acp/README.zh.md) | 经 Agent Client Protocol 运行进程外子 agent | 注册到 `ctx.subagents` |
@@ -14,7 +14,7 @@ class MockDelegatingAdapter extends LlmAdapter {
: ''
if (!options.messages.some(message => message.role === 'tool')) {
const args = JSON.stringify({ name: 'cwd-probe', description: 'cwd probe', prompt: 'report your workspace' })
const args = JSON.stringify({ 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}\nname: lead/cwd-probe` },
{ type: 'text', text: `started subagent ${notice.data.source.senderSessionId}` },
])
return notice.data.content.slice(1).filter(block => block.type === 'text').map(block => block.text)
}
@@ -79,22 +79,22 @@ describe('product-provider public Loader composition', () => {
tools: [
{
name: 'subagent_codex',
parameterNames: ['cwd', 'description', 'name', 'prompt'],
parameterNames: ['cwd', 'description', 'prompt'],
required: ['description', 'prompt'],
},
{
name: 'subagent_claude_code',
parameterNames: ['cwd', 'description', 'name', 'prompt'],
parameterNames: ['cwd', 'description', 'prompt'],
required: ['description', 'prompt'],
},
{
name: 'subagent_claude_primary',
parameterNames: ['cwd', 'description', 'name', 'prompt'],
parameterNames: ['cwd', 'description', 'prompt'],
required: ['description', 'prompt'],
},
{
name: 'subagent_claude_secondary',
parameterNames: ['cwd', 'description', 'name', 'prompt'],
parameterNames: ['cwd', 'description', 'prompt'],
required: ['description', 'prompt'],
},
],
@@ -69,17 +69,17 @@ describe('Codex provider public Loader composition', () => {
tools: [
{
name: 'subagent_codex',
parameterNames: ['cwd', 'description', 'name', 'prompt'],
parameterNames: ['cwd', 'description', 'prompt'],
required: ['description', 'prompt'],
},
{
name: 'subagent_codex_primary',
parameterNames: ['cwd', 'description', 'name', 'prompt'],
parameterNames: ['cwd', 'description', 'prompt'],
required: ['description', 'prompt'],
},
{
name: 'subagent_codex_secondary',
parameterNames: ['cwd', 'description', 'name', 'prompt'],
parameterNames: ['cwd', 'description', 'prompt'],
required: ['description', 'prompt'],
},
],
@@ -33,7 +33,6 @@ class MockDelegatingAdapter extends LlmAdapter {
? { reasoning_effort: 'max' }
: { provider: 'mock', model: 'mock-routed', reasoning_effort: 'max' }
const args = JSON.stringify({
name: 'route-probe',
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({ name: 'nested-fork', description: 'nested fork', prompt: 'nested-child-task' })
const args = JSON.stringify({ 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}\nname: lead/route-probe` },
{ type: 'text', text: `started subagent ${notice.data.source.senderSessionId}` },
])
return notice.data.content.slice(1).filter(block => block.type === 'text').map(block => block.text)
}
@@ -1,46 +0,0 @@
# Bilingual-pair consistency record for README.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 packages/subagent/subagent-names/README.md
/:
en: 26564f09306cf08f
zh: b8933154b4911a0c
/deepseek-ai-dsh-subagent-names:
en: 622989d6dcd8611a
zh: 6f0a3beb8e665332
/deepseek-ai-dsh-subagent-names/summary:
en: 1af1d0f9ab2e446e
zh: 4846168336979ca6
/deepseek-ai-dsh-subagent-names/table-of-contents:
en: d152484eb41ac6b4
zh: c614dc45fe9577a5
/deepseek-ai-dsh-subagent-names/use-this-package:
en: 15e2ab025baa3f2e
zh: eab7ed264c78a4e4
/deepseek-ai-dsh-subagent-names/use-this-package/minimal-configuration:
en: 434c0ecf540e5fd6
zh: e3db4a17ad90b531
/deepseek-ai-dsh-subagent-names/use-this-package/choosing-and-finding-names:
en: d7f5ce2da602deb1
zh: 9862710f0cd113b9
/deepseek-ai-dsh-subagent-names/use-this-package/binding-after-startup:
en: f262bc2818040a38
zh: edd0a989f1b50dc5
/deepseek-ai-dsh-subagent-names/understand-the-implementation:
en: 5f15aa5294842556
zh: 790309123df2b152
/deepseek-ai-dsh-subagent-names/further-exploration:
en: ed0fb57dc13736e6
zh: acd0cd0756510e65
/deepseek-ai-dsh-subagent-names/model-experience:
en: 40f9481ebd3e077d
zh: 118c16ec3fed9652
/deepseek-ai-dsh-subagent-names/model-experience/kv-cache-effect:
en: cf0b791cc8ab26f4
zh: 73f733b039240070
/deepseek-ai-dsh-subagent-names/known-limitations-and-deferred-work:
en: 81988fdd0823cffa
zh: 96bacb5621c1e874
/deepseek-ai-dsh-subagent-names/known-limitations-and-deferred-work/dev-note:
en: e67f633dc6fe7773
zh: 8ec3e73528ddb1a6
-113
View File
@@ -1,113 +0,0 @@
---
description: "Bind readable subagent names to Session ids and recover the mappings from the root Session."
kind: "package-reference"
---
# @deepseek-ai/dsh-subagent-names
English | [中文](README.zh.md)
## Summary
Address delegated agents by readable names such as `lead/research` and `lead/research/checker`. Bound names survive root Session reloads and keep the same spelling after agents finish. Choose this package when delegation tools need names while preserving Session ids for execution and authorization. Agents without a bound name remain addressable by id.
## Table of Contents
- [Use this package](#use-this-package)
- [Understand the implementation](#understand-the-implementation)
- [Further Exploration](#further-exploration)
- [Model Experience](#model-experience)
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
- [Dev Note](#dev-note)
-----
<a id="use-this-package"></a>
## Use this package
Mount this plugin with the naming-aware [delegation tool](../tool-subagent/README.md) and [control tools](../tool-subagent-control/README.md).
### Minimal configuration
Add this row to a composition providing `sessionProjections`:
```yaml
- name: '@deepseek-ai/dsh-subagent-names'
```
This plugin has no configuration fields. It exposes `ctx.subagentNames`; the subagent service owns execution, messages, discovery, and authorization.
### Choosing and finding names
A requested name segment starts with a lower-case letter and contains only lower-case letters, digits, underscores, and hyphens. Omission selects the first available `agent-N` under the parent's name, starting at `agent-1`. Reservations prevent concurrent starts from choosing the same full name. A bound name remains occupied after execution settles.
The root is `lead`. `nameOf` returns a bound full name, or the original Session id when no binding exists. `resolveName` accepts `lead` and bound full names; it rejects an unknown `lead/...` name and passes other ids through. The subsequent subagent operation still authorizes the caller.
### Binding after startup
Resolve `rootSession = ctx.subagents.rootOf(parent).session`, then call `reserve(rootSession, parent.id, requested?)`, start the child through `ctx.subagents.startActivation`, then call `bind(rootSession, childId, reservation.name)` with the successful receipt. Release the reservation with `dispose()` in `finally`, whether startup and binding succeed or fail.
Binding records the full name in the root Session. If binding fails after startup succeeds, the caller must report the already-started child id; the child remains managed by the subagent service. A crash before binding can likewise leave an unnamed child that remains addressable by id.
-----
<a id="understand-the-implementation"></a>
## Understand the implementation
<details>
<summary>Implementation internals — click to expand</summary>
Each root Session stores its own name-to-id bindings. All naming operations receive that root Session explicitly; the naming service does not resolve live Agent membership. A projection recovers those bindings and excludes inherited fork records. Lookups read this root mapping without observing ancestor Sessions or joining child catalogs. Callers own transient reservations and release them after their operation.
The mapping retains one binding per named child: storage is O(n), a lookup or binding is O(n), and replaying n distinct bindings is O(n²) in the worst case because each event searches the binding array.
| File | Role |
|---|---|
| [`src/index.ts`](src/index.ts) | Name reservations, binding, and lookup |
| [`src/projection.ts`](src/projection.ts) | Durable bindings and replay validation |
</details>
-----
<a id="further-exploration"></a>
## Further Exploration
These pages own execution and the model-facing tools that use names.
- [Subagent service](../subagent/README.md) — activation ownership, catalogs, and authorization.
- [Delegation tool](../tool-subagent/README.md) — named creation and start acknowledgements.
- [Control tools](../tool-subagent-control/README.md) — discovery, messaging, and interruption.
- [Session projections](../../session/session-projection/README.md) — incremental recovery.
-----
<a id="model-experience"></a>
## Model Experience
Indirectly, through the [delegation tool](../tool-subagent/README.md) and [control tools](../tool-subagent-control/README.md), which render names alongside Session ids and accept names as addresses.
#### KV Cache effect
The subagent message service reads sender names from this service automatically. This plugin adds no prompt text or tool schema. Its consumers own any model-visible name text and its cache effect.
## Known Limitations and Deferred Work
<a id="known-limitations-and-deferred-work"></a>
Name bindings have these limits:
- **Names are available after binding** — the child's first request may precede its binding; this plugin adds no initial identity message.
- **Names are immutable** — bound names and ids cannot be reassigned or reused after an activation ends.
- **Paths capture the parent's name at reservation time** — an unnamed parent uses `lead/legacy:<parentId>` as its prefix; a later parent binding does not rewrite the child's stored path.
- **Naming is explicit** — callers that do not bind a name continue to use Session ids.
<a id="dev-note"></a>
### Dev Note
<details>
<summary>Working context for maintainers — click to expand</summary>
None.
</details>
@@ -1,113 +0,0 @@
---
description: "将可读的 subagent 名称绑定到 Session id,并从根 Session 恢复映射。"
kind: "package-reference"
---
# @deepseek-ai/dsh-subagent-names
[English](README.md) | 中文
## 概述
通过 `lead/research` 和 `lead/research/checker` 这样的可读名称寻址委派的 agent(智能体)。绑定名称在根 Session 重新加载后仍然保留,agent 执行结束后也保持原名称。委派工具需要名称,同时仍以 Session id 执行操作和检查授权时,可选择此包。未绑定名称的 agent 仍可通过 id 寻址。
## 目录
- [使用此包](#use-this-package)
- [了解实现](#understand-the-implementation)
- [进一步探索](#further-exploration)
- [模型体验](#model-experience)
- [已知限制与待办工作](#known-limitations-and-deferred-work)
- [开发备注](#dev-note)
-----
<a id="use-this-package"></a>
## 使用此包
将此插件与支持命名的[委派工具](../tool-subagent/README.zh.md)和[控制工具](../tool-subagent-control/README.zh.md)一起挂载。
### 最小配置
在提供 `sessionProjections` 的组合中添加此配置项:
```yaml
- name: '@deepseek-ai/dsh-subagent-names'
```
此插件没有配置字段。它提供 `ctx.subagentNames`;subagent 服务负责执行、消息、发现和授权。
### 选择和查找名称
请求的名称段以小写字母开头,只能包含小写字母、数字、下划线和连字符。省略时,从 `agent-1` 开始选择父名称下第一个可用的 `agent-N`。预留可防止并发启动选择相同的完整名称。执行结算后,绑定名称仍被占用。
根名称为 `lead`。`nameOf` 返回已绑定的完整名称;没有绑定时,返回原 Session id。`resolveName` 接受 `lead` 和已绑定的完整名称;它拒绝未知的 `lead/...` 名称,并透传其他 id。后续 subagent 操作仍会对调用方进行授权检查。
### 启动后绑定
先解析 `rootSession = ctx.subagents.rootOf(parent).session`,再调用 `reserve(rootSession, parent.id, requested?)`,通过 `ctx.subagents.startActivation` 启动子 agent,再根据成功回执调用 `bind(rootSession, childId, reservation.name)`。无论启动和绑定成功还是失败,都要在 `finally` 中调用 `dispose()` 释放预留。
绑定将完整名称记录到根 Session。若启动成功后绑定失败,调用方必须报告已启动的子 agent id;该子 agent 仍由 subagent 服务管理。绑定前崩溃也可能留下未命名的子 agent,它仍可通过 id 寻址。
-----
<a id="understand-the-implementation"></a>
## 了解实现
<details>
<summary>实现内部细节 — 点击展开</summary>
每个根 Session 保存自己的名称到 id 绑定。所有命名操作都显式接收该根 Session;命名服务不解析在线 Agent 的树成员关系。投影恢复这些绑定,并排除 fork 继承的记录。查询读取根映射,无需观察祖先 Session 或关联子 agent 目录。调用方拥有临时预留,并在操作后释放它们。
映射为每个具名子 agent 保留一条绑定:空间为 O(n),单次查询或绑定为 O(n);由于每个事件都会搜索绑定数组,回放 n 条不同绑定的最坏时间复杂度为 O(n²)。
| 文件 | 职责 |
|---|---|
| [`src/index.ts`](src/index.ts) | 名称预留、绑定与查询 |
| [`src/projection.ts`](src/projection.ts) | 持久绑定与回放校验 |
</details>
-----
<a id="further-exploration"></a>
## 进一步探索
以下页面负责执行行为以及使用名称的模型侧工具。
- [Subagent 服务](../subagent/README.zh.md) — 激活所有权、目录与授权。
- [委派工具](../tool-subagent/README.zh.md) — 命名创建与启动确认。
- [控制工具](../tool-subagent-control/README.zh.md) — 发现、消息与中断。
- [Session 投影](../../session/session-projection/README.zh.md) — 增量恢复。
-----
<a id="model-experience"></a>
## 模型体验
通过[委派工具](../tool-subagent/README.zh.md)和[控制工具](../tool-subagent-control/README.zh.md)间接影响模型;这些工具在 Session id 旁展示名称,并接受名称作为地址。
#### KV Cache 影响
subagent 消息服务自动从此服务读取发送者名称。此插件不添加提示词文本或工具 schema。消费方负责模型可见的名称文本及其缓存影响。
## 已知限制与待办工作
<a id="known-limitations-and-deferred-work"></a>
名称绑定存在以下限制:
- **绑定后才可查询名称** — 子 agent 的首个请求可能早于绑定;此插件不添加初始身份消息。
- **名称不可变** — 激活结束后,绑定的名称和 id 也不能重新分配或复用。
- **路径采用预留时的父名称** — 未命名的父 agent 使用 `lead/legacy:<parentId>` 作为前缀;后续父级绑定不会改写子 agent 已存储的路径。
- **命名需要显式绑定** — 不绑定名称的调用方继续使用 Session id。
<a id="dev-note"></a>
### 开发备注
<details>
<summary>维护者工作上下文 — 点击展开</summary>
无。
</details>
@@ -1,5 +0,0 @@
{
"meta": {
"title": "Namen der Unteragenten"
}
}
@@ -1,5 +0,0 @@
{
"meta": {
"title": "Subagent names"
}
}
@@ -1,5 +0,0 @@
{
"meta": {
"title": "Nombres de subagentes"
}
}
@@ -1,5 +0,0 @@
{
"meta": {
"title": "Noms des sous-agents"
}
}
@@ -1,5 +0,0 @@
{
"meta": {
"title": "サブエージェント名"
}
}
@@ -1,5 +0,0 @@
{
"meta": {
"title": "Nomes de subagentes"
}
}
@@ -1,5 +0,0 @@
{
"meta": {
"title": "Имена субагентов"
}
}
@@ -1,5 +0,0 @@
{
"meta": {
"title": "子智能体名称"
}
}
@@ -1,54 +0,0 @@
{
"name": "@deepseek-ai/dsh-subagent-names",
"description": "Durable name mappings for subagent tools",
"version": "0.2.2-alpha.2",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/subagent/subagent-names"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json",
"./locale/*.json": "./locale/*.json"
},
"files": [
"locale/*.json",
"lib/index.js",
"lib/types/**/*.d.ts"
],
"license": "MIT",
"dependencies": {
"zod": "4.4.3"
},
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/dsh-llm": "workspace:*",
"@deepseek-ai/dsh-session": "workspace:*",
"@deepseek-ai/dsh-session-projection": "workspace:*"
},
"devDependencies": {
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/dsh-agent": "workspace:*",
"@deepseek-ai/dsh-agent-loop": "workspace:*",
"@deepseek-ai/dsh-agent-loop-testkit": "workspace:*",
"@deepseek-ai/dsh-llm": "workspace:*",
"@deepseek-ai/dsh-session": "workspace:*",
"@deepseek-ai/dsh-session-persistence": "workspace:*",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:*",
"@deepseek-ai/dsh-session-projection": "workspace:*",
"@deepseek-ai/dsh-subagent": "workspace:*",
"@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:*",
"@deepseek-ai/dsh-working-directory": "workspace:*"
}
}
@@ -1,130 +0,0 @@
/** Durable aliases for subagent tools; execution remains owned by subagents. @module */
import { Context, Service } from '@deepseek-ai/cordis'
import type { Session } from '@deepseek-ai/dsh-session'
import { HarnessError } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection'
import { fullNameSchema, nameSchema, subagentNamesProjection } from './projection.ts'
import type { SubagentNamesState } from './projection.ts'
export type { SubagentNameBinding } from './projection.ts'
declare module '@deepseek-ai/cordis' {
interface Context {
subagentNames: SubagentNames
}
}
/** Stable categories for alias validation and lookup failures. */
export type SubagentNameErrorCode = 'INVALID_AGENT_NAME' | 'DUPLICATE_AGENT_NAME' | 'AGENT_NAME_NOT_FOUND'
/** An alias could not be reserved, bound, or resolved. */
export class SubagentNameError extends HarnessError {
constructor(message: string, code: SubagentNameErrorCode) {
super(message, code)
this.name = 'SubagentNameError'
}
}
/** A caller-owned name reservation; release it after startup and binding settle. */
export interface SubagentNameReservation {
readonly name: string
/** Release the reservation; repeated disposal is harmless. */
dispose(): void
}
/** Root-Session aliases shared by named creation, discovery, and control tools. */
export class SubagentNames extends Service {
static inject = ['sessionProjections']
private projection!: ProjectionHandle<SubagentNamesState>
private readonly pending = new WeakMap<Session, Set<string>>()
constructor(ctx: Context) {
super(ctx, 'subagentNames')
}
protected async [Service.init](): Promise<void> {
this.projection = await this.ctx.sessionProjections.register(subagentNamesProjection)
}
/**
* Reserve a complete name without starting an agent or writing a Session event.
* @param rootSession - root Session selected by the caller through the subagent service.
* @param parentId - parent identity whose name prefixes the child name.
* @param requested - optional sibling segment; omission selects the first free agent-N.
* @returns a reservation the caller must dispose after startup and binding.
*/
reserve(rootSession: Session, parentId: SessionId, requested?: string): SubagentNameReservation {
if (requested !== undefined && !nameSchema.safeParse(requested).success) {
throw new SubagentNameError('agent name must start with a lower-case letter and contain only lower-case letters, digits, underscores, or hyphens', 'INVALID_AGENT_NAME')
}
const bindings = this.projection.stateOf(rootSession).bindings
const prefix = parentId === rootSession.id ? 'lead'
: bindings.find(binding => binding.childId === parentId)?.name ?? `lead/legacy:${parentId}`
const pending = this.pending.get(rootSession) ?? new Set<string>()
this.pending.set(rootSession, pending)
const occupied = new Set([...bindings.map(binding => binding.name), ...pending])
let segment = requested
if (segment === undefined) {
let ordinal = 1
while (occupied.has(`${prefix}/agent-${ordinal}`)) ordinal++
segment = `agent-${ordinal}`
}
const name = `${prefix}/${segment}`
if (occupied.has(name)) throw new SubagentNameError(`agent name "${name}" is already in use; choose another name`, 'DUPLICATE_AGENT_NAME')
pending.add(name)
let active = true
return { name, dispose: () => {
if (!active) return
active = false
pending.delete(name)
} }
}
/**
* Record an accepted child's full name in the caller's root Session.
* Binding failure does not cancel or dispose the child; callers report its id.
* @param rootSession - root Session selected by the caller through the subagent service.
* @param childId - identity returned by the subagent service.
* @param name - complete name returned by reserve.
*/
bind(rootSession: Session, childId: SessionId, name: string): void {
if (!fullNameSchema.safeParse(name).success) throw new SubagentNameError('a bound agent name must be a complete lead/... path', 'INVALID_AGENT_NAME')
const bindings = this.projection.stateOf(rootSession).bindings
const existing = bindings.find(binding => binding.childId === childId || binding.name === name)
if (existing !== undefined) {
if (existing.childId === childId && existing.name === name) return
throw new SubagentNameError(`agent name "${name}" or identity ${childId} is already bound`, 'DUPLICATE_AGENT_NAME')
}
rootSession.append('subagent-name/bound', { version: 1, childId, name })
}
/**
* Read a bound name from the root projection without loading ancestors.
* @param rootSession - root Session selected by the caller through the subagent service.
* @param targetId - identity whose name is requested.
* @returns lead for the root, a bound full name, or the unchanged id.
*/
nameOf(rootSession: Session, targetId: SessionId): string {
if (targetId === rootSession.id) return 'lead'
return this.projection.stateOf(rootSession).bindings.find(binding => binding.childId === targetId)?.name ?? targetId
}
/**
* Resolve an alias or pass through an id for the subagent service to authorize.
* @param rootSession - root Session selected by the caller through the subagent service.
* @param name - lead, a bound full name, or a raw Session id.
* @returns the target identity; an unknown lead/... name throws.
*/
resolveName(rootSession: Session, name: string): SessionId {
if (name === 'lead') return rootSession.id
const binding = this.projection.stateOf(rootSession).bindings.find(binding => binding.name === name)
if (binding !== undefined) return binding.childId
if (name.startsWith('lead/')) throw new SubagentNameError(`agent "${name}" was not found; use list_agents to discover names or ids`, 'AGENT_NAME_NOT_FOUND')
return SessionId(name)
}
}
export default SubagentNames
@@ -1,73 +0,0 @@
/** Root-Session aliases restored independently of subagent execution. @module */
import { z } from 'zod'
import { SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
/** One immutable full name bound to an accepted child identity. */
export interface SubagentNameBinding {
readonly childId: SessionId
readonly name: string
}
declare module '@deepseek-ai/dsh-session/types' {
interface SessionEventMap {
/**
* A root-owned alias for an accepted subagent identity.
* @param data - versioned child identity and complete name.
*/
'subagent-name/bound': SubagentNameBinding & { readonly version: 1 }
}
}
declare module '@deepseek-ai/dsh-session-projection/types' {
interface SessionProjectionMap {
subagentNames: SubagentNameBinding[]
}
}
/** Requested sibling names use one lower-case path segment. */
export const nameSchema = z.string().regex(/^[a-z][a-z0-9_-]*$/)
/** Bound paths include the root prefix and may include an unnamed parent's id. */
export const fullNameSchema = z.string().startsWith('lead/').min(6)
const bindingSchema = z.object({ childId: z.string().transform(SessionId), name: fullNameSchema }).strict()
const boundSchema = bindingSchema.extend({ version: z.literal(1) })
const bindingsSchema = z.array(bindingSchema).refine(
bindings => new Set(bindings.map(binding => binding.childId)).size === bindings.length
&& new Set(bindings.map(binding => binding.name)).size === bindings.length,
'child identities and names must each be unique',
)
const stateSchema = z.object({
inheritedEventCount: z.number().int().nonnegative().transform(SessionLogOffset),
bindings: bindingsSchema,
}).strict()
/** Recoverable root aliases, excluding inherited fork records. */
export type SubagentNamesState = z.infer<typeof stateSchema>
/** Alias recovery does not start agents or read other Sessions. */
export const subagentNamesProjection = {
key: 'subagentNames',
stateVersion: 1,
stateSchema,
init: (_header: SessionHeader, inheritedEventCount: SessionLogOffset): SubagentNamesState => ({
inheritedEventCount, bindings: [],
}),
apply(state: SubagentNamesState, event: SessionEvent): SubagentNamesState {
if (event.seq < state.inheritedEventCount) return state
if (event.type === 'subagent-name/bound') {
const { version: _version, ...binding } = boundSchema.parse(event.data)
const previous = state.bindings.find(entry => entry.childId === binding.childId || entry.name === binding.name)
if (previous !== undefined) {
if (previous.name !== binding.name || previous.childId !== binding.childId) throw new Error('subagent names and identities must have one immutable binding')
return state
}
return { ...state, bindings: [...state.bindings, binding] }
}
return state
},
wire: { viewSchema: z.array(bindingSchema), view: (state: SubagentNamesState) => state.bindings },
} satisfies ProjectionDefinition<'subagentNames', SubagentNamesState>
@@ -1,224 +0,0 @@
import { randomUUID } from 'node:crypto'
import { describe, expect, it, onTestFinished, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import { mountLocalActivations } from '../../subagent/tests/local-activation.ts'
import { loadStoredSession } from '../../subagent/tests/persistence-helpers.ts'
import SubagentNames from '../src/index.ts'
async function setup(script: ConstructorParameters<typeof MockAdapter>[0] = []) {
const ctx = new Context()
onTestFinished(() => ctx.fiber.dispose())
await mountAgentLoopTestDependencies(ctx)
await mountLocalActivations(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
const namesFiber = await ctx.plugin(SubagentNames)
ctx.llm.registerAdapter(['mock'], new MockAdapter(script))
const parentHandle = await ctx.agents.create({
sessionId: SessionId(randomUUID()), agentOptions: { provider: 'mock', model: 'mock' },
})
const parent = parentHandle.agent
ctx.on('agent/pre-step', async ({ agent }, next) => agent.id === parent.id ? { kind: 'reject' } : next())
const start = (creator: Agent = parent) => ctx.subagents.startActivation({
provider: 'spawn', label: 'Review', parent: creator, delivery: 'parent',
prompt: [{ type: 'text', text: 'Review the change' }], signal: new AbortController().signal,
})
return { ctx, parent, parentHandle, namesFiber, start }
}
describe('root Session name mappings', () => {
it('isolates root mappings and reservations without a subagent runtime', async () => {
const ctx = new Context()
onTestFinished(() => ctx.fiber.dispose())
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SubagentNames)
const first = ctx.sessions.create(SessionId('first-root'))
const second = ctx.sessions.create(SessionId('second-root'))
const names = ctx.subagentNames
const firstName = names.reserve(first, first.id, 'reviewer')
const secondName = names.reserve(second, second.id, 'reviewer')
onTestFinished(() => { firstName.dispose(); secondName.dispose() })
names.bind(first, SessionId('first-child'), firstName.name)
names.bind(second, SessionId('second-child'), secondName.name)
expect(ctx.get('subagents')).toBeUndefined()
expect(names.resolveName(first, 'lead/reviewer')).toBe('first-child')
expect(names.resolveName(second, 'lead/reviewer')).toBe('second-child')
expect(names.nameOf(first, SessionId('second-child'))).toBe('second-child')
expect(names.nameOf(second, second.id)).toBe('lead')
})
it('binds an accepted child without changing its id, prompt, or execution', async () => {
const { ctx, parent, start } = await setup([textResponse('done')])
const reservation = ctx.subagentNames.reserve(parent.session, parent.id, 'reviewer')
const activation = await start()
expect(ctx.subagentNames.nameOf(parent.session, activation.childId)).toBe(activation.childId)
ctx.subagentNames.bind(parent.session, activation.childId, reservation.name)
reservation.dispose()
await activation.result
expect(ctx.subagentNames.nameOf(parent.session, parent.id)).toBe('lead')
expect(ctx.subagentNames.nameOf(parent.session, activation.childId)).toBe('lead/reviewer')
expect(ctx.subagentNames.resolveName(parent.session, 'lead')).toBe(parent.id)
expect(ctx.subagentNames.resolveName(parent.session, 'lead/reviewer')).toBe(activation.childId)
expect(ctx.subagentNames.resolveName(parent.session, activation.childId)).toBe(activation.childId)
expect(ctx.subagentNames.resolveName(parent.session, 'unmapped-id')).toBe('unmapped-id')
expect(() => ctx.subagentNames.resolveName(parent.session, 'lead/misspelled')).toThrow('was not found')
const stored = await loadStoredSession(ctx.sessionPersistence, activation.childId)
const message = stored.events.find(event => event.type === 'user/message')
expect(message?.data.content[0]).toEqual({ type: 'text', text: 'Review the change' })
expect(JSON.stringify(message)).not.toContain('Your name is')
expect(stored.events.some(event => event.type === 'subagent-name/bound')).toBe(false)
})
it('reserves distinct defaults, rejects overlap, and reuses released unbound names', async () => {
const { ctx, parent } = await setup()
const first = ctx.subagentNames.reserve(parent.session, parent.id)
const second = ctx.subagentNames.reserve(parent.session, parent.id)
expect([first.name, second.name]).toEqual(['lead/agent-1', 'lead/agent-2'])
expect(() => ctx.subagentNames.reserve(parent.session, parent.id, 'agent-1')).toThrow('already in use')
first.dispose()
first.dispose()
const retry = ctx.subagentNames.reserve(parent.session, parent.id)
expect(retry.name).toBe('lead/agent-1')
first.dispose()
expect(() => ctx.subagentNames.reserve(parent.session, parent.id, 'agent-1')).toThrow('already in use')
ctx.subagentNames.bind(parent.session, SessionId('accepted-child'), retry.name)
retry.dispose()
expect(() => ctx.subagentNames.reserve(parent.session, parent.id, 'agent-1')).toThrow('already in use')
second.dispose()
const available = ctx.subagentNames.reserve(parent.session, parent.id)
expect(available.name).toBe('lead/agent-2')
available.dispose()
})
it.each(['', 'Reviewer', 'lead/reviewer', 'two words', '1reviewer', 'legacy:child'])(
'rejects invalid requested segment %j before reserving it', async (name) => {
const { ctx, parent } = await setup()
expect(() => ctx.subagentNames.reserve(parent.session, parent.id, name)).toThrow('agent name must')
expect(ctx.sessionProjections.snapshot(parent.session).values.subagentNames).toEqual([])
},
)
it('keeps exact bindings idempotent while rejecting name and identity conflicts', async () => {
const { ctx, parent } = await setup()
const id = SessionId('accepted-child')
expect(() => { ctx.subagentNames.bind(parent.session, id, 'reviewer') }).toThrow('complete lead/... path')
ctx.subagentNames.bind(parent.session, id, 'lead/reviewer')
const seq = parent.session.seq
ctx.subagentNames.bind(parent.session, id, 'lead/reviewer')
expect(parent.session.seq).toBe(seq)
expect(() => { ctx.subagentNames.bind(parent.session, id, 'lead/renamed') }).toThrow('already bound')
expect(() => { ctx.subagentNames.bind(parent.session, SessionId('another-child'), 'lead/reviewer') }).toThrow('already bound')
})
it('records nested aliases only on the root and restores them without ancestor reads', async () => {
const { ctx, parent, parentHandle, start } = await setup(['hang', 'hang'])
const creator = await start()
ctx.subagentNames.bind(parent.session, creator.childId, 'lead/reviewer')
const creatorAgent = ctx.agents.get(creator.childId)!
const reservation = ctx.subagentNames.reserve(parent.session, creatorAgent.id, 'facts')
const leaf = await start(creatorAgent)
ctx.subagentNames.bind(parent.session, leaf.childId, reservation.name)
reservation.dispose()
expect(ctx.subagentNames.nameOf(parent.session, creatorAgent.id)).toBe('lead/reviewer')
expect(ctx.sessionProjections.snapshot(creatorAgent.session).values.subagentNames).toEqual([])
expect(ctx.sessionProjections.snapshot(parent.session).values.subagentNames).toEqual([
{ childId: creator.childId, name: 'lead/reviewer' },
{ childId: leaf.childId, name: 'lead/reviewer/facts' },
])
await creator.dispose()
expect(ctx.agents.get(creator.childId)).toBeUndefined()
expect(ctx.subagentNames.nameOf(parent.session, leaf.childId)).toBe('lead/reviewer/facts')
await parentHandle.dispose()
const restored = await ctx.agents.resume({ resumeSessionId: parent.id })
expect(ctx.subagentNames.resolveName(restored.agent.session, 'lead/reviewer/facts')).toBe(leaf.childId)
expect(ctx.get('sessionQuery')).toBeUndefined()
})
it('uses a stable id prefix for children whose parent has no name', async () => {
const { ctx, parent, start } = await setup(['hang'])
const creator = await start()
const creatorAgent = ctx.agents.get(creator.childId)!
expect(ctx.subagentNames.nameOf(parent.session, creatorAgent.id)).toBe(creator.childId)
const reservation = ctx.subagentNames.reserve(parent.session, creatorAgent.id, 'facts')
expect(reservation.name).toBe(`lead/legacy:${creator.childId}/facts`)
const childId = SessionId('accepted-leaf')
ctx.subagentNames.bind(parent.session, childId, reservation.name)
reservation.dispose()
ctx.subagentNames.bind(parent.session, creator.childId, 'lead/reviewer')
expect(ctx.subagentNames.nameOf(parent.session, childId)).toBe(`lead/legacy:${creator.childId}/facts`)
})
it('does not stop an accepted child when the alias append fails', async () => {
const { ctx, parent, start } = await setup(['hang'])
const reservation = ctx.subagentNames.reserve(parent.session, parent.id, 'reviewer')
const child = await start()
const failure = new Error('binding append failed')
const append = vi.spyOn(parent.session, 'append').mockImplementationOnce(() => { throw failure })
expect(() => { ctx.subagentNames.bind(parent.session, child.childId, reservation.name) }).toThrow(failure)
append.mockRestore()
reservation.dispose()
expect(ctx.agents.get(child.childId)).toBeDefined()
expect(ctx.subagentNames.nameOf(parent.session, child.childId)).toBe(child.childId)
const retry = ctx.subagentNames.reserve(parent.session, parent.id, 'reviewer')
retry.dispose()
await child.dispose()
})
it('resolves message attribution automatically across naming service unload and reload', async () => {
const { ctx, parent, namesFiber, start } = await setup(['hang'])
const child = await start()
const sender = ctx.agents.get(child.childId)!
ctx.subagentNames.bind(parent.session, child.childId, 'lead/reviewer')
const steer = vi.spyOn(parent, 'steer')
const send = () => ctx.subagents.sendMessage(sender, parent.id, [{ type: 'text', text: 'finding' }], {
signal: new AbortController().signal,
})
await send()
expect(steer.mock.lastCall?.[0]).toMatchObject({
content: [{ type: 'text', text: 'Agent lead/reviewer sent a message: ' }, { type: 'text', text: 'finding' }],
source: { kind: 'agent-message', senderSessionId: child.childId },
})
const lookupFailure = new Error('name lookup failed')
const lookup = vi.spyOn(ctx.subagentNames, 'nameOf').mockImplementationOnce(() => { throw lookupFailure })
await expect(send()).rejects.toBe(lookupFailure)
expect(steer).toHaveBeenCalledTimes(1)
lookup.mockRestore()
await namesFiber.dispose()
await send()
expect(steer.mock.lastCall?.[0].content[0]).toEqual({ type: 'text', text: `Agent ${child.childId} sent a message: ` })
await ctx.plugin(SubagentNames)
await send()
expect(steer.mock.lastCall?.[0].content[0]).toEqual({ type: 'text', text: 'Agent lead/reviewer sent a message: ' })
await child.dispose()
})
it('unregisters its projection and leaves accepted execution with its owner', async () => {
const { ctx, parent, namesFiber, start } = await setup(['hang'])
const names = ctx.subagentNames
const reservation = names.reserve(parent.session, parent.id, 'reviewer')
const child = await start()
await namesFiber.dispose()
expect(ctx.get('subagentNames')).toBeUndefined()
expect(ctx.sessionProjections.snapshot(parent.session).values.subagentNames).toBeUndefined()
expect(ctx.agents.get(child.childId)).toBeDefined()
// Accepted callers retain their projection handle while completing a binding.
names.bind(parent.session, child.childId, reservation.name)
reservation.dispose()
await ctx.plugin(SubagentNames)
expect(ctx.subagentNames.nameOf(parent.session, child.childId)).toBe('lead/reviewer')
await child.dispose()
})
})
@@ -1,177 +0,0 @@
import { describe, expect, it, onTestFinished } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { subagentNamesProjection } from '../src/projection.ts'
const header: SessionHeader = {
version: SESSION_FORMAT_VERSION,
id: SessionId('parent'),
createdAt: 0,
isSeeded: false,
}
function bound(seq: number, childId = 'child', name = 'lead/worker'): SessionEvent<'subagent-name/bound'> {
return {
type: 'subagent-name/bound', seq: SessionSeq(seq), time: seq,
data: { version: 1, childId: SessionId(childId), name },
}
}
async function registry() {
const ctx = new Context()
onTestFinished(async () => { await ctx.fiber.dispose() })
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.sessionProjections.register(subagentNamesProjection)
return ctx.sessionProjections
}
describe('subagent names projection', () => {
it('keeps each child binding immutable without mutating earlier states', () => {
const initial = subagentNamesProjection.init(header, SessionLogOffset(0))
const first = subagentNamesProjection.apply(initial, bound(0))
const second = subagentNamesProjection.apply(first, bound(1, 'other-child', 'lead/reviewer'))
expect(initial.bindings).toEqual([])
expect(first.bindings).toEqual([{ childId: 'child', name: 'lead/worker' }])
expect(second.bindings).toEqual([
{ childId: 'child', name: 'lead/worker' },
{ childId: 'other-child', name: 'lead/reviewer' },
])
expect(() => subagentNamesProjection.apply(second, bound(2, 'child', 'lead/renamed')))
.toThrow('one immutable binding')
expect(() => subagentNamesProjection.apply(second, bound(2, 'different-child', 'lead/worker')))
.toThrow('one immutable binding')
expect(second.bindings).toEqual([
{ childId: 'child', name: 'lead/worker' },
{ childId: 'other-child', name: 'lead/reviewer' },
])
})
it('preserves state and view references for repeated bindings and unrelated events', () => {
const state = subagentNamesProjection.apply(
subagentNamesProjection.init(header, SessionLogOffset(0)), bound(0),
)
const duplicate = subagentNamesProjection.apply(state, bound(1))
const unrelated = subagentNamesProjection.apply(state, {
type: 'turn/start', seq: SessionSeq(2), time: 2, data: { turn: 1 },
})
expect(duplicate).toBe(state)
expect(unrelated).toBe(state)
expect(subagentNamesProjection.wire.view(duplicate)).toBe(state.bindings)
expect(subagentNamesProjection.wire.view(unrelated)).toBe(state.bindings)
})
it('excludes inherited bindings and admits the event at the exact inherited cut', () => {
const initial = subagentNamesProjection.init(header, SessionLogOffset(2))
expect(subagentNamesProjection.apply(initial, bound(0, 'child', 'lead/inherited'))).toBe(initial)
expect(subagentNamesProjection.apply(initial, bound(1, 'other-child', 'lead/inherited-too'))).toBe(initial)
const own = subagentNamesProjection.apply(initial, bound(2))
expect(own.bindings).toEqual([{ childId: 'child', name: 'lead/worker' }])
expect(own.inheritedEventCount).toBe(2)
})
it.each([
{ version: 2, childId: 'child', name: 'lead/worker' },
{ childId: 'child', name: 'lead/worker' },
{ version: 1, childId: 1, name: 'lead/worker' },
{ version: 1, name: 'lead/worker' },
{ version: 1, childId: 'child' },
{ version: 1, childId: 'child', name: 1 },
{ version: 1, childId: 'child', name: 'lead/worker', extra: true },
...['', 'Worker', '1worker', 'parent/worker', 'legacy:child', 'worker name'].map(name => ({
version: 1, childId: 'child', name,
})),
])('rejects malformed persisted binding data %j', (data) => {
const initial = subagentNamesProjection.init(header, SessionLogOffset(0))
// Persisted events can violate the declared event fields before replay validation.
const event = { ...bound(0), data } as SessionEvent<'subagent-name/bound'>
expect(() => subagentNamesProjection.apply(initial, event)).toThrow()
expect(initial.bindings).toEqual([])
})
it.each([
{},
{ inheritedEventCount: -1, bindings: [] },
{ inheritedEventCount: 0.5, bindings: [] },
{ inheritedEventCount: '0', bindings: [] },
{ inheritedEventCount: 0, bindings: {} },
{ inheritedEventCount: 0, bindings: [{ childId: 'child' }] },
{ inheritedEventCount: 0, bindings: [{ childId: 1, name: 'lead/worker' }] },
{ inheritedEventCount: 0, bindings: [{ childId: 'child', name: 'parent/worker' }] },
{ inheritedEventCount: 0, bindings: [{ childId: 'child', name: 'lead/worker', version: 1 }] },
{ inheritedEventCount: 0, bindings: [{ childId: 'child', name: 'lead/worker' }, { childId: 'child', name: 'lead/reviewer' }] },
{ inheritedEventCount: 0, bindings: [{ childId: 'child', name: 'lead/worker' }, { childId: 'other', name: 'lead/worker' }] },
{ inheritedEventCount: 0, bindings: [], extra: true },
])('rejects malformed checkpoint state %j', (state) => {
expect(subagentNamesProjection.stateSchema.safeParse(state).success).toBe(false)
})
it('restores a versioned checkpoint and suffix to the same state as complete replay', async () => {
const projections = await registry()
const inheritedEventCount = SessionLogOffset(1)
const events: SessionEvent[] = [
bound(0, 'ancestor-child', 'lead/inherited'),
bound(1),
bound(2),
bound(3, 'other-child', 'lead/reviewer'),
{ type: 'turn/start', seq: SessionSeq(4), time: 4, data: { turn: 1 } },
]
const prefix = projections.restore({}, events.slice(0, 2), SessionLogOffset(0), header, inheritedEventCount)
expect(prefix.checkpoint.subagentNames).toEqual({
ver: 1, seq: 1,
val: { inheritedEventCount: 1, bindings: [{ childId: 'child', name: 'lead/worker' }] },
})
const resumed = projections.restore(
prefix.checkpoint, events.slice(2), SessionLogOffset(2), header, inheritedEventCount,
)
const replayed = projections.restore({}, events, SessionLogOffset(0), header, inheritedEventCount)
expect(resumed).toEqual(replayed)
expect(resumed.snapshot).toEqual({
asOfSeq: 4,
values: { subagentNames: [
{ childId: 'child', name: 'lead/worker' },
{ childId: 'other-child', name: 'lead/reviewer' },
] },
})
expect(projections.viewCheckpoint(resumed.checkpoint)).toEqual(resumed.snapshot.values)
expect(projections.restore(
resumed.checkpoint, [], SessionLogOffset(5), header, inheritedEventCount,
)).toEqual(resumed)
})
it('replays from the log when the checkpoint version is incompatible', async () => {
const projections = await registry()
const incompatible = {
subagentNames: { ver: 2, seq: SessionSeq(0), val: { unsupported: true } },
}
expect(projections.viewCheckpoint(incompatible)).toEqual({})
expect(() => projections.restore(
incompatible, [bound(1, 'other-child', 'lead/reviewer')], SessionLogOffset(1), header, SessionLogOffset(0),
)).toThrow(/re-read from seq 0/)
expect(projections.restore(
incompatible, [bound(0)], SessionLogOffset(0), header, SessionLogOffset(0),
).snapshot.values.subagentNames).toEqual([{ childId: 'child', name: 'lead/worker' }])
})
it('rejects a version-matching checkpoint with invalid binding fields', async () => {
const projections = await registry()
const invalid = {
subagentNames: {
ver: 1, seq: SessionSeq(0),
val: { inheritedEventCount: 0, bindings: [{ childId: 'child', name: 'Invalid' }] },
},
}
expect(projections.viewCheckpoint(invalid)).toEqual({})
expect(() => projections.restore(
invalid, [], SessionLogOffset(1), header, SessionLogOffset(0),
)).toThrow()
})
})
@@ -1,27 +0,0 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": [
"src"
],
"references": [
{
"path": "../../../vendor/cosmokit"
},
{
"path": "../../../vendor/cordis"
},
{
"path": "../../llm/llm"
},
{
"path": "../../core/session"
},
{
"path": "../../session/session-projection"
}
]
}
@@ -47,7 +47,6 @@
"@deepseek-ai/dsh-llm-deepseek-api-key": "workspace:*",
"@deepseek-ai/dsh-session": "workspace:*",
"@deepseek-ai/dsh-subagent": "workspace:*",
"@deepseek-ai/dsh-subagent-names": "workspace:*",
"@deepseek-ai/dsh-tool-bash": "workspace:*",
"@deepseek-ai/dsh-tool-subagent": "workspace:*",
"@deepseek-ai/cordis": "workspace:~",
@@ -11,7 +11,6 @@ import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as ToolBash from '@deepseek-ai/dsh-tool-bash'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek-api-key'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import SubagentNames from '@deepseek-ai/dsh-subagent-names'
import * as Spawn from '../src/index.ts'
import * as ToolSubagent from '@deepseek-ai/dsh-tool-subagent'
@@ -41,7 +40,6 @@ export async function spawnHarness(workdir: string): Promise<Context> {
await ctx.plugin(JsonlSessionPersistence, { root: join(workdir, '.sessions') })
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
await ctx.plugin(Spawn, { providerName: 'spawn' })
// The model-facing subagent tool, bound to the spawn backend.
await ctx.plugin(ToolSubagent, { provider: 'spawn' })
+2 -2
View File
@@ -33,8 +33,8 @@
en: a740e5e9bdd661d9
zh: 712a5458fc8b3615
/deepseek-ai-dsh-subagent/use-this-package/messaging-interrupting-and-discovering:
en: c6b551c4dc6dde80
zh: a03e93ca3ca3e67d
en: bf3175c2218a16dd
zh: 4be9271efe71ddb7
/deepseek-ai-dsh-subagent/use-this-package/failure-and-recovery:
en: 0dc416ded19e304e
zh: 4726e5fe5f188709
+1 -1
View File
@@ -68,7 +68,7 @@ In-process spawn accepts an independent `agentPreset`; fork and external backend
### Messaging, interrupting, and discovering
`sendMessage()` lets the live root and resident continuable subagents contact any other root or continuable member in the same tree by Session id. The target receives Steer input at its next step or starts an idle turn. Only the cold target resumes, under the subagent tree root; its creation ancestors stay offline and the message retains its actual sender. The message service resolves visible attribution from the actual sender through the naming service and the tree root Session; without a provider, the Session id is used. Durable attribution always keeps `senderSessionId`. `rootOf()` resolves the caller’s live root. Agent interruption requires ancestor authority. Browser prompts and user interrupts address only the child Session; the Gateway checks the corresponding action on that Session before any side effect. Parent or global grants do not imply child access. Prompts require a live subagent tree root; the creator may be offline. User interrupts perform no restoration and accept absent targets as no-ops. They select Queue or Steer and admit image batches through the attachment store; incompatible target models reject images. `listDescendants()` discovers members recursively through parent-owned `subagentCatalog` projections. It preserves catalog order, reads each reachable Session once, skips external leaves, and reports unreadable branches without activating Agents.
`sendMessage()` lets the live root and resident continuable subagents contact any other root or continuable member in the same tree by Session id. The target receives Steer input at its next step or starts an idle turn. Only the cold target resumes, under the subagent tree root; its creation ancestors stay offline and the message retains its actual sender. `rootOf()` resolves the caller’s live root. Agent interruption requires ancestor authority. Browser prompts and user interrupts address only the child Session; the Gateway checks the corresponding action on that Session before any side effect. Parent or global grants do not imply child access. Prompts require a live subagent tree root; the creator may be offline. User interrupts perform no restoration and accept absent targets as no-ops. They select Queue or Steer and admit image batches through the attachment store; incompatible target models reject images. `listDescendants()` discovers members recursively through parent-owned `subagentCatalog` projections. It preserves catalog order, reads each reachable Session once, skips external leaves, and reports unreadable branches without activating Agents.
### Failure and recovery
+1 -1
View File
@@ -68,7 +68,7 @@ kind: "package-reference"
### 消息、中断与发现
`sendMessage()` 允许在线根和驻留的可继续子代理,按 Session id 联系同一棵树中的任意其他根或可继续成员。目标在下一步接收 Steer 输入,或在空闲时开始轮次。仅在 subagent 树根下恢复冷目标,其创建祖先保持离线;消息保留实际发送者。`rootOf()` 解析调用方的在线根。Agent 中断要求祖先权限。浏览器提示词和用户中断仅按子 Session 寻址;Gateway 在任何副作用之前检查该 Session 上对应操作的权限。父级或 global 授权不意味着子级访问权。提示词要求 subagent 树根在线;创建者可以离线。用户中断不恢复目标,目标缺席时接受为无操作。提示词选择 Queue 或 Steer,经附件存储接受图片批次;不兼容的目标模型拒绝图片。`listDescendants()` 通过父级拥有的 `subagentCatalog` 投影递归发现成员。它保留目录顺序,每个可达 Session 读取一次,跳过外部叶子,并报告不可读分支而不激活 Agent。 消息服务通过命名服务和树根 Session,从实际发送者自动解析消息中显示的名称;没有提供方时使用 Session id。持久归属始终保留 `senderSessionId`。
`sendMessage()` 允许在线根和驻留的可继续子代理,按 Session id 联系同一棵树中的任意其他根或可继续成员。目标在下一步接收 Steer 输入,或在空闲时开始轮次。仅在 subagent 树根下恢复冷目标,其创建祖先保持离线;消息保留实际发送者。`rootOf()` 解析调用方的在线根。Agent 中断要求祖先权限。浏览器提示词和用户中断仅按子 Session 寻址;Gateway 在任何副作用之前检查该 Session 上对应操作的权限。父级或 global 授权不意味着子级访问权。提示词要求 subagent 树根在线;创建者可以离线。用户中断不恢复目标,目标缺席时接受为无操作。提示词选择 Queue 或 Steer,经附件存储接受图片批次;不兼容的目标模型拒绝图片。`listDescendants()` 通过父级拥有的 `subagentCatalog` 投影递归发现成员。它保留目录顺序,每个可达 Session 读取一次,跳过外部叶子,并报告不可读分支而不激活 Agent。
### 失败与恢复
+2 -7
View File
@@ -72,8 +72,7 @@
"@deepseek-ai/dsh-util-time": "workspace:*",
"@deepseek-ai/dsh-permission-presets": "workspace:*",
"@deepseek-ai/dsh-workspace": "workspace:*",
"@deepseek-ai/dsh-working-directory": "workspace:*",
"@deepseek-ai/dsh-subagent-names": "workspace:*"
"@deepseek-ai/dsh-working-directory": "workspace:*"
},
"peerDependenciesMeta": {
"@deepseek-ai/dsh-agent-preset-registry": {
@@ -99,9 +98,6 @@
},
"@deepseek-ai/dsh-user-approval": {
"optional": true
},
"@deepseek-ai/dsh-subagent-names": {
"optional": true
}
},
"devDependencies": {
@@ -132,7 +128,6 @@
"@deepseek-ai/dsh-fs-sandbox": "workspace:*",
"@deepseek-ai/dsh-tool-fs": "workspace:*",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:*",
"@deepseek-ai/dsh-working-directory": "workspace:*",
"@deepseek-ai/dsh-subagent-names": "workspace:*"
"@deepseek-ai/dsh-working-directory": "workspace:*"
}
}
@@ -58,17 +58,15 @@ function agentMessageSource(sender: Agent): AgentMessageSource {
* Build the model-visible and durable representation of one Agent message.
* @param sender - exact live Agent that authored the message.
* @param content - model-visible message blocks supplied by the sender.
* @param senderName - resolved sender name or Session id for the visible attribution.
* @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 ${senderName} sent a message: ` },
{ type: 'text' as const, text: `Agent ${sender.id} sent a message: ` },
...content,
],
source: agentMessageSource(sender),
-2
View File
@@ -311,8 +311,6 @@ export class SubagentRuntime extends TypertRemoteService {
* A cold target resumes under its runtime subagent tree root without restoring
* its creation ancestors. Sender identity is rechecked before inbox acceptance; cancellation
* stops only pre-acceptance work and never retracts an accepted message.
* Display attribution uses the sender's bound name when the naming service is mounted,
* otherwise its Session id. Name lookup failures reject before inbox acceptance.
* @param sender - exact live root or resident continuable Agent originating the message.
* @param targetId - another root or continuable member's durable Session id.
* @param content - model-authored content to deliver.
+1 -4
View File
@@ -9,7 +9,6 @@
import { randomUUID } from 'node:crypto'
import { isAbsolute, resolve } from 'node:path'
import type {} from '@deepseek-ai/dsh-working-directory'
import type {} from '@deepseek-ai/dsh-subagent-names'
import type { Context } from '@deepseek-ai/cordis'
import type { Agent, AgentOptions, CreateAgentOptions } from '@deepseek-ai/dsh-agent'
import { brandString } from '@deepseek-ai/dsh-brand'
@@ -486,9 +485,7 @@ export class SubagentManager {
const root = this.rootOf(sender)
if (sender.id === targetId) throw new SubagentError('send_message requires another agent in the subagent tree', 'UNAUTHORIZED')
this.assertAdmitting(sender)
const names = this.ctx.get('subagentNames')
const senderName = names === undefined ? sender.id : names.nameOf(root.session, sender.id)
const message = createAgentMessage(sender, content, senderName)
const message = createAgentMessage(sender, content)
const activation = this.resident.get(sender.id)
return this.deliverToMember(root, targetId, [...message.content], {
signal: options.signal, delivery: 'steer', source: message.source,
@@ -12,7 +12,6 @@ import { agentEvents } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import { SessionId } from '@deepseek-ai/dsh-session'
import SubagentNames from '@deepseek-ai/dsh-subagent-names'
import type { SessionEvent, SessionEventMap } from '@deepseek-ai/dsh-session'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
@@ -2965,7 +2964,6 @@ describe('continuable settlement delivery', () => {
it('includes the child answer when messaging is available', async () => {
const { ctx, parent } = await setup([textResponse('the answer'), textResponse('parent ack')])
await ctx.plugin(SubagentNames)
await ctx.plugin(SubagentControl)
const started = await ctx.subagents.startActivation({ ...startSpec(parent), delivery: 'parent' })
await waitNoActivation(ctx, started.childId)
@@ -3086,7 +3084,7 @@ describe('continuable settlement delivery', () => {
const parentId = /Your parent agent id is \\"([^\\]+)\\"/.exec(history)![1]!
const calls = order.map(name => name === 'noop'
? { id: ToolCallId('n1'), name, arguments: '{}' }
: { id: ToolCallId('s1'), name, arguments: JSON.stringify({ name: parentId, message: 'interim' }) })
: { id: ToolCallId('s1'), name, arguments: JSON.stringify({ agent_id: parentId, message: 'interim' }) })
return [
...calls.flatMap((call, index): StreamChunk[] => [
{ type: 'block-start', index, blockType: 'tool-call' },
@@ -3098,7 +3096,6 @@ describe('continuable settlement delivery', () => {
]
}
const { ctx, parent } = await setup(Array.from({ length: 6 }, () => respond))
await ctx.plugin(SubagentNames)
await ctx.plugin(SubagentControl)
ctx.tools.register(defineTool({
name: 'noop',
@@ -6,7 +6,6 @@ import { Context } from '@deepseek-ai/cordis'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import { SessionId } from '@deepseek-ai/dsh-session'
import SubagentNames from '@deepseek-ai/dsh-subagent-names'
import * as Spawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import * as Control from '@deepseek-ai/dsh-tool-subagent-control'
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
@@ -20,7 +19,6 @@ it.each(['absent', 'unused'] as const)('returns the closing answer when send_mes
await ctx.plugin(AgentLoop, { agents: [] })
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
await ctx.plugin(Spawn, { providerName: 'spawn' })
if (availability !== 'absent') await ctx.plugin(Control)
const adapter = new MockAdapter([textResponse('CHILD_RESULT_42'), textResponse('Parent received the answer.')])
@@ -50,7 +48,6 @@ it('delivers structured output to the parent when the child has send_message', a
await ctx.plugin(AgentLoop, { agents: [] })
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
await ctx.plugin(Spawn, { providerName: 'spawn' })
await ctx.plugin(Control)
const adapter = new MockAdapter([
-3
View File
@@ -73,9 +73,6 @@
},
{
"path": "../../session/working-directory"
},
{
"path": "../subagent-names"
}
]
}
@@ -9,38 +9,38 @@
en: 465d36925c792167
zh: 269cac314f638827
/deepseek-ai-dsh-tool-subagent-control/summary:
en: 02f6c3d0e96dfc56
zh: 79eb0cf759d9a849
en: 7e68f9a2d58e1ab6
zh: 24edd923e898cc3b
/deepseek-ai-dsh-tool-subagent-control/table-of-contents:
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
/deepseek-ai-dsh-tool-subagent-control/use-this-package:
en: a64710fadb2b8913
zh: 7eb30752c24604b2
en: f486d856fd8e392c
zh: 6ddbc59dd08d3c49
/deepseek-ai-dsh-tool-subagent-control/use-this-package/minimal-configuration:
en: d224de77b41ed4e4
zh: f1e94f9cc9e04384
/deepseek-ai-dsh-tool-subagent-control/use-this-package/send-message:
en: b4475ddaeb2dd461
zh: e907663d014a0639
en: 849b9b3e576aed01
zh: d52239ca40d3a168
/deepseek-ai-dsh-tool-subagent-control/use-this-package/interrupt-agent:
en: a01fe963745c654c
zh: 71466b72505cf80c
en: de45d5a4b98d3c91
zh: 666aa187170b9c0d
/deepseek-ai-dsh-tool-subagent-control/use-this-package/wait-agent:
en: 29a3466bcc8e8e9d
zh: ea38770df450a43e
/deepseek-ai-dsh-tool-subagent-control/use-this-package/list-agents:
en: d8cbaa3189c9ecc5
zh: 7409b14df342fb53
en: 912206fb3da120b5
zh: 3b247046a52d5f2a
/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: 23d67aa784753b65
zh: b4064f922b104992
en: 3b6f1956d232964d
zh: 9949b906020c9a79
/deepseek-ai-dsh-tool-subagent-control/understand-the-implementation/delivery-and-signal-ownership:
en: 186b5c0fad1a176b
zh: 6c3a104e7f4ba0fe
en: 65137f5fe14ebb7f
zh: ce00e4f32709246a
/deepseek-ai-dsh-tool-subagent-control/understand-the-implementation/listing-projection:
en: ee3136b09f0c9ef2
zh: 5cbe189bb857fdc7
@@ -57,32 +57,32 @@
en: e4c7376d4f29dbd5
zh: 6b7a9eaf69c9ea53
/deepseek-ai-dsh-tool-subagent-control/model-experience/tool-schema/what-the-model-sees:
en: 22389e32a54dc053
zh: 37ad7b7a9c2f67d0
en: db2e45684a4d0117
zh: 5cd3b646c010b530
/deepseek-ai-dsh-tool-subagent-control/model-experience/tool-schema/token-effect:
en: 5eb1d46a94b7ec1b
zh: 5670760ca695d4ad
/deepseek-ai-dsh-tool-subagent-control/model-experience/tool-schema/kv-cache-effect:
en: 53a81243f3acd543
zh: 472cf727e5180501
en: 89619e31f3ea7585
zh: 5681bc8096cd8e32
/deepseek-ai-dsh-tool-subagent-control/model-experience/communication-context:
en: 95d76d1aba983086
zh: b645a3837ec17ef4
/deepseek-ai-dsh-tool-subagent-control/model-experience/communication-context/what-the-model-sees:
en: adb5b38af8f1cfb1
zh: 2a7f2779ed5e8a9f
en: 037ef2cc0d09692c
zh: 57691926fb5b4e41
/deepseek-ai-dsh-tool-subagent-control/model-experience/communication-context/token-effect:
en: 24d9c15986cb4d3d
zh: 6fb66cf84f8bc812
/deepseek-ai-dsh-tool-subagent-control/model-experience/communication-context/kv-cache-effect:
en: be04fa8fa539cad0
zh: 797fd37af7d32f90
en: d5c696fb6be74875
zh: 433881338cffd55e
/deepseek-ai-dsh-tool-subagent-control/model-experience/interrupt-result:
en: d64e936e83e89a2b
zh: b9905c33af6cca45
/deepseek-ai-dsh-tool-subagent-control/model-experience/interrupt-result/what-the-model-sees:
en: 00d6ca03eab6e7fa
zh: 202e242f06ea8818
en: 89305619897680ba
zh: e610a844fc09c53f
/deepseek-ai-dsh-tool-subagent-control/model-experience/interrupt-result/token-effect:
en: 5d1bcef28b5f18da
zh: 027d67d93d660e76
@@ -93,11 +93,11 @@
en: ab964e307bd37dca
zh: 03f975252f838a42
/deepseek-ai-dsh-tool-subagent-control/model-experience/delivery-result/what-the-model-sees:
en: b8d03632a5550f87
zh: 084e74cf10944b23
en: 8746cb9fa6557bfe
zh: 49828e113e901160
/deepseek-ai-dsh-tool-subagent-control/model-experience/delivery-result/token-effect:
en: 721928e32768b3ad
zh: a314801790bd58a9
en: f7e47f835a1e0628
zh: d2818b98c773ad35
/deepseek-ai-dsh-tool-subagent-control/model-experience/delivery-result/kv-cache-effect:
en: f5d5f97a153f7c70
zh: 3d6bc05b9eadfbc2
@@ -105,8 +105,8 @@
en: 094e55d258f2ec2e
zh: f364bc9046530bff
/deepseek-ai-dsh-tool-subagent-control/model-experience/listing-result/what-the-model-sees:
en: aeef4da057b78b1f
zh: f500ca4ceab1f87a
en: b56b4d01b6516e13
zh: f9089c18c732539c
/deepseek-ai-dsh-tool-subagent-control/model-experience/listing-result/token-effect:
en: 027aaeb808238388
zh: 5b974abc429d9e23
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
## Summary
`dsh-tool-subagent-control` lets agents discover and message their subagent tree by path or Session id, interrupt descendants, and wait for their own next-step inbox. By default, one plugin instance registers all four tools; members inherit the same definitions and ordering.
`dsh-tool-subagent-control` lets agents discover and message their subagent tree by Session id, interrupt descendants, and wait for their own next-step inbox. By default, one plugin instance registers all four tools; members inherit the same definitions and ordering.
## Table of Contents
@@ -25,7 +25,7 @@ English | [中文](README.zh.md)
<a id="use-this-package"></a>
## Use this package
Mount this package once per tool scope. It requires `tools`, `subagents`, `subagentNames`, `agents`, and `systemPrompt`. The [subagent-names](../subagent-names/README.md) plugin resolves names and Session ids through the fixed `name` argument. Cordis removes these tools when the naming service unloads and restores them when it returns.
Mount this package once per tool scope. It requires `tools`, `subagents`, `agents`, and `systemPrompt`.
### Minimal configuration
@@ -33,7 +33,6 @@ With the required services available, load the subagent service, a backend, the
```yaml
- name: '@deepseek-ai/dsh-subagent'
- name: '@deepseek-ai/dsh-subagent-names'
- name: '@deepseek-ai/dsh-subagent-spawn-in-process'
- name: '@deepseek-ai/dsh-tool-subagent'
config:
@@ -45,11 +44,11 @@ Set `config.interrupt: false` to omit `interrupt_agent`. The default is `true`.
### send_message
`name` selects a full path or Session id of another agent in the same subagent tree. The caller must be the live root or a resident continuable subagent. A working target receives Steer input at its next step; an inactive target starts or resumes a turn. Only the target resumes; its creation ancestors stay offline. Success returns inbox acceptance (`messageId`), never a reply. Self, unrelated trees, unsupported targets, unknown ids, unavailable history, and rejected admission return errors.
Sends a message to another agent in the same subagent tree by `agent_id`. The caller must be the live root or a resident continuable subagent. A working target receives Steer input at its next step; an inactive target starts or resumes a turn. Only the target resumes; its creation ancestors stay offline. Success returns inbox acceptance (`messageId`), never a reply. Self, unrelated trees, unsupported targets, unknown ids, unavailable history, and rejected admission return errors.
### interrupt_agent
`name` identifies a descendant by full path or 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.
`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.
<a id="wait_agent"></a>
### wait_agent
@@ -58,7 +57,7 @@ Waits for the exact caller’s next-step inbox. Pending input returns immediatel
### list_agents
Lists the root and all reachable continuable descendants, including the caller, in stable pre-order. Rows retain Session ids, parent ids, and depth from the root. The naming plugin adds `name` and `parentName` when a mapping is available; the root is named `lead`. Unnamed children retain their ids. Each child’s label comes from the `description` supplied at creation. Live status is `running` or `inactive`. External entries are omitted without reading child Sessions; readable one-shot children remain traversal nodes. Unknown modes and unreadable catalogs produce diagnostics. Ordinary Session forks lead separate subagent trees.
Lists the root and all reachable continuable descendants, including the caller, in stable pre-order. Descendant rows carry their direct parent id and depth from the root. Live status is `running` or `inactive`. External entries are omitted without reading child Sessions; readable one-shot children remain traversal nodes. Unknown modes and unreadable catalogs produce diagnostics. Ordinary Session forks lead separate subagent trees.
-----
@@ -72,11 +71,11 @@ This section explains what the tools delegate to the subagent service; the obser
### Design concept
`ctx.subagentNames` resolves names synchronously; messaging, interruption, and catalog traversal delegate to `ctx.subagents`; residency, cold resume, and authorization belong to that service. The wait tool subscribes to the exact caller’s inbox before starting the timer and checking pending next-step input, and removes its listeners and timer on every settlement.
Messaging, interruption, and discovery delegate to `ctx.subagents`; residency, cold resume, and authorization belong to that service. The wait tool subscribes to the exact caller’s inbox events before starting the timer and checking pending next-step input, and removes its listeners and timer on every settlement.
### Delivery and signal ownership
The execution signal owns work only until inbox acceptance. Accepted messages cannot be cancelled through this tool. Every message is framed as `Agent <sender-name-or-id> sent a message:` and recorded with `{ kind: 'agent-message', form: 'relay', senderSessionId: sender.id }`. Tree membership authorizes communication; the subagent tree root owns runtime instances and capacity.
The execution signal owns work only until inbox acceptance. Accepted messages cannot be cancelled through this tool. Every message is framed as `Agent <sender-id> sent a message:` and recorded with `{ kind: 'agent-message', form: 'relay', senderSessionId: sender.id }`. Tree membership authorizes communication; the subagent tree root owns runtime instances and capacity.
### Listing projection
@@ -112,7 +111,7 @@ Read these pages when the package-level contract is not enough; they move from t
#### What the model sees
The generated [schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent-control) define path- or id-addressed messaging and interruption, whole-tree discovery, and inbox waiting with `timeout_ms`. `wait_agent` returns `{ "timedOut": false }` for available input, and `{ "timedOut": true }` on timeout. It returns no message body or peer-activity diagnosis.
The generated [schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent-control) define id-addressed messaging and interruption, whole-tree discovery, and inbox waiting with `timeout_ms`. `wait_agent` returns `{ "timedOut": false }` for available input, and `{ "timedOut": true }` on timeout. It returns no message body or peer-activity diagnosis.
#### Token effect
@@ -120,13 +119,13 @@ Fixed schema cost per request, independent of the number or descriptions of chil
#### KV Cache effect
The schema prefix stays stable while these tools are available. A parent and its continuable children, including fork children, inherit the same definitions.
Prefix-stable; the schemas do not change at runtime, and a parent and its continuable children, including fork children, receive identical definitions.
### Communication context
#### What the model sees
The logged `subagent:communication` runtime context tells delegated children their direct creator’s JSON-encoded Session id and how to send findings that change the parent’s next action. Named controls also accept this id. Sending a message does not end the turn. Root Agents and ordinary Session forks receive no communication context; delegated children receive it only when the standard messaging definition is admitted in the current scope and step. Automatic result delivery remains owned by the subagent service.
The logged `subagent:communication` runtime context tells delegated children their direct creator’s JSON-encoded id and how to send findings that change the parent’s next action. Sending a message does not end the turn. Root Agents and ordinary Session forks receive no communication context; delegated children receive it only when the standard messaging definition is admitted in the current scope and step. Automatic result delivery remains owned by the subagent service.
#### Token effect
@@ -134,13 +133,13 @@ One short context entry per delegated child, independent of tree size.
#### KV Cache effect
Communication guidance is retained as runtime context after inherited history, leaving the shared system prompt and tool definitions unchanged between parent and child. The parent id comes from the child Session header; prompt assembly does not wait for a name binding or read the name map.
Communication guidance is retained as runtime context after inherited history, leaving the shared system prompt and tool definitions unchanged between parent and child. Cold resume derives the parent id again from the target Session.
### Interrupt result
#### What the model sees
`interrupt requested for agent <address>` on acceptance. An unauthorized caller — self, sibling, stale, or non-ancestor — is an errored result naming the rejection; a known settled target still renders the acceptance line. Unknown name paths are rejected; an absent raw Session id retains an accepted no-op.
`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.
#### Token effect
@@ -154,11 +153,11 @@ Append-only; each result follows the reusable request prefix.
#### What the model sees
`message delivered to agent <address>` on acceptance; canonical output carries the accepted `messageId`. A target outside the caller’s subagent tree or rejected admission produces an errored result.
`message delivered to agent <id>` on acceptance; canonical output carries the accepted `messageId`. A target outside the caller’s subagent tree or rejected admission produces an errored result.
#### Token effect
One short acknowledgement per call; replies arrive as separately addressed messages. Use addresses from `list_agents` for follow-ups across the subagent tree.
One short acknowledgement per call; replies arrive as separately addressed messages. Use ids from `list_agents` for follow-ups across the subagent tree.
#### KV Cache effect
@@ -168,7 +167,7 @@ Append-only; newly visible content follows the reusable request prefix and does
#### What the model sees
The first row is `<id> [<status>] — lead: root agent`. Each continuable descendant follows as `<id> [<status>] parent=<parent-id> depth=<n> — <label>`. When a name is available, the text prefixes the label with `<name>: `; canonical rows retain the original `label` and add `name` separately. The root has `kind: root`; descendants have `kind: child` relative to that root. Diagnostics retain the id, reason, parent, and depth, with an optional name after ` — `. Status describes turn activity, not task completion.
The first row is `<id> [<status>] — root agent`. Each continuable descendant follows as `<id> [<status>] parent=<id> depth=<n> — <label>`. The root has `kind: root`; descendants have `kind: child` relative to that root. Diagnostics render `<id> [diagnostic: <reason>] parent=<id> depth=<n>`. Status describes turn activity, not task completion.
#### Token effect
@@ -9,7 +9,7 @@ kind: "package-reference"
## 概述
`dsh-tool-subagent-control` 让 agent(智能体)按路径或 Session id 发现和联系其 subagent(子智能体)树、中断后代,并等待自身下一步收件箱。默认情况下,一个插件实例注册全部四个工具;成员继承相同定义和顺序。
`dsh-tool-subagent-control` 让 agent(智能体)按 Session id 发现和联系其 subagent(子智能体)树、中断后代,并等待自身下一步收件箱。默认情况下,一个插件实例注册全部四个工具;成员继承相同定义和顺序。
## 目录
@@ -25,7 +25,7 @@ kind: "package-reference"
<a id="use-this-package"></a>
## 使用本包
每个工具作用域挂载一次此包。它需要 `tools`、`subagents`、`subagentNames`、`agents` 和 `systemPrompt`。[subagent-names](../subagent-names/README.zh.md) 插件通过固定的 `name` 参数解析名称和 Session id。命名服务卸载时 Cordis 移除这些工具,服务恢复时重新注册它们。
每个工具作用域挂载一次此包。它需要 `tools`、`subagents`、`agents` 和 `systemPrompt`。
### 最小配置
@@ -33,7 +33,6 @@ kind: "package-reference"
```yaml
- name: '@deepseek-ai/dsh-subagent'
- name: '@deepseek-ai/dsh-subagent-names'
- name: '@deepseek-ai/dsh-subagent-spawn-in-process'
- name: '@deepseek-ai/dsh-tool-subagent'
config:
@@ -45,11 +44,11 @@ kind: "package-reference"
### send_message
通过 `name` 指定完整路径或 Session id,向同一 subagent 树中的另一个 agent 发送消息。调用方必须是在线根代理或驻留的可继续子代理。工作中的目标在下一步接收 Steer 输入;不活跃目标开始或恢复一个轮次。只恢复目标,其创建祖先保持离线。成功返回收件箱接受结果(`messageId`),不返回回复。自身、无关树、不支持的目标、未知 id、不可用历史和被拒绝的输入都会返回错误。
通过 `agent_id` 向同一 subagent 树中的另一个 agent 发送消息。调用方必须是在线根代理或驻留的可继续子代理。工作中的目标在下一步接收 Steer 输入;不活跃目标开始或恢复一个轮次。只恢复目标,其创建祖先保持离线。成功返回收件箱接受结果(`messageId`),不返回回复。自身、无关树、不支持的目标、未知 id、不可用历史和被拒绝的输入都会返回错误。
### interrupt_agent
`name` 使用完整路径或 Session id 指定后代。外部执行永久停止,不能接收后续消息。本地目标只停止当前轮次:排队消息保留到后续 `send_message`,后代继续运行,子级仍可接受后续任务。调用在停止请求被接受时返回,不等待目标静止;中断已结束代理是成功的空操作,自身、同级、过期或非祖先调用方会收到错误。
`agent_id` 使用 Session id 指定后代。外部执行永久停止,不能接收后续消息。本地目标只停止当前轮次:排队消息保留到后续 `send_message`,后代继续运行,子级仍可接受后续任务。调用在停止请求被接受时返回,不等待目标静止;中断已结束代理是成功的空操作,自身、同级、过期或非祖先调用方会收到错误。
<a id="wait_agent"></a>
### wait_agent
@@ -58,7 +57,7 @@ kind: "package-reference"
### list_agents
按稳定前序列出根与所有可达的可继续后代,包括调用方。行保留 Session id、父级 id 与相对根的深度。命名插件在映射可用时增加 `name` 与 `parentName`;根的名称为 `lead`。未命名子级保留其 id。每个子级的标签来自创建时提供的 `description`。在线状态为 `running` 或 `inactive`。外部条目被省略,且不读取其子 Session;可读的一次性子级仍作为遍历节点。未知模式与不可读目录产生诊断。普通 Session 分叉形成独立的 subagent 树。
按稳定前序列出根与所有可达的可继续后代,包括调用方。后代行包含直接父级 id 与相对根的深度。在线状态为 `running` 或 `inactive`。外部条目被省略,且不读取其子 Session;可读的一次性子级仍作为遍历节点。未知模式与不可读目录产生诊断。普通 Session 分叉形成独立的 subagent 树。
-----
@@ -72,11 +71,11 @@ kind: "package-reference"
### 设计理念
命名由 `ctx.subagentNames` 同步解析;通信、中断和目录遍历委派给 `ctx.subagents`;驻留、冷恢复和授权属于该服务。等待工具在启动定时器并检查待处理下一步输入前订阅当前调用者的收件箱,并在每次结束时移除监听器和定时器。
通信、中断和发现委派给 `ctx.subagents`;驻留、冷恢复和授权属于该服务。等待工具在启动定时器并检查待处理下一步输入前订阅当前调用者的收件箱事件,并在每次结束时移除监听器和定时器。
### 投递与信号所有权
执行信号只负责收件箱接受前的工作。已接受的消息不能通过此工具撤回。每条消息以 `Agent <sender-name-or-id> sent a message:` 为前缀,并记录 `{ kind: 'agent-message', form: 'relay', senderSessionId: sender.id }`。树成员身份授予通信权限;subagent 树根管理运行时实例和容量。
执行信号只负责收件箱接受前的工作。已接受的消息不能通过此工具撤回。每条消息以 `Agent <sender-id> sent a message:` 为前缀,并记录 `{ kind: 'agent-message', form: 'relay', senderSessionId: sender.id }`。树成员身份授予通信权限;subagent 树根管理运行时实例和容量。
### 列表投影
@@ -112,7 +111,7 @@ kind: "package-reference"
#### 模型看到什么
生成的 [schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-subagent-control) 定义按路径或 id 寻址的通信与中断、整树发现,以及带 `timeout_ms` 的收件箱等待。`wait_agent` 在输入可用时返回 `{ "timedOut": false }`,超时时返回 `{ "timedOut": true }`。它不返回消息正文或其他成员活动诊断。
生成的 [schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-subagent-control) 定义按 id 寻址的通信与中断、整树发现,以及带 `timeout_ms` 的收件箱等待。`wait_agent` 在输入可用时返回 `{ "timedOut": false }`,超时时返回 `{ "timedOut": true }`。它不返回消息正文或其他成员活动诊断。
#### Token 影响
@@ -120,13 +119,13 @@ kind: "package-reference"
#### KV Cache 影响
这些工具可用期间,schema 前缀保持稳定。父级与其可继续子级(包括 fork 子级)继承相同的定义。
前缀保持稳定;schema 不会在运行时改变,父级与其可继续子级(包括 fork 子级)收到相同的定义。
### 通信上下文
#### 模型看到什么
持久记录的 `subagent:communication` 运行时上下文向委派子级提供其直接创建者经 JSON 编码的 Session id,并说明如何发送会改变父级下一步行动的发现。命名控制工具也接受该 id。发送消息不会结束轮次。根 Agent 和普通 Session fork 不接收通信上下文;委派子级仅在标准通信定义于当前作用域和 step 中可用时接收。自动结果交付仍由 subagent 服务负责。
持久记录的 `subagent:communication` 运行时上下文向委派子级提供直接创建者的 JSON 编码 id,并说明如何发送会改变父级下一步行动的发现。发送消息不会结束轮次。根 Agent 和普通 Session fork 不接收通信上下文;委派子级仅在标准通信定义于当前作用域和 step 中可用时接收。自动结果交付仍由 subagent 服务负责。
#### Token 影响
@@ -134,13 +133,13 @@ kind: "package-reference"
#### KV Cache 影响
通信指导作为运行时上下文保留在继承历史之后,父子级共享的系统提示词和工具定义不变。父级 id 来自子级 Session 头;提示组装不等待名称绑定,也不读取名称映射。
通信指导作为运行时上下文保留在继承历史之后,父子级共享的系统提示词和工具定义不变。冷恢复会再次从目标 Session 推导父级 id。
### 中断结果
#### 模型看到什么
接受时返回 `interrupt requested for agent <address>`。未授权的调用方——self、sibling、陈旧或非 ancestor——会成为指明拒绝原因的出错结果;已知目标已结算时仍渲染接受行;未知名称路径会被拒绝;不存在的原始 Session id 保留成功空操作。
接受时返回 `interrupt requested for agent <agent_id>`。未授权的调用方——self、sibling、陈旧或非 ancestor——会成为指明拒绝原因的出错结果;目标不存在或已结算仍渲染接受行。
#### Token 影响
@@ -154,11 +153,11 @@ kind: "package-reference"
#### 模型看到什么
接受时返回 `message delivered to agent <address>`;规范输出携带已接受的 `messageId`。目标不属于调用方的 subagent 树或输入被拒绝时,返回错误结果。
接受时返回 `message delivered to agent <id>`;规范输出携带已接受的 `messageId`。目标不属于调用方的 subagent 树或输入被拒绝时,返回错误结果。
#### Token 影响
每次调用返回一条简短确认;回复通过单独指定收件人的消息到达。使用 `list_agents` 中的地址 向 subagent 树内成员发送后续消息。
每次调用返回一条简短确认;回复通过单独指定收件人的消息到达。使用 `list_agents` 中的 id 向 subagent 树内成员发送后续消息。
#### KV Cache 影响
@@ -168,7 +167,7 @@ kind: "package-reference"
#### 模型看到什么
首行为 `<id> [<status>] — lead: root agent`。各可继续后代随后显示为 `<id> [<status>] parent=<parent-id> depth=<n> — <label>`。名称可用时,文本在标签前增加 `<name>: `;规范结果行保留原始 `label`,并另加 `name`。根使用 `kind: root`;后代相对该根使用 `kind: child`。诊断保留 id、原因、父级与深度,并可在 ` — ` 后显示名称。状态描述轮次活动,不表示任务完成。
首行为 `<id> [<status>] — root agent`。各可继续后代随后显示为 `<id> [<status>] parent=<id> depth=<n> — <label>`。根使用 `kind: root`;后代相对该根使用 `kind: child`。诊断显示为 `<id> [diagnostic: <reason>] parent=<id> depth=<n>`。状态描述轮次活动,不表示任务完成。
#### Token 影响
@@ -35,8 +35,7 @@
"@deepseek-ai/dsh-subagent": "workspace:*",
"@deepseek-ai/dsh-tools": "workspace:*",
"@deepseek-ai/dsh-agent": "workspace:*",
"@deepseek-ai/dsh-system-prompt": "workspace:*",
"@deepseek-ai/dsh-subagent-names": "workspace:*"
"@deepseek-ai/dsh-system-prompt": "workspace:*"
},
"devDependencies": {
"@deepseek-ai/cordis": "workspace:~",
@@ -54,11 +53,11 @@
"@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:*",
"@deepseek-ai/dsh-system-prompt": "workspace:*",
"@deepseek-ai/dsh-tool-testkit": "workspace:*",
"@deepseek-ai/dsh-tools": "workspace:*",
"@deepseek-ai/dsh-subagent-names": "workspace:*"
"@deepseek-ai/dsh-tools": "workspace:*"
},
"dependencies": {
"@deepseek-ai/schemastery": "workspace:~",
"@deepseek-ai/dsh-brand": "workspace:*",
"@deepseek-ai/dsh-util-values": "workspace:*"
}
}
@@ -10,16 +10,17 @@
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
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 type {} from '@deepseek-ai/dsh-subagent-names'
import type {} from '@deepseek-ai/dsh-system-prompt'
import { registerListAgents } from './list-agents.ts'
import { registerWaitAgent } from './wait-agent.ts'
export const name = 'tool-subagent-control'
export const inject = ['tools', 'subagents', 'subagentNames', 'agents', 'systemPrompt']
export const inject = ['tools', 'subagents', 'agents', 'systemPrompt']
/** Optional subagent control tools. */
export interface Config {
@@ -38,33 +39,51 @@ export const Config: z<Config> = z.object({
* @param config - optional tool registration settings.
*/
export function apply(ctx: Context, config: Config = {}): void {
registerWaitAgent(ctx)
const interrupt = config.interrupt !== false
registerListAgents(ctx)
registerWaitAgent(ctx)
const sendMessage = defineTool({
name: 'send_message',
description:
'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: {
name: {
type: 'string', required: true,
description: 'The full agent path (such as lead/reviewer) or Session id of another member of your subagent tree.',
agent_id: {
type: 'string',
required: true,
description: 'The agent id of another member of your subagent tree, as shown by list_agents.',
},
message: {
type: 'string',
required: true,
description: 'The message to deliver to the agent.',
},
message: { type: 'string', required: true, description: 'The message to deliver to the agent.' },
},
output: {
schema: {
type: 'object', additionalProperties: false,
properties: { messageId: { type: 'string', required: true } },
type: 'object',
additionalProperties: false,
properties: {
messageId: { type: 'string', required: true },
},
},
render: (args, _value) => [{ type: 'text', text: `message delivered to agent ${args.name}` }],
render: (args, _value) => [{
type: 'text',
text: `message delivered to agent ${args.agent_id}`,
}],
},
async execute(args, exec) {
const sender = exec.agent
if (!sender) throw new Error('send_message requires a calling agent (exec.agent was undefined)')
if (!sender) {
throw new Error('send_message requires a calling agent (exec.agent was undefined)')
}
const message: ContentBlock[] = [{ type: 'text', text: args.message }]
const target = ctx.subagentNames.resolveName(ctx.subagents.rootOf(sender).session, args.name)
const messageId = await ctx.subagents.sendMessage(sender, target, message, { signal: exec.signal })
const messageId = await ctx.subagents.sendMessage(
sender,
brandString<SessionId>(args.agent_id),
message,
{ signal: exec.signal },
)
return { messageId }
},
})
@@ -79,35 +98,47 @@ export function apply(ctx: Context, config: Config = {}): void {
|| ctx.tools.get(sendMessage.name, scope, toolSet) !== sendMessage) return ''
const parentId = JSON.stringify(agent.session.header.parentSession)
return `Your parent agent id is ${parentId}. Use `
+ `send_message({ name: ${parentId}, message: "<finding>" }) when a finding changes what the `
+ `send_message({ agent_id: ${parentId}, message: "<finding>" }) when a finding changes what the `
+ 'parent should do next; sending a message does not end your turn.'
},
})
if (config.interrupt !== false) ctx.tools.register(defineTool({
if (!interrupt) return
ctx.tools.register(defineTool({
name: 'interrupt_agent',
description:
'Ask a subagent to stop its current work. This call returns without waiting for it to stop. '
+ 'You can continue a local subagent\'s conversation later with send_message. '
+ 'External executions stop permanently and cannot receive follow-ups. Subagents it started will keep running.',
parameters: {
name: {
type: 'string', required: true,
description: 'The full agent path or Session id of an agent created under you: your direct child or a deeper descendant.',
agent_id: {
type: 'string',
required: true,
description: 'The id of an agent created under you: your direct child or a deeper descendant.',
},
},
output: {
schema: {
type: 'object', additionalProperties: false,
properties: { accepted: { type: 'boolean', required: true } },
type: 'object',
additionalProperties: false,
properties: {
accepted: { type: 'boolean', required: true },
},
},
render: (args, _value) => [{ type: 'text', text: `interrupt requested for agent ${args.name}` }],
render: (args, _value) => [{
type: 'text',
text: `interrupt requested for agent ${args.agent_id}`,
}],
},
execute(args, exec) {
const caller = exec.agent
if (!caller) throw new Error('interrupt_agent requires a calling agent (exec.agent was undefined)')
const target = ctx.subagentNames.resolveName(ctx.subagents.rootOf(caller).session, args.name)
exec.signal.throwIfAborted()
ctx.subagents.interrupt(target, { kind: 'ancestor', agent: caller })
if (!caller) {
// Ancestor authority requires an exact live calling agent.
throw new Error('interrupt_agent requires a calling agent (exec.agent was undefined)')
}
// 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 })
},
}))
@@ -5,33 +5,27 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { SessionId } from '@deepseek-ai/dsh-session'
import type { SubagentDescendantListEntry } from '@deepseek-ai/dsh-subagent'
import type {} from '@deepseek-ai/dsh-subagent-names'
type ListAgentsEntry =
| {
readonly kind: 'root'
readonly id: SessionId
readonly name?: string
readonly status: 'running' | 'inactive'
}
| {
readonly kind: 'child'
readonly id: SessionId
readonly name?: string
readonly label: string
readonly status: 'running' | 'inactive'
readonly parent?: SessionId
readonly parentName?: string
readonly depth?: number
}
| {
readonly kind: 'diagnostic'
readonly id: SessionId
readonly name?: string
readonly reason: 'corrupt' | 'unsupported' | 'unavailable'
readonly parent?: SessionId
readonly parentName?: string
readonly depth?: number
}
@@ -44,13 +38,8 @@ function statusOf(agents: { get(id: SessionId): Agent | undefined }, id: Session
function project(
agents: { get(id: SessionId): Agent | undefined },
entry: SubagentDescendantListEntry,
nameOf: (id: SessionId) => string | undefined,
): ListAgentsEntry | undefined {
const name = nameOf(entry.id)
const parentName = nameOf(entry.parentId)
const at = { parent: entry.parentId, depth: entry.depth,
...name === undefined ? {} : { name },
...parentName === undefined ? {} : { parentName } }
const at = { parent: entry.parentId, depth: entry.depth }
if (entry.kind === 'diagnostic') {
return { kind: 'diagnostic', id: entry.id, reason: entry.reason, ...at }
}
@@ -69,16 +58,13 @@ function project(
/**
* Register the `list_agents` tool.
* @param ctx - context carrying the tool registry, subagent service, and live Agent registry.
* @returns the registration disposer.
*/
export function registerListAgents(ctx: Context): () => void {
const names = ctx.subagentNames
return ctx.tools.register(defineTool({
export function registerListAgents(ctx: Context): void {
ctx.tools.register(defineTool({
name: 'list_agents',
description:
'List your entire subagent tree: the root agent and all reachable continuable descendants, including yourself. '
+ 'Rows carry ids, descriptions, and status; descendant rows carry parent ids and depth from the root. '
+ 'Rows also include available names alongside Session ids. '
+ 'You can message any other agent in the subagent tree listed as running or inactive. '
+ 'running means it is working; inactive means it is not currently working. '
+ 'While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. '
@@ -95,7 +81,6 @@ export function registerListAgents(ctx: Context): () => void {
properties: {
kind: { type: 'string', required: true, enum: ['root'] },
id: { type: 'string', required: true },
name: { type: 'string' },
status: { type: 'string', required: true, enum: ['running', 'inactive'] },
},
},
@@ -105,11 +90,9 @@ export function registerListAgents(ctx: Context): () => void {
properties: {
kind: { type: 'string', required: true, enum: ['child'] },
id: { type: 'string', required: true },
name: { type: 'string' },
label: { type: 'string', required: true },
status: { type: 'string', required: true, enum: ['running', 'inactive'] },
parent: { type: 'string' },
parentName: { type: 'string' },
depth: { type: 'number' },
},
},
@@ -119,10 +102,8 @@ export function registerListAgents(ctx: Context): () => void {
properties: {
kind: { type: 'string', required: true, enum: ['diagnostic'] },
id: { type: 'string', required: true },
name: { type: 'string' },
reason: { type: 'string', required: true, enum: ['corrupt', 'unsupported', 'unavailable'] },
parent: { type: 'string' },
parentName: { type: 'string' },
depth: { type: 'number' },
},
},
@@ -133,12 +114,11 @@ export function registerListAgents(ctx: Context): () => void {
return [{
type: 'text',
text: entries.map((entry) => {
const prefix = entry.name === undefined ? '' : `${entry.name}: `
if (entry.kind === 'root') return `${entry.id} [${entry.status}] — ${prefix}root agent`
if (entry.kind === 'root') return `${entry.id} [${entry.status}] — root agent`
const at = ` parent=${String(entry.parent)} depth=${String(entry.depth)}`
return entry.kind === 'diagnostic'
? `${entry.id} [diagnostic: ${entry.reason}]${at}` + (entry.name === undefined ? '' : ` — ${entry.name}`)
: `${entry.id} [${entry.status}]${at} — ${prefix}${entry.label}`
? `${entry.id} [diagnostic: ${entry.reason}]${at}`
: `${entry.id} [${entry.status}]${at} — ${entry.label}`
}).join('\n'),
}]
},
@@ -151,15 +131,10 @@ export function registerListAgents(ctx: Context): () => void {
}
const root = ctx.subagents.rootOf(parent)
const entries = await ctx.subagents.listDescendants(root.id, exec.signal)
const nameOf = (id: SessionId): string | undefined => {
const name = names.nameOf(root.session, id)
return name === id ? undefined : name
}
return [
{ kind: 'root' as const, id: root.id, status: statusOf(ctx.agents, root.id),
name: names.nameOf(root.session, root.id) },
{ kind: 'root' as const, id: root.id, status: statusOf(ctx.agents, root.id) },
...entries
.map(entry => project(ctx.agents, entry, nameOf))
.map(entry => project(ctx.agents, entry))
.filter(entry => entry !== undefined),
]
},
@@ -11,7 +11,6 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test
import { SessionId } from '@deepseek-ai/dsh-session'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import SubagentNames from '@deepseek-ai/dsh-subagent-names'
import type { SubagentCatalogEntry } from '@deepseek-ai/dsh-subagent'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
@@ -69,7 +68,6 @@ async function setupWith(adapter: MockAdapter | GatedAdapter) {
await ctx.plugin(AgentLoop, { agents: [] })
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
await ctx.plugin(tool)
ctx.llm.registerAdapter(['mock'], adapter)
@@ -123,52 +121,7 @@ describe('list_agents', () => {
const result = await callTool(ctx, 'list_agents', {}, parent)
expect(result.isError).toBe(false)
if (result.isError) throw new Error(text(result))
expect(result.value).toEqual([
{ kind: 'root', id: parent.id, name: 'lead', status: 'inactive' },
{ kind: 'child', id: started.childId, label: 'workflow child', parent: parent.id, parentName: 'lead', depth: 1, status: 'inactive' },
])
expect(text(result)).toBe(`${parent.id} [inactive] — lead: root agent\n${started.childId} [inactive] parent=${parent.id} depth=1 — workflow child`)
})
it('annotates the original child rows with names while retaining Session ids and labels', async () => {
const { ctx, parent } = await setup([textResponse('named result'), textResponse('unnamed result')])
const reservation = ctx.subagentNames.reserve(parent.session, parent.id, 'reviewer')
const child = await ctx.subagents.startActivation({
delivery: 'parent', provider: 'spawn', label: 'review implementation',
prompt: [{ type: 'text', text: 'review' }], parent, signal: testToolSignal,
})
ctx.subagentNames.bind(parent.session, child.childId, reservation.name)
reservation.dispose()
await child.result
const unnamed = await ctx.subagents.startActivation({
delivery: 'parent', provider: 'spawn', label: 'unnamed task',
prompt: [{ type: 'text', text: 'check' }], parent, signal: testToolSignal,
})
await unnamed.result
const missing = SessionId('missing-child')
parent.session.append('subagent/catalog', {
version: 0, childId: missing, childCreatedAt: 1, mode: 'continuable', label: 'missing child',
})
const missingName = ctx.subagentNames.reserve(parent.session, parent.id, 'missing')
ctx.subagentNames.bind(parent.session, missing, missingName.name)
missingName.dispose()
const result = await callTool(ctx, 'list_agents', {}, parent)
expect(result.isError, text(result)).toBe(false)
if (result.isError) throw new Error(text(result))
expect(result.value).toEqual([
{ kind: 'root', id: parent.id, name: 'lead', status: 'inactive' },
{ kind: 'child', id: child.childId, name: 'lead/reviewer', label: 'review implementation',
parent: parent.id, parentName: 'lead', depth: 1, status: 'inactive' },
{ kind: 'child', id: unnamed.childId, label: 'unnamed task',
parent: parent.id, parentName: 'lead', depth: 1, status: 'inactive' },
{ kind: 'diagnostic', id: missing, name: 'lead/missing', reason: 'unavailable',
parent: parent.id, parentName: 'lead', depth: 1 },
])
expect(text(result)).toBe('parent [inactive] — lead: root agent\n'
+ `${child.childId} [inactive] parent=parent depth=1 — lead/reviewer: review implementation\n`
+ `${unnamed.childId} [inactive] parent=parent depth=1 — unnamed task\n`
+ 'missing-child [diagnostic: unavailable] parent=parent depth=1 — lead/missing')
expect(text(result)).toBe(`${parent.id} [inactive] — root agent\n${started.childId} [inactive] parent=${parent.id} depth=1 — workflow child`)
})
it('omits external leaves without diagnostics', async () => {
@@ -179,7 +132,7 @@ describe('list_agents', () => {
})
const result = await callTool(ctx, 'list_agents', {}, parent)
expect(result.isError).toBe(false)
expect(text(result)).toBe(`${parent.id} [inactive] — lead: root agent`)
expect(text(result)).toBe(`${parent.id} [inactive] — root agent`)
})
it('registers list_agents once, globally, without parameters', async () => {
@@ -200,7 +153,7 @@ describe('list_agents', () => {
await ctx.sessions.flush(parent.session)
const result = await callTool(ctx, 'list_agents', {}, parent)
expect(result.isError).toBe(false)
expect(text(result)).toBe(`${parent.id} [inactive] — lead: root agent`)
expect(text(result)).toBe(`${parent.id} [inactive] — root agent`)
})
it('renders direct children in array order with registry statuses', async () => {
@@ -255,7 +208,7 @@ describe('list_agents', () => {
const result = await callTool(ctx, 'list_agents', {}, parent)
expect(result.isError).toBe(false)
expect(text(result)).toBe(
`${parent.id} [inactive] — lead: root agent\n`
`${parent.id} [inactive] — root agent\n`
+ `${started.childId} [inactive] parent=${parent.id} depth=1 — real child\n`
+ `running-child [running] parent=${parent.id} depth=1 — still working\n`
+ `waiting-child [inactive] parent=${parent.id} depth=1 — waiting on descendants`,
@@ -289,7 +242,7 @@ describe('list_agents', () => {
const result = await callTool(ctx, 'list_agents', {}, parent)
expect(result.isError).toBe(false)
expect(text(result)).toBe(
`${parent.id} [inactive] — lead: root agent\n`
`${parent.id} [inactive] — root agent\n`
+ `historical-one-shot [diagnostic: unavailable] parent=${parent.id} depth=1\n`
+ `${started.childId} [inactive] parent=${parent.id} depth=1 — summarize the doc`,
)
@@ -323,7 +276,6 @@ describe('list_agents', () => {
await ctx.plugin(AgentLoop, { agents: [] })
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
const fiber = await ctx.plugin(tool)
expect(ctx.tools.schemas().some(schema => schema.name === 'list_agents')).toBe(true)
await fiber.dispose()
@@ -333,7 +285,7 @@ describe('list_agents', () => {
it('has the namespace-plugin export shape', () => {
expect('default' in tool).toBe(false)
expect(tool.name).toBe('tool-subagent-control')
expect(tool.inject).toEqual(['tools', 'subagents', 'subagentNames', 'agents', 'systemPrompt'])
expect(tool.inject).toEqual(['tools', 'subagents', 'agents', 'systemPrompt'])
expect(typeof tool.apply).toBe('function')
})
@@ -369,7 +321,7 @@ describe('list_agents', () => {
const result = await callTool(ctx, 'list_agents', {}, parent)
expect(result.isError).toBe(false)
expect(text(result)).toBe(
`${parent.id} [inactive] — lead: root agent\n`
`${parent.id} [inactive] — root agent\n`
+ `${started.childId} [inactive] parent=${parent.id} depth=1 — waiting branch\n`
+ `${grandchild.childId} [running] parent=${started.childId} depth=2 — nested leaf`,
)
@@ -417,7 +369,7 @@ describe('list_agents', () => {
const result = await callTool(ctx, 'list_agents', {}, parent)
expect(result.isError).toBe(false)
expect(text(result)).toBe(
`${parent.id} [inactive] — lead: root agent\n`
`${parent.id} [inactive] — root agent\n`
+ 'deep-leaf [inactive] parent=one-shot-mid depth=2 — deep leaf\n'
+ `broken-node [diagnostic: unavailable] parent=${parent.id} depth=1`,
)
@@ -447,7 +399,7 @@ describe('list_agents', () => {
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(4) })
const expected = [
`${parent.id} [inactive] — lead: root agent`,
`${parent.id} [inactive] — root agent`,
`${alpha} [running] parent=${parent.id} depth=1 — alpha task`,
`${beta} [inactive] parent=${parent.id} depth=1 — beta task`,
`${gamma} [inactive] parent=${parent.id} depth=1 — gamma task`,
@@ -14,7 +14,6 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test
import { SessionId } from '@deepseek-ai/dsh-session'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import SubagentNames from '@deepseek-ai/dsh-subagent-names'
import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork-in-process'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
@@ -72,14 +71,13 @@ async function setupWith(adapter: MockAdapter | GatedAdapter, park = true) {
await ctx.plugin(AgentLoop, { agents: [] })
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
const names = await ctx.plugin(SubagentNames)
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
await ctx.plugin(SubagentFork, { providerName: 'fork' })
const controlFiber = await ctx.plugin(tool)
ctx.llm.registerAdapter(['mock'], adapter)
const parent = await ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
if (park) parkParent(ctx, parent)
return { ctx, parent, adapter, controlFiber, names }
return { ctx, parent, adapter, controlFiber }
}
async function setup(script: ConstructorParameters<typeof MockAdapter>[0]) {
@@ -115,100 +113,18 @@ async function waitNoActivation(ctx: Context, childId: SessionId): Promise<void>
}
describe('dsh-tool-subagent-control', () => {
it.each(['root disposal', 'plugin restart'] as const)('does not reinstall controls while %s drains', async (mode) => {
const { ctx, controlFiber, names } = await setup([])
await controlFiber.dispose()
const controls = await ctx.plugin(tool)
const cleanupEntered = Promise.withResolvers<undefined>()
const releaseCleanup = Promise.withResolvers<undefined>()
let cleaned = false
controls.ctx.effect(() => async () => {
cleanupEntered.resolve(undefined)
await releaseCleanup.promise
cleaned = true
})
const register = vi.spyOn(ctx.tools, 'register')
let settled = false
const stopping = mode === 'root disposal' ? ctx.fiber.dispose() : controls.restart()
const removing = mode === 'root disposal' ? Promise.resolve() : names.dispose()
const drained = Promise.allSettled([stopping, removing]).then((results) => { settled = true; return results })
try {
await cleanupEntered.promise
expect(settled).toBe(false)
expect(cleaned).toBe(false)
expect(register).not.toHaveBeenCalled()
} finally {
releaseCleanup.resolve(undefined)
}
const results = await drained
expect(results.every(result => result.status === 'fulfilled')).toBe(true)
expect(cleaned).toBe(true)
})
it('removes partially registered controls when another tool already owns send_message', async () => {
const { ctx, controlFiber } = await setup([])
await controlFiber.dispose()
const existing = defineTool({
name: 'send_message', description: 'Another messaging tool.', parameters: {},
output: { schema: { type: 'object', additionalProperties: false, properties: {} }, render: () => [] },
execute: () => Promise.resolve({}),
})
ctx.tools.register(existing)
await expect(ctx.plugin(tool)).rejects.toThrow('already registered')
expect(ctx.tools.get('send_message')).toBe(existing)
expect(ctx.tools.get('list_agents')).toBeUndefined()
expect(ctx.tools.get('wait_agent')).toBeUndefined()
})
it('removes controls without names and restores their fixed schemas with the dependency', async () => {
const { ctx, parent, names, controlFiber } = await setup([])
const schemas = ctx.tools.schemas()
expect(ctx.tools.get('send_message')!.parameters.properties).toHaveProperty('name')
expect(ctx.tools.get('send_message')!.parameters.properties).not.toHaveProperty('agent_id')
expect(text(await callTool(ctx, 'list_agents', {}, parent))).toBe('parent [inactive] — lead: root agent')
await names.dispose()
for (const toolName of ['send_message', 'interrupt_agent', 'list_agents', 'wait_agent']) {
expect(ctx.tools.get(toolName)).toBeUndefined()
}
await ctx.plugin(SubagentNames)
await controlFiber.await()
expect(ctx.tools.schemas()).toEqual(schemas)
})
it('continues a named child by path with named message text and durable Session id attribution', async () => {
const { ctx, parent } = await setup([textResponse('first result'), textResponse('continued result')])
const reservation = ctx.subagentNames.reserve(parent.session, parent.id, 'reviewer')
const child = await ctx.subagents.startActivation({
provider: 'spawn', label: 'review', delivery: 'parent', parent,
prompt: [{ type: 'text', text: 'review' }], signal: testToolSignal,
})
ctx.subagentNames.bind(parent.session, child.childId, reservation.name)
reservation.dispose()
await child.result
const result = await callTool(ctx, 'send_message', { name: reservation.name, message: 'check once more' }, parent)
expect(result.isError, text(result)).toBe(false)
expect(text(result)).toBe('message delivered to agent lead/reviewer')
await waitNoActivation(ctx, child.childId)
const interrupted = await callTool(ctx, 'interrupt_agent', { name: reservation.name }, parent)
expect(interrupted.isError, text(interrupted)).toBe(false)
const stored = await loadStoredSession(ctx.sessionPersistence, child.childId)
expect(stored.events.some(event => event.type === 'user/message'
&& event.data.source.kind === 'agent-message' && event.data.source.senderSessionId === parent.id
&& text(event.data).includes('Agent lead sent a message: '))).toBe(true)
})
it('registers send_message once, globally, with the two required parameters', async () => {
const { ctx } = await setup([])
const schemas = ctx.tools.schemas().filter(schema => schema.name === 'send_message')
expect(schemas).toHaveLength(1)
const props = (schemas[0]!.parameters as { properties?: Record<string, unknown> }).properties ?? {}
expect(Object.keys(props).sort()).toEqual(['message', 'name'])
expect(Object.keys(props).sort()).toEqual(['agent_id', 'message'])
// The continuable path has no Task, so the schema must not promise one.
expect(schemas[0]!.description).not.toContain('job_output')
expect(schemas[0]!.description).not.toContain('job id')
expect(schemas[0]!.description).toContain('receives it at its next step')
expect(props.name).toMatchObject({
description: 'The full agent path (such as lead/reviewer) or Session id of another member of your subagent tree.',
expect(props.agent_id).toMatchObject({
description: 'The agent id of another member of your subagent tree, as shown by list_agents.',
})
})
@@ -312,26 +228,6 @@ describe('dsh-tool-subagent-control', () => {
toolSet.dispose()
})
it('uses the full child name in messages while preserving the sender Session id', async () => {
const { ctx, parent } = await setup([])
const handle = await ctx.agents.create({
sessionId: SessionId('named-sender'),
meta: { origin: 'subagent', parentSession: parent.id },
setup: (_ctx, agent) => { bindSubagentActivation(ctx, agent, parent) },
})
ctx.subagentNames.bind(parent.session, handle.agent.id, 'lead/reviewer')
const followup = vi.spyOn(parent, 'steer')
const result = await callTool(ctx, 'send_message', { name: 'lead', message: 'Review complete.' }, handle.agent)
expect(result.isError).toBe(false)
const message = followup.mock.calls.map(([message]) => message).find(message => message.source.kind === 'agent-message')
expect(message?.content).toEqual([
{ type: 'text', text: 'Agent lead/reviewer sent a message: ' },
{ type: 'text', text: 'Review complete.' },
])
expect(message?.source).toEqual({ kind: 'agent-message', form: 'relay', senderSessionId: handle.agent.id })
await handle.dispose()
})
it.each(['parent', 'caller'] as const)('keeps %s result delivery independent of messaging tools', async (delivery) => {
const { ctx, parent, adapter, controlFiber } = await setup([textResponse('child result')])
await controlFiber.dispose()
@@ -369,7 +265,7 @@ describe('dsh-tool-subagent-control', () => {
})
const result = await callTool(ctx, 'send_message', {
name: parent.id,
agent_id: parent.id,
message: 'CHILD_FINDING',
}, child)
@@ -402,7 +298,7 @@ describe('dsh-tool-subagent-control', () => {
await waitNoActivation(ctx, started.childId)
const result = await callTool(ctx, 'send_message', {
name: started.childId,
agent_id: started.childId,
message: 'and then?',
}, parent)
@@ -419,7 +315,7 @@ describe('dsh-tool-subagent-control', () => {
senderSessionId: parent.id,
})
expect(followUp?.type === 'user/message' && followUp.data.content).toEqual([
{ type: 'text', text: 'Agent lead sent a message: ' },
{ type: 'text', text: `Agent ${parent.id} sent a message: ` },
{ type: 'text', text: 'and then?' },
])
})
@@ -436,7 +332,7 @@ describe('dsh-tool-subagent-control', () => {
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
const result = await callTool(ctx, 'send_message', {
name: started.childId,
agent_id: started.childId,
message: 'also consider Y',
}, parent)
expect(result.isError).toBe(false)
@@ -451,7 +347,7 @@ describe('dsh-tool-subagent-control', () => {
: [])
expect(prompts).toEqual([
'long work',
'Agent lead sent a message: ',
`Agent ${parent.id} sent a message: `,
'also consider Y',
])
})
@@ -459,7 +355,7 @@ describe('dsh-tool-subagent-control', () => {
it('reports a delivery failure as an errored, not-delivered result', async () => {
const { ctx, parent } = await setup([])
const result = await callTool(ctx, 'send_message', {
name: 'no-such-child',
agent_id: 'no-such-child',
message: 'hello?',
}, parent)
expect(result.isError).toBe(true)
@@ -479,7 +375,7 @@ describe('dsh-tool-subagent-control', () => {
const stranger = await ctx.agentLoop.create(SessionId('stranger'), { provider: 'mock', model: 'mock' })
const result = await callTool(ctx, 'send_message', {
name: started.childId,
agent_id: started.childId,
message: 'mine now',
}, stranger)
expect(result.isError).toBe(true)
@@ -488,7 +384,7 @@ describe('dsh-tool-subagent-control', () => {
it('fails loud when invoked without a calling agent', async () => {
const { ctx } = await setup([])
const result = await callTool(ctx, 'send_message', { name: 'x', message: 'y' })
const result = await callTool(ctx, 'send_message', { agent_id: 'x', message: 'y' })
expect(result.isError).toBe(true)
expect(text(result)).toContain('requires a calling agent')
})
@@ -500,7 +396,6 @@ describe('dsh-tool-subagent-control', () => {
await ctx.plugin(AgentLoop, { agents: [] })
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
const fiber = await ctx.plugin(tool, config)
const parent = await ctx.agentLoop.create(SessionId('hmr-parent'), { provider: 'mock', model: 'mock' })
const { agent: child } = await ctx.agents.create({
@@ -539,18 +434,18 @@ describe('dsh-tool-subagent-control', () => {
it('has the namespace-plugin export shape (no stray default)', () => {
expect('default' in tool).toBe(false)
expect(tool.name).toBe('tool-subagent-control')
expect(tool.inject).toEqual(['tools', 'subagents', 'subagentNames', 'agents', 'systemPrompt'])
expect(tool.inject).toEqual(['tools', 'subagents', 'agents', 'systemPrompt'])
expect(typeof tool.apply).toBe('function')
})
})
describe('dsh-tool-subagent-control interrupt_agent', () => {
it('registers interrupt_agent with the single name parameter and current-turn wording', async () => {
it('registers interrupt_agent with the single agent_id parameter and current-turn wording', async () => {
const { ctx } = await setup([])
const schemas = ctx.tools.schemas().filter(schema => schema.name === 'interrupt_agent')
expect(schemas).toHaveLength(1)
const props = (schemas[0]!.parameters as { properties?: Record<string, unknown> }).properties ?? {}
expect(Object.keys(props)).toEqual(['name'])
expect(Object.keys(props)).toEqual(['agent_id'])
expect(schemas[0]!.description).toContain('stop its current work')
expect(schemas[0]!.description).toContain('send_message')
})
@@ -573,13 +468,13 @@ describe('dsh-tool-subagent-control interrupt_agent', () => {
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
const child = ctx.agents.get(started.childId)!
const queued = await callTool(ctx, 'send_message', {
name: started.childId,
agent_id: started.childId,
message: 'parked follow-up',
}, parent)
expect(queued.isError).toBe(false)
const cancelSpy = vi.spyOn(child, 'cancel')
const result = await callTool(ctx, 'interrupt_agent', { name: started.childId }, parent)
const result = await callTool(ctx, 'interrupt_agent', { agent_id: started.childId }, parent)
expect(result.isError).toBe(false)
expect(text(result)).toBe(`interrupt requested for agent ${started.childId}`)
@@ -591,7 +486,7 @@ describe('dsh-tool-subagent-control interrupt_agent', () => {
expect(child.inbox.nextStep).toHaveLength(1)
const waking = await callTool(ctx, 'send_message', {
name: started.childId,
agent_id: started.childId,
message: 'wake up',
}, parent)
expect(waking.isError).toBe(false)
@@ -605,9 +500,9 @@ describe('dsh-tool-subagent-control interrupt_agent', () => {
: [])
expect(prompts).toEqual([
'long work',
'Agent lead sent a message: ',
`Agent ${parent.id} sent a message: `,
'parked follow-up',
'Agent lead sent a message: ',
`Agent ${parent.id} sent a message: `,
'wake up',
])
})
@@ -640,7 +535,7 @@ describe('dsh-tool-subagent-control interrupt_agent', () => {
const grandchildAgent = ctx.agents.get(grandchild.childId)!
const cancelSpy = vi.spyOn(grandchildAgent, 'cancel')
const result = await callTool(ctx, 'interrupt_agent', { name: grandchild.childId }, parent)
const result = await callTool(ctx, 'interrupt_agent', { agent_id: grandchild.childId }, parent)
expect(result.isError).toBe(false)
expect(cancelSpy).toHaveBeenCalledExactlyOnceWith({ kind: 'parent' }, { keepInbox: true })
@@ -679,13 +574,13 @@ describe('dsh-tool-subagent-control interrupt_agent', () => {
const stranger = await ctx.agentLoop.create(SessionId('stranger'), { provider: 'mock', model: 'mock' })
const cancelSpy = vi.spyOn(targetAgent, 'cancel')
const self = await callTool(ctx, 'interrupt_agent', { name: target.childId }, targetAgent)
const self = await callTool(ctx, 'interrupt_agent', { agent_id: target.childId }, targetAgent)
expect(self.isError).toBe(true)
expect(text(self)).toContain('cannot interrupt itself')
const fromSibling = await callTool(ctx, 'interrupt_agent', { name: target.childId }, siblingAgent)
const fromSibling = await callTool(ctx, 'interrupt_agent', { agent_id: target.childId }, siblingAgent)
expect(fromSibling.isError).toBe(true)
expect(text(fromSibling)).toContain('not a live descendant')
const fromStranger = await callTool(ctx, 'interrupt_agent', { name: target.childId }, stranger)
const fromStranger = await callTool(ctx, 'interrupt_agent', { agent_id: target.childId }, stranger)
expect(fromStranger.isError).toBe(true)
expect(text(fromStranger)).toContain('not a live descendant')
expect(cancelSpy).not.toHaveBeenCalled()
@@ -707,10 +602,10 @@ describe('dsh-tool-subagent-control interrupt_agent', () => {
})
await waitNoActivation(ctx, started.childId)
const settled = await callTool(ctx, 'interrupt_agent', { name: started.childId }, parent)
const settled = await callTool(ctx, 'interrupt_agent', { agent_id: started.childId }, parent)
expect(settled.isError).toBe(false)
expect(text(settled)).toBe(`interrupt requested for agent ${started.childId}`)
const unknown = await callTool(ctx, 'interrupt_agent', { name: 'no-such-agent' }, parent)
const unknown = await callTool(ctx, 'interrupt_agent', { agent_id: 'no-such-agent' }, parent)
expect(unknown.isError).toBe(false)
// No cold resume: the settled target never rematerialized.
expect(ctx.agents.get(started.childId)).toBeUndefined()
@@ -718,7 +613,7 @@ describe('dsh-tool-subagent-control interrupt_agent', () => {
it('fails loud when invoked without a calling agent', async () => {
const { ctx } = await setup([])
const result = await callTool(ctx, 'interrupt_agent', { name: 'x' })
const result = await callTool(ctx, 'interrupt_agent', { agent_id: 'x' })
expect(result.isError).toBe(true)
expect(text(result)).toContain('requires a calling agent')
})
@@ -7,7 +7,6 @@ import { mountAgentLoopTestDependencies, mountAgentLoopTestHarness } from '@deep
import { createUserMessage, HarnessError, ToolCallId } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import SubagentNames from '@deepseek-ai/dsh-subagent-names'
import { renderContextSnapshot } from '@deepseek-ai/dsh-system-prompt'
import * as control from '../src/index.ts'
@@ -24,7 +23,6 @@ async function setup() {
await mountAgentLoopTestDependencies(ctx, { workingDirectory: true, systemPrompt: { includeRuntimeContext: false } })
const harness = await mountAgentLoopTestHarness(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
const fiber = await ctx.plugin(control)
const handle = await ctx.agents.create({ sessionId: SessionId('waiting') })
const other = await harness.create(SessionId('other'))
@@ -34,9 +34,6 @@
},
{
"path": "../../core/system-prompt"
},
{
"path": "../subagent-names"
}
]
}
@@ -9,20 +9,20 @@
en: 686fb5136bef0a3b
zh: d25d6569174a01b5
/deepseek-ai-dsh-tool-subagent/summary:
en: 7e130a0ae9710707
zh: c0064a555e8e82c6
en: c09d40329253a9d8
zh: 91c1bbe5557bdda9
/deepseek-ai-dsh-tool-subagent/table-of-contents:
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
/deepseek-ai-dsh-tool-subagent/use-this-package:
en: be1cbff3ccdd95ec
zh: 9e29cad430abef7b
en: 93f1701f5209ecb5
zh: 6310fd012400aead
/deepseek-ai-dsh-tool-subagent/use-this-package/minimal-configuration:
en: a5b64c9db50c1185
zh: aef1e819d998921b
en: 1a23808ca32b8f2d
zh: e637a35220da0564
/deepseek-ai-dsh-tool-subagent/use-this-package/managed-delegation:
en: 79b0d162faffd76a
zh: 205daaa0170b0c5e
en: 5f0602e05596941a
zh: 678534e204f63cb6
/deepseek-ai-dsh-tool-subagent/use-this-package/selecting-a-child-preset:
en: e28a206c65151ec0
zh: 1372242076549ca4
@@ -36,11 +36,11 @@
en: 373abf7c7aa3e4f1
zh: be0b4d4a7371d360
/deepseek-ai-dsh-tool-subagent/understand-the-implementation/activation-ownership:
en: ce85ede25adcdf18
zh: 575c97085c2a8a6e
en: ef692c4d17290920
zh: b1d0d3dab011e2cd
/deepseek-ai-dsh-tool-subagent/understand-the-implementation/result-acknowledgement:
en: f7a9fefb0e1b6e7d
zh: 2a6f220830f716ad
en: 3cdb913209febc42
zh: ea6ebfbe9d21fb3e
/deepseek-ai-dsh-tool-subagent/understand-the-implementation/context-sensitive-wording:
en: 1733728ac5aa6bb0
zh: 341ffc6e21a289af
@@ -111,8 +111,8 @@
en: 45854c8f4da73af7
zh: 7a9803ca8c03c40f
/deepseek-ai-dsh-tool-subagent/model-experience/start-result/what-the-model-sees:
en: 1c92f306efebc166
zh: 34e0029c5d93834d
en: 15772cb4c28bf562
zh: 7452e7a7114d587c
/deepseek-ai-dsh-tool-subagent/model-experience/start-result/token-effect:
en: 807d71626c81c858
zh: ba4b85709fa46c84
@@ -123,8 +123,8 @@
en: b6ed0eef154aa546
zh: 34160884aa30ffec
/deepseek-ai-dsh-tool-subagent/model-experience/completion-delivery/what-the-model-sees:
en: 9dc85479c8de6a8b
zh: ca0a2e436b1469af
en: 8f5e3a083eb7f807
zh: 6bfc2b38f49a6f79
/deepseek-ai-dsh-tool-subagent/model-experience/completion-delivery/token-effect:
en: 615348e27c0c9bc0
zh: 1655fdbc42932909
+7 -10
View File
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
## Summary
Use this package to give an agent a named tool that delegates work to a configured backend. Every call starts a managed activation and returns a child Session id; the runtime owns completion notices and cleanup. Local children support messages and restoration, while external backends execute one task and return their final answer through the completion notice. Supported backends can expose approved child LLM routes. Each instance can select a child preset and depth limit.
Use this package to give an agent a named tool that delegates work to a configured backend. Every call starts a managed activation and returns a child id; the runtime owns completion notices and cleanup. Local children support messages and restoration, while external backends execute one task and return their final answer through the completion notice. Supported backends can expose approved child LLM routes. Each instance can select a child preset and depth limit.
## Table of Contents
@@ -29,15 +29,12 @@ The optional `cwd` argument selects the child's initial working directory. Relat
Mount one instance per delegation target, each with a distinct `toolName`. The tool exists exactly while its provider does, so sibling load order and provider reloads never strand it.
The tool requires [subagent-names](../subagent-names/README.md). The optional `name` argument chooses a unique sibling name; omission selects the first unused `agent-N`. Naming guidance asks for a short role such as `reviewer` or `researcher`, leaving the task in `description`; a specialty or numeric suffix distinguishes repeated roles. The result contains the child Session id and a full path such as `lead/reviewer`. Cordis removes the tool when the naming service unloads and restores it when the service returns.
### Minimal configuration
Load the subagent service, the naming plugin, an in-process or remote backend, and this tool; then name the provider. This composition exposes a `subagent` tool that delegates to the `spawn` backend:
Load the subagent service, an in-process or remote backend, and this tool; then name the provider. This composition exposes a `subagent` tool that delegates to the `spawn` backend:
```yaml
- name: '@deepseek-ai/dsh-subagent'
- name: '@deepseek-ai/dsh-subagent-names'
- name: '@deepseek-ai/dsh-subagent-spawn-in-process'
- name: '@deepseek-ai/dsh-tool-subagent'
config:
@@ -60,7 +57,7 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
### Managed delegation
Every successful call returns `started subagent <id>` and a second line, `name: <path>`. Top-level calls also persist `{ kind: 'activation', subagentId: <id> }` in result presentation metadata; nested Code dispatches omit this metadata. The call does not wait for the child's result. The activation owns the work independently of the completed tool call; the runtime sends a completion notice and releases execution resources.
Every call returns `started subagent <childId>` after the runtime accepts the child. Top-level calls also persist `{ kind: 'activation', subagentId: <childId> }` in result presentation metadata; nested Code dispatches omit this metadata. It does not wait for the child's result. The activation owns the work independently of the completed tool call; the runtime sends a completion notice and releases execution resources.
For every backend, the runtime automatically delivers the completion notice if the calling Agent is still online when the child finishes. The notice includes the final answer unless the child sent that Agent a message just before finishing. Local Spawn and Fork children can also send messages, accept more work through `send_message` while active, or resume after settling. Codex, Claude Code, ACP, and DSH SDK backends do not accept follow-up messages.
@@ -97,11 +94,11 @@ One instance is one provider plus one tool name. The plugin registers its defini
### Activation ownership
The tool calls `ctx.subagents.startActivation()` with parent delivery. It reserves the requested name before startup, binds the returned child id afterward, and releases the reservation when the call settles. A failed binding reports the already-started child id without stopping its work; callers must check that child before retrying. The runtime owns startup cancellation, execution, outcome reporting, and cleanup. Failures after publication arrive through the completion notice, with diagnostics and partial output preserved by the runtime.
The tool calls `ctx.subagents.startActivation()` with parent delivery selected. The runtime owns startup cancellation, execution, outcome reporting, and cleanup. A startup failure is an errored tool result; failures after publication arrive through the completion notice, with diagnostics and partial output preserved by the runtime.
### Result acknowledgement
The result is `{ kind: 'activation', subagentId, name }`. It contains no execution Promise or final output; programmatic consumers that need to await results use the subagent service directly.
The canonical tool result is `{ kind: 'activation', subagentId }`. It contains no execution Promise or final output; programmatic consumers that need to await results use the subagent service directly.
### Context-sensitive wording
@@ -210,7 +207,7 @@ Depth changes do not alter the system prompt or tool schemas. Updated runtime co
#### What the model sees
The call retains the task description and prompt. Success returns `started subagent <id>` and a `name: <path>` line; startup rejection returns an error. Child execution details are delivered separately from this acknowledgement.
The call retains the task description and prompt. Success returns `started subagent <childId>`; startup rejection returns an error. Child execution details are delivered separately from this acknowledgement.
#### Token effect
@@ -224,7 +221,7 @@ Append-only; newly visible content follows the reusable request prefix and does
#### What the model sees
Answer delivery follows the managed delegation rules above. The returned child address identifies subsequent control and catalog operations; only backends with continuation support accept follow-up messages.
Answer delivery follows the managed delegation rules above. The returned child id identifies subsequent control and catalog operations; only backends with continuation support accept follow-up messages.
#### Token effect
+7 -10
View File
@@ -9,7 +9,7 @@ kind: "package-reference"
## 概述
使用本包可为 agent 提供具名工具,把工作委派给配置的后端。每次调用都会启动受管理的 activation 并返回子任务 Session id;运行时负责完成通知与清理。本地子任务支持消息和恢复,外部后端执行一次任务,并通过完成通知返回最终回答。受支持的后端可以公开获准的子级 LLM 路由。每个实例可选择子级 preset 和深度限制。
使用本包可为 agent 提供具名工具,把工作委派给配置的后端。每次调用都会启动受管理的 activation 并返回子任务 id;运行时负责完成通知与清理。本地子任务支持消息和恢复,外部后端执行一次任务,并通过完成通知返回最终回答。受支持的后端可以公开获准的子级 LLM 路由。每个实例可选择子级 preset 和深度限制。
## 目录
@@ -29,15 +29,12 @@ kind: "package-reference"
每个委派目标挂载一个实例,且每个实例的 `toolName` 必须不同。工具与其提供方同时存在、同时消失,因此同级加载顺序与提供方重新加载都不会让工具悬空。
工具需要 [subagent-names](../subagent-names/README.zh.md)。可选 `name` 参数选择同级唯一名称,省略时分配首个未使用的 `agent-N`。命名提示要求使用 `reviewer` 或 `researcher` 等简短角色名称,将任务放在 `description` 中;同类角色通过专业方向或数字后缀区分。结果包含子任务 Session id 和完整路径,例如 `lead/reviewer`。命名服务卸载时 Cordis 移除工具,服务恢复时重新注册工具。
### 最小配置
先加载 subagent 服务、命名插件、一个进程内或远程后端与本工具,然后指定提供方名称。此组合暴露一个委派给 `spawn` 后端的 `subagent` 工具:
先加载 subagent 服务、一个进程内或远程后端与本工具,然后指定提供方名称。此组合暴露一个委派给 `spawn` 后端的 `subagent` 工具:
```yaml
- name: '@deepseek-ai/dsh-subagent'
- name: '@deepseek-ai/dsh-subagent-names'
- name: '@deepseek-ai/dsh-subagent-spawn-in-process'
- name: '@deepseek-ai/dsh-tool-subagent'
config:
@@ -60,7 +57,7 @@ kind: "package-reference"
### 受管理的委派
每次成功调用都返回 `started subagent <id>` 和第二行 `name: <path>`。顶层调用还会在结果展示 metadata 中持久化 `{ kind: 'activation', subagentId: <id> }`;嵌套 Code 派发不附加这份 metadata。调用不等待子任务结果。activation 独立于已完成的工具调用拥有这项工作;运行时发送完成通知并释放执行资源。
运行时接受子任务后,每次调用都会返回 `started subagent <childId>`。顶层调用还会在结果展示 metadata 中持久化 `{ kind: 'activation', subagentId: <childId> }`;嵌套 Code 派发不附加这份 metadata。调用不等待子任务结果。activation 独立于已完成的工具调用拥有这项工作;运行时发送完成通知并释放执行资源。
所有后端都由运行时自动递送完成通知,前提是子任务结束时调用方 Agent 仍在线。通知包含最终回答,除非子任务在结束前刚向该 Agent 发过消息。本地 Spawn 和 Fork 子任务还可以发送消息,在活跃期间通过 `send_message` 接受更多工作,或在结束后恢复。Codex、Claude Code、ACP 与 DSH SDK 后端不接受后续消息。
@@ -97,11 +94,11 @@ kind: "package-reference"
### Activation 所有权
工具调用 `ctx.subagents.startActivation()`,并选择向父任务交付。工具先预留请求的名称,启动后绑定返回的子任务 id,在调用结束时释放预留。绑定失败会报告已启动的子任务 id,但不会停止其工作;调用方必须先检查该子任务再重试。运行时拥有启动取消、执行、结果汇报与清理。发布后的失败通过完成通知到达,运行时保留诊断和部分输出。
工具调用 `ctx.subagents.startActivation()`,并选择向父任务交付。运行时拥有启动取消、执行、结果汇报与清理。启动失败产生出错的工具结果;发布后的失败通过完成通知到达,运行时保留诊断和部分输出。
### 结果确认
结果为 `{ kind: 'activation', subagentId, name }`。其中没有执行 Promise 或最终输出;需要等待结果的程序调用方直接使用 subagent 服务。
规范工具结果为 `{ kind: 'activation', subagentId }`。其中没有执行 Promise 或最终输出;需要等待结果的程序调用方直接使用 subagent 服务。
### 随上下文变化的措辞
@@ -210,7 +207,7 @@ Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do no
#### 模型看到什么
调用保留任务描述与提示词。成功返回 `started subagent <id>`,并附带 `name: <path>` 行;启动被拒绝则返回错误。子任务执行详情独立于这条确认交付。
调用保留任务描述与提示词。成功返回 `started subagent <childId>`;启动被拒绝则返回错误。子任务执行详情独立于这条确认交付。
#### Token 影响
@@ -224,7 +221,7 @@ Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do no
#### 模型看到什么
回答投递遵循上文的受管理委派规则。返回的子任务地址用于后续控制与目录操作;只有支持继续执行的后端才接受后续消息。
回答投递遵循上文的受管理委派规则。返回的子任务 id 用于后续控制与目录操作;只有支持继续执行的后端才接受后续消息。
#### Token 影响
+2 -4
View File
@@ -44,8 +44,7 @@
"@deepseek-ai/dsh-subagent": "workspace:*",
"@deepseek-ai/dsh-system-prompt": "workspace:*",
"@deepseek-ai/dsh-tools": "workspace:*",
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/dsh-subagent-names": "workspace:*"
"@deepseek-ai/cordis": "workspace:~"
},
"peerDependenciesMeta": {
"@deepseek-ai/dsh-agent-preset-registry": {
@@ -71,7 +70,6 @@
"@deepseek-ai/dsh-tool-testkit": "workspace:*",
"@deepseek-ai/dsh-tools": "workspace:*",
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/dsh-session-projection": "workspace:*",
"@deepseek-ai/dsh-subagent-names": "workspace:*"
"@deepseek-ai/dsh-session-projection": "workspace:*"
}
}
+10 -34
View File
@@ -2,7 +2,7 @@
* Model-facing delegation through one configured `ctx.subagents` provider.
* Provider lifecycle controls tool registration and context-sensitive schema
* wording. Every delegation starts a managed activation and returns its child
* address; the subagent runtime owns result delivery and resource cleanup.
* id; the subagent runtime owns result delivery and resource cleanup.
* @module @deepseek-ai/dsh-tool-subagent
*/
@@ -12,7 +12,7 @@ import type {} from '@deepseek-ai/dsh-agent-preset-registry'
import { defineTool } from '@deepseek-ai/dsh-tools'
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
import type { AgentOptions } from '@deepseek-ai/dsh-agent'
import { HarnessError, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import { ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import {
assertSubagentMaxDepth,
@@ -20,7 +20,6 @@ import {
parentAgentOptionsForDelegation,
} from '@deepseek-ai/dsh-subagent'
import type { SubagentProvider } from '@deepseek-ai/dsh-subagent'
import type {} from '@deepseek-ai/dsh-subagent-names'
import {
assertAllowedModelSelection,
hasConfiguredLlmSelection,
@@ -37,7 +36,7 @@ import {
} from './model-selection-state.ts'
export const name = 'tool-subagent'
export const inject = ['tools', 'subagents', 'subagentNames', 'systemPrompt', 'sessionProjections']
export const inject = ['tools', 'subagents', 'systemPrompt', 'sessionProjections']
// Definition identity keeps shared guidance scoped to the actual visible tools,
// including scoped overrides and independently loaded tool registries.
@@ -198,10 +197,9 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
// Load order and HMR replacement can change provider availability while
// this fiber remains active.
let mounted: { definition: ToolDefinition; disposeTool: () => void } | undefined
let mounted: { subagentProvider: SubagentProvider; definition: ToolDefinition; disposeTool: () => void } | undefined
const mount = (subagentProvider: SubagentProvider): void => {
assertSubagentProviderConfiguration(subagentProvider)
const names = ctx.subagentNames
const wording = providerWording(subagentProvider.inheritsParentContext)
const providerRouteDefaults = subagentProvider.agentRouteDefaults
const selectionDescription = providerRouteDefaults !== undefined
@@ -219,10 +217,6 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
? ' Changing the route can prevent provider-side reuse of the inherited conversation prefix.'
: ''),
parameters: {
name: {
type: 'string',
description: 'Optional name unique among your direct children. Use a short role name, such as reviewer or researcher. Put the task in description. Add a specialty or numeric suffix when needed for uniqueness. Defaults to the first unused agent-N. Returns the full path, e.g. lead/reviewer.',
},
cwd: {
type: 'string',
description: 'Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent.',
@@ -266,11 +260,9 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
properties: {
kind: { type: 'string', required: true, const: 'activation' },
subagentId: { type: 'string', required: true },
name: { type: 'string', required: true },
},
},
render: (_args, value) => [{ type: 'text', text: `started subagent ${value.subagentId}`
+ `\nname: ${value.name}` }],
render: (_args, value) => [{ type: 'text', text: `started subagent ${value.subagentId}` }],
presentationMeta: (_args, value) => ({ kind: value.kind, subagentId: value.subagentId }),
},
isConcurrencySafe: () => true,
@@ -330,30 +322,14 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
...maxDepth !== undefined ? { maxDepth } : {},
}
const spec = {
const started = await ctx.subagents.startActivation({
provider: config.provider,
label: args.description,
...request,
signal: exec.signal,
delivery: 'parent' as const,
}
const rootSession = ctx.subagents.rootOf(parent).session
const reservation = names.reserve(rootSession, parent.id, args.name)
try {
const started = await ctx.subagents.startActivation(spec)
try {
names.bind(rootSession, started.childId, reservation.name)
} catch (cause: unknown) {
throw new HarnessError(
`Subagent ${started.childId} started, but its name ${reservation.name} could not be recorded. `
+ 'Its execution is still managed by the subagent service. Check this child before retrying.',
'SUBAGENT_NAME_BIND_FAILED', { cause },
)
}
return { kind: 'activation' as const, subagentId: started.childId, name: reservation.name }
} finally {
reservation.dispose()
}
delivery: 'parent',
})
return { kind: 'activation' as const, subagentId: started.childId }
},
})
const disposeTool = ctx.effect(() => {
@@ -365,7 +341,7 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
}
})
// oxlint-disable-next-line typescript/no-misused-promises -- Tool and guidance disposal are synchronous.
mounted = { definition, disposeTool }
mounted = { subagentProvider, definition, disposeTool }
}
// Register listeners before checking presence so no synchronous change is missed.
@@ -7,7 +7,6 @@ import type { Agent, AgentOptions } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import SubagentNames from '@deepseek-ai/dsh-subagent-names'
import { SessionId } from '@deepseek-ai/dsh-session'
import * as mock from './scripted-provider.ts'
import type { Config as toolConfig } from '../src/index.ts'
@@ -63,7 +62,6 @@ export async function setup(toolConfig: SetupConfig, mockConfig: Partial<mock.Co
await ctx.plugin(AgentLoop, { agents: [] })
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
const provider = await mock.mountScriptedProvider(ctx, { name: 'mock', ...mockConfig })
setupProviders.set(ctx, provider)
const handle = await ctx.agents.create({
@@ -82,7 +80,6 @@ export async function setup(toolConfig: SetupConfig, mockConfig: Partial<mock.Co
await ctx.plugin(AgentLoop, { agents: [] })
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
const provider = await mock.mountScriptedProvider(ctx, { name: 'mock', ...mockConfig })
setupProviders.set(ctx, provider)
await ctx.plugin(tool, config)
@@ -18,7 +18,6 @@ import type {
import ToolRuntime from '@deepseek-ai/dsh-tools'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import SubagentNames from '@deepseek-ai/dsh-subagent-names'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { SessionId } from '@deepseek-ai/dsh-session'
import * as tool from '../src/index.ts'
@@ -81,7 +80,6 @@ async function setupListTool(routes = [
await ctx.plugin(SubagentModelSelectionConfig, { enabled: true, allowedModels: routes })
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
await ctx.plugin(tool, { provider: 'unused', modelDiscovery: true })
const { agent } = await ctx.agents.create({ sessionId: SessionId('discovery-parent') })
callingAgents.set(ctx, agent)
@@ -178,7 +176,6 @@ describe('list_subagent_models', () => {
await ctx.plugin(AgentRegistry)
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
await ctx.plugin(tool, { provider: 'unused', modelDiscovery: true })
expect(text(await call(ctx, {}))).toContain('requires a calling agent')
await ctx.fiber.dispose()
@@ -214,7 +211,6 @@ describe('list_subagent_models', () => {
await ctx.plugin(AgentRegistry)
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
await ctx.plugin(tool, { provider: 'unused', modelDiscovery: true })
callingAgents.set(ctx, callingAgents.get(parent)!)
expect(ctx.tools.get('list_subagent_models')).toBeDefined()
@@ -15,7 +15,6 @@ import { liveConfig } from '../../../settings/settings/tests/live-config.ts'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import SubagentNames from '@deepseek-ai/dsh-subagent-names'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import * as tool from '../src/index.ts'
import SubagentModelSelectionConfig from '../src/model-selection-settings.ts'
@@ -52,7 +51,6 @@ async function boot(withPreset = true): Promise<Context> {
await ctx.plugin(AgentLoop, { agents: [] })
await mountWorkingDirectoryFixture(ctx)
subagentConfigs.set(ctx, await liveConfig(ctx, SubagentRuntime))
await ctx.plugin(SubagentNames)
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
await ctx.plugin(AgentPresets, { default: 'model-selection-test' })
if (withPreset) {
@@ -409,7 +407,6 @@ describe('SubagentModelSelectionConfig', () => {
await mountAgentLoopTestDependencies(ctx)
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
await ctx.plugin(tool, { provider: 'spawn', modelSelection: true })
await ctx.plugin(tool, { provider: 'spawn', toolName: 'another_spawn', modelSelection: true })
@@ -8,7 +8,6 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import SubagentNames from '@deepseek-ai/dsh-subagent-names'
import type { SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
@@ -120,7 +119,6 @@ describe('dsh-tool-subagent model selection', () => {
'cwd',
'description',
'model',
'name',
'prompt',
'provider',
'reasoning_effort',
@@ -150,7 +148,7 @@ describe('dsh-tool-subagent model selection', () => {
const ctx = await setup({ provider: 'mock', parentAgentOptions: { provider: 'alpha', model: 'parent-model' } })
const schema = ctx.tools.schemas().find(entry => entry.name === 'subagent')!
const props = (schema.parameters as { properties?: Record<string, unknown> }).properties ?? {}
expect(Object.keys(props).sort()).toEqual(['cwd', 'description', 'name', 'prompt'])
expect(Object.keys(props).sort()).toEqual(['cwd', 'description', 'prompt'])
expect(schema.description).not.toContain('list_subagent_models')
expect(ctx.tools.get('list_subagent_models')).toBeUndefined()
@@ -211,7 +209,7 @@ describe('dsh-tool-subagent model selection', () => {
const agent = modelSelectionSetupAgent(ctx)
const schema = ctx.tools.schemas(agent).find(entry => entry.name === 'subagent')!
const props = (schema.parameters as { properties?: Record<string, unknown> }).properties ?? {}
expect(Object.keys(props).sort()).toEqual(['cwd', 'description', 'model', 'name', 'prompt', 'provider', 'reasoning_effort'])
expect(Object.keys(props).sort()).toEqual(['cwd', 'description', 'model', 'prompt', 'provider', 'reasoning_effort'])
expect(schema.description).not.toContain('list_subagent_models')
expect(schema.description).not.toContain('Changing the route can prevent provider-side reuse')
expect(ctx.tools.get('list_subagent_models', agent)).toBeDefined()
@@ -510,7 +508,6 @@ describe('dsh-tool-subagent model selection', () => {
await ctx.plugin(NativeToolExecutor)
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
await mock.mountScriptedProvider(ctx, { name: 'mock' })
await ctx.plugin(tool, {
provider: 'mock',
@@ -587,7 +584,7 @@ describe('dsh-tool-subagent model selection', () => {
})
const schema = ctx.tools.schemas(agent).find(entry => entry.name === 'subagent')!
const props = (schema.parameters as { properties?: Record<string, unknown> }).properties ?? {}
expect(Object.keys(props).sort()).toEqual(['cwd', 'description', 'name', 'prompt'])
expect(Object.keys(props).sort()).toEqual(['cwd', 'description', 'prompt'])
expect(schema.description).toContain('inherits this conversation')
expect(schema.description).not.toContain('list_subagent_models')
expect(ctx.tools.get('list_subagent_models', agent)).toBeUndefined()
@@ -11,7 +11,6 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import AgentPresets from '@deepseek-ai/dsh-agent-preset-registry'
import SubagentRuntime, { applyChildComposition } from '@deepseek-ai/dsh-subagent'
import SubagentNames from '@deepseek-ai/dsh-subagent-names'
import { assembleContextFor } from '@deepseek-ai/dsh-agent'
import { SessionId } from '@deepseek-ai/dsh-session'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
@@ -54,7 +53,6 @@ async function setup(config: Omit<tool.Config, 'provider'>, providerConfig: Part
}
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
const onStart = vi.fn()
const provider = await mountScriptedProvider(ctx, {
name: 'custom-spawn', capabilities: { agentPreset: true }, onStart, ...providerConfig,
@@ -15,8 +15,7 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import SubagentNames from '@deepseek-ai/dsh-subagent-names'
import type { SubagentActivation, SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import type { SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import { loadStoredSession } from '../../subagent/tests/persistence-helpers.ts'
@@ -65,7 +64,7 @@ describe('dsh-tool-subagent', () => {
)).rejects.toThrow('does not support child agentOptions')
})
it('starts a managed subagent and returns its ID and name without collecting its output', async () => {
it('starts a managed subagent and returns its id without collecting its output', async () => {
const ctx = await setup({ provider: 'mock' }, { reply: 'child says hi' })
const result = await callSubagent(ctx, {
description: 'do a thing',
@@ -74,101 +73,11 @@ describe('dsh-tool-subagent', () => {
expect(result.isError).toBe(false)
if (result.isError) throw new Error('expected subagent success')
expect(result.value).toHaveProperty('kind', 'activation')
expect(result.value).toHaveProperty('name', 'lead/agent-1')
expect(result.value).toHaveProperty('subagentId', expect.any(String))
expect(result.meta).toEqual({ kind: 'activation', subagentId: result.value.subagentId })
expect(text(result)).toMatch(/^started subagent /)
})
it('rejects invalid names before starting a child', async () => {
const ctx = await setup({ provider: 'mock' })
const start = vi.spyOn(ctx.subagents, 'startActivation')
const result = await callSubagent(ctx, { name: 'bad/name', description: 'invalid name', prompt: 'work' })
expect(result.isError).toBe(true)
expect(start).not.toHaveBeenCalled()
})
it('reports a binding failure with the accepted child ID and leaves its work running', async () => {
const release = Promise.withResolvers<undefined>()
const ctx = await setup({ provider: 'mock' }, { onStart: () => release.promise })
const parent = modelSelectionSetupAgent(ctx)
const accepted = Promise.withResolvers<SubagentActivation>()
const start = ctx.subagents.startActivation.bind(ctx.subagents)
vi.spyOn(ctx.subagents, 'startActivation').mockImplementation(async (spec) => {
const child = await start(spec)
accepted.resolve(child)
return child
})
vi.spyOn(ctx.subagentNames, 'bind').mockImplementationOnce(() => { throw new Error('name storage unavailable') })
try {
const result = await callSubagent(ctx, { name: 'reviewer', description: 'review code', prompt: 'work' })
const child = await accepted.promise
expect(result.error?.info?.code).toBe('SUBAGENT_NAME_BIND_FAILED')
expect(text(result)).toContain(child.childId)
const retry = ctx.subagentNames.reserve(parent.session, parent.id, 'reviewer')
retry.dispose()
release.resolve(undefined)
await expect(child.result).resolves.toMatchObject({ stopReason: 'completed' })
} finally {
release.resolve(undefined)
}
})
it.each(['root disposal', 'plugin restart'] as const)('does not reinstall delegation while %s drains', async (mode) => {
const ctx = await projectedContext()
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await mock.mountScriptedProvider(ctx, { name: 'mock' })
const names = await ctx.plugin(SubagentNames)
const delegation = await ctx.plugin(tool, { provider: 'mock' })
expect(ctx.tools.get('subagent')).toBeDefined()
const entered = Promise.withResolvers<undefined>()
const release = Promise.withResolvers<undefined>()
let cleanupDone = false
delegation.ctx.effect(() => async () => {
entered.resolve(undefined)
await release.promise
cleanupDone = true
})
const register = vi.spyOn(ctx.tools, 'register')
let settled = false
const stop = mode === 'root disposal' ? ctx.fiber.dispose() : delegation.restart()
const removeNames = mode === 'root disposal' ? Promise.resolve() : names.dispose()
const drained = Promise.allSettled([stop, removeNames]).then((results) => { settled = true; return results })
try {
await entered.promise
expect(settled).toBe(false)
expect(cleanupDone).toBe(false)
expect(register).not.toHaveBeenCalled()
} finally {
release.resolve(undefined)
}
expect((await drained).every(result => result.status === 'fulfilled')).toBe(true)
expect(cleanupDone).toBe(true)
})
it('waits for names and restores delegation when the dependency returns', async () => {
const ctx = await projectedContext()
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await mock.mountScriptedProvider(ctx, { name: 'mock' })
const delegation = await ctx.plugin(tool, { provider: 'mock' })
expect(ctx.tools.get('subagent')).toBeUndefined()
const names = await ctx.plugin(SubagentNames)
await delegation.await()
const schema = ctx.tools.schemas().find(entry => entry.name === 'subagent')
expect(schema?.parameters.properties).toHaveProperty('name')
const result = await callSubagent(ctx, { name: 'reviewer', description: 'named task', prompt: 'review' })
expect(result.isError).toBe(false)
if (result.isError) throw new Error('expected named subagent success')
expect(result.value).toMatchObject({ kind: 'activation', name: 'lead/reviewer' })
expect(result.value).toHaveProperty('subagentId', expect.any(String))
await names.dispose()
expect(ctx.tools.get('subagent')).toBeUndefined()
await ctx.plugin(SubagentNames)
await delegation.await()
expect(ctx.tools.schemas().find(entry => entry.name === 'subagent')).toEqual(schema)
})
it('resolves an explicit child directory without changing the parent directory', async () => {
let childCwd: string | undefined
const ctx = await setup({ provider: 'mock' }, {
@@ -197,7 +106,6 @@ describe('dsh-tool-subagent', () => {
expect(Object.keys(props).sort()).toEqual([
'cwd',
'description',
'name',
'prompt',
])
expect(schema!.description).not.toContain('job_output')
@@ -248,7 +156,6 @@ describe('dsh-tool-subagent', () => {
const ctx = await projectedContext()
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
await mock.mountScriptedProvider(ctx, { name: 'spawn', reply: 'from spawn' })
await mock.mountScriptedProvider(ctx, { name: 'acp', reply: 'from acp' })
await ctx.plugin(tool, { provider: 'spawn', toolName: 'subagent' })
@@ -408,7 +315,6 @@ describe('dsh-tool-subagent', () => {
const ctx = await projectedContext()
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
ctx.subagents.registerProvider({
name: 'bare',
capabilities: { agentOptions: false, agentPreset: false, outputSchema: false, depthLimit: false },
@@ -442,7 +348,6 @@ describe('dsh-tool-subagent', () => {
const ctx = await projectedContext()
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
// Tool first: no provider yet — the tool must be absent, not broken.
// Direct apply (schema bypass): also covers the waiting-note's default
// toolName fallback, which validated config pre-fills.
@@ -459,7 +364,6 @@ describe('dsh-tool-subagent', () => {
const ctx = await projectedContext()
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
await tool.apply(ctx, {
provider: 'later-continuable',
maxDepth: 'provider-managed',
@@ -474,7 +378,6 @@ describe('dsh-tool-subagent', () => {
const ctx = await projectedContext()
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
const forkTool = await ctx.plugin(tool, { provider: 'fork', toolName: 'subagent_fork' })
const spawnTool = await ctx.plugin(tool, { provider: 'spawn' })
expect(await delegationGuidance(ctx)).toEqual([])
@@ -532,7 +435,6 @@ describe('dsh-tool-subagent', () => {
const ctx = await projectedContext()
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
const backend = await mock.mountScriptedProvider(ctx, { name: 'mock' }) // fresh conversation (descriptor: false)
await ctx.plugin(tool, { provider: 'mock' })
expect(ctx.tools.schemas().find(s => s.name === 'subagent')!.description).toContain('works in its own context')
@@ -551,7 +453,6 @@ describe('dsh-tool-subagent', () => {
const ctx = await projectedContext()
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
// Arm 1: a mounted tool and its prompt section die with the plugin fiber;
// the provider survives.
@@ -586,7 +487,6 @@ describe('dsh-tool-subagent', () => {
const ctx = await projectedContext()
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
await mock.mountScriptedProvider(ctx, { name: 'mock' })
await ctx.plugin(tool, { provider: 'mock' })
// An unrelated provider registering (added-event with another name) and
@@ -624,7 +524,6 @@ describe('dsh-tool-subagent', () => {
const ctx = await projectedContext()
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
ctx.subagents.registerProvider({
name: 'spy',
capabilities: { agentOptions: false, agentPreset: false, outputSchema: false, depthLimit: false },
@@ -670,13 +569,13 @@ describe('dsh-tool-subagent', () => {
// load with "cannot get property … without inject". Guard the shape directly.
expect('default' in tool).toBe(false)
expect(tool.name).toBe('tool-subagent')
expect(tool.inject).toEqual(['tools', 'subagents', 'subagentNames', 'systemPrompt', 'sessionProjections'])
expect(tool.inject).toEqual(['tools', 'subagents', 'systemPrompt', 'sessionProjections'])
const loader = Object.create(Loader.prototype) as Loader
const unwrapped = loader.unwrapExports(tool) as Record<string, unknown>
expect(unwrapped).toBe(tool)
expect(unwrapped.name).toBe('tool-subagent')
expect(unwrapped.inject).toEqual(['tools', 'subagents', 'subagentNames', 'systemPrompt', 'sessionProjections'])
expect(unwrapped.inject).toEqual(['tools', 'subagents', 'systemPrompt', 'sessionProjections'])
expect(typeof unwrapped.apply).toBe('function')
expect(unwrapped.Config).toBeDefined()
})
@@ -686,7 +585,6 @@ describe('dsh-tool-subagent', () => {
const ctx = await projectedContext()
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
ctx.subagents.registerProvider({
name: 'capture2',
capabilities: { agentOptions: false, agentPreset: false, outputSchema: false, depthLimit: true },
@@ -749,7 +647,6 @@ describe('dsh-tool-subagent', () => {
const ctx = await projectedContext()
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
ctx.subagents.registerProvider({
name: 'capture4',
capabilities: { agentOptions: false, agentPreset: false, outputSchema: false, depthLimit: false },
@@ -790,7 +687,6 @@ describe('dsh-tool-subagent local activation', () => {
await ctx.plugin(AgentLoop, { agents: [] })
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
await ctx.plugin(tool, { provider: 'spawn' })
ctx.llm.registerAdapter(['mock'], new MockAdapter([
@@ -810,7 +706,7 @@ describe('dsh-tool-subagent local activation', () => {
})).toEqual({ kind: 'parallel' })
})
it('starts a local activation and keeps its Session id alongside its full name', async () => {
it('starts a local activation and returns only its durable id', async () => {
const { ctx, parent } = await continuableSetup()
const schema = ctx.tools.schemas().find(s => s.name === 'subagent')!
// Continuable delegation has no Task, so the schema promises no collection.
@@ -835,18 +731,16 @@ describe('dsh-tool-subagent local activation', () => {
{ agent: parent },
)
expect(started.isError, text(started)).toBe(false)
const match = /^started subagent (\S+)\nname: lead\/agent-1$/.exec(text(started))
const match = /^started subagent (\S+)$/.exec(text(started))
expect(match).not.toBeNull()
const catalog = ctx.sessionProjections.snapshot(parent.session, ['subagentCatalog']).values.subagentCatalog!
expect(catalog).toHaveLength(1)
const childId = catalog[0]!.id
expect(match?.[1]).toBe(childId)
const [, childId] = match!
// No Task was created for the continuable child.
await vi.waitFor(() => {
expect(ctx.agents.get(childId)).toBeUndefined()
expect(ctx.agents.get(SessionId(childId!))).toBeUndefined()
}, { timeout: 5_000 })
// The child id names a durable session carrying its continuation descriptor.
const loaded = await loadStoredSession(ctx.sessionPersistence, childId)
const loaded = await loadStoredSession(ctx.sessionPersistence, SessionId(childId!))
expect(loaded.events.some(event => event.type === 'subagent/descriptor')).toBe(true)
expect(loaded.events.some(event => event.type === 'assistant/message')).toBe(true)
})
@@ -913,7 +807,6 @@ describe('dsh-tool-subagent local activation', () => {
expect(succeeded.isError ? undefined : succeeded.value).toEqual({
kind: 'activation',
subagentId: survivingChildId,
name: 'lead/agent-2',
})
await vi.waitFor(() => {
expect(ctx.agents.get(survivingChildId!)).toBeUndefined()
@@ -932,7 +825,6 @@ describe('depth budget configuration', () => {
const ctx = await projectedContext()
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
ctx.subagents.registerProvider({
name: 'capture',
capabilities: { agentOptions: false, agentPreset: false, outputSchema: true, depthLimit: true },
@@ -961,7 +853,6 @@ describe('depth budget configuration', () => {
const ctx = await projectedContext()
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
ctx.subagents.registerProvider({
name: 'no-depth',
capabilities: { agentOptions: false, agentPreset: false, outputSchema: false, depthLimit: false },
@@ -977,7 +868,6 @@ describe('depth budget configuration', () => {
const ctx = await projectedContext()
await mountWorkingDirectoryFixture(ctx)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentNames)
ctx.subagents.registerProvider({
name: 'external',
capabilities: { agentOptions: false, agentPreset: false, outputSchema: false, depthLimit: false },
@@ -40,9 +40,6 @@
},
{
"path": "../subagent"
},
{
"path": "../subagent-names"
}
]
}
+2 -2
View File
@@ -78,8 +78,8 @@
en: 8b29cb821f80db40
zh: 43e402736a553ce5
/deepseek-ai-dsh-agent-team/model-experience/task-views-and-errors/what-the-model-sees:
en: 0df504fe2d4ec8b0
zh: 2f76991e050b6a99
en: 6ec90541f0b387a9
zh: 3bd7e44385cef22b
/deepseek-ai-dsh-agent-team/model-experience/task-views-and-errors/token-effect:
en: 9e87d5f5aec04775
zh: da4230e3cad15de6
+1 -1
View File
@@ -172,7 +172,7 @@ Read these pages when the package-level contract is not enough. They move from t
#### What the model sees
The model reads task views through task-tool results. Model-facing views identify owners by `ownerName`; host views retain `ownerId`; stale updates report the current revision. Task updates do not end `wait_agent`; send an explicit message when a member needs to act on an update.
The model reads task views through task-tool results. Model-facing and host views identify owners by `ownerId`; stale updates report the current revision. Task updates do not end `wait_agent`; send an explicit message when a member needs to act on an update.
#### Token effect
+1 -1
View File
@@ -172,7 +172,7 @@ Team 在激活前要求 `sessionPersistence` 可用。`Service.init` 等待 `age
#### 模型看到什么
模型通过任务工具结果读取任务视图。面向模型的视图以 `ownerName` 标识所有者,host 视图保留 `ownerId`;过期更新报告当前修订。任务更新不会结束 `wait_agent`;需要成员处理更新时,发送明确消息。
模型通过任务工具结果读取任务视图。面向模型和 host 的视图均以 `ownerId` 标识所有者;过期更新报告当前修订。任务更新不会结束 `wait_agent`;需要成员处理更新时,发送明确消息。
#### Token 影响
+23 -20
View File
@@ -15,38 +15,38 @@
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
/deepseek-ai-dsh-tool-agent-team/use-this-package:
en: 42584bc98cb1d0bf
zh: af5a4d2e7aa2f876
en: a91291c96142c3d4
zh: db8e14f2359659c0
/deepseek-ai-dsh-tool-agent-team/use-this-package/when-to-choose-it:
en: c27d5457e1183675
zh: 3c36d513e6400446
/deepseek-ai-dsh-tool-agent-team/use-this-package/smallest-working-example:
en: b6194a6256aedb12
zh: dd2cf9ba58a2e389
en: 4df140a03006896c
zh: 705f10a798e58898
/deepseek-ai-dsh-tool-agent-team/use-this-package/what-the-model-can-do:
en: d8294e282ee26b0d
zh: f6579be994043294
en: 4aee48f5debcc7d1
zh: 4eb148aa5e42398b
/deepseek-ai-dsh-tool-agent-team/use-this-package/what-success-and-failure-look-like:
en: f83d223649e1470d
zh: 071db9c5986383ed
en: bf1eb0e693610f5b
zh: 2b29e5cf63623909
/deepseek-ai-dsh-tool-agent-team/understand-the-implementation:
en: 3952eb1d9de1f988
zh: 789d51b88f41b710
/deepseek-ai-dsh-tool-agent-team/understand-the-implementation/design-philosophy:
en: 236b044c249412f6
zh: b4886ad8eff97b8b
en: c66b02cbc0636122
zh: a000a4c8c30427f1
/deepseek-ai-dsh-tool-agent-team/understand-the-implementation/source-map:
en: 29ff96b23b25b13e
zh: 7d2cdf9be86d1ed4
en: 204bfccd132d8624
zh: 45dcc49bbdc60867
/deepseek-ai-dsh-tool-agent-team/understand-the-implementation/policy-and-tools:
en: 96cc025675e18be2
zh: c0cabbecec9090fa
en: 158ae900e0b84d65
zh: b4678cba3fb85dda
/deepseek-ai-dsh-tool-agent-team/understand-the-implementation/scoped-registration-and-teardown:
en: b2a5584bdf075644
zh: c4e9eb5ac0809833
/deepseek-ai-dsh-tool-agent-team/further-exploration:
en: 855e3e7a0f9162e3
zh: 04adc19e419e3655
en: bcd5d6faf995a0fb
zh: edf20792aa9eadcb
/deepseek-ai-dsh-tool-agent-team/model-experience:
en: 215e7ba838619b7b
zh: a311da3843709f90
@@ -69,10 +69,13 @@
en: ed8642015d306d6d
zh: 31496981d115ba40
/deepseek-ai-dsh-tool-agent-team/model-experience/team-identity-runtime-context/what-the-model-sees:
en: 38287bddc98fad61
zh: 97641df400e74315
/deepseek-ai-dsh-tool-agent-team/model-experience/team-identity-runtime-context/what-the-model-sees/agent-identity:
en: d3393ec1daa12ada
en: 593189f8ddbcb548
zh: 416a3865a3c96eb0
/deepseek-ai-dsh-tool-agent-team/model-experience/team-identity-runtime-context/what-the-model-sees/root-identity:
en: 58d2b04f507baa5f
zh: 773786c59605a154
/deepseek-ai-dsh-tool-agent-team/model-experience/team-identity-runtime-context/what-the-model-sees/teammate-identity:
en: f0d12764966d3005
zh: fba882f6aa274017
/deepseek-ai-dsh-tool-agent-team/model-experience/team-identity-runtime-context/token-effect:
en: b9c8f7524fbe3204
+18 -18
View File
@@ -27,7 +27,7 @@ This package lets every agent in a subagent tree create, claim, and complete sha
Team tools are available to each model step only while the root Session enables Team mode. Prompt providers read the same selection when evaluated. Ordinary subagent controls remain available.
Mount this package with `@deepseek-ai/dsh-agent-team` and `@deepseek-ai/dsh-subagent-names` when models need the shared board. The root and descendants receive identical task schemas and task policy.
Mount this package with `@deepseek-ai/dsh-agent-team` when models need the shared board. The root and descendants receive identical task schemas and task policy.
### When to choose it
@@ -35,17 +35,16 @@ Choose it whenever the Team domain is mounted and models should use the board; w
### Smallest working example
The plugin takes no configuration. With the [Team domain](../agent-team/README.md#smallest-working-setup) and shared subagent services available, mount the naming service once alongside the tools:
The plugin takes no configuration; mount it beside the Team domain as in the [agent-team setup](../agent-team/README.md#smallest-working-setup):
```yaml
- name: '@deepseek-ai/dsh-subagent-names'
- id: tool-agent-team
name: '@deepseek-ai/dsh-tool-agent-team'
```
A row that sets `interrupt`, `freshProvider`, or `forkProvider` does not activate, and dsh names the key in its startup warning. The [upgrade guide](../../../docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.md) maps the removed tools and keys to their replacements, and the generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-tool-agent-team) records the empty configuration.
Ask the Lead to start a subagent to review the diff, add a review task, and wait for its result. The `description` explains the subagent’s work; the returned name or Session id addresses it.
Ask the Lead to start a subagent to review the diff, add a review task, and wait for its result. The `description` explains the subagent’s work; the returned id addresses it.
### What the model can do
@@ -55,11 +54,11 @@ The task tools complement the ordinary subagent controls:
- **Wait for inbox input** — `wait_agent` belongs to `@deepseek-ai/dsh-tool-subagent-control` and remains available without this package.
- **Create and reach teammates** — `subagent`, `subagent_fork`, `send_message`, `list_agents`, and `interrupt_agent` come from the subagent tool packages and are unchanged by this package.
Task owners use bound names or Session ids in `owner` arguments and `ownerName` results. The root may reassign to itself or any reachable continuable descendant; historical roster records grant no authority. Other agents claim ready tasks themselves. Updates enforce ownership and revision checks.
Task `owner` arguments and `ownerId` results use Session ids. The root may reassign to itself or any reachable continuable descendant; historical roster records grant no authority. Other agents claim ready tasks themselves. Updates enforce ownership and revision checks.
### What success and failure look like
Task tools return compact JSON task views or pages. Stale revisions, unauthorized actions, and unready tasks return errors. Updates resolve input names and capture result-owner names before mutation. An unbound owner displays its original Session id; display names require no Session query or child catalog traversal. Name resolution grants no authority: the Team service still checks every reassignment target.
Task tools return compact JSON task views or pages. Stale revisions, unauthorized actions, and unready tasks return errors.
-----
@@ -75,24 +74,22 @@ This section explains the design decisions behind the adapter and points at the
The adapter is built on four commitments:
- **Agent-scoped registration.** Every live Agent receives its task tools and policy on its own `ctx`; the service resolves tree authority when called.
- **Session-selected tools.** Shared tool registrations select availability from each caller’s root Session; the service resolves tree authority when called.
- **One tool family.** Team registers no messaging, listing, or interruption tool; the standard subagent tools serve Leads and teammates alike.
- **Declared results, compact JSON.** Every tool declares its complete result schema and renders that value as compact JSON, so the compiler checks `execute` against what the model is promised and no result spends tokens on indentation.
- **The domain owns authority.** Tools delegate to `ctx.agentTeams`, which enforces Lead authority and revision checks; the adapter adds no weaker path.
The adapter reads the root Session’s name-to-id mapping synchronously through `ctx.subagentNames`; `ctx.agentTeams` retains Session ids for task ownership and authority. Task listing applies status, readiness, and unowned filters before resolving names. Without an exact owner-name filter, only the requested page needs name resolution; an exact owner-name filter resolves the remaining candidates before pagination.
The [subagent teammates Agent Note](../../../.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md) owns the model-facing and scoping decisions.
### Source map
| File | Role |
|---|---|
| [`src/index.ts`](src/index.ts) | Plugin entry, removed-key rejection, Team policy and identity, and four scoped task tools |
| [`src/index.ts`](src/index.ts) | Plugin entry, removed-key rejection, Team policy and identity, and four conditional task tools |
### Policy and tools
One plugin registration provides a dynamic `team:policy` section, required `team:identity` context, and four conditional task tools. Each provider reads the caller’s root Session selection; prompt visibility is independent of tool restrictions. Tree authority comes from `ctx.subagents.rootOf(agent)`; `ctx.subagentNames` supplies bound display names or original Session ids. An unavailable root disables Team tools and guidance. Communication and waiting belong to [subagent controls](../../subagent/tool-subagent-control/README.md); the subagent runtime delivers final results.
One plugin registration provides a dynamic `team:policy` section, required `team:identity` context, and four conditional task tools. Each provider reads the caller’s root Session selection; prompt visibility is independent of tool restrictions. Tree authority and root identity come from `ctx.subagents.rootOf(agent)`. An unavailable root disables Team tools and guidance. Communication and waiting belong to [subagent controls](../../subagent/tool-subagent-control/README.md); the subagent runtime delivers final results.
### Scoped registration and teardown
@@ -108,7 +105,6 @@ Tool `when` conditions resolve at step preparation, including the first step of
Read these pages when the package-level contract is not enough. They move from the domain service to the exact schemas and the decisions behind the design.
- [agent-team package](../agent-team/README.md) — the `ctx.agentTeams` domain service behind these tools.
- [Subagent names](../../subagent/subagent-names/README.md) — name allocation and full-path lookup.
- [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, interruption, and inbox waiting.
- [Generated tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-tool-agent-team) — every tool schema the model receives.
@@ -128,7 +124,7 @@ Every Team member's system prompt carries one `team:policy` section; the Lead an
##### Team policy
```markdown
A shared task board is available to every agent in your subagent tree. Task owners use agent names from list_agents, or Session ids when no name is bound. The root can reassign tasks to itself or any continuable descendant; other agents claim ready tasks themselves.
A shared task board is available to every agent in your subagent tree. Task owners are agent ids from list_agents. The root can reassign tasks to itself or any continuable descendant; other agents claim ready tasks themselves.
Read tasks with team_task_list or team_task_get, claim with the current revision, perform the work, then complete the task. Dependencies determine readiness; write scopes describe expected edits and overlapping scopes are advisory, not locks. Task readiness does not start an agent.
@@ -147,14 +143,18 @@ The text is identical for the Lead and every descendant and does not change whil
#### What the model sees
While Team is enabled and its root is available, required `team:identity` context uses the following text for the root and its teammates. Otherwise it contributes no text. Optional-context suppression cannot remove an enabled identity; the ordinary runtime-context mechanism restores it after compaction. The identity uses the current agent’s full name (`lead` for the root), or its Session id until a name is bound.
While Team is enabled and its root is available, required `team:identity` context uses the following text, replacing `<agent-id>` or `<root-id>` with the JSON-encoded Session id derived from the subagent tree. Otherwise it contributes no text. Optional-context suppression cannot remove an enabled identity; the ordinary runtime-context mechanism restores it after compaction.
##### Agent identity
##### Root identity
```markdown
Your name is "<full agent path>". Your team lead is "lead".
Use list_agents({}) to find your teammates and their full names.
Use send_message({ name: "<full agent path>", message: "..." }) to message a teammate.
You are the Team Lead. Your agent id is <agent-id>.
```
##### Teammate identity
```markdown
You are a teammate in the Team led by agent <root-id>.
```
#### Token effect
+17 -17
View File
@@ -27,7 +27,7 @@ kind: "package-reference"
仅当根 Session 开启 Team 模式时,各模型步骤才可使用 Team 工具。提示词提供方在求值时读取同一选择。普通 subagent 控制工具始终可用。
模型需要共享任务板时,将此包与 `@deepseek-ai/dsh-agent-team` 和 `@deepseek-ai/dsh-subagent-names` 一起挂载。根和后代接收相同的任务 schema 和任务策略。
模型需要共享任务板时,将此包与 `@deepseek-ai/dsh-agent-team` 一起挂载。根和后代接收相同的任务 schema 和任务策略。
### 何时选择
@@ -35,17 +35,16 @@ kind: "package-reference"
### 最小工作示例
本插件不接受配置。在 [Team 领域包](../agent-team/README.zh.md#smallest-working-setup)和共享 subagent 服务可用后,在工具旁挂载一次命名服务:
本插件不接受配置;按照 [agent-team 配置](../agent-team/README.zh.md#smallest-working-setup),把它挂载在 Team 领域包旁边:
```yaml
- name: '@deepseek-ai/dsh-subagent-names'
- id: tool-agent-team
name: '@deepseek-ai/dsh-tool-agent-team'
```
设置 `interrupt`、`freshProvider` 或 `forkProvider` 的配置行不会激活,dsh 会在启动警告中指出该键。[升级指南](../../../docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.zh.md)把已移除的工具与键对应到替代方式,生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-tool-agent-team)记录空配置。
让 Lead 启动子代理审查差异、创建审查任务并等待结果。`description` 说明子代理的工作;返回的名称或 Session id 用于寻址。
让 Lead 启动子代理审查差异、创建审查任务并等待结果。`description` 说明子代理的工作;返回的 id 用于寻址。
### 模型能做什么
@@ -55,11 +54,11 @@ kind: "package-reference"
- **等待收件箱输入** — `wait_agent` 属于 `@deepseek-ai/dsh-tool-subagent-control`,无需此包即可使用。
- **创建和联系队友**——`subagent`、`subagent_fork`、`send_message`、`list_agents` 和 `interrupt_agent` 来自子代理工具包,本包不修改它们。
任务负责人在 `owner` 参数和 `ownerName` 结果中使用已绑定名称或 Session id。根可将任务重新分配给自身或任意可达的可继续执行后代;历史名册记录不授予权限。其他 agent 自行认领就绪任务。更新会检查归属和修订。
任务 `owner` 参数和 `ownerId` 结果使用 Session id。根可将任务重新分配给自身或任意可达的可继续执行后代;历史名册记录不授予权限。其他 agent 自行认领就绪任务。更新会检查归属和修订。
### 成功与失败的表现
任务工具返回紧凑的 JSON 任务视图或分页。过期修订、未授权操作和未就绪任务返回错误。更新在修改前解析输入名称并保存结果中的负责人名称。未绑定名称的负责人显示原始 Session id;显示名称无需查询 Session 或遍历子级目录。名称解析不授予权限:Team 服务仍检查每个重新分配的目标。
任务工具返回紧凑的 JSON 任务视图或分页。过期修订、未授权操作和未就绪任务返回错误。
-----
@@ -75,24 +74,22 @@ kind: "package-reference"
适配器建立在四项承诺之上:
- **按 Agent 注册。** 每个在线 Agent 在自身 `ctx` 上接收任务工具和策略;服务在调用时解析树权限。
- **按 Session 选择工具。** 共享工具注册项根据调用方根 Session 选择可用性;服务在调用时解析树权限。
- **一个工具族。** Team 不注册消息、列表或中断工具;标准 subagent 工具同样服务 Lead 与 teammate。
- **声明式结果,紧凑 JSON。** 每个工具都声明完整结果 schema,并把该值渲染为紧凑 JSON,因此编译器会对照向模型承诺的结果检查 `execute`,任何结果都不会在缩进上消耗 token。
- **领域掌握裁决权。** 工具委托给 `ctx.agentTeams`,后者强制执行 Lead 权限与 revision 校验;适配器不添加更弱的路径。
适配器通过 `ctx.subagentNames` 同步读取根 Session 中的名称与 id 映射;`ctx.agentTeams` 继续使用 Session id 处理任务归属和权限。任务列表先按状态、就绪情况和未分配条件筛选,再解析名称。没有精确负责人名称筛选时,只解析请求页中的名称;按精确负责人名称筛选时,在分页前解析剩余候选任务的负责人名称。
[subagent teammate Agent Note](../../../.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.zh.md)负责模型侧与 scoping 决策。
### 源码地图
| 文件 | 职责 |
|---|---|
| [`src/index.ts`](src/index.ts) | 插件入口、已移除键拒绝、Team 策略与身份,以及四个作用域内任务工具 |
| [`src/index.ts`](src/index.ts) | 插件入口、已移除键拒绝、Team 策略与身份,以及四个条件任务工具 |
### 策略与工具
插件注册一次动态 `team:policy` 段落、必需的 `team:identity` 上下文和四个条件任务工具。各提供方读取调用方根 Session 的选择;提示词可见性独立于工具限制。树权限从 `ctx.subagents.rootOf(agent)` 派生;`ctx.subagentNames` 提供已绑定的显示名称或原始 Session id。根不可用时禁用 Team 工具和提示词。通信和等待属于 [subagent 控制工具](../../subagent/tool-subagent-control/README.zh.md);subagent 运行时负责投递最终结果。
插件注册一次动态 `team:policy` 段落、必需的 `team:identity` 上下文和四个条件任务工具。各提供方读取调用方根 Session 的选择;提示词可见性独立于工具限制。树权限和根身份从 `ctx.subagents.rootOf(agent)` 派生。根不可用时禁用 Team 工具和提示词。通信和等待属于 [subagent 控制工具](../../subagent/tool-subagent-control/README.zh.md);subagent 运行时负责投递最终结果。
### 按作用域注册与拆除
@@ -108,7 +105,6 @@ kind: "package-reference"
当包级约定不够用时阅读以下页面。它们从领域服务逐步进入精确 schema 与设计背后的决策。
- [agent-team 包](../agent-team/README.zh.md)——这些工具背后的 `ctx.agentTeams` 领域服务。
- [Subagent 命名](../../subagent/subagent-names/README.zh.md)——名字分配与完整路径查询。
- [Agent Teams 子系统](../../../docs/subsystems/agent-team.zh.md)——持久 Team 类型与服务 API。
- [subagent 控制工具](../../subagent/tool-subagent-control/README.zh.md) — 通信、列表、中断和收件箱等待。
- [生成的工具目录](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-agent-team)——模型接收的每个工具 schema。
@@ -128,7 +124,7 @@ kind: "package-reference"
##### Team 策略
```markdown
A shared task board is available to every agent in your subagent tree. Task owners use agent names from list_agents, or Session ids when no name is bound. The root can reassign tasks to itself or any continuable descendant; other agents claim ready tasks themselves.
A shared task board is available to every agent in your subagent tree. Task owners are agent ids from list_agents. The root can reassign tasks to itself or any continuable descendant; other agents claim ready tasks themselves.
Read tasks with team_task_list or team_task_get, claim with the current revision, perform the work, then complete the task. Dependencies determine readiness; write scopes describe expected edits and overlapping scopes are advisory, not locks. Task readiness does not start an agent.
@@ -147,14 +143,18 @@ Task changes do not end wait_agent calls, add inbox messages, or start idle agen
#### 模型看到什么
Team 已开启且根可用时,必需的 `team:identity` 上下文对根和队友使用以下文本,否则不贡献文本。可选上下文抑制不能移除已启用的身份;普通运行时上下文机制会在压缩后恢复它。身份使用当前 agent 的完整名称(根为 `lead`);名称绑定前使用其 Session id。
Team 已开启且根可用时,必需的 `team:identity` 上下文使用以下文本,将 `<agent-id>` 或 `<root-id>` 替换为从 subagent 树派生的 JSON 编码 Session id;否则不贡献文本。可选上下文抑制不能移除已启用的身份;普通运行时上下文机制会在压缩后恢复它。
##### 根身份
```markdown
You are the Team Lead. Your agent id is <agent-id>.
```
##### Agent 身份
```markdown
Your name is "<full agent path>". Your team lead is "lead".
Use list_agents({}) to find your teammates and their full names.
Use send_message({ name: "<full agent path>", message: "..." }) to message a teammate.
You are a teammate in the Team led by agent <root-id>.
```
#### Token 影响
+2 -4
View File
@@ -38,8 +38,7 @@
"@deepseek-ai/dsh-system-prompt": "workspace:*",
"@deepseek-ai/dsh-agent-team": "workspace:*",
"@deepseek-ai/dsh-tools": "workspace:*",
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/dsh-subagent-names": "workspace:*"
"@deepseek-ai/cordis": "workspace:~"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:*",
@@ -60,7 +59,6 @@
"@deepseek-ai/dsh-tools": "workspace:*",
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/dsh-llm": "workspace:*",
"@deepseek-ai/dsh-llm-deepseek": "workspace:*",
"@deepseek-ai/dsh-subagent-names": "workspace:*"
"@deepseek-ai/dsh-llm-deepseek": "workspace:*"
}
}
+20 -55
View File
@@ -3,17 +3,16 @@
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { SessionId } from '@deepseek-ai/dsh-session'
import { SessionId } from '@deepseek-ai/dsh-session'
import { SubagentError } from '@deepseek-ai/dsh-subagent'
import type {} from '@deepseek-ai/dsh-subagent-names'
import { TeamTaskId, type TeamTaskView } from '@deepseek-ai/dsh-agent-team'
import { TeamTaskId } from '@deepseek-ai/dsh-agent-team'
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 = ['agentTeams', 'subagents', 'subagentNames', 'tools', 'systemPrompt']
export const inject = ['agentTeams', 'subagents', 'tools', 'systemPrompt']
/** The Team tool plugin takes no configuration. */
export type Config = Record<never, never>
@@ -25,7 +24,7 @@ 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 = `A shared task board is available to every agent in your subagent tree. Task owners use agent names from list_agents, or Session ids when no name is bound. The root can reassign tasks to itself or any continuable descendant; other agents claim ready tasks themselves.
const TEAM_POLICY = `A shared task board is available to every agent in your subagent tree. Task owners are agent ids from list_agents. The root can reassign tasks to itself or any continuable descendant; other agents claim ready tasks themselves.
Read tasks with team_task_list or team_task_get, claim with the current revision, perform the work, then complete the task. Dependencies determine readiness; write scopes describe expected edits and overlapping scopes are advisory, not locks. Task readiness does not start an agent.
@@ -41,7 +40,7 @@ const TASK_VIEW_SCHEMA = {
subject: { type: 'string', required: true },
description: { type: 'string', required: true },
status: { type: 'string', required: true, enum: ['pending', 'in_progress', 'completed', 'deleted'] },
ownerName: { type: 'string' },
ownerId: { type: 'string' },
blockedBy: { type: 'array', required: true, items: { type: 'string' } },
writeScopes: { type: 'array', required: true, items: { type: 'string' } },
ready: { type: 'boolean', required: true },
@@ -75,31 +74,9 @@ function jsonOutput<const S extends ValueSchemaSpec>(schema: S): {
}
}
type NamedTaskView = Omit<TeamTaskView, 'ownerId'> & { readonly ownerName?: string }
/** Capture owner display names before a mutation or for the requested result rows. */
function taskOwnerNames(
ctx: Context, caller: Agent, owners: readonly (SessionId | undefined)[],
): ReadonlyMap<SessionId, string> {
const rootSession = ctx.subagents.rootOf(caller).session
const names = new Map<SessionId, string>()
for (const ownerId of owners) {
if (ownerId !== undefined && !names.has(ownerId)) names.set(ownerId, ctx.subagentNames.nameOf(rootSession, ownerId))
}
return names
}
/** Format a task from names captured for each possible result owner before its commit. */
function namedTask({ ownerId, ...view }: TeamTaskView, names: ReadonlyMap<SessionId, string>): NamedTaskView {
return {
...view,
...ownerId === undefined ? {} : { ownerName: names.get(ownerId) as string },
}
}
/** Recover the exact caller guaranteed by Agent-scoped tool discovery. */
/** Recover the exact caller guaranteed by conditional Team-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. */
/* v8 ignore next 2 -- The Team condition rejects callers without an Agent before execution. */
if (agent === undefined) throw new Error(`${toolName} requires a calling Agent`)
return agent
}
@@ -137,9 +114,10 @@ export function apply(ctx: Context, config: Config = {}): void {
interpolate: false,
text: ({ agent }) => {
if (agent === undefined || !enabled(agent)) return ''
return `Your name is ${JSON.stringify(ctx.subagentNames.nameOf(ctx.subagents.rootOf(agent).session, agent.id))}. Your team lead is "lead".
Use list_agents({}) to find your teammates and their full names.
Use send_message({ name: "<full agent path>", message: "..." }) to message a teammate.`
const root = ctx.subagents.rootOf(agent)
return root === agent
? `You are the Team Lead. Your agent id is ${JSON.stringify(agent.id)}.`
: `You are a teammate in the Team led by agent ${JSON.stringify(root.id)}.`
},
})
ctx.tools.register(defineTool({
@@ -164,7 +142,7 @@ Use send_message({ name: "<full agent path>", message: "..." }) to message a tea
...args.blocked_by === undefined ? {} : { blockedBy: args.blocked_by.map(TeamTaskId) },
...args.write_scopes === undefined ? {} : { writeScopes: args.write_scopes },
}, exec.signal)
return namedTask(task, new Map())
return task
},
}), { when: ({ agent }) => enabled(agent) })
@@ -177,7 +155,7 @@ Use send_message({ name: "<full agent path>", message: "..." }) to message a tea
enum: ['pending', 'in_progress', 'completed'],
description: 'Optional exact status filter.',
},
owner: { type: 'string', description: 'Optional exact ownerName filter; use unowned for tasks without an owner.' },
owner: { type: 'string', description: 'Optional exact ownerId 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.' },
@@ -189,16 +167,12 @@ Use send_message({ name: "<full agent path>", message: "..." }) to message a tea
if (!Number.isSafeInteger(cursor) || cursor < 0) throw new Error('cursor must be a non-negative safe integer')
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) throw new Error('limit must be an integer from 1 through 100')
const caller = callingAgent(exec.agent, 'team_task_list')
let filtered = ctx.agentTeams.listTasks(caller).filter(task =>
const filtered = ctx.agentTeams.listTasks(caller).filter(task =>
(args.status === undefined || task.status === args.status)
&& (args.owner !== 'unowned' || task.ownerId === undefined)
&& (args.owner === undefined || (args.owner === 'unowned' ? task.ownerId === undefined : task.ownerId === args.owner))
&& (args.ready === undefined || task.ready === args.ready))
const filterOwner = args.owner !== undefined && args.owner !== 'unowned'
const candidates = filterOwner ? filtered : filtered.slice(cursor, cursor + limit)
const names = taskOwnerNames(ctx, caller, candidates.map(task => task.ownerId))
if (filterOwner) filtered = filtered.filter(task => namedTask(task, names).ownerName === args.owner)
return Promise.resolve({
tasks: filtered.slice(cursor, cursor + limit).map(task => namedTask(task, names)),
tasks: filtered.slice(cursor, cursor + limit),
...(cursor + limit < filtered.length ? { nextCursor: cursor + limit } : {}),
})
},
@@ -214,8 +188,7 @@ Use send_message({ name: "<full agent path>", message: "..." }) to message a tea
execute(args, exec) {
const caller = callingAgent(exec.agent, 'team_task_get')
const task = ctx.agentTeams.getTask(caller, TeamTaskId(args.task_id))
const names = taskOwnerNames(ctx, caller, [task.ownerId])
return Promise.resolve(namedTask(task, names))
return Promise.resolve(task)
},
}), { when: ({ agent }) => enabled(agent) })
@@ -235,20 +208,12 @@ Use send_message({ name: "<full agent path>", message: "..." }) to message a tea
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 name or Session id of the root or any continuable descendant for root-only reassign; omit to unassign.' },
owner: { type: 'string', description: 'Agent id of the root or any continuable descendant for root-only reassign; omit to unassign.' },
},
output: jsonOutput(TASK_VIEW_SCHEMA),
async execute(args, exec) {
const caller = callingAgent(exec.agent, 'team_task_update')
const taskId = TeamTaskId(args.task_id)
const current = ctx.agentTeams.getTask(caller, taskId)
const owner = args.owner === undefined ? undefined
: ctx.subagentNames.resolveName(ctx.subagents.rootOf(caller).session, args.owner)
const keepsOwner = !['claim', 'release', 'reopen', 'reassign'].includes(args.action)
// Names are immutable; the service still checks the revision and authority after these reads.
const names = taskOwnerNames(ctx, caller, [
caller.id, owner, keepsOwner ? current.ownerId : undefined,
])
const task = await ctx.agentTeams.updateTask(caller, {
taskId,
expectedRevision: args.expected_revision,
@@ -257,9 +222,9 @@ Use send_message({ name: "<full agent path>", message: "..." }) to message a tea
...args.description === undefined ? {} : { description: args.description },
...args.blocked_by === undefined ? {} : { blockedBy: args.blocked_by.map(TeamTaskId) },
...args.write_scopes === undefined ? {} : { writeScopes: args.write_scopes },
...owner === undefined ? {} : { owner },
...args.owner === undefined ? {} : { owner: SessionId(args.owner) },
}, exec.signal)
return namedTask(task, names)
return task
},
}), { when: ({ agent }) => enabled(agent) })
}
Loaded 100 of 321 files, more files were not shown because too many files have changed in this diff. Show more