pi-web-ui 0.96.1 → 0.97.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +48 -2
  2. package/README.md +26 -0
  3. package/README.zh-CN.md +25 -0
  4. package/bin/pi-web-ui.mjs +11 -2
  5. package/dist/server/agent-service.js +1195 -120
  6. package/dist/server/client-state.js +89 -1
  7. package/dist/server/compacted-history.js +146 -0
  8. package/dist/server/delegate-mode.js +149 -0
  9. package/dist/server/delegate-task.js +10 -10
  10. package/dist/server/dsh/dsh-agent-service.js +35 -2
  11. package/dist/server/dsh/dsh-client.js +3 -1
  12. package/dist/server/eval-tool.js +3 -1
  13. package/dist/server/goal-review-gate.js +263 -0
  14. package/dist/server/goal-service.js +1225 -445
  15. package/dist/server/index.js +35 -1
  16. package/dist/server/lsp-tool.js +13 -10
  17. package/dist/server/markers/builtins/todo.js +0 -2
  18. package/dist/server/mcp-bridge.js +13 -0
  19. package/dist/server/patch-tool.js +1 -0
  20. package/dist/server/patch-turn-end-boundary.js +77 -0
  21. package/dist/server/permission-preset.js +44 -0
  22. package/dist/server/plan-mode.js +376 -0
  23. package/dist/server/plugin-facilities.js +36 -3
  24. package/dist/server/plugins.js +6 -3
  25. package/dist/server/protocol-version.js +1 -1
  26. package/dist/server/serialize.js +11 -0
  27. package/dist/server/settings-service.js +18 -6
  28. package/dist/server/subagents.js +360 -408
  29. package/dist/server/text-sniff.js +15 -9
  30. package/dist/server/tool-manager.js +48 -4
  31. package/dist/server/update-check.js +11 -3
  32. package/dist/server/wait-subscription-scan.js +5 -1
  33. package/package.json +3 -3
  34. package/themes/zhupi-dark.css +5 -30
  35. package/themes/zhupi.css +5 -30
  36. package/web/dist/assets/{TerminalPanel-O3GyFIGG.js → TerminalPanel-CITBqQrb.js} +1 -1
  37. package/web/dist/assets/index-DwToli0J.css +1 -0
  38. package/web/dist/assets/index-oI7UKOqJ.js +368 -0
  39. package/web/dist/index.html +2 -2
  40. package/web/dist/assets/index-B7Zp23Vc.css +0 -1
  41. package/web/dist/assets/index-ByfoaDwp.js +0 -366
@@ -65,450 +65,402 @@ export function withSubagentOwner(host, ownerId) {
65
65
  handoffSubagent: (fromRunId, toRunId, payload) => host.handoffSubagent(fromRunId || ownerId, toRunId, payload),
66
66
  };
67
67
  }
68
+ export const SUBAGENT_TOOL_NAME = "subagent";
69
+ export const SUBAGENT_ACTIONS = [
70
+ "spawn",
71
+ "get_result",
72
+ "steer",
73
+ "list",
74
+ "stop",
75
+ "wait_all",
76
+ "templates",
77
+ "handoff",
78
+ ];
68
79
  /**
69
- * 子代理工具集(注册进每个会话的 customTools,供主 agent 驱动子代理)。
70
- * 用 `subagent_*` 前缀命名,避免与第三方 pi-subagents 的
71
- * `Agent`/`get_subagent_result`/`steer_subagent` 冲突。
80
+ * 单 action 子代理总工具(注册进每个会话的 customTools,供主 agent 驱动子代理)。
81
+ * 合并原 subagent_* 8 个工具为一个统一入口,节省会话上下文 token。
72
82
  *
73
- * `selfConvId`(可选):这套工具所注册进的会话 convId。`subagent_wait_all`
74
- * 永远排除调用者自身——子代理会话上同样注册了全套工具,不传 runIds 时
83
+ * `selfConvId`(可选):这套工具所注册进的会话 convId。`wait_all`
84
+ * 永远排除调用者自身——子代理会话上同样注册了该工具,不传 runIds 时
75
85
  * 「全部」会含它自己,而它正在执行本工具(streaming=true),不排除就是
76
86
  * 自己等自己、永远到超时(self-wait deadlock)。主会话调用时传它自己的
77
87
  * 普通对话 id 即可(不在子代理列表里,delete 是 no-op)。
78
88
  */
79
- export function makeSubagentTools(host, lang, selfConvId) {
89
+ export function makeSubagentTool(host, lang, selfConvId) {
80
90
  const getLang = lang ?? host.lang ?? (() => "en");
81
91
  const text = (t, details = {}) => ({
82
92
  content: [{ type: "text", text: t }],
83
93
  details,
84
94
  });
85
- return [
86
- defineTool({
87
- name: "subagent_spawn",
88
- label: "Spawn subagent",
89
- description: "Spawn an independent background subagent conversation for a self-contained deliverable task (research/implement/review). " +
90
- "Subagents appear in the running-conversations list; the user can open, supplement, or stop them. Several may run in parallel — " +
91
- "subagent_wait_all waits for all (no polling), subagent_get_result fetches a result, subagent_list shows status, subagent_steer redirects, subagent_stop stops. " +
92
- "Good for long-running exploration, parallel research, and independent subtasks.",
93
- promptSnippet: "spawn an independent background subagent for a deliverable task (parallel work)",
94
- parameters: Type.Object({
95
- prompt: Type.String({
96
- description: "Full instructions for the subagent (goal + constraints + expected output).",
97
- }),
98
- type: Type.Optional(Type.String({
99
- description: "Subagent type/role name (e.g. explore/implement/review), for display. Default general.",
100
- })),
101
- template: Type.Optional(Type.String({
102
- description: "Subagent template name (preset from Settings → Subagent Templates; see subagent_templates): " +
103
- "role system prompt + skills/extensions whitelist + optional model/thinking level. " +
104
- "Omit to run with the main session defaults.",
105
- })),
106
- model: Type.Optional(Type.String({
107
- description: 'Subagent model "provider/id" (e.g. "anthropic/claude-opus-4-5") for this run; ' +
108
- "overrides template model then panel default. Omit = template model → panel default → main conversation model.",
109
- })),
110
- cwd: Type.Optional(Type.String({
111
- description: "Subagent working directory (relative/absolute). Defaults to the main session's cwd.",
112
- })),
113
- persist: Type.Optional(Type.Boolean({
114
- description: "Persist this conversation to disk as a regular session (saved in history, resumable). " +
115
- "Default false (lightweight in-memory subagent); use true for tasks needing long-term retention or human follow-up.",
116
- })),
117
- }),
118
- execute: async (_id, p, _signal, _onUpdate, ctx) => {
119
- if (p.template && !host.isTemplateUsable(p.template)) {
120
- return text(pick(getLang(), `子代理模板不可用:${p.template}(不存在或已停用)。用 subagent_templates 查看当前可用模板清单;不传 template 则按默认配置运行。`, `Subagent template unavailable: ${p.template} (missing or disabled). Use subagent_templates to list available templates; omit template to run with defaults.`, "subagents.spawn.template.unavailable", { "p.template": p.template }));
121
- }
122
- // spawn 通道是唯一的失败面(数量上限 / runtime 创建失败 / 坏 cwd 都经由
123
- // host 抛错):转成返回文本而不是直接抛,让 AI 能读到原因并调整重试。
124
- let convId;
125
- try {
126
- convId = await host.spawnSubagent(p.prompt, p.type ?? "general", p.cwd ?? ctx.cwd, p.template, p.model, undefined, p.persist);
127
- }
128
- catch (err) {
129
- const msg = err instanceof Error ? err.message : String(err);
130
- return text(pick(getLang(), `子代理启动失败:${msg}`, `Failed to start subagent: ${msg}`, "subagents.spawn.failed", {
131
- error: msg,
132
- }));
133
- }
134
- const subagentType = p.type ?? "general";
135
- const subagentTitleText = subagentTitle(p.prompt);
136
- // Optional segments are pre-rendered per language (translators pick
137
- // the Zh/En variant through the vars table; inline ternaries would
138
- // leak source syntax into packs that copy them verbatim).
139
- const templateLineZh = p.template ? `\n模板:${p.template}` : "";
140
- const templateLineEn = p.template ? `\nTemplate: ${p.template}` : "";
141
- const modelLineZh = p.model ? `\n模型:${p.model}` : "";
142
- const modelLineEn = p.model ? `\nModel: ${p.model}` : "";
143
- const kindLabelZh = p.persist ? "普通持久化对话" : "子代理";
144
- const kindLabelEn = p.persist ? "Persistent conversation" : "Subagent";
145
- return text(pick(getLang(), `${kindLabelZh}已启动(运行列表可见):${convId}\n类型:${subagentType} · 标题:${subagentTitleText}${templateLineZh}${modelLineZh}` +
146
- `\n用 subagent_wait_all 一次等全部完成(不用轮询),subagent_get_result 取单个结果,subagent_list 看运行态,subagent_steer 改向,subagent_stop 停止。`, `${kindLabelEn} started (visible in the running list): ${convId}\nType: ${subagentType} · Title: ${subagentTitleText}${templateLineEn}${modelLineEn}` +
147
- `\nUse subagent_wait_all to wait for all at once (no polling), subagent_get_result for a single result, subagent_list for live status, subagent_steer to redirect, subagent_stop to stop.`, "subagents.spawn.started", {
148
- convId: convId,
149
- subagentType: subagentType,
150
- subagentTitleText: subagentTitleText,
151
- "p.template": p.template,
152
- "p.model": p.model,
153
- templateLineZh: templateLineZh,
154
- templateLineEn: templateLineEn,
155
- modelLineZh: modelLineZh,
156
- modelLineEn: modelLineEn,
157
- }), { convId, template: p.template, model: p.model, persisted: !!p.persist });
158
- },
159
- }),
160
- defineTool({
161
- name: "subagent_get_result",
162
- label: "Get subagent result",
163
- description: "Fetch a subagent's result or current progress. " +
164
- "If not finished yet, returns the current status and partial output; " +
165
- "runtime errors (e.g. provider 400) are surfaced here as explicit errors.",
166
- promptSnippet: "fetch a subagent's result / current progress",
167
- parameters: Type.Object({
168
- runId: Type.String({
169
- description: "ConvId returned by subagent_spawn. Clicking the same conversation in the left panel opens it directly.",
170
- }),
95
+ const currentWaitCapMs = typeof host.getWatchdogTimeoutMs === "function"
96
+ ? Math.max(Math.min(60_000, Math.floor(host.getWatchdogTimeoutMs() * 0.8)), 5_000)
97
+ : WAIT_CAP_MS;
98
+ const capSeconds = Math.floor(currentWaitCapMs / 1000);
99
+ return defineTool({
100
+ name: SUBAGENT_TOOL_NAME,
101
+ label: "Subagent manager",
102
+ description: "Manage background subagent conversations for parallel delegation, research, and execution. Actions:\n" +
103
+ "- `spawn`: start a subagent with `prompt` (optional `template`, `type`, `model`, `cwd`, `persist`).\n" +
104
+ "- `get_result`: inspect status/output with `runId`.\n" +
105
+ "- `steer`: inject a `message` into a running subagent with `runId`.\n" +
106
+ "- `list`: list managed subagents (optional `kind`).\n" +
107
+ "- `stop`: abort a running subagent with `runId`.\n" +
108
+ "- `wait_all`: block until subagents finish (optional `runIds`, `timeoutSeconds`).\n" +
109
+ "- `templates`: list available subagent templates.\n" +
110
+ "- `handoff`: transfer `payload` to peer subagent with `toRunId` (optional `fromRunId`).",
111
+ promptSnippet: "manage background subagents: spawn, get_result, steer, list, stop, wait_all, templates, handoff",
112
+ parameters: Type.Object({
113
+ action: Type.Unsafe({
114
+ type: "string",
115
+ enum: [...SUBAGENT_ACTIONS],
116
+ description: "The subagent action to perform.",
171
117
  }),
172
- execute: async (_id, p) => {
173
- const r = host.getSubagent(p.runId);
174
- const missingId = shortId(p.runId);
175
- if (!r)
176
- return text(pick(getLang(), `未找到子代理 ${missingId}(可能已移出)。`, `Subagent ${missingId} not found (may have been dismissed).`, "subagents.get.not.found", { missingId: missingId }), undefined);
177
- const verdict = subagentVerdict(r, getLang());
178
- const doneId = shortId(r.convId);
179
- const doneDetail = verdictText(r, getLang());
180
- const doneOutput = r.output || (getLang() === "zh" ? "(无结果)" : "(no result)");
181
- if (r.streaming || r.state === "running") {
182
- const runningId = shortId(r.convId);
183
- const runningOutput = r.output || (getLang() === "zh" ? "(暂无输出)" : "(no output yet)");
184
- return text(pick(getLang(), `子代理 ${runningId}(${r.type})仍在运行(状态 ${r.state})。\n当前输出:\n${runningOutput}`, `Subagent ${runningId} (${r.type}) is still running (state ${r.state}).\nCurrent output:\n${runningOutput}`, "subagents.get.running", { runningId: runningId, "r.type": r.type, "r.state": r.state, runningOutput: runningOutput }), r);
185
- }
186
- return text(pick(getLang(), `子代理 ${doneId}(${r.type})状态:${verdict}\n${doneDetail}\n${doneOutput}`, `Subagent ${doneId} (${r.type}) status: ${verdict}\n${doneDetail}\n${doneOutput}`, "subagents.get.done", { doneId: doneId, "r.type": r.type, verdict: verdict, doneDetail: doneDetail, doneOutput: doneOutput }), r);
187
- },
118
+ prompt: Type.Optional(Type.String({
119
+ description: "spawn: instructions for the subagent (goal, constraints, expected output).",
120
+ })),
121
+ runId: Type.Optional(Type.String({
122
+ description: "get_result/steer/stop: target subagent conversation ID.",
123
+ })),
124
+ runIds: Type.Optional(Type.Array(Type.String(), {
125
+ description: "wait_all: subagent conversation IDs to wait for (omit for all active).",
126
+ })),
127
+ message: Type.Optional(Type.String({
128
+ description: "steer: follow-up instruction to inject into the subagent.",
129
+ })),
130
+ payload: Type.Optional(Type.String({
131
+ description: "handoff: artifact, data, or instructions to transfer to peer subagent.",
132
+ })),
133
+ toRunId: Type.Optional(Type.String({
134
+ description: "handoff: target peer subagent conversation ID.",
135
+ })),
136
+ fromRunId: Type.Optional(Type.String({
137
+ description: "handoff: source subagent ID (defaults to current subagent).",
138
+ })),
139
+ type: Type.Optional(Type.String({
140
+ description: "spawn: role/type name (e.g. explore/implement/review, default: general).",
141
+ })),
142
+ template: Type.Optional(Type.String({
143
+ description: "spawn: subagent template name (see action=templates).",
144
+ })),
145
+ model: Type.Optional(Type.String({
146
+ description: 'spawn: model override "provider/id".',
147
+ })),
148
+ cwd: Type.Optional(Type.String({
149
+ description: "spawn: working directory (defaults to current cwd).",
150
+ })),
151
+ persist: Type.Optional(Type.Boolean({
152
+ description: "spawn: persist as a regular session instead of ephemeral subagent.",
153
+ })),
154
+ kind: Type.Optional(Type.String({
155
+ enum: ["all", "subagent", "persistent"],
156
+ description: "list: filter subagents by kind (default: all).",
157
+ })),
158
+ timeoutSeconds: Type.Optional(Type.Integer({
159
+ description: `wait_all: max wait in seconds (1-${capSeconds}, default: ${capSeconds}s).`,
160
+ minimum: 1,
161
+ maximum: capSeconds,
162
+ })),
188
163
  }),
189
- defineTool({
190
- name: "subagent_steer",
191
- label: "Steer subagent",
192
- description: "Inject a message into a subagent to redirect or supplement its work (same as the user sending a message in its conversation).",
193
- promptSnippet: "inject a message into a running subagent to redirect its work",
194
- parameters: Type.Object({
195
- runId: Type.String({
196
- description: "Target subagent convId.",
197
- }),
198
- message: Type.String({
199
- description: "Redirect / supplementary info to inject.",
200
- }),
201
- }),
202
- execute: async (_id, p) => {
203
- if (!host.getSubagent(p.runId)) {
204
- const missingSteerId = shortId(p.runId);
205
- return text(pick(getLang(), `未找到子代理 ${missingSteerId}(可能已移出),消息未注入。请用 subagent_list 确认仍在运行的子代理。`, `Subagent ${missingSteerId} not found (may have been dismissed); message not injected. Use subagent_list to confirm running subagents.`, "subagents.steer.not.found", { missingSteerId: missingSteerId }));
164
+ execute: async (_id, p, signal, _onUpdate, ctx) => {
165
+ const action = (p.action ?? "").trim().toLowerCase();
166
+ if (!action) {
167
+ return text(pick(getLang(), "subagent 工具缺少 action 参数(可选:spawn, get_result, steer, list, stop, wait_all, templates, handoff)。", "subagent tool requires an action parameter (one of: spawn, get_result, steer, list, stop, wait_all, templates, handoff).", "subagents.action.missing"));
168
+ }
169
+ switch (action) {
170
+ case "spawn": {
171
+ if (!p.prompt || typeof p.prompt !== "string" || !p.prompt.trim()) {
172
+ return text(pick(getLang(), 'subagent(action="spawn") 缺少 prompt 参数。', 'subagent(action="spawn") requires prompt parameter.', "subagents.spawn.missing.prompt"));
173
+ }
174
+ if (p.template && !host.isTemplateUsable(p.template)) {
175
+ return text(pick(getLang(), `子代理模板不可用:${p.template}(不存在或已停用)。用 subagent(action="templates") 查看当前可用模板清单;不传 template 则按默认配置运行。`, `Subagent template unavailable: ${p.template} (missing or disabled). Use subagent(action="templates") to list available templates; omit template to run with defaults.`, "subagents.spawn.template.unavailable", { "p.template": p.template }));
176
+ }
177
+ let convId;
178
+ try {
179
+ convId = await host.spawnSubagent(p.prompt, p.type ?? "general", p.cwd ?? ctx.cwd, p.template, p.model, undefined, p.persist);
180
+ }
181
+ catch (err) {
182
+ const msg = err instanceof Error ? err.message : String(err);
183
+ return text(pick(getLang(), `子代理启动失败:${msg}`, `Failed to start subagent: ${msg}`, "subagents.spawn.failed", {
184
+ error: msg,
185
+ }));
186
+ }
187
+ const subagentType = p.type ?? "general";
188
+ const subagentTitleText = subagentTitle(p.prompt);
189
+ const templateLineZh = p.template ? `\n模板:${p.template}` : "";
190
+ const templateLineEn = p.template ? `\nTemplate: ${p.template}` : "";
191
+ const modelLineZh = p.model ? `\n模型:${p.model}` : "";
192
+ const modelLineEn = p.model ? `\nModel: ${p.model}` : "";
193
+ const kindLabelZh = p.persist ? "普通持久化对话" : "子代理";
194
+ const kindLabelEn = p.persist ? "Persistent conversation" : "Subagent";
195
+ return text(pick(getLang(), `${kindLabelZh}已启动(运行列表可见):${convId}\n类型:${subagentType} · 标题:${subagentTitleText}${templateLineZh}${modelLineZh}` +
196
+ `\n用 subagent(action="wait_all") 一次等全部完成(不用轮询),subagent(action="get_result") 取单个结果,subagent(action="list") 看运行态,subagent(action="steer") 改向,subagent(action="stop") 停止。`, `${kindLabelEn} started (visible in the running list): ${convId}\nType: ${subagentType} · Title: ${subagentTitleText}${templateLineEn}${modelLineEn}` +
197
+ `\nUse subagent(action="wait_all") to wait for all at once (no polling), subagent(action="get_result") for a single result, subagent(action="list") for live status, subagent(action="steer") to redirect, subagent(action="stop") to stop.`, "subagents.spawn.started", {
198
+ convId,
199
+ subagentType,
200
+ subagentTitleText,
201
+ "p.template": p.template,
202
+ "p.model": p.model,
203
+ templateLineZh,
204
+ templateLineEn,
205
+ modelLineZh,
206
+ modelLineEn,
207
+ }), { convId, template: p.template, model: p.model, persisted: !!p.persist });
206
208
  }
207
- await host.steerSubagent(p.runId, p.message);
208
- const steerId = shortId(p.runId);
209
- return text(pick(getLang(), `已向子代理 ${steerId} 注入消息。`, `Message injected into subagent ${steerId}.`, "subagents.steer.injected", { steerId: steerId }));
210
- },
211
- }),
212
- defineTool({
213
- name: "subagent_list",
214
- label: "List subagents",
215
- description: "List all subagents and managed conversations with their live status: " +
216
- "convId, type, state, title, message count (errors/aborts are marked in the state).",
217
- promptSnippet: "list all subagents and their live status",
218
- parameters: Type.Object({
219
- kind: Type.Optional(Type.String({
220
- enum: ["all", "subagent", "persistent"],
221
- description: "Filter: all (default) = all managed tasks; " +
222
- "subagent = only ephemeral in-memory subagents; persistent = only persistent conversations.",
223
- })),
224
- }),
225
- execute: async (_id, p) => {
226
- const list = host.listSubagents(p.kind);
227
- if (list.length === 0)
228
- return text(pick(getLang(), "当前没有运行中的对话/子代理。", "No managed conversations or subagents running.", "subagents.list.empty"));
229
- const tLang = getLang();
230
- const lines = list.map((r) => {
231
- const tag = r.persisted ? (tLang === "zh" ? "持久化" : "persistent") : tLang === "zh" ? "子代理" : "subagent";
232
- return (`- ${r.convId} · ${r.type} · ${subagentVerdict(r, tLang)} · ${r.title}` +
233
- (tLang === "zh" ? `(${tag} · msg: ${r.messageCount})` : ` (${tag} · msg: ${r.messageCount})`));
234
- });
235
- return text(lines.join("\n"));
236
- },
237
- }),
238
- defineTool({
239
- name: "subagent_stop",
240
- label: "Stop subagent",
241
- description: "Stop a running subagent (same as the user aborting it in its conversation). " +
242
- "Already-finished ones are unaffected.",
243
- promptSnippet: "stop a running subagent",
244
- parameters: Type.Object({
245
- runId: Type.String({
246
- description: "Target subagent convId.",
247
- }),
248
- }),
249
- execute: async (_id, p) => {
250
- if (!host.getSubagent(p.runId)) {
251
- const missingStopId = shortId(p.runId);
252
- return text(pick(getLang(), `未找到子代理 ${missingStopId}(可能已移出或已结束),无需停止。请用 subagent_list 确认仍在运行的子代理。`, `Subagent ${missingStopId} not found (may have been dismissed or already finished); nothing to stop. Use subagent_list to confirm running subagents.`, "subagents.stop.not.found", { missingStopId: missingStopId }));
209
+ case "get_result": {
210
+ if (!p.runId || typeof p.runId !== "string" || !p.runId.trim()) {
211
+ return text(pick(getLang(), 'subagent(action="get_result") 缺少 runId 参数。', 'subagent(action="get_result") requires runId parameter.', "subagents.get.missing.runId"));
212
+ }
213
+ const r = host.getSubagent(p.runId);
214
+ const missingId = shortId(p.runId);
215
+ if (!r)
216
+ return text(pick(getLang(), `未找到子代理 ${missingId}(可能已移出)。`, `Subagent ${missingId} not found (may have been dismissed).`, "subagents.get.not.found", { missingId }), undefined);
217
+ const verdict = subagentVerdict(r, getLang());
218
+ const doneId = shortId(r.convId);
219
+ const doneDetail = verdictText(r, getLang());
220
+ const doneOutput = r.output || (getLang() === "zh" ? "(无结果)" : "(no result)");
221
+ if (r.streaming || r.state === "running") {
222
+ const runningId = shortId(r.convId);
223
+ const runningOutput = r.output || (getLang() === "zh" ? "(暂无输出)" : "(no output yet)");
224
+ return text(pick(getLang(), `子代理 ${runningId}(${r.type})仍在运行(状态 ${r.state})。\n当前输出:\n${runningOutput}`, `Subagent ${runningId} (${r.type}) is still running (state ${r.state}).\nCurrent output:\n${runningOutput}`, "subagents.get.running", { runningId, "r.type": r.type, "r.state": r.state, runningOutput }), r);
225
+ }
226
+ return text(pick(getLang(), `子代理 ${doneId}(${r.type})状态:${verdict}\n${doneDetail}\n${doneOutput}`, `Subagent ${doneId} (${r.type}) status: ${verdict}\n${doneDetail}\n${doneOutput}`, "subagents.get.done", { doneId, "r.type": r.type, verdict, doneDetail, doneOutput }), r);
253
227
  }
254
- await host.stopSubagent(p.runId);
255
- const stopId = shortId(p.runId);
256
- return text(pick(getLang(), `已请求停止子代理 ${stopId}。`, `Stop requested for subagent ${stopId}.`, "subagents.stop.requested", { stopId: stopId }));
257
- },
258
- }),
259
- defineTool({
260
- name: "subagent_wait_all",
261
- label: "Wait for subagents",
262
- description: "Wait for multiple subagents to finish (blocks until all reach a terminal state or time out), then summarizes each result/error — no polling. " +
263
- "Pass runIds for specific subagents; omit = own descendant subagents when called from a subagent, else all currently running. " +
264
- "The calling session and its ancestors are never waited on (no self-deadlock); descendants spawned during the wait are picked up automatically. " +
265
- "On timeout returns the remaining unfinished list — call again to keep waiting.",
266
- promptSnippet: "wait for multiple subagents to finish (no polling) and get all results",
267
- parameters: Type.Object({
268
- runIds: Type.Optional(Type.Array(Type.String({
269
- description: "ConvId of a subagent to wait for (returned by subagent_spawn). " +
270
- "Omit = wait for all currently running.",
271
- }))),
272
- timeoutSeconds: Type.Optional(Type.Integer({
273
- description: `Max wait in seconds (default 600, cap ~${Math.floor(WAIT_CAP_MS / 1000)} — stays below the tool watchdog; on timeout returns the unfinished list so you can call again).`,
274
- minimum: 1,
275
- maximum: Math.floor(WAIT_CAP_MS / 1000),
276
- })),
277
- }),
278
- execute: async (_id, p, signal) => {
279
- // 祖先链:调用者往上经 parentId 能走到的子代理集合。父级正在执行中的
280
- // wait_all 卡着(streaming=true),等它 = 父子互等、必到超时才返回。
281
- // parentId 可能指向普通主对话(不在子代理列表里),走到空即停;环按 visited 截断。
282
- const ancestorIdsOf = (selfId) => {
283
- const out = new Set();
284
- const seen = new Set([selfId]);
285
- let cur = host.getSubagent(selfId)?.parentId;
286
- while (cur && !seen.has(cur)) {
287
- seen.add(cur);
288
- // 只有子代理才可能被圈进等待集;普通主对话记下来也无妨(wanted 里没有它)。
289
- out.add(cur);
290
- cur = host.getSubagent(cur)?.parentId;
228
+ case "steer": {
229
+ if (!p.runId || typeof p.runId !== "string" || !p.runId.trim()) {
230
+ return text(pick(getLang(), 'subagent(action="steer") 缺少 runId 参数。', 'subagent(action="steer") requires runId parameter.', "subagents.steer.missing.runId"));
291
231
  }
292
- return out;
293
- };
294
- // 后代展开:roots 里任一 id 经 parentId 链能向上走到的子代理。等待集
295
- // 传了显式 runIds 时也要顺带等它们的后代(fire-and-forget 的孙子辈否则会漏)。
296
- const expandDescendants = (roots) => {
297
- if (roots instanceof Set ? roots.size === 0 : roots.length === 0)
298
- return [];
299
- const rootSet = roots instanceof Set ? roots : new Set(roots);
300
- const all = host.listSubagents();
301
- const byId = new Map(all.map((s) => [s.convId, s]));
302
- const out = [];
303
- for (const s of all) {
304
- if (rootSet.has(s.convId))
305
- continue;
306
- let curParent = s.parentId;
307
- const seen = new Set();
308
- while (curParent) {
309
- if (rootSet.has(curParent)) {
310
- out.push(s.convId);
311
- break;
312
- }
313
- if (seen.has(curParent))
314
- break;
315
- seen.add(curParent);
316
- curParent = byId.get(curParent)?.parentId;
317
- // parentId 指向普通主对话(不在 byId 里):链到此为止,
318
- // 但 root 本身可能就是那个主对话 id(主调 wait_all 的 selfConvId)。
319
- if (!curParent)
320
- break;
321
- }
232
+ if (typeof p.message !== "string" || !p.message.trim()) {
233
+ return text(pick(getLang(), 'subagent(action="steer") 缺少 message 参数。', 'subagent(action="steer") requires message parameter.', "subagents.steer.missing.message"));
322
234
  }
323
- return out;
324
- };
325
- const explicit = p.runIds && p.runIds.length > 0;
326
- const wanted = new Set();
327
- const roots = new Set();
328
- let implicitGlobal = false;
329
- // 调用者自身 + 祖先永不等待:子代理调本工具时它自己正在 streaming,
330
- // 父级同样 streaming(卡在它自己的 wait 里),等谁都是互等死锁到超时。
331
- // 先算好排除集,显式 runIds 里的祖先直接剔除(不再展开它的子树,
332
- // 否则等父会顺带把整棵子树含兄弟分支都圈进来)。
333
- const excluded = new Set();
334
- if (selfConvId) {
335
- excluded.add(selfConvId);
336
- for (const a of ancestorIdsOf(selfConvId))
337
- excluded.add(a);
235
+ if (!host.getSubagent(p.runId)) {
236
+ const missingSteerId = shortId(p.runId);
237
+ return text(pick(getLang(), `未找到子代理 ${missingSteerId}(可能已移出),消息未注入。请用 subagent(action="list") 确认仍在运行的子代理。`, `Subagent ${missingSteerId} not found (may have been dismissed); message not injected. Use subagent(action="list") to confirm running subagents.`, "subagents.steer.not.found", { missingSteerId }));
238
+ }
239
+ await host.steerSubagent(p.runId, p.message);
240
+ const steerId = shortId(p.runId);
241
+ return text(pick(getLang(), `已向子代理 ${steerId} 注入消息。`, `Message injected into subagent ${steerId}.`, "subagents.steer.injected", { steerId }));
242
+ }
243
+ case "list": {
244
+ const list = host.listSubagents(p.kind);
245
+ if (list.length === 0)
246
+ return text(pick(getLang(), "当前没有运行中的对话/子代理。", "No managed conversations or subagents running.", "subagents.list.empty"));
247
+ const tLang = getLang();
248
+ const lines = list.map((r) => {
249
+ const tag = r.persisted
250
+ ? tLang === "zh"
251
+ ? "持久化"
252
+ : "persistent"
253
+ : tLang === "zh"
254
+ ? "子代理"
255
+ : "subagent";
256
+ return (`- ${r.convId} · ${r.type} · ${subagentVerdict(r, tLang)} · ${r.title}` +
257
+ (tLang === "zh" ? `(${tag} · msg: ${r.messageCount})` : ` (${tag} · msg: ${r.messageCount})`));
258
+ });
259
+ return text(lines.join("\n"));
338
260
  }
339
- if (explicit) {
340
- for (const id of p.runIds) {
341
- if (!excluded.has(id))
342
- roots.add(id);
261
+ case "stop": {
262
+ if (!p.runId || typeof p.runId !== "string" || !p.runId.trim()) {
263
+ return text(pick(getLang(), 'subagent(action="stop") 缺少 runId 参数。', 'subagent(action="stop") requires runId parameter.', "subagents.stop.missing.runId"));
343
264
  }
344
- for (const id of roots)
345
- wanted.add(id);
346
- for (const id of expandDescendants(roots)) {
347
- if (!excluded.has(id))
348
- wanted.add(id);
265
+ if (!host.getSubagent(p.runId)) {
266
+ const missingStopId = shortId(p.runId);
267
+ return text(pick(getLang(), `未找到子代理 ${missingStopId}(可能已移出或已结束),无需停止。请用 subagent(action="list") 确认仍在运行的子代理。`, `Subagent ${missingStopId} not found (may have been dismissed or already finished); nothing to stop. Use subagent(action="list") to confirm running subagents.`, "subagents.stop.not.found", { missingStopId }));
349
268
  }
269
+ await host.stopSubagent(p.runId);
270
+ const stopId = shortId(p.runId);
271
+ return text(pick(getLang(), `已请求停止子代理 ${stopId}。`, `Stop requested for subagent ${stopId}.`, "subagents.stop.requested", { stopId }));
350
272
  }
351
- else if (selfConvId && host.getSubagent(selfConvId)) {
352
- // 子代理无参:只等自己的后代(不含自己)。全局等会把父级/无关兄弟
353
- // 也圈进来:父级卡在 wait 里 streaming=true,子等父 = 父子互等到超时。
354
- roots.add(selfConvId);
355
- for (const id of expandDescendants(roots)) {
356
- if (!excluded.has(id))
273
+ case "wait_all": {
274
+ const ancestorIdsOf = (selfId) => {
275
+ const out = new Set();
276
+ const seen = new Set([selfId]);
277
+ let cur = host.getSubagent(selfId)?.parentId;
278
+ while (cur && !seen.has(cur)) {
279
+ seen.add(cur);
280
+ out.add(cur);
281
+ cur = host.getSubagent(cur)?.parentId;
282
+ }
283
+ return out;
284
+ };
285
+ const expandDescendants = (roots) => {
286
+ if (roots instanceof Set ? roots.size === 0 : roots.length === 0)
287
+ return [];
288
+ const rootSet = roots instanceof Set ? roots : new Set(roots);
289
+ const all = host.listSubagents();
290
+ const out = [];
291
+ for (const s of all) {
292
+ if (rootSet.has(s.convId))
293
+ continue;
294
+ let curParent = s.parentId;
295
+ const seen = new Set();
296
+ while (curParent) {
297
+ if (rootSet.has(curParent)) {
298
+ out.push(s.convId);
299
+ break;
300
+ }
301
+ if (seen.has(curParent))
302
+ break;
303
+ seen.add(curParent);
304
+ curParent = host.getSubagent(curParent)?.parentId;
305
+ }
306
+ }
307
+ return out;
308
+ };
309
+ const explicit = p.runIds && p.runIds.length > 0;
310
+ const wanted = new Set();
311
+ const selfAncestorIds = selfConvId ? ancestorIdsOf(selfConvId) : new Set();
312
+ if (explicit) {
313
+ const seedRoots = [];
314
+ for (const id of p.runIds) {
315
+ if (selfConvId && id === selfConvId)
316
+ continue;
317
+ if (selfAncestorIds.has(id))
318
+ continue;
357
319
  wanted.add(id);
320
+ seedRoots.push(id);
321
+ }
322
+ for (const descId of expandDescendants(seedRoots)) {
323
+ if (selfConvId && descId === selfConvId)
324
+ continue;
325
+ if (selfAncestorIds.has(descId))
326
+ continue;
327
+ wanted.add(descId);
328
+ }
358
329
  }
359
- }
360
- else {
361
- // 主对话无参:等当前全部(保持老语义),但同样动态追后代。
362
- implicitGlobal = true;
363
- for (const r of host.listSubagents()) {
364
- if (!excluded.has(r.convId))
365
- wanted.add(r.convId);
330
+ else if (selfConvId) {
331
+ for (const descId of expandDescendants(new Set([selfConvId]))) {
332
+ if (descId === selfConvId)
333
+ continue;
334
+ if (selfAncestorIds.has(descId))
335
+ continue;
336
+ wanted.add(descId);
337
+ }
366
338
  }
367
- }
368
- if (wanted.size === 0) {
369
- const emptyLang = getLang();
370
- return text(pick(emptyLang, "没有需要等待的子代理(调用者自身与祖先不计入等待;子代理无参时只等自己的后代)。", "No subagents to wait for (the calling session itself and its ancestors are never waited on; a subagent without runIds only waits for its own descendants).", "subagents.wait.empty"));
371
- }
372
- const currentWatchdogMs = host.getWatchdogTimeoutMs?.() ?? WAIT_CAP_MS;
373
- // 同 WAIT_CAP_MS:0.8×看门狗、60s 封顶、5s 兜底,wait 必须先于看门狗干净超时。
374
- const currentWaitCapMs = currentWatchdogMs > 0 ? Math.max(Math.min(60_000, Math.floor(currentWatchdogMs * 0.8)), 5_000) : 3600_000;
375
- const timeoutMs = Math.min(Math.max(p.timeoutSeconds ?? 600, 1), Math.floor(currentWaitCapMs / 1000)) * 1000;
376
- const waitStart = Date.now();
377
- const deadline = waitStart + timeoutMs;
378
- // 已到终态的、(或已被移出找不到的)直接归位;剩下的阻塞轮询到
379
- // 全部完成/超时/中止(移出 = 无法再等,立即按收口处理)。
380
- // 每轮动态追踪:等待期间新派生的后代(roots 的子孙、或主调全局新增)自动纳入,
381
- // fire-and-forget 的孙子辈不会漏;自身与祖先永远排除(父子互等死锁)。
382
- const trackNewArrivals = () => {
383
- if (implicitGlobal) {
339
+ else {
384
340
  for (const r of host.listSubagents()) {
385
- if (excluded.has(r.convId))
386
- continue;
387
341
  wanted.add(r.convId);
388
342
  }
389
343
  }
390
- else {
391
- for (const id of expandDescendants(roots)) {
392
- if (excluded.has(id))
344
+ if (selfConvId)
345
+ wanted.delete(selfConvId);
346
+ for (const ancId of selfAncestorIds)
347
+ wanted.delete(ancId);
348
+ if (wanted.size === 0) {
349
+ return text(pick(getLang(), "没有需要等待的子代理(调用者自身与其祖先永不计入等待;子代理无参数调用时只等待属于它自己的后代)。", "No subagents to wait for (the calling session itself and its ancestors are never waited on; a subagent without runIds only waits for its own descendants).", "subagents.wait.empty"));
350
+ }
351
+ const timeoutMs = Math.min(Math.max(p.timeoutSeconds ?? capSeconds, 1), capSeconds) * 1000;
352
+ const startedAt = Date.now();
353
+ const POLL_INTERVAL_MS = 250;
354
+ while (!signal?.aborted && Date.now() - startedAt < timeoutMs) {
355
+ for (const descId of expandDescendants(wanted)) {
356
+ if (selfConvId && descId === selfConvId)
393
357
  continue;
394
- wanted.add(id);
358
+ if (selfAncestorIds.has(descId))
359
+ continue;
360
+ wanted.add(descId);
395
361
  }
362
+ const allDone = Array.from(wanted).every((id) => {
363
+ const r = host.getSubagent(id);
364
+ return isSubagentTerminal(r);
365
+ });
366
+ if (allDone)
367
+ break;
368
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
396
369
  }
397
- };
398
- const pending = () => {
399
- trackNewArrivals();
400
- return [...wanted].filter((id) => {
370
+ const tLang = getLang();
371
+ const waitedSecs = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
372
+ const timeoutSecs = Math.round(timeoutMs / 1000);
373
+ const lines = Array.from(wanted)
374
+ .map((id) => {
375
+ const r = host.getSubagent(id);
376
+ const sId = shortId(id);
377
+ if (!r) {
378
+ return pick(tLang, `- ${sId}:已移出或不存在`, `- ${sId}: dismissed or not found`, "subagents.wait.item.missing", { sId });
379
+ }
380
+ const verdict = subagentVerdict(r, tLang);
381
+ const detail = verdictText(r, tLang);
382
+ const out = clipSubagentOutput(r.output || (tLang === "zh" ? "(无输出)" : "(no output)"), tLang);
383
+ return `- ${sId}(${r.type} · ${verdict} · ${r.title})${detail ? ` · ${detail}` : ""}:\n${out}`;
384
+ })
385
+ .join("\n\n");
386
+ const remaining = Array.from(wanted).filter((id) => {
401
387
  const r = host.getSubagent(id);
402
- return r !== undefined && !isSubagentTerminal(r);
388
+ return !isSubagentTerminal(r);
403
389
  });
404
- };
405
- while (pending().length > 0 && Date.now() < deadline && !(signal?.aborted ?? false)) {
406
- await new Promise((resolve) => setTimeout(resolve, 300));
390
+ const head = remaining.length === 0
391
+ ? pick(tLang, `全部 ${wanted.size} 个子代理已收口:`, `All ${wanted.size} subagent(s) collected:`, "subagents.wait.collected", { "wanted.size": wanted.size })
392
+ : signal?.aborted
393
+ ? pick(tLang, `本轮被中止,${remaining.length} 个仍在运行:`, `This round was aborted, ${remaining.length} still running:`, "subagents.wait.aborted", { "remaining.length": remaining.length })
394
+ : pick(tLang, `等待超过 ${timeoutSecs}s 超时(实际等待 ${waitedSecs}s),${remaining.length} 个仍在运行:`, `Wait timed out after ${timeoutSecs}s (actually waited ${waitedSecs}s), ${remaining.length} still running:`, "subagents.wait.timeout", { timeoutSecs, waitedSecs, "remaining.length": remaining.length });
395
+ return text(`${head}\n${lines}\n` + promptRemaining(remaining, tLang));
407
396
  }
408
- const tLang = getLang();
409
- const remaining = pending();
410
- const lines = [...wanted]
411
- .map((id) => {
412
- const r = host.getSubagent(id);
413
- if (!r)
414
- return tLang === "zh"
415
- ? `- ${shortId(id)}:未找到(可能已移出)`
416
- : `- ${shortId(id)}: not found (may have been dismissed)`;
417
- const body = verdictText(r, tLang);
418
- return ((tLang === "zh"
419
- ? `- ${shortId(r.convId)}(${r.type})· ${r.title} · ${subagentVerdict(r, tLang)}`
420
- : `- ${shortId(r.convId)} (${r.type}) · ${r.title} · ${subagentVerdict(r, tLang)}`) +
421
- (body ? `\n ${body}` : "") +
422
- (r.output ? `\n ${clipSubagentOutput(r.output, tLang).split("\n").join("\n ")}` : ""));
423
- })
424
- .join("\n");
425
- const timeoutSecs = Math.round(timeoutMs / 1000);
426
- const waitedSecs = Math.round((Date.now() - waitStart) / 1000);
427
- const head = remaining.length === 0
428
- ? pick(tLang, `全部 ${wanted.size} 个子代理已收口:`, `All ${wanted.size} subagent(s) collected:`, "subagents.wait.collected", { "wanted.size": wanted.size })
429
- : signal?.aborted
430
- ? pick(tLang, `本轮被中止,${remaining.length} 个仍在运行:`, `This round was aborted, ${remaining.length} still running:`, "subagents.wait.aborted", { "remaining.length": remaining.length })
431
- : pick(tLang, `等待超过 ${timeoutSecs}s 超时(实际等待 ${waitedSecs}s),${remaining.length} 个仍在运行:`, `Wait timed out after ${timeoutSecs}s (actually waited ${waitedSecs}s), ${remaining.length} still running:`, "subagents.wait.timeout", { timeoutSecs: timeoutSecs, waitedSecs: waitedSecs, "remaining.length": remaining.length });
432
- return text(`${head}\n${lines}\n` + promptRemaining(remaining, tLang));
433
- },
434
- }),
435
- defineTool({
436
- name: "subagent_templates",
437
- label: "List subagent templates",
438
- description: "List the configurable subagent templates (role system prompt + skills/extensions whitelist + optional model/thinking level) usable via the subagent_spawn template param. " +
439
- "Disabled templates never appear; empty list = subagents run with defaults.",
440
- promptSnippet: "list configurable subagent templates (role prompt + skills/extensions whitelist presets)",
441
- parameters: Type.Object({}),
442
- execute: async () => {
443
- const list = host.listTemplates();
444
- const tLang = getLang();
445
- if (list.length === 0) {
446
- return text(pick(tLang, "当前没有可用的子代理模板(设置面板 → 子代理模板 添加后可用)。子代理默认按主会话配置运行。", "No subagent templates available (add some under Settings → Subagent Templates). Subagents run with the main session defaults.", "subagents.templates.empty"));
447
- }
448
- const lines = list.map((t) => {
449
- const desc = tLang === "zh" ? t.description : t.descriptionEn || t.description;
450
- // 模型与思考强度分开报:思考强度是模板固定值(空 = 跟主对话当前强度)。
451
- const modelPart = tLang === "zh"
452
- ? t.model
453
- ? `模型:${t.model}`
454
- : "跟随主对话模型"
455
- : t.model
456
- ? `model: ${t.model}`
457
- : "follows the main conversation model";
458
- const thinkingPart = tLang === "zh"
459
- ? t.thinkingLevel
460
- ? `思考强度:${t.thinkingLevel}`
461
- : "跟随主对话思考强度"
462
- : t.thinkingLevel
463
- ? `thinking: ${t.thinkingLevel}`
464
- : "follows the main conversation thinking level";
465
- return ((tLang === "zh" ? `- ${t.name}${desc ? `:${desc}` : ""}` : `- ${t.name}${desc ? `: ${desc}` : ""}`) +
466
- (tLang === "zh" ? `(${modelPart},${thinkingPart})` : ` (${modelPart}, ${thinkingPart})`));
467
- });
468
- const firstTemplateName = list[0]?.name;
469
- const templateLines = lines.join("\n");
470
- return text(pick(getLang(), `可用的子代理模板(subagent_spawn 的 template 参数传名字,如 subagent_spawn(template="${firstTemplateName}")):\n${templateLines}`, `Available subagent templates (pass the name as subagent_spawn's template param, e.g. subagent_spawn(template="${firstTemplateName}")):\n${templateLines}`, "subagents.templates.list", { firstTemplateName: firstTemplateName, templateLines: templateLines }));
471
- },
472
- }),
473
- defineTool({
474
- name: "subagent_handoff",
475
- label: "Hand off to peer subagent",
476
- description: "Hand off artifacts, context, or results directly to a peer subagent (peer-to-peer, bypassing the parent conversation).",
477
- promptSnippet: "hand off task artifacts or results directly to another peer subagent",
478
- parameters: Type.Object({
479
- toRunId: Type.String({
480
- description: "Target peer subagent convId (from subagent_list).",
481
- }),
482
- payload: Type.String({
483
- description: "The artifact, analysis result, or instruction to hand off to the peer subagent.",
484
- }),
485
- fromRunId: Type.Optional(Type.String({
486
- description: "Source subagent convId. Optional: defaults to the current calling subagent.",
487
- })),
488
- }),
489
- execute: async (_id, p) => {
490
- const fromId = p.fromRunId || selfConvId || "unknown";
491
- if (fromId === p.toRunId) {
492
- return text(pick(getLang(), `交接失败:不能交接给自身(${shortId(fromId)})。请指定其他同行子代理。`, `Handoff failed: cannot hand off to oneself (${shortId(fromId)}). Please specify a different peer subagent.`, "subagents.handoff.self", { id: shortId(fromId) }));
493
- }
494
- const target = host.getSubagent(p.toRunId);
495
- if (!target) {
496
- const missingId = shortId(p.toRunId);
497
- return text(pick(getLang(), `未找到目标子代理 ${missingId}(可能已移出或不存在)。请用 subagent_list 确认可用的子代理。`, `Target subagent ${missingId} not found (may have been dismissed or non-existent). Use subagent_list to check available subagents.`, "subagents.handoff.not.found", { missingId }));
498
- }
499
- try {
500
- await host.handoffSubagent(fromId, p.toRunId, p.payload);
501
- const fromShort = shortId(fromId);
502
- const toShort = shortId(p.toRunId);
503
- return text(pick(getLang(), `已成功将产物交接给同行子代理 ${toShort}(${target.type}),协同路由已建立并开始执行。`, `Successfully handed off payload to peer subagent ${toShort} (${target.type}); peer routing established and executing.`, "subagents.handoff.success", { from: fromShort, to: toShort, type: target.type }), { fromRunId: fromId, toRunId: p.toRunId, timestamp: Date.now() });
397
+ case "templates": {
398
+ const list = host.listTemplates();
399
+ const tLang = getLang();
400
+ if (list.length === 0) {
401
+ return text(pick(tLang, "当前没有可用的子代理模板(设置面板 → 子代理模板 添加后可用)。子代理默认按主会话配置运行。", "No subagent templates available (add some under Settings → Subagent Templates). Subagents run with the main session defaults.", "subagents.templates.empty"));
402
+ }
403
+ const lines = list.map((t) => {
404
+ const desc = tLang === "zh" ? t.description : t.descriptionEn || t.description;
405
+ const modelPart = tLang === "zh"
406
+ ? t.model
407
+ ? `模型:${t.model}`
408
+ : "跟随主对话模型"
409
+ : t.model
410
+ ? `model: ${t.model}`
411
+ : "follows the main conversation model";
412
+ const thinkingPart = tLang === "zh"
413
+ ? t.thinkingLevel
414
+ ? `思考强度:${t.thinkingLevel}`
415
+ : "跟随主对话思考强度"
416
+ : t.thinkingLevel
417
+ ? `thinking: ${t.thinkingLevel}`
418
+ : "follows the main conversation thinking level";
419
+ return ((tLang === "zh" ? `- ${t.name}${desc ? `:${desc}` : ""}` : `- ${t.name}${desc ? `: ${desc}` : ""}`) +
420
+ (tLang === "zh" ? `(${modelPart},${thinkingPart})` : ` (${modelPart}, ${thinkingPart})`));
421
+ });
422
+ const firstTemplateName = list[0]?.name;
423
+ const templateLines = lines.join("\n");
424
+ return text(pick(getLang(), `可用的子代理模板(subagent 的 template 参数传名字,如 subagent(action="spawn", template="${firstTemplateName}")):\n${templateLines}`, `Available subagent templates (pass the name as subagent's template param, e.g. subagent(action="spawn", template="${firstTemplateName}")):\n${templateLines}`, "subagents.templates.list", { firstTemplateName, templateLines }));
504
425
  }
505
- catch (err) {
506
- const msg = err instanceof Error ? err.message : String(err);
507
- return text(pick(getLang(), `交接失败:${msg}`, `Handoff failed: ${msg}`, "subagents.handoff.failed", { error: msg }));
426
+ case "handoff": {
427
+ const fromId = p.fromRunId || selfConvId || "unknown";
428
+ if (!p.toRunId || typeof p.toRunId !== "string" || !p.toRunId.trim()) {
429
+ return text(pick(getLang(), 'subagent(action="handoff") 缺少 toRunId 参数。', 'subagent(action="handoff") requires toRunId parameter.', "subagents.handoff.missing.toRunId"));
430
+ }
431
+ if (typeof p.payload !== "string" || !p.payload.trim()) {
432
+ return text(pick(getLang(), 'subagent(action="handoff") 缺少 payload 参数。', 'subagent(action="handoff") requires payload parameter.', "subagents.handoff.missing.payload"));
433
+ }
434
+ if (fromId === p.toRunId) {
435
+ return text(pick(getLang(), `交接失败:不能交接给自身(${shortId(fromId)})。请指定其他同行子代理。`, `Handoff failed: cannot hand off to oneself (${shortId(fromId)}). Please specify a different peer subagent.`, "subagents.handoff.self", { id: shortId(fromId) }));
436
+ }
437
+ const target = host.getSubagent(p.toRunId);
438
+ if (!target) {
439
+ const missingId = shortId(p.toRunId);
440
+ return text(pick(getLang(), `未找到目标子代理 ${missingId}(可能已移出或不存在)。请用 subagent(action="list") 确认可用的子代理。`, `Target subagent ${missingId} not found (may have been dismissed or non-existent). Use subagent(action="list") to check available subagents.`, "subagents.handoff.not.found", { missingId }));
441
+ }
442
+ try {
443
+ await host.handoffSubagent(fromId, p.toRunId, p.payload);
444
+ const fromShort = shortId(fromId);
445
+ const toShort = shortId(p.toRunId);
446
+ return text(pick(getLang(), `已成功将产物交接给同行子代理 ${toShort}(${target.type}),协同路由已建立并开始执行。`, `Successfully handed off payload to peer subagent ${toShort} (${target.type}); peer routing established and executing.`, "subagents.handoff.success", { from: fromShort, to: toShort, type: target.type }), { fromRunId: fromId, toRunId: p.toRunId, timestamp: Date.now() });
447
+ }
448
+ catch (err) {
449
+ const msg = err instanceof Error ? err.message : String(err);
450
+ return text(pick(getLang(), `交接失败:${msg}`, `Handoff failed: ${msg}`, "subagents.handoff.failed", { error: msg }));
451
+ }
508
452
  }
509
- },
510
- }),
511
- ];
453
+ default:
454
+ return text(pick(getLang(), `未知 action:${action}。支持的 action:spawn, get_result, steer, list, stop, wait_all, templates, handoff。`, `Unknown action: ${action}. Supported actions: spawn, get_result, steer, list, stop, wait_all, templates, handoff.`, "subagents.action.unknown", { action }));
455
+ }
456
+ },
457
+ });
458
+ }
459
+ /**
460
+ * 保持数组签名的子代理工具导出,兼容原有调用点与批处理包装。
461
+ */
462
+ export function makeSubagentTools(host, lang, selfConvId) {
463
+ return [makeSubagentTool(host, lang, selfConvId)];
512
464
  }
513
465
  /** 短 id 前缀(前端展示/日志用)。 */
514
466
  function shortId(id) {