pi-web-ui 0.96.1 → 0.98.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.
- package/CHANGELOG.md +144 -2
- package/README.md +29 -3
- package/README.zh-CN.md +28 -3
- package/bin/pi-web-ui.mjs +75 -25
- package/dist/server/agent-service.js +1829 -262
- package/dist/server/approval-rules.js +5 -10
- package/dist/server/atomic-file.js +40 -0
- package/dist/server/attachments.js +22 -0
- package/dist/server/claim-store.js +5 -11
- package/dist/server/client-state.js +296 -11
- package/dist/server/compact-context-tool.js +3 -1
- package/dist/server/compacted-history.js +148 -0
- package/dist/server/composer-drafts.js +4 -6
- package/dist/server/control-socket.js +0 -40
- package/dist/server/delegate-mode.js +143 -0
- package/dist/server/delegate-task.js +10 -10
- package/dist/server/dsh/dsh-agent-service.js +66 -31
- package/dist/server/dsh/dsh-client.js +48 -10
- package/dist/server/dsh/dsh-serialize.js +12 -10
- package/dist/server/dsh/dsh-sessions.js +20 -6
- package/dist/server/dsh/runtime/goal-rpc.mjs +14 -1
- package/dist/server/dsh/runtime/runtime-root.mjs +1 -1
- package/dist/server/ensure-bash.js +3 -5
- package/dist/server/eval-tool.js +11 -5
- package/dist/server/goal-review-gate.js +250 -0
- package/dist/server/goal-service.js +1272 -448
- package/dist/server/index.js +118 -16
- package/dist/server/launch-origin.js +0 -4
- package/dist/server/locales.js +3 -5
- package/dist/server/lsp-tool.js +123 -24
- package/dist/server/markers/builtins/notify.js +4 -2
- package/dist/server/markers/builtins/rename.js +0 -1
- package/dist/server/markers/builtins/todo.js +6 -28
- package/dist/server/markers/marker.js +0 -15
- package/dist/server/markers/registry.js +0 -3
- package/dist/server/markers/store.js +0 -3
- package/dist/server/mcp-bridge.js +13 -0
- package/dist/server/mcp-hot-reload.js +12 -5
- package/dist/server/model-admin.js +19 -18
- package/dist/server/model-enrich.js +0 -3
- package/dist/server/patch-tool.js +22 -6
- package/dist/server/patch-turn-end-boundary.js +77 -0
- package/dist/server/permission-preset.js +69 -0
- package/dist/server/plan-manager.js +150 -5
- package/dist/server/plan-mode.js +414 -0
- package/dist/server/plugin-api-catalog.js +2 -0
- package/dist/server/plugin-catalog.js +6 -6
- package/dist/server/plugin-dom.js +3 -4
- package/dist/server/plugin-facilities.js +39 -8
- package/dist/server/plugin-grants.js +5 -8
- package/dist/server/plugin-permissions.js +4 -6
- package/dist/server/plugin-schedule.js +4 -7
- package/dist/server/plugin-updater.js +151 -38
- package/dist/server/plugins.js +22 -5
- package/dist/server/prompt-composer.js +12 -3
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/resolve-global-sdk.js +8 -0
- package/dist/server/schedule-agent-tool.js +5 -1
- package/dist/server/scheduler-tasks.js +46 -27
- package/dist/server/sdk-origin.js +27 -0
- package/dist/server/serialize.js +33 -9
- package/dist/server/settings-service.js +18 -6
- package/dist/server/slash-commands.js +16 -0
- package/dist/server/subagent-templates.js +4 -10
- package/dist/server/subagents.js +379 -416
- package/dist/server/terminals.js +11 -6
- package/dist/server/text-quote.js +59 -0
- package/dist/server/text-sniff.js +15 -9
- package/dist/server/tool-manager.js +60 -7
- package/dist/server/tool-overrides.js +24 -0
- package/dist/server/update-check.js +80 -28
- package/dist/server/wait-subscription-scan.js +5 -1
- package/extensions/webui.ts +15 -1
- package/package.json +33 -21
- package/plugin-sdk/client-utils.mjs +92 -0
- package/plugin-sdk/index.d.ts +21 -0
- package/plugin-sdk/index.mjs +2 -0
- package/plugins/catalog.json +7 -0
- package/themes/zhupi-dark.css +5 -30
- package/themes/zhupi.css +5 -30
- package/web/dist/assets/{TerminalPanel-O3GyFIGG.js → TerminalPanel-C88r3r3Q.js} +1 -1
- package/web/dist/assets/index-2jDtjHvW.css +1 -0
- package/web/dist/assets/index-BjdK7s1M.js +368 -0
- package/web/dist/index.html +2 -2
- package/web/dist/sw.js +2 -1
- package/web/public/sw.js +2 -1
- package/web/dist/assets/index-B7Zp23Vc.css +0 -1
- package/web/dist/assets/index-ByfoaDwp.js +0 -366
package/dist/server/subagents.js
CHANGED
|
@@ -31,14 +31,22 @@ import { pick } from "./i18n.js";
|
|
|
31
31
|
/**
|
|
32
32
|
* subagent_wait_all 的最长阻塞时间:必须短暂低于工具看门狗(默认 20 分钟,
|
|
33
33
|
* PI_WEB_TOOL_TIMEOUT_MS 可调),否则看门狗会先中止整个会话而不是让 wait
|
|
34
|
-
* 干净地超时返回。取 0.8
|
|
35
|
-
*
|
|
34
|
+
* 干净地超时返回。取 0.8×看门狗(默认 ~16min,支持 PI_WEB_SUBAGENT_WAIT_CAP_SECONDS 覆盖),
|
|
35
|
+
* 兜底不低于 5s。
|
|
36
36
|
*/
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
37
|
+
export function computeWaitCapMs(watchdogMs) {
|
|
38
|
+
const customSecs = Number(process.env.PI_WEB_SUBAGENT_WAIT_CAP_SECONDS);
|
|
39
|
+
if (Number.isFinite(customSecs) && customSecs > 0) {
|
|
40
|
+
return Math.max(Math.floor(customSecs * 1000), 5_000);
|
|
41
|
+
}
|
|
42
|
+
const envWatchdog = Number(process.env.PI_WEB_TOOL_TIMEOUT_MS);
|
|
43
|
+
const watchdog = typeof watchdogMs === "number" && Number.isFinite(watchdogMs) && watchdogMs > 0
|
|
44
|
+
? watchdogMs
|
|
45
|
+
: Number.isFinite(envWatchdog) && envWatchdog > 0
|
|
46
|
+
? envWatchdog
|
|
47
|
+
: 20 * 60_000;
|
|
48
|
+
return Math.max(Math.floor(watchdog * 0.8), 5_000);
|
|
49
|
+
}
|
|
42
50
|
/** 子代理是否已到终态(运行结束、被中止或出错)。wait 工具据此判断
|
|
43
51
|
* 是否可以取结果;streaming=false 即可(error/canceled 都在快照里带标记)。 */
|
|
44
52
|
export function isSubagentTerminal(r) {
|
|
@@ -65,450 +73,405 @@ export function withSubagentOwner(host, ownerId) {
|
|
|
65
73
|
handoffSubagent: (fromRunId, toRunId, payload) => host.handoffSubagent(fromRunId || ownerId, toRunId, payload),
|
|
66
74
|
};
|
|
67
75
|
}
|
|
76
|
+
export const SUBAGENT_TOOL_NAME = "subagent";
|
|
77
|
+
export const SUBAGENT_ACTIONS = [
|
|
78
|
+
"spawn",
|
|
79
|
+
"get_result",
|
|
80
|
+
"steer",
|
|
81
|
+
"list",
|
|
82
|
+
"stop",
|
|
83
|
+
"wait_all",
|
|
84
|
+
"templates",
|
|
85
|
+
"handoff",
|
|
86
|
+
];
|
|
68
87
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* `Agent`/`get_subagent_result`/`steer_subagent` 冲突。
|
|
88
|
+
* 单 action 子代理总工具(注册进每个会话的 customTools,供主 agent 驱动子代理)。
|
|
89
|
+
* 合并原 subagent_* 8 个工具为一个统一入口,节省会话上下文 token。
|
|
72
90
|
*
|
|
73
|
-
* `selfConvId`(可选):这套工具所注册进的会话 convId。`
|
|
74
|
-
*
|
|
91
|
+
* `selfConvId`(可选):这套工具所注册进的会话 convId。`wait_all`
|
|
92
|
+
* 永远排除调用者自身——子代理会话上同样注册了该工具,不传 runIds 时
|
|
75
93
|
* 「全部」会含它自己,而它正在执行本工具(streaming=true),不排除就是
|
|
76
94
|
* 自己等自己、永远到超时(self-wait deadlock)。主会话调用时传它自己的
|
|
77
95
|
* 普通对话 id 即可(不在子代理列表里,delete 是 no-op)。
|
|
78
96
|
*/
|
|
79
|
-
export function
|
|
97
|
+
export function makeSubagentTool(host, lang, selfConvId) {
|
|
80
98
|
const getLang = lang ?? host.lang ?? (() => "en");
|
|
81
99
|
const text = (t, details = {}) => ({
|
|
82
100
|
content: [{ type: "text", text: t }],
|
|
83
101
|
details,
|
|
84
102
|
});
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
}),
|
|
103
|
+
const currentWaitCapMs = typeof host.getWatchdogTimeoutMs === "function"
|
|
104
|
+
? computeWaitCapMs(host.getWatchdogTimeoutMs())
|
|
105
|
+
: computeWaitCapMs();
|
|
106
|
+
const capSeconds = Math.floor(currentWaitCapMs / 1000);
|
|
107
|
+
return defineTool({
|
|
108
|
+
name: SUBAGENT_TOOL_NAME,
|
|
109
|
+
label: "Subagent manager",
|
|
110
|
+
description: "Manage background subagent conversations for parallel delegation, research, and execution. Actions:\n" +
|
|
111
|
+
"- `spawn`: start a subagent with `prompt` (optional `template`, `type`, `model`, `cwd`, `persist`).\n" +
|
|
112
|
+
"- `get_result`: inspect status/output with `runId`.\n" +
|
|
113
|
+
"- `steer`: inject a `message` into a running subagent with `runId`.\n" +
|
|
114
|
+
"- `list`: list managed subagents (optional `kind`).\n" +
|
|
115
|
+
"- `stop`: abort a running subagent with `runId`.\n" +
|
|
116
|
+
"- `wait_all`: block until subagents finish (optional `runIds`, `timeoutSeconds`).\n" +
|
|
117
|
+
"- `templates`: list available subagent templates.\n" +
|
|
118
|
+
"- `handoff`: transfer `payload` to peer subagent with `toRunId` (optional `fromRunId`).",
|
|
119
|
+
promptSnippet: "manage background subagents: spawn, get_result, steer, list, stop, wait_all, templates, handoff",
|
|
120
|
+
parameters: Type.Object({
|
|
121
|
+
action: Type.Unsafe({
|
|
122
|
+
type: "string",
|
|
123
|
+
enum: [...SUBAGENT_ACTIONS],
|
|
124
|
+
description: "The subagent action to perform.",
|
|
171
125
|
}),
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
126
|
+
prompt: Type.Optional(Type.String({
|
|
127
|
+
description: "spawn: instructions for the subagent (goal, constraints, expected output).",
|
|
128
|
+
})),
|
|
129
|
+
runId: Type.Optional(Type.String({
|
|
130
|
+
description: "get_result/steer/stop: target subagent conversation ID.",
|
|
131
|
+
})),
|
|
132
|
+
runIds: Type.Optional(Type.Array(Type.String(), {
|
|
133
|
+
description: "wait_all: subagent conversation IDs to wait for (omit for all active).",
|
|
134
|
+
})),
|
|
135
|
+
message: Type.Optional(Type.String({
|
|
136
|
+
description: "steer: follow-up instruction to inject into the subagent.",
|
|
137
|
+
})),
|
|
138
|
+
payload: Type.Optional(Type.String({
|
|
139
|
+
description: "handoff: artifact, data, or instructions to transfer to peer subagent.",
|
|
140
|
+
})),
|
|
141
|
+
toRunId: Type.Optional(Type.String({
|
|
142
|
+
description: "handoff: target peer subagent conversation ID.",
|
|
143
|
+
})),
|
|
144
|
+
fromRunId: Type.Optional(Type.String({
|
|
145
|
+
description: "handoff: source subagent ID (defaults to current subagent).",
|
|
146
|
+
})),
|
|
147
|
+
type: Type.Optional(Type.String({
|
|
148
|
+
description: "spawn: role/type name (e.g. explore/implement/review, default: general).",
|
|
149
|
+
})),
|
|
150
|
+
template: Type.Optional(Type.String({
|
|
151
|
+
description: "spawn: subagent template name (see action=templates).",
|
|
152
|
+
})),
|
|
153
|
+
model: Type.Optional(Type.String({
|
|
154
|
+
description: 'spawn: model override "provider/id".',
|
|
155
|
+
})),
|
|
156
|
+
cwd: Type.Optional(Type.String({
|
|
157
|
+
description: "spawn: working directory (defaults to current cwd).",
|
|
158
|
+
})),
|
|
159
|
+
persist: Type.Optional(Type.Boolean({
|
|
160
|
+
description: "spawn: persist as a regular session instead of ephemeral subagent.",
|
|
161
|
+
})),
|
|
162
|
+
kind: Type.Optional(Type.String({
|
|
163
|
+
enum: ["all", "subagent", "persistent"],
|
|
164
|
+
description: "list: filter subagents by kind (default: all).",
|
|
165
|
+
})),
|
|
166
|
+
timeoutSeconds: Type.Optional(Type.Integer({
|
|
167
|
+
description: `wait_all: max wait in seconds (1-${capSeconds}, default: ${capSeconds}s).`,
|
|
168
|
+
minimum: 1,
|
|
169
|
+
maximum: capSeconds,
|
|
170
|
+
})),
|
|
188
171
|
}),
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
172
|
+
execute: async (_id, p, signal, _onUpdate, ctx) => {
|
|
173
|
+
const action = (p.action ?? "").trim().toLowerCase();
|
|
174
|
+
if (!action) {
|
|
175
|
+
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"));
|
|
176
|
+
}
|
|
177
|
+
switch (action) {
|
|
178
|
+
case "spawn": {
|
|
179
|
+
if (!p.prompt || typeof p.prompt !== "string" || !p.prompt.trim()) {
|
|
180
|
+
return text(pick(getLang(), 'subagent(action="spawn") 缺少 prompt 参数。', 'subagent(action="spawn") requires prompt parameter.', "subagents.spawn.missing.prompt"));
|
|
181
|
+
}
|
|
182
|
+
if (p.template && !host.isTemplateUsable(p.template)) {
|
|
183
|
+
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 }));
|
|
184
|
+
}
|
|
185
|
+
let convId;
|
|
186
|
+
try {
|
|
187
|
+
convId = await host.spawnSubagent(p.prompt, p.type ?? "general", p.cwd ?? ctx.cwd, p.template, p.model, undefined, p.persist);
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
191
|
+
return text(pick(getLang(), `子代理启动失败:${msg}`, `Failed to start subagent: ${msg}`, "subagents.spawn.failed", {
|
|
192
|
+
error: msg,
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
const subagentType = p.type ?? "general";
|
|
196
|
+
const subagentTitleText = subagentTitle(p.prompt);
|
|
197
|
+
const templateLineZh = p.template ? `\n模板:${p.template}` : "";
|
|
198
|
+
const templateLineEn = p.template ? `\nTemplate: ${p.template}` : "";
|
|
199
|
+
const modelLineZh = p.model ? `\n模型:${p.model}` : "";
|
|
200
|
+
const modelLineEn = p.model ? `\nModel: ${p.model}` : "";
|
|
201
|
+
const kindLabelZh = p.persist ? "普通持久化对话" : "子代理";
|
|
202
|
+
const kindLabelEn = p.persist ? "Persistent conversation" : "Subagent";
|
|
203
|
+
return text(pick(getLang(), `${kindLabelZh}已启动(运行列表可见):${convId}\n类型:${subagentType} · 标题:${subagentTitleText}${templateLineZh}${modelLineZh}` +
|
|
204
|
+
`\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}` +
|
|
205
|
+
`\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", {
|
|
206
|
+
convId,
|
|
207
|
+
subagentType,
|
|
208
|
+
subagentTitleText,
|
|
209
|
+
"p.template": p.template,
|
|
210
|
+
"p.model": p.model,
|
|
211
|
+
templateLineZh,
|
|
212
|
+
templateLineEn,
|
|
213
|
+
modelLineZh,
|
|
214
|
+
modelLineEn,
|
|
215
|
+
}), { convId, template: p.template, model: p.model, persisted: !!p.persist });
|
|
206
216
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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 }));
|
|
217
|
+
case "get_result": {
|
|
218
|
+
if (!p.runId || typeof p.runId !== "string" || !p.runId.trim()) {
|
|
219
|
+
return text(pick(getLang(), 'subagent(action="get_result") 缺少 runId 参数。', 'subagent(action="get_result") requires runId parameter.', "subagents.get.missing.runId"));
|
|
220
|
+
}
|
|
221
|
+
const r = host.getSubagent(p.runId);
|
|
222
|
+
const missingId = shortId(p.runId);
|
|
223
|
+
if (!r)
|
|
224
|
+
return text(pick(getLang(), `未找到子代理 ${missingId}(可能已移出)。`, `Subagent ${missingId} not found (may have been dismissed).`, "subagents.get.not.found", { missingId }), undefined);
|
|
225
|
+
const verdict = subagentVerdict(r, getLang());
|
|
226
|
+
const doneId = shortId(r.convId);
|
|
227
|
+
const doneDetail = verdictText(r, getLang());
|
|
228
|
+
const doneOutput = r.output || (getLang() === "zh" ? "(无结果)" : "(no result)");
|
|
229
|
+
if (r.streaming || r.state === "running") {
|
|
230
|
+
const runningId = shortId(r.convId);
|
|
231
|
+
const runningOutput = r.output || (getLang() === "zh" ? "(暂无输出)" : "(no output yet)");
|
|
232
|
+
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);
|
|
233
|
+
}
|
|
234
|
+
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
235
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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;
|
|
236
|
+
case "steer": {
|
|
237
|
+
if (!p.runId || typeof p.runId !== "string" || !p.runId.trim()) {
|
|
238
|
+
return text(pick(getLang(), 'subagent(action="steer") 缺少 runId 参数。', 'subagent(action="steer") requires runId parameter.', "subagents.steer.missing.runId"));
|
|
291
239
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
}
|
|
240
|
+
if (typeof p.message !== "string" || !p.message.trim()) {
|
|
241
|
+
return text(pick(getLang(), 'subagent(action="steer") 缺少 message 参数。', 'subagent(action="steer") requires message parameter.', "subagents.steer.missing.message"));
|
|
242
|
+
}
|
|
243
|
+
if (!host.getSubagent(p.runId)) {
|
|
244
|
+
const missingSteerId = shortId(p.runId);
|
|
245
|
+
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 }));
|
|
322
246
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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);
|
|
247
|
+
await host.steerSubagent(p.runId, p.message);
|
|
248
|
+
const steerId = shortId(p.runId);
|
|
249
|
+
return text(pick(getLang(), `已向子代理 ${steerId} 注入消息。`, `Message injected into subagent ${steerId}.`, "subagents.steer.injected", { steerId }));
|
|
338
250
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
251
|
+
case "list": {
|
|
252
|
+
const list = host.listSubagents(p.kind);
|
|
253
|
+
if (list.length === 0)
|
|
254
|
+
return text(pick(getLang(), "当前没有运行中的对话/子代理。", "No managed conversations or subagents running.", "subagents.list.empty"));
|
|
255
|
+
const tLang = getLang();
|
|
256
|
+
const lines = list.map((r) => {
|
|
257
|
+
const tag = r.persisted
|
|
258
|
+
? tLang === "zh"
|
|
259
|
+
? "持久化"
|
|
260
|
+
: "persistent"
|
|
261
|
+
: tLang === "zh"
|
|
262
|
+
? "子代理"
|
|
263
|
+
: "subagent";
|
|
264
|
+
return (`- ${r.convId} · ${r.type} · ${subagentVerdict(r, tLang)} · ${r.title}` +
|
|
265
|
+
(tLang === "zh" ? `(${tag} · msg: ${r.messageCount})` : ` (${tag} · msg: ${r.messageCount})`));
|
|
266
|
+
});
|
|
267
|
+
return text(lines.join("\n"));
|
|
268
|
+
}
|
|
269
|
+
case "stop": {
|
|
270
|
+
if (!p.runId || typeof p.runId !== "string" || !p.runId.trim()) {
|
|
271
|
+
return text(pick(getLang(), 'subagent(action="stop") 缺少 runId 参数。', 'subagent(action="stop") requires runId parameter.', "subagents.stop.missing.runId"));
|
|
343
272
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (!excluded.has(id))
|
|
348
|
-
wanted.add(id);
|
|
273
|
+
if (!host.getSubagent(p.runId)) {
|
|
274
|
+
const missingStopId = shortId(p.runId);
|
|
275
|
+
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
276
|
}
|
|
277
|
+
await host.stopSubagent(p.runId);
|
|
278
|
+
const stopId = shortId(p.runId);
|
|
279
|
+
return text(pick(getLang(), `已请求停止子代理 ${stopId}。`, `Stop requested for subagent ${stopId}.`, "subagents.stop.requested", { stopId }));
|
|
350
280
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
281
|
+
case "wait_all": {
|
|
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
|
+
out.add(cur);
|
|
289
|
+
cur = host.getSubagent(cur)?.parentId;
|
|
290
|
+
}
|
|
291
|
+
return out;
|
|
292
|
+
};
|
|
293
|
+
const expandDescendants = (roots) => {
|
|
294
|
+
if (roots instanceof Set ? roots.size === 0 : roots.length === 0)
|
|
295
|
+
return [];
|
|
296
|
+
const rootSet = roots instanceof Set ? roots : new Set(roots);
|
|
297
|
+
const all = host.listSubagents();
|
|
298
|
+
const out = [];
|
|
299
|
+
for (const s of all) {
|
|
300
|
+
if (rootSet.has(s.convId))
|
|
301
|
+
continue;
|
|
302
|
+
let curParent = s.parentId;
|
|
303
|
+
const seen = new Set();
|
|
304
|
+
while (curParent) {
|
|
305
|
+
if (rootSet.has(curParent)) {
|
|
306
|
+
out.push(s.convId);
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
if (seen.has(curParent))
|
|
310
|
+
break;
|
|
311
|
+
seen.add(curParent);
|
|
312
|
+
curParent = host.getSubagent(curParent)?.parentId;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
316
|
+
};
|
|
317
|
+
const explicit = p.runIds && p.runIds.length > 0;
|
|
318
|
+
const wanted = new Set();
|
|
319
|
+
const selfAncestorIds = selfConvId ? ancestorIdsOf(selfConvId) : new Set();
|
|
320
|
+
if (explicit) {
|
|
321
|
+
const seedRoots = [];
|
|
322
|
+
for (const id of p.runIds) {
|
|
323
|
+
if (selfConvId && id === selfConvId)
|
|
324
|
+
continue;
|
|
325
|
+
if (selfAncestorIds.has(id))
|
|
326
|
+
continue;
|
|
357
327
|
wanted.add(id);
|
|
328
|
+
seedRoots.push(id);
|
|
329
|
+
}
|
|
330
|
+
for (const descId of expandDescendants(seedRoots)) {
|
|
331
|
+
if (selfConvId && descId === selfConvId)
|
|
332
|
+
continue;
|
|
333
|
+
if (selfAncestorIds.has(descId))
|
|
334
|
+
continue;
|
|
335
|
+
wanted.add(descId);
|
|
336
|
+
}
|
|
358
337
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
wanted.add(
|
|
338
|
+
else if (selfConvId) {
|
|
339
|
+
for (const descId of expandDescendants(new Set([selfConvId]))) {
|
|
340
|
+
if (descId === selfConvId)
|
|
341
|
+
continue;
|
|
342
|
+
if (selfAncestorIds.has(descId))
|
|
343
|
+
continue;
|
|
344
|
+
wanted.add(descId);
|
|
345
|
+
}
|
|
366
346
|
}
|
|
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) {
|
|
347
|
+
else {
|
|
384
348
|
for (const r of host.listSubagents()) {
|
|
385
|
-
if (excluded.has(r.convId))
|
|
386
|
-
continue;
|
|
387
349
|
wanted.add(r.convId);
|
|
388
350
|
}
|
|
389
351
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
352
|
+
if (selfConvId)
|
|
353
|
+
wanted.delete(selfConvId);
|
|
354
|
+
for (const ancId of selfAncestorIds)
|
|
355
|
+
wanted.delete(ancId);
|
|
356
|
+
if (wanted.size === 0) {
|
|
357
|
+
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"));
|
|
358
|
+
}
|
|
359
|
+
const timeoutMs = Math.min(Math.max(p.timeoutSeconds ?? capSeconds, 1), capSeconds) * 1000;
|
|
360
|
+
const startedAt = Date.now();
|
|
361
|
+
const POLL_INTERVAL_MS = 250;
|
|
362
|
+
while (!signal?.aborted && Date.now() - startedAt < timeoutMs) {
|
|
363
|
+
for (const descId of expandDescendants(wanted)) {
|
|
364
|
+
if (selfConvId && descId === selfConvId)
|
|
393
365
|
continue;
|
|
394
|
-
|
|
366
|
+
if (selfAncestorIds.has(descId))
|
|
367
|
+
continue;
|
|
368
|
+
wanted.add(descId);
|
|
395
369
|
}
|
|
370
|
+
const allDone = Array.from(wanted).every((id) => {
|
|
371
|
+
const r = host.getSubagent(id);
|
|
372
|
+
// #488:getSubagent 返回 undefined = 已被移出(强行关闭/收尾清理)
|
|
373
|
+
// ——视为终态,与下方报告段「已移出或不存在」口径一致;否则等待
|
|
374
|
+
// 集合含被移出子代理时 allDone 恒 false,必然白等满 timeoutMs。
|
|
375
|
+
return r === undefined || isSubagentTerminal(r);
|
|
376
|
+
});
|
|
377
|
+
if (allDone)
|
|
378
|
+
break;
|
|
379
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
396
380
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
381
|
+
const tLang = getLang();
|
|
382
|
+
const waitedSecs = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
|
|
383
|
+
const timeoutSecs = Math.round(timeoutMs / 1000);
|
|
384
|
+
const lines = Array.from(wanted)
|
|
385
|
+
.map((id) => {
|
|
401
386
|
const r = host.getSubagent(id);
|
|
402
|
-
|
|
387
|
+
const sId = shortId(id);
|
|
388
|
+
if (!r) {
|
|
389
|
+
return pick(tLang, `- ${sId}:已移出或不存在`, `- ${sId}: dismissed or not found`, "subagents.wait.item.missing", { sId });
|
|
390
|
+
}
|
|
391
|
+
const verdict = subagentVerdict(r, tLang);
|
|
392
|
+
const detail = verdictText(r, tLang);
|
|
393
|
+
const out = clipSubagentOutput(r.output || (tLang === "zh" ? "(无输出)" : "(no output)"), tLang);
|
|
394
|
+
return `- ${sId}(${r.type} · ${verdict} · ${r.title})${detail ? ` · ${detail}` : ""}:\n${out}`;
|
|
395
|
+
})
|
|
396
|
+
.join("\n\n");
|
|
397
|
+
const remaining = Array.from(wanted).filter((id) => {
|
|
398
|
+
const r = host.getSubagent(id);
|
|
399
|
+
return !isSubagentTerminal(r);
|
|
403
400
|
});
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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"));
|
|
401
|
+
const head = remaining.length === 0
|
|
402
|
+
? pick(tLang, `全部 ${wanted.size} 个子代理已收口:`, `All ${wanted.size} subagent(s) collected:`, "subagents.wait.collected", { "wanted.size": wanted.size })
|
|
403
|
+
: signal?.aborted
|
|
404
|
+
? pick(tLang, `本轮被中止,${remaining.length} 个仍在运行:`, `This round was aborted, ${remaining.length} still running:`, "subagents.wait.aborted", { "remaining.length": remaining.length })
|
|
405
|
+
: 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 });
|
|
406
|
+
return text(`${head}\n${lines}\n` + promptRemaining(remaining, tLang));
|
|
447
407
|
}
|
|
448
|
-
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
: t.
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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() });
|
|
408
|
+
case "templates": {
|
|
409
|
+
const list = host.listTemplates();
|
|
410
|
+
const tLang = getLang();
|
|
411
|
+
if (list.length === 0) {
|
|
412
|
+
return text(pick(tLang, "当前没有可用的子代理模板(设置面板 → 子代理模板 添加后可用)。子代理默认按主会话配置运行。", "No subagent templates available (add some under Settings → Subagent Templates). Subagents run with the main session defaults.", "subagents.templates.empty"));
|
|
413
|
+
}
|
|
414
|
+
const lines = list.map((t) => {
|
|
415
|
+
const desc = tLang === "zh" ? t.description : t.descriptionEn || t.description;
|
|
416
|
+
const modelPart = tLang === "zh"
|
|
417
|
+
? t.model
|
|
418
|
+
? `模型:${t.model}`
|
|
419
|
+
: "跟随主对话模型"
|
|
420
|
+
: t.model
|
|
421
|
+
? `model: ${t.model}`
|
|
422
|
+
: "follows the main conversation model";
|
|
423
|
+
const thinkingPart = tLang === "zh"
|
|
424
|
+
? t.thinkingLevel
|
|
425
|
+
? `思考强度:${t.thinkingLevel}`
|
|
426
|
+
: "跟随主对话思考强度"
|
|
427
|
+
: t.thinkingLevel
|
|
428
|
+
? `thinking: ${t.thinkingLevel}`
|
|
429
|
+
: "follows the main conversation thinking level";
|
|
430
|
+
return ((tLang === "zh" ? `- ${t.name}${desc ? `:${desc}` : ""}` : `- ${t.name}${desc ? `: ${desc}` : ""}`) +
|
|
431
|
+
(tLang === "zh" ? `(${modelPart},${thinkingPart})` : ` (${modelPart}, ${thinkingPart})`));
|
|
432
|
+
});
|
|
433
|
+
const firstTemplateName = list[0]?.name;
|
|
434
|
+
const templateLines = lines.join("\n");
|
|
435
|
+
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
436
|
}
|
|
505
|
-
|
|
506
|
-
const
|
|
507
|
-
|
|
437
|
+
case "handoff": {
|
|
438
|
+
const fromId = p.fromRunId || selfConvId || "unknown";
|
|
439
|
+
if (!p.toRunId || typeof p.toRunId !== "string" || !p.toRunId.trim()) {
|
|
440
|
+
return text(pick(getLang(), 'subagent(action="handoff") 缺少 toRunId 参数。', 'subagent(action="handoff") requires toRunId parameter.', "subagents.handoff.missing.toRunId"));
|
|
441
|
+
}
|
|
442
|
+
if (typeof p.payload !== "string" || !p.payload.trim()) {
|
|
443
|
+
return text(pick(getLang(), 'subagent(action="handoff") 缺少 payload 参数。', 'subagent(action="handoff") requires payload parameter.', "subagents.handoff.missing.payload"));
|
|
444
|
+
}
|
|
445
|
+
if (fromId === p.toRunId) {
|
|
446
|
+
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) }));
|
|
447
|
+
}
|
|
448
|
+
const target = host.getSubagent(p.toRunId);
|
|
449
|
+
if (!target) {
|
|
450
|
+
const missingId = shortId(p.toRunId);
|
|
451
|
+
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 }));
|
|
452
|
+
}
|
|
453
|
+
try {
|
|
454
|
+
await host.handoffSubagent(fromId, p.toRunId, p.payload);
|
|
455
|
+
const fromShort = shortId(fromId);
|
|
456
|
+
const toShort = shortId(p.toRunId);
|
|
457
|
+
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() });
|
|
458
|
+
}
|
|
459
|
+
catch (err) {
|
|
460
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
461
|
+
return text(pick(getLang(), `交接失败:${msg}`, `Handoff failed: ${msg}`, "subagents.handoff.failed", { error: msg }));
|
|
462
|
+
}
|
|
508
463
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
464
|
+
default:
|
|
465
|
+
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 }));
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* 保持数组签名的子代理工具导出,兼容原有调用点与批处理包装。
|
|
472
|
+
*/
|
|
473
|
+
export function makeSubagentTools(host, lang, selfConvId) {
|
|
474
|
+
return [makeSubagentTool(host, lang, selfConvId)];
|
|
512
475
|
}
|
|
513
476
|
/** 短 id 前缀(前端展示/日志用)。 */
|
|
514
477
|
function shortId(id) {
|
|
@@ -582,5 +545,5 @@ function promptRemaining(remaining, lang = "en") {
|
|
|
582
545
|
if (remaining.length === 0)
|
|
583
546
|
return "";
|
|
584
547
|
const pendingIds = remaining.map(shortId).join(", ");
|
|
585
|
-
return pick(lang, `\n未完成:${pendingIds}。可再次调用
|
|
548
|
+
return pick(lang, `\n未完成:${pendingIds}。可再次调用 subagent(action="wait_all")继续等待耗时任务,或使用 steer 补充指令 / stop 中止。`, `\nPending: ${pendingIds}. You may call subagent (action="wait_all") again to continue waiting for long tasks, or use steer to add instructions / stop to abort.`, "subagents.wait.pending", { pendingIds: pendingIds });
|
|
586
549
|
}
|