@ai-agent-forge/plugin-subagent-delegate 0.88.13 → 0.88.15

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/dist/render.js ADDED
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Structural renderCall / renderResult for `subagent_run`
3
+ * (docs/design/子代理运行时设计.md §6, 2026-10-19 user ruling).
4
+ *
5
+ * Quiet transcript contract: while the tool runs, its card rides the group
6
+ * marker's live tail; after it settles the card stays hidden by the group.
7
+ * Both states must therefore stay one human-readable line when collapsed —
8
+ * never the raw pretty-printed args or the JSON result. Expanded (ctrl+o)
9
+ * adds the role description and per-task details.
10
+ *
11
+ * No `@agent-forge/tui` import: capability plugins ship self-contained and the
12
+ * host accepts structural components only (tool-definition.ts hosting
13
+ * boundary; goal/plan/alignment precedent).
14
+ */
15
+ function isRecord(value) {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17
+ }
18
+ /** Collapse whitespace so multi-line prompts stay on the single-line card. */
19
+ function oneLine(value) {
20
+ return value.replace(/\s+/g, " ").trim();
21
+ }
22
+ /**
23
+ * Simplified East Asian width table: the CJK blocks prompts realistically use
24
+ * count as 2 columns each; every other code point counts as 1. Combining marks
25
+ * and ambiguous widths fall back to 1, the safe (narrower) default.
26
+ */
27
+ function isWideCodePoint(code) {
28
+ return ((code >= 0x1100 && code <= 0x115f) ||
29
+ (code >= 0x2e80 && code <= 0xa4cf) ||
30
+ (code >= 0xac00 && code <= 0xd7a3) ||
31
+ (code >= 0xf900 && code <= 0xfaff) ||
32
+ (code >= 0xfe30 && code <= 0xfe4f) ||
33
+ (code >= 0xff00 && code <= 0xff60) ||
34
+ (code >= 0xffe0 && code <= 0xffe6) ||
35
+ (code >= 0x20000 && code <= 0x3fffd));
36
+ }
37
+ function displayWidth(value) {
38
+ let columns = 0;
39
+ for (const character of value)
40
+ columns += isWideCodePoint(character.codePointAt(0) ?? 0) ? 2 : 1;
41
+ return columns;
42
+ }
43
+ /** Cut one logical line to the terminal width, ending with an ellipsis. */
44
+ function truncateLine(value, width) {
45
+ if (width <= 0 || displayWidth(value) <= width)
46
+ return value;
47
+ const budget = width - 1;
48
+ let row = "";
49
+ let columns = 0;
50
+ for (const character of value) {
51
+ const cost = isWideCodePoint(character.codePointAt(0) ?? 0) ? 2 : 1;
52
+ if (columns + cost > budget)
53
+ break;
54
+ row += character;
55
+ columns += cost;
56
+ }
57
+ return `${row}…`;
58
+ }
59
+ /** Fold one logical line into rows of at most `width` display columns. */
60
+ function wrapLine(line, width) {
61
+ if (width <= 0)
62
+ return [line];
63
+ const rows = [];
64
+ let row = "";
65
+ let columns = 0;
66
+ for (const character of line) {
67
+ const cost = isWideCodePoint(character.codePointAt(0) ?? 0) ? 2 : 1;
68
+ if (row !== "" && columns + cost > width) {
69
+ rows.push(row);
70
+ row = character;
71
+ columns = cost;
72
+ }
73
+ else {
74
+ row += character;
75
+ columns += cost;
76
+ }
77
+ }
78
+ rows.push(row);
79
+ return rows;
80
+ }
81
+ /** One physical line whose overflow is cut, not wrapped. */
82
+ class OneLineComponent {
83
+ text;
84
+ theme;
85
+ color;
86
+ cachedWidth;
87
+ cachedRows;
88
+ constructor(text, theme, color) {
89
+ this.text = text;
90
+ this.theme = theme;
91
+ this.color = color;
92
+ }
93
+ render(width) {
94
+ if (this.cachedRows !== undefined && this.cachedWidth === width)
95
+ return this.cachedRows;
96
+ this.cachedRows = [this.theme.fg(this.color, truncateLine(this.text, width))];
97
+ this.cachedWidth = width;
98
+ return this.cachedRows;
99
+ }
100
+ invalidate() {
101
+ this.cachedWidth = undefined;
102
+ this.cachedRows = undefined;
103
+ }
104
+ }
105
+ /** Multi-line detail block; rows are cached per width like the goal renderer. */
106
+ class TextBlockComponent {
107
+ text;
108
+ theme;
109
+ color;
110
+ rowsByWidth = new Map();
111
+ constructor(text, theme, color) {
112
+ this.text = text;
113
+ this.theme = theme;
114
+ this.color = color;
115
+ }
116
+ render(width) {
117
+ const cached = this.rowsByWidth.get(width);
118
+ if (cached)
119
+ return cached;
120
+ const rows = [];
121
+ for (const line of this.text.split("\n")) {
122
+ for (const row of wrapLine(line, width))
123
+ rows.push(this.theme.fg(this.color, row));
124
+ }
125
+ this.rowsByWidth.set(width, rows);
126
+ return rows;
127
+ }
128
+ invalidate() {
129
+ this.rowsByWidth.clear();
130
+ }
131
+ }
132
+ /** Deliberately renders nothing: a collapsed result stays out of the live tail. */
133
+ class EmptyComponent {
134
+ render() {
135
+ return [];
136
+ }
137
+ invalidate() { }
138
+ }
139
+ function promptList(value) {
140
+ if (!Array.isArray(value))
141
+ return [];
142
+ const prompts = [];
143
+ for (const item of value) {
144
+ if (typeof item !== "string")
145
+ continue;
146
+ const line = oneLine(item);
147
+ if (line !== "")
148
+ prompts.push(line);
149
+ }
150
+ return prompts;
151
+ }
152
+ function readRole(args) {
153
+ if (!isRecord(args))
154
+ return undefined;
155
+ const role = args.role;
156
+ return typeof role === "string" && role.trim() !== "" ? role.trim() : undefined;
157
+ }
158
+ /**
159
+ * The single collapsed call line: label, background marker, role, task count
160
+ * (only when it adds information) and the first non-empty prompt line.
161
+ */
162
+ export function subagentRunCallLine(args) {
163
+ const record = isRecord(args) ? args : {};
164
+ const segments = ["创建子代理"];
165
+ if (record.background === true)
166
+ segments.push("后台");
167
+ const role = readRole(record);
168
+ if (role !== undefined)
169
+ segments.push(role);
170
+ const prompts = promptList(record.prompts);
171
+ if (prompts.length > 1)
172
+ segments.push(`${prompts.length} 个任务`);
173
+ const first = prompts[0];
174
+ if (first !== undefined)
175
+ segments.push(first);
176
+ return segments.join(" · ");
177
+ }
178
+ /**
179
+ * renderCall factory. `roleDescription` lets the expanded card show the role
180
+ * menu's one-line description without importing plugin state into this module.
181
+ */
182
+ export function createSubagentRunRenderCall(roleDescription) {
183
+ return (args, theme, context) => {
184
+ const line = subagentRunCallLine(args);
185
+ if (context.expanded !== true) {
186
+ return new OneLineComponent(line, theme, "toolOutput");
187
+ }
188
+ const role = readRole(args);
189
+ const description = role === undefined ? undefined : roleDescription?.(role);
190
+ if (description === undefined || description.trim() === "") {
191
+ return new TextBlockComponent(line, theme, "toolOutput");
192
+ }
193
+ return new TextBlockComponent(`${line}\n角色:${role} — ${oneLine(description)}`, theme, "toolOutput");
194
+ };
195
+ }
196
+ /**
197
+ * Shape-gate the tool payload. Anything that is not this tool's JSON result
198
+ * (progress text, error text, foreign data) returns undefined so the host
199
+ * fallback keeps it visible — never silently swallowed.
200
+ */
201
+ function parseResultView(text) {
202
+ let parsed;
203
+ try {
204
+ parsed = JSON.parse(text);
205
+ }
206
+ catch {
207
+ return undefined;
208
+ }
209
+ if (!isRecord(parsed) || !Array.isArray(parsed.tasks))
210
+ return undefined;
211
+ const tasks = [];
212
+ for (const item of parsed.tasks) {
213
+ if (!isRecord(item))
214
+ continue;
215
+ const taskId = typeof item.taskId === "string" ? item.taskId : "";
216
+ if (parsed.background === true) {
217
+ tasks.push({ taskId, status: "submitted" });
218
+ continue;
219
+ }
220
+ tasks.push({
221
+ taskId,
222
+ status: typeof item.status === "string" ? item.status : "unknown",
223
+ ...(typeof item.messageCount === "number" ? { messageCount: item.messageCount } : {}),
224
+ ...(typeof item.reasonCode === "string" ? { reasonCode: item.reasonCode } : {}),
225
+ ...(typeof item.reason === "string" ? { reason: item.reason } : {}),
226
+ });
227
+ }
228
+ return { background: parsed.background === true, tasks };
229
+ }
230
+ const STATUS_LABELS = {
231
+ queued: "排队",
232
+ running: "运行中",
233
+ succeeded: "成功",
234
+ failed: "失败",
235
+ cancelled: "已取消",
236
+ timed_out: "超时",
237
+ interrupted: "已中断",
238
+ incomplete: "未完成",
239
+ rejected: "已拒绝",
240
+ submitted: "已提交",
241
+ unknown: "未知",
242
+ };
243
+ function statusLabel(status) {
244
+ return STATUS_LABELS[status] ?? status;
245
+ }
246
+ function statusSummary(tasks) {
247
+ if (tasks.length === 0)
248
+ return "无任务";
249
+ const counts = new Map();
250
+ for (const task of tasks)
251
+ counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
252
+ if (counts.size === 1) {
253
+ const status = tasks[0]?.status ?? "unknown";
254
+ return status === "succeeded" ? "全部成功" : statusLabel(status);
255
+ }
256
+ return [...counts.entries()].map(([status, count]) => `${count} ${statusLabel(status)}`).join(" · ");
257
+ }
258
+ /** Short task handle for the expanded card (`...:1:85c146d8` → `85c146d8`). */
259
+ function shortTaskId(taskId) {
260
+ if (taskId === "")
261
+ return "(未创建)";
262
+ const parts = taskId.split(":");
263
+ return parts[parts.length - 1] ?? taskId;
264
+ }
265
+ function taskLine(task) {
266
+ const parts = [` - ${shortTaskId(task.taskId)} [${statusLabel(task.status)}]`];
267
+ if (task.messageCount !== undefined)
268
+ parts.push(`${task.messageCount} 条消息`);
269
+ if (task.reasonCode !== undefined)
270
+ parts.push(task.reasonCode);
271
+ if (task.reason !== undefined)
272
+ parts.push(task.reason);
273
+ return parts.join(" · ");
274
+ }
275
+ /**
276
+ * The single result line shared by the collapsed summary and the expanded
277
+ * header. Keeps the role/task context from the call visible after settlement.
278
+ */
279
+ export function subagentRunResultLine(args, tasks) {
280
+ const segments = ["创建子代理"];
281
+ const role = readRole(args);
282
+ if (role !== undefined)
283
+ segments.push(role);
284
+ segments.push(`${tasks.length} 个任务`, statusSummary(tasks));
285
+ return segments.join(" · ");
286
+ }
287
+ const FAILURE_STATUSES = new Set(["failed", "rejected", "timed_out"]);
288
+ function statusColor(tasks) {
289
+ return tasks.some((task) => FAILURE_STATUSES.has(task.status)) ? "error" : "warning";
290
+ }
291
+ /**
292
+ * renderResult: collapsed stays empty for successful/background tickets (the
293
+ * call line is the one visible line in the live tail). A failed task is never
294
+ * silent (D-083): its status rides one warning/error-colored line. Expanded
295
+ * returns the readable task detail.
296
+ */
297
+ export function subagentRunResultRenderer(result, options, theme, context) {
298
+ const text = result.content.find((part) => part.type === "text")?.text;
299
+ if (text === undefined || text === "")
300
+ return undefined;
301
+ const view = parseResultView(text);
302
+ if (view === undefined) {
303
+ // Partial progress is process noise on the one-line card. A final
304
+ // non-JSON result is usually the error text: keep the host fallback so
305
+ // failures stay visible (D-083 failure visibility).
306
+ return options.isPartial ? new EmptyComponent() : undefined;
307
+ }
308
+ if (!options.expanded) {
309
+ if (view.background || view.tasks.every((task) => task.status === "succeeded")) {
310
+ return new EmptyComponent();
311
+ }
312
+ return new OneLineComponent(subagentRunResultLine(context.args, view.tasks), theme, statusColor(view.tasks));
313
+ }
314
+ const lines = [subagentRunResultLine(context.args, view.tasks)];
315
+ if (view.background) {
316
+ for (const task of view.tasks)
317
+ lines.push(` - ${shortTaskId(task.taskId)} 后台运行,结算后自动回注`);
318
+ }
319
+ else {
320
+ for (const task of view.tasks)
321
+ lines.push(taskLine(task));
322
+ }
323
+ return new TextBlockComponent(lines.join("\n"), theme, "toolOutput");
324
+ }
325
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Structural renderCall / renderResult for `subagent_run`\n * (docs/design/子代理运行时设计.md §6, 2026-10-19 user ruling).\n *\n * Quiet transcript contract: while the tool runs, its card rides the group\n * marker's live tail; after it settles the card stays hidden by the group.\n * Both states must therefore stay one human-readable line when collapsed —\n * never the raw pretty-printed args or the JSON result. Expanded (ctrl+o)\n * adds the role description and per-task details.\n *\n * No `@agent-forge/tui` import: capability plugins ship self-contained and the\n * host accepts structural components only (tool-definition.ts hosting\n * boundary; goal/plan/alignment precedent).\n */\n\ninterface ThemeLike {\n\treadonly fg: (color: string, text: string) => string;\n}\n\ninterface RenderContextLike {\n\treadonly args?: unknown;\n\treadonly expanded?: boolean;\n}\n\ninterface ToolResultTextPartLike {\n\treadonly type: string;\n\treadonly text?: string;\n}\n\ninterface ToolResultLike {\n\treadonly content: readonly ToolResultTextPartLike[];\n}\n\ninterface RenderResultOptionsLike {\n\treadonly expanded: boolean;\n\treadonly isPartial: boolean;\n}\n\ninterface StructuralComponent {\n\trender(width: number): string[];\n\tinvalidate(): void;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Collapse whitespace so multi-line prompts stay on the single-line card. */\nfunction oneLine(value: string): string {\n\treturn value.replace(/\\s+/g, \" \").trim();\n}\n\n/**\n * Simplified East Asian width table: the CJK blocks prompts realistically use\n * count as 2 columns each; every other code point counts as 1. Combining marks\n * and ambiguous widths fall back to 1, the safe (narrower) default.\n */\nfunction isWideCodePoint(code: number): boolean {\n\treturn (\n\t\t(code >= 0x1100 && code <= 0x115f) ||\n\t\t(code >= 0x2e80 && code <= 0xa4cf) ||\n\t\t(code >= 0xac00 && code <= 0xd7a3) ||\n\t\t(code >= 0xf900 && code <= 0xfaff) ||\n\t\t(code >= 0xfe30 && code <= 0xfe4f) ||\n\t\t(code >= 0xff00 && code <= 0xff60) ||\n\t\t(code >= 0xffe0 && code <= 0xffe6) ||\n\t\t(code >= 0x20000 && code <= 0x3fffd)\n\t);\n}\n\nfunction displayWidth(value: string): number {\n\tlet columns = 0;\n\tfor (const character of value) columns += isWideCodePoint(character.codePointAt(0) ?? 0) ? 2 : 1;\n\treturn columns;\n}\n\n/** Cut one logical line to the terminal width, ending with an ellipsis. */\nfunction truncateLine(value: string, width: number): string {\n\tif (width <= 0 || displayWidth(value) <= width) return value;\n\tconst budget = width - 1;\n\tlet row = \"\";\n\tlet columns = 0;\n\tfor (const character of value) {\n\t\tconst cost = isWideCodePoint(character.codePointAt(0) ?? 0) ? 2 : 1;\n\t\tif (columns + cost > budget) break;\n\t\trow += character;\n\t\tcolumns += cost;\n\t}\n\treturn `${row}…`;\n}\n\n/** Fold one logical line into rows of at most `width` display columns. */\nfunction wrapLine(line: string, width: number): string[] {\n\tif (width <= 0) return [line];\n\tconst rows: string[] = [];\n\tlet row = \"\";\n\tlet columns = 0;\n\tfor (const character of line) {\n\t\tconst cost = isWideCodePoint(character.codePointAt(0) ?? 0) ? 2 : 1;\n\t\tif (row !== \"\" && columns + cost > width) {\n\t\t\trows.push(row);\n\t\t\trow = character;\n\t\t\tcolumns = cost;\n\t\t} else {\n\t\t\trow += character;\n\t\t\tcolumns += cost;\n\t\t}\n\t}\n\trows.push(row);\n\treturn rows;\n}\n\n/** One physical line whose overflow is cut, not wrapped. */\nclass OneLineComponent implements StructuralComponent {\n\tprivate readonly text: string;\n\tprivate readonly theme: ThemeLike;\n\tprivate readonly color: string;\n\tprivate cachedWidth: number | undefined;\n\tprivate cachedRows: string[] | undefined;\n\n\tconstructor(text: string, theme: ThemeLike, color: string) {\n\t\tthis.text = text;\n\t\tthis.theme = theme;\n\t\tthis.color = color;\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.cachedRows !== undefined && this.cachedWidth === width) return this.cachedRows;\n\t\tthis.cachedRows = [this.theme.fg(this.color, truncateLine(this.text, width))];\n\t\tthis.cachedWidth = width;\n\t\treturn this.cachedRows;\n\t}\n\n\tinvalidate(): void {\n\t\tthis.cachedWidth = undefined;\n\t\tthis.cachedRows = undefined;\n\t}\n}\n\n/** Multi-line detail block; rows are cached per width like the goal renderer. */\nclass TextBlockComponent implements StructuralComponent {\n\tprivate readonly text: string;\n\tprivate readonly theme: ThemeLike;\n\tprivate readonly color: string;\n\tprivate readonly rowsByWidth = new Map<number, string[]>();\n\n\tconstructor(text: string, theme: ThemeLike, color: string) {\n\t\tthis.text = text;\n\t\tthis.theme = theme;\n\t\tthis.color = color;\n\t}\n\n\trender(width: number): string[] {\n\t\tconst cached = this.rowsByWidth.get(width);\n\t\tif (cached) return cached;\n\t\tconst rows: string[] = [];\n\t\tfor (const line of this.text.split(\"\\n\")) {\n\t\t\tfor (const row of wrapLine(line, width)) rows.push(this.theme.fg(this.color, row));\n\t\t}\n\t\tthis.rowsByWidth.set(width, rows);\n\t\treturn rows;\n\t}\n\n\tinvalidate(): void {\n\t\tthis.rowsByWidth.clear();\n\t}\n}\n\n/** Deliberately renders nothing: a collapsed result stays out of the live tail. */\nclass EmptyComponent implements StructuralComponent {\n\trender(): string[] {\n\t\treturn [];\n\t}\n\n\tinvalidate(): void {}\n}\n\nfunction promptList(value: unknown): string[] {\n\tif (!Array.isArray(value)) return [];\n\tconst prompts: string[] = [];\n\tfor (const item of value) {\n\t\tif (typeof item !== \"string\") continue;\n\t\tconst line = oneLine(item);\n\t\tif (line !== \"\") prompts.push(line);\n\t}\n\treturn prompts;\n}\n\nfunction readRole(args: unknown): string | undefined {\n\tif (!isRecord(args)) return undefined;\n\tconst role = args.role;\n\treturn typeof role === \"string\" && role.trim() !== \"\" ? role.trim() : undefined;\n}\n\n/**\n * The single collapsed call line: label, background marker, role, task count\n * (only when it adds information) and the first non-empty prompt line.\n */\nexport function subagentRunCallLine(args: unknown): string {\n\tconst record = isRecord(args) ? args : {};\n\tconst segments = [\"创建子代理\"];\n\tif (record.background === true) segments.push(\"后台\");\n\tconst role = readRole(record);\n\tif (role !== undefined) segments.push(role);\n\tconst prompts = promptList(record.prompts);\n\tif (prompts.length > 1) segments.push(`${prompts.length} 个任务`);\n\tconst first = prompts[0];\n\tif (first !== undefined) segments.push(first);\n\treturn segments.join(\" · \");\n}\n\n/**\n * renderCall factory. `roleDescription` lets the expanded card show the role\n * menu's one-line description without importing plugin state into this module.\n */\nexport function createSubagentRunRenderCall(\n\troleDescription?: (roleId: string) => string | undefined,\n): (args: unknown, theme: ThemeLike, context: RenderContextLike) => StructuralComponent {\n\treturn (args, theme, context) => {\n\t\tconst line = subagentRunCallLine(args);\n\t\tif (context.expanded !== true) {\n\t\t\treturn new OneLineComponent(line, theme, \"toolOutput\");\n\t\t}\n\t\tconst role = readRole(args);\n\t\tconst description = role === undefined ? undefined : roleDescription?.(role);\n\t\tif (description === undefined || description.trim() === \"\") {\n\t\t\treturn new TextBlockComponent(line, theme, \"toolOutput\");\n\t\t}\n\t\treturn new TextBlockComponent(`${line}\\n角色:${role} — ${oneLine(description)}`, theme, \"toolOutput\");\n\t};\n}\n\ninterface TaskView {\n\treadonly taskId: string;\n\treadonly status: string;\n\treadonly messageCount?: number;\n\treadonly reasonCode?: string;\n\treadonly reason?: string;\n}\n\ninterface ResultView {\n\treadonly background: boolean;\n\treadonly tasks: readonly TaskView[];\n}\n\n/**\n * Shape-gate the tool payload. Anything that is not this tool's JSON result\n * (progress text, error text, foreign data) returns undefined so the host\n * fallback keeps it visible — never silently swallowed.\n */\nfunction parseResultView(text: string): ResultView | undefined {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(text);\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (!isRecord(parsed) || !Array.isArray(parsed.tasks)) return undefined;\n\tconst tasks: TaskView[] = [];\n\tfor (const item of parsed.tasks) {\n\t\tif (!isRecord(item)) continue;\n\t\tconst taskId = typeof item.taskId === \"string\" ? item.taskId : \"\";\n\t\tif (parsed.background === true) {\n\t\t\ttasks.push({ taskId, status: \"submitted\" });\n\t\t\tcontinue;\n\t\t}\n\t\ttasks.push({\n\t\t\ttaskId,\n\t\t\tstatus: typeof item.status === \"string\" ? item.status : \"unknown\",\n\t\t\t...(typeof item.messageCount === \"number\" ? { messageCount: item.messageCount } : {}),\n\t\t\t...(typeof item.reasonCode === \"string\" ? { reasonCode: item.reasonCode } : {}),\n\t\t\t...(typeof item.reason === \"string\" ? { reason: item.reason } : {}),\n\t\t});\n\t}\n\treturn { background: parsed.background === true, tasks };\n}\n\nconst STATUS_LABELS: Record<string, string> = {\n\tqueued: \"排队\",\n\trunning: \"运行中\",\n\tsucceeded: \"成功\",\n\tfailed: \"失败\",\n\tcancelled: \"已取消\",\n\ttimed_out: \"超时\",\n\tinterrupted: \"已中断\",\n\tincomplete: \"未完成\",\n\trejected: \"已拒绝\",\n\tsubmitted: \"已提交\",\n\tunknown: \"未知\",\n};\n\nfunction statusLabel(status: string): string {\n\treturn STATUS_LABELS[status] ?? status;\n}\n\nfunction statusSummary(tasks: readonly TaskView[]): string {\n\tif (tasks.length === 0) return \"无任务\";\n\tconst counts = new Map<string, number>();\n\tfor (const task of tasks) counts.set(task.status, (counts.get(task.status) ?? 0) + 1);\n\tif (counts.size === 1) {\n\t\tconst status = tasks[0]?.status ?? \"unknown\";\n\t\treturn status === \"succeeded\" ? \"全部成功\" : statusLabel(status);\n\t}\n\treturn [...counts.entries()].map(([status, count]) => `${count} ${statusLabel(status)}`).join(\" · \");\n}\n\n/** Short task handle for the expanded card (`...:1:85c146d8` → `85c146d8`). */\nfunction shortTaskId(taskId: string): string {\n\tif (taskId === \"\") return \"(未创建)\";\n\tconst parts = taskId.split(\":\");\n\treturn parts[parts.length - 1] ?? taskId;\n}\n\nfunction taskLine(task: TaskView): string {\n\tconst parts = [` - ${shortTaskId(task.taskId)} [${statusLabel(task.status)}]`];\n\tif (task.messageCount !== undefined) parts.push(`${task.messageCount} 条消息`);\n\tif (task.reasonCode !== undefined) parts.push(task.reasonCode);\n\tif (task.reason !== undefined) parts.push(task.reason);\n\treturn parts.join(\" · \");\n}\n\n/**\n * The single result line shared by the collapsed summary and the expanded\n * header. Keeps the role/task context from the call visible after settlement.\n */\nexport function subagentRunResultLine(args: unknown, tasks: readonly TaskView[]): string {\n\tconst segments = [\"创建子代理\"];\n\tconst role = readRole(args);\n\tif (role !== undefined) segments.push(role);\n\tsegments.push(`${tasks.length} 个任务`, statusSummary(tasks));\n\treturn segments.join(\" · \");\n}\n\nconst FAILURE_STATUSES = new Set([\"failed\", \"rejected\", \"timed_out\"]);\n\nfunction statusColor(tasks: readonly TaskView[]): string {\n\treturn tasks.some((task) => FAILURE_STATUSES.has(task.status)) ? \"error\" : \"warning\";\n}\n\n/**\n * renderResult: collapsed stays empty for successful/background tickets (the\n * call line is the one visible line in the live tail). A failed task is never\n * silent (D-083): its status rides one warning/error-colored line. Expanded\n * returns the readable task detail.\n */\nexport function subagentRunResultRenderer(\n\tresult: ToolResultLike,\n\toptions: RenderResultOptionsLike,\n\ttheme: ThemeLike,\n\tcontext: RenderContextLike,\n): StructuralComponent | undefined {\n\tconst text = result.content.find((part) => part.type === \"text\")?.text;\n\tif (text === undefined || text === \"\") return undefined;\n\tconst view = parseResultView(text);\n\tif (view === undefined) {\n\t\t// Partial progress is process noise on the one-line card. A final\n\t\t// non-JSON result is usually the error text: keep the host fallback so\n\t\t// failures stay visible (D-083 failure visibility).\n\t\treturn options.isPartial ? new EmptyComponent() : undefined;\n\t}\n\tif (!options.expanded) {\n\t\tif (view.background || view.tasks.every((task) => task.status === \"succeeded\")) {\n\t\t\treturn new EmptyComponent();\n\t\t}\n\t\treturn new OneLineComponent(subagentRunResultLine(context.args, view.tasks), theme, statusColor(view.tasks));\n\t}\n\tconst lines = [subagentRunResultLine(context.args, view.tasks)];\n\tif (view.background) {\n\t\tfor (const task of view.tasks) lines.push(` - ${shortTaskId(task.taskId)} 后台运行,结算后自动回注`);\n\t} else {\n\t\tfor (const task of view.tasks) lines.push(taskLine(task));\n\t}\n\treturn new TextBlockComponent(lines.join(\"\\n\"), theme, \"toolOutput\");\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"runner-pool.d.ts","sourceRoot":"","sources":["../src/runner-pool.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,gBAAgB,EAAqD,MAAM,yBAAyB,CAAC;AACnH,OAAO,EAEN,KAAK,qBAAqB,EAE1B,MAAM,gCAAgC,CAAC;AAGxC,MAAM,WAAW,2BAA2B;IAC3C,sDAA4B;IAC5B,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAC;IACxC,6DAA6B;IAC7B,MAAM,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;IAC/C,gEAA0B;IAC1B,IAAI,IAAI,IAAI,CAAC;CACb;AAED,MAAM,WAAW,2BAA2B;IAC3C,WAAW,IAAI,OAAO,CAAC,2BAA2B,CAAC,GAAG,2BAA2B,CAAC;IAClF,kBAAkB,EAAE,MAAM,CAAC;IAC3B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,mBAAmB,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,uGAAqD;IACrD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,8CAA0B;IAC1B,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;IACrB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,YAAY,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,yBAAyB;IACzC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,oBAAqB,SAAQ,gBAAgB;IAC7D,+FAAsE;IACtE,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACzG,qFAAiC;IACjC,KAAK,IAAI,IAAI,CAAC;IACd,KAAK,IAAI,yBAAyB,CAAC;IACnC,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,qBAAa,uBAAwB,SAAQ,KAAK;IACjD,QAAQ,CAAC,IAAI,EACV,0BAA0B,GAC1B,sBAAsB,GACtB,sBAAsB,GACtB,8BAA8B,CAAC;IAElC,YACC,IAAI,EACD,0BAA0B,GAC1B,sBAAsB,GACtB,sBAAsB,GACtB,8BAA8B,EACjC,OAAO,EAAE,MAAM,EAKf;CACD;AAKD;;;;GAIG;AACH,wBAAgB,mCAAmC,CAAC,OAAO,EAAE;IAC5D,kBAAkB,EAAE,MAAM,CAAC;IAC3B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B,GAAG,IAAI,CAkCP;AA6ED,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,2BAA2B,GAAG,oBAAoB,CAuPzG","sourcesContent":["/**\n * 池化 runner adapter(D-045 / pooled 切片增量 2)。\n *\n * `AgentTaskAdapter` 的 runner 池实现:按 `maxSessionsPerRunner` 选\"空闲槽位最多\"\n * 的 runner(确定性);无空闲槽位且未达 `maxRunnerProcesses` 时按需 spawn;runner\n * 空闲超时退出(不迁移任务);进程死亡时在途任务以 `runner_lost` 结算(D-044 §4.3\n * 投影 failed + reasonCode subagent_runner_lost)。\n *\n * 分层约束:调度器(SubagentRuntime)继续持有队列与准入;本层只提供\"容量 + 进程\n * 生命周期\"。宿主接线必须保证 `maxConcurrentSubagents <= maxRunnerProcesses *\n * maxSessionsPerRunner`,否则超出容量的准入会以 `subagent_runner_capacity` 失败。\n * spawn 由 `spawnRunner` 注入:生产侧经 stdio 派生子进程,测试注入进程内 fake。\n *\n * 迁自宿主 `src/plugins/subagent-task/runner-pool.ts`(D-075 S4 第三批);公共契约改自\n * plugin-sdk,wire 协议类型改自 @agent-forge/protocol(宿主侧同名模块已下沉该包)。\n */\nimport { randomUUID } from \"node:crypto\";\nimport type { AgentTaskAdapter, AgentTaskCreateRequest, AgentTaskHost, SessionAPI } from \"@agent-forge/plugin-sdk\";\nimport {\n\tSUBAGENT_TASK_PROTOCOL_VERSION,\n\ttype SubagentTaskChannelV1,\n\tSubagentTaskProtocolError,\n} from \"@agent-forge/protocol/subagent\";\nimport { createRemoteAgentTaskAdapter } from \"./remote-adapter.ts\";\n\nexport interface SubagentRunnerSpawnHandleV1 {\n\t/** 与 runner 进程对话的通道(父侧)。 */\n\treadonly channel: SubagentTaskChannelV1;\n\t/** 进程退出通知:error 非空表示异常退出。 */\n\tonExit(handler: (error?: Error) => void): void;\n\t/** 请求进程退出(空闲回收与池关闭使用)。 */\n\tkill(): void;\n}\n\nexport interface SubagentRunnerPoolOptionsV1 {\n\tspawnRunner(): Promise<SubagentRunnerSpawnHandleV1> | SubagentRunnerSpawnHandleV1;\n\tmaxRunnerProcesses: number;\n\tmaxSessionsPerRunner: number;\n\tidleRunnerTimeoutMs: number;\n\t/**\n\t * 宿主全局并发(调度器 maxConcurrentSubagents)。提供时参与 fail-fast 校验:\n\t * 必须 <= maxRunnerProcesses * maxSessionsPerRunner,否则超出容量的准入会以容量错误失败。\n\t */\n\tmaxConcurrentSubagents?: number;\n\t/** 就绪握手超时(默认 5000ms):runner 未在期限内应答 hello 视为启动失败。 */\n\thandshakeTimeoutMs?: number;\n\t/** 确定性时钟(默认 Date.now)。 */\n\tclock?: () => number;\n\tidleCheckIntervalMs?: number;\n\tonDiagnostic?(message: string): void;\n}\n\nexport interface SubagentRunnerPoolStatsV1 {\n\treadonly runners: number;\n\treadonly aliveRunners: number;\n\treadonly liveTasks: number;\n\treadonly spawnedTotal: number;\n}\n\nexport interface SubagentRunnerPoolV1 extends AgentTaskAdapter {\n\t/** D-044 §5.6:按 wire taskId(= host.session.getSessionId())订阅运行中进度。 */\n\tonTaskProgress(taskId: string, listener: (message: string | undefined, seq: number) => void): () => void;\n\t/** 空闲回收扫描(宿主心跳调用;也可由内部定时器驱动)。 */\n\tsweep(): void;\n\tstats(): SubagentRunnerPoolStatsV1;\n\tshutdown(): Promise<void>;\n}\n\nexport class SubagentRunnerPoolError extends Error {\n\treadonly code:\n\t\t| \"subagent_runner_capacity\"\n\t\t| \"subagent_runner_lost\"\n\t\t| \"subagent_pool_closed\"\n\t\t| \"subagent_pool_config_invalid\";\n\n\tconstructor(\n\t\tcode:\n\t\t\t| \"subagent_runner_capacity\"\n\t\t\t| \"subagent_runner_lost\"\n\t\t\t| \"subagent_pool_closed\"\n\t\t\t| \"subagent_pool_config_invalid\",\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"SubagentRunnerPoolError\";\n\t\tthis.code = code;\n\t}\n}\n\nconst MAX_RUNNER_PROCESSES = 5;\nconst DEFAULT_HANDSHAKE_TIMEOUT_MS = 5000;\n\n/**\n * 宿主接线 fail-fast 校验(D-044 §8):配置违规在构造期抛出,不静默钳制。\n * `maxRunnerProcesses = 0` 表示不启用 pooled(由宿主决定不构造本 adapter),\n * 因此本校验要求 >= 1。\n */\nexport function validateSubagentRunnerPoolOptionsV1(options: {\n\tmaxRunnerProcesses: number;\n\tmaxSessionsPerRunner: number;\n\tmaxConcurrentSubagents?: number;\n\tidleRunnerTimeoutMs: number;\n\thandshakeTimeoutMs?: number;\n}): void {\n\tconst fail = (message: string): never => {\n\t\tthrow new SubagentRunnerPoolError(\"subagent_pool_config_invalid\", message);\n\t};\n\tif (\n\t\t!Number.isInteger(options.maxRunnerProcesses) ||\n\t\toptions.maxRunnerProcesses < 1 ||\n\t\toptions.maxRunnerProcesses > MAX_RUNNER_PROCESSES\n\t) {\n\t\tfail(\n\t\t\t`maxRunnerProcesses must be an integer between 1 and ${MAX_RUNNER_PROCESSES}, got ${String(options.maxRunnerProcesses)}`,\n\t\t);\n\t}\n\tif (!Number.isInteger(options.maxSessionsPerRunner) || options.maxSessionsPerRunner < 1) {\n\t\tfail(`maxSessionsPerRunner must be a positive integer, got ${String(options.maxSessionsPerRunner)}`);\n\t}\n\tif (typeof options.maxConcurrentSubagents === \"number\") {\n\t\tif (!Number.isInteger(options.maxConcurrentSubagents) || options.maxConcurrentSubagents < 1) {\n\t\t\tfail(`maxConcurrentSubagents must be a positive integer, got ${String(options.maxConcurrentSubagents)}`);\n\t\t}\n\t\tconst capacity = options.maxRunnerProcesses * options.maxSessionsPerRunner;\n\t\tif (options.maxConcurrentSubagents > capacity) {\n\t\t\tfail(`maxConcurrentSubagents (${options.maxConcurrentSubagents}) exceeds runner capacity (${capacity})`);\n\t\t}\n\t}\n\tif (!Number.isInteger(options.idleRunnerTimeoutMs) || options.idleRunnerTimeoutMs < 1) {\n\t\tfail(`idleRunnerTimeoutMs must be a positive integer, got ${String(options.idleRunnerTimeoutMs)}`);\n\t}\n\tif (\n\t\toptions.handshakeTimeoutMs !== undefined &&\n\t\t(!Number.isInteger(options.handshakeTimeoutMs) || options.handshakeTimeoutMs < 1)\n\t) {\n\t\tfail(`handshakeTimeoutMs must be a positive integer, got ${String(options.handshakeTimeoutMs)}`);\n\t}\n}\n\n/**\n * 就绪握手:在把通道交给 remote adapter 之前做一次 hello 往返,确认 runner 存活\n * 且协议版本一致;超时/版本不符视为启动失败(调用方负责 kill)。\n */\nasync function handshakeRunnerV1(\n\tchannel: SubagentTaskChannelV1,\n\ttimeoutMs: number,\n\tsignal: AbortSignal,\n): Promise<void> {\n\tsignal.throwIfAborted();\n\tconst id = `hello:${randomUUID().slice(0, 8)}`;\n\tconst iterator = channel[Symbol.asyncIterator]();\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tconst timeout = new Promise<never>((_resolve, reject) => {\n\t\ttimer = setTimeout(\n\t\t\t() => reject(new SubagentTaskProtocolError(`runner handshake timed out after ${timeoutMs}ms`, \"runner_lost\")),\n\t\t\ttimeoutMs,\n\t\t);\n\t});\n\tlet rejectCancelled!: (reason: unknown) => void;\n\tconst cancelled = new Promise<never>((_resolve, reject) => {\n\t\trejectCancelled = reject;\n\t});\n\tconst onAbort = (): void => rejectCancelled(signal.reason);\n\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\ttry {\n\t\tawait Promise.race([\n\t\t\tchannel.write({ v: SUBAGENT_TASK_PROTOCOL_VERSION, kind: \"request\", id, op: \"hello\" }),\n\t\t\tcancelled,\n\t\t\ttimeout,\n\t\t]);\n\t\tfor (;;) {\n\t\t\tconst nextPromise = iterator.next();\n\t\t\tvoid nextPromise.catch(() => {});\n\t\t\tconst next = await Promise.race([nextPromise, timeout, cancelled]);\n\t\t\tif (next.done) {\n\t\t\t\tthrow new SubagentTaskProtocolError(\"runner closed during handshake\", \"runner_lost\");\n\t\t\t}\n\t\t\tconst env = next.value;\n\t\t\tif (env.kind !== \"response\" || env.id !== id) continue; // 握手期不应有其它信封:忽略等待\n\t\t\tif (!env.ok) {\n\t\t\t\tthrow new SubagentTaskProtocolError(`runner rejected handshake: ${env.message}`, env.code);\n\t\t\t}\n\t\t\tif (env.protocolVersion !== SUBAGENT_TASK_PROTOCOL_VERSION) {\n\t\t\t\tthrow new SubagentTaskProtocolError(\n\t\t\t\t\t`subagent-task protocol mismatch: runner=${String(env.protocolVersion)} host=${SUBAGENT_TASK_PROTOCOL_VERSION}`,\n\t\t\t\t\t\"invalid_request\",\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t} finally {\n\t\tif (timer !== undefined) clearTimeout(timer);\n\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t}\n}\n\ninterface PoolRunner {\n\treadonly adapter: AgentTaskAdapter;\n\treadonly handle: SubagentRunnerSpawnHandleV1;\n\treadonly liveTasks: Set<string>;\n\t/**\n\t * 分配中(adapter.create 已发起、回执未到)的预留槽位。并发 create 必须在等待\n\t * 回执期间就占位,否则同一 runner 会被超发(压测:40 个并发 create 全落在同一个\n\t * 刚 spawn 的 runner 上,扇出从未发生)。\n\t */\n\tpending: number;\n\talive: boolean;\n\tidleSince: number;\n}\n\nfunction slotsUsed(runner: PoolRunner): number {\n\treturn runner.liveTasks.size + runner.pending;\n}\n\nexport function createPooledAgentTaskAdapterV1(options: SubagentRunnerPoolOptionsV1): SubagentRunnerPoolV1 {\n\t// 配置违规在构造期 fail-fast(D-044 §8),不静默钳制。\n\tvalidateSubagentRunnerPoolOptionsV1(options);\n\tconst clock = options.clock ?? Date.now;\n\tconst handshakeTimeoutMs = options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;\n\tconst diagnostic = options.onDiagnostic ?? ((): void => {});\n\tconst runners: PoolRunner[] = [];\n\t/** wire taskId(= host.session.getSessionId())-> 承载它的 runner adapter,用于进度转发。 */\n\tconst taskRunnerAdapters = new Map<string, AgentTaskAdapter>();\n\tlet spawnedTotal = 0;\n\tlet closed = false;\n\tlet sweepTimer: ReturnType<typeof setInterval> | undefined;\n\tlet spawnInFlight: Promise<PoolRunner> | undefined;\n\tlet pendingSpawnHandle: SubagentRunnerSpawnHandleV1 | undefined;\n\tconst shutdownController = new AbortController();\n\n\t/** 摘除已死 runner(句柄与状态都不再需要),避免长寿命池按 spawn 次数增长。 */\n\tconst pruneDeadRunners = (): void => {\n\t\tfor (let index = runners.length - 1; index >= 0; index -= 1) {\n\t\t\tif (!runners[index]?.alive) runners.splice(index, 1);\n\t\t}\n\t};\n\n\tconst markDead = (runner: PoolRunner, error?: Error): void => {\n\t\tif (!runner.alive) return;\n\t\trunner.alive = false;\n\t\trunner.liveTasks.clear();\n\t\tdiagnostic(`subagent runner lost: ${error?.message ?? \"process exited\"}`);\n\t};\n\n\tconst trackRunner = (handle: SubagentRunnerSpawnHandleV1): PoolRunner => {\n\t\tconst runner: PoolRunner = {\n\t\t\tadapter: createRemoteAgentTaskAdapter(handle.channel),\n\t\t\thandle,\n\t\t\tliveTasks: new Set<string>(),\n\t\t\tpending: 0,\n\t\t\talive: true,\n\t\t\tidleSince: clock(),\n\t\t};\n\t\thandle.onExit((error?: Error) => markDead(runner, error));\n\t\trunners.push(runner);\n\t\tspawnedTotal += 1;\n\t\treturn runner;\n\t};\n\n\tconst spawnRunner = async (): Promise<PoolRunner> => {\n\t\tshutdownController.signal.throwIfAborted();\n\t\t// 并发 create 共享同一次 spawn,避免超发进程。\n\t\tspawnInFlight ??= (async () => {\n\t\t\tconst handle = await options.spawnRunner();\n\t\t\tpendingSpawnHandle = handle;\n\t\t\ttry {\n\t\t\t\tshutdownController.signal.throwIfAborted();\n\t\t\t\t// 就绪握手在把通道交给 remote adapter 之前完成(adapter 启动后独占读取)。\n\t\t\t\tawait handshakeRunnerV1(handle.channel, handshakeTimeoutMs, shutdownController.signal);\n\t\t\t\tshutdownController.signal.throwIfAborted();\n\t\t\t} catch (error) {\n\t\t\t\t// 握手失败:进程不可用,回收并上抛(调用方按启动失败处理)。\n\t\t\t\ttry {\n\t\t\t\t\thandle.kill();\n\t\t\t\t} catch {\n\t\t\t\t\t// 已退出:忽略\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\treturn trackRunner(handle);\n\t\t})().finally(() => {\n\t\t\t// 清理在赋值后的微任务执行;同步 spawn 抛错不能把失败 Promise 留在池中。\n\t\t\tspawnInFlight = undefined;\n\t\t\tpendingSpawnHandle = undefined;\n\t\t});\n\t\treturn spawnInFlight;\n\t};\n\n\tconst pickRunner = (): PoolRunner | undefined => {\n\t\tlet best: PoolRunner | undefined;\n\t\tlet bestFree = 0;\n\t\tfor (const runner of runners) {\n\t\t\tif (!runner.alive) continue;\n\t\t\tconst free = options.maxSessionsPerRunner - slotsUsed(runner);\n\t\t\tif (free > bestFree) {\n\t\t\t\tbest = runner;\n\t\t\t\tbestFree = free;\n\t\t\t}\n\t\t}\n\t\treturn best;\n\t};\n\n\tconst createOnRunner = async (runner: PoolRunner, request: AgentTaskCreateRequest): Promise<AgentTaskHost> => {\n\t\t// 首选和恢复 runner 共用准入,异步 spawn 完成不代表池仍可创建任务。\n\t\tshutdownController.signal.throwIfAborted();\n\t\trunner.pending += 1;\n\t\tlet host: AgentTaskHost;\n\t\ttry {\n\t\t\thost = await runner.adapter.create(request);\n\t\t} finally {\n\t\t\trunner.pending -= 1;\n\t\t}\n\t\t// 回执可能晚于 shutdown;关闭后的任务由 runner 回收,不再发布给调度器。\n\t\tshutdownController.signal.throwIfAborted();\n\t\tif (!runner.alive) {\n\t\t\tthrow new SubagentTaskProtocolError(\"subagent runner lost before task publication\", \"runner_lost\");\n\t\t}\n\t\tconst taskId = host.session.getSessionId();\n\t\trunner.liveTasks.add(taskId);\n\t\ttaskRunnerAdapters.set(taskId, runner.adapter);\n\t\trunner.idleSince = 0;\n\t\tconst remoteSession = host.session;\n\t\t// 包装 session:保留远程会话身份与 steer;dispose 时归还槽位。\n\t\t// 事实面(getActiveTools/getModel)必须一并透传:档案核验靠它自证,包装层吞掉\n\t\t// 就等于把\"已核验\"变成\"无法核验\"。\n\t\tconst remoteGetActiveTools = remoteSession.getActiveTools?.bind(remoteSession);\n\t\tconst session: SessionAPI = {\n\t\t\tgetSessionId: () => remoteSession.getSessionId(),\n\t\t\t...(remoteSession.getAgentSessionId === undefined\n\t\t\t\t? {}\n\t\t\t\t: { getAgentSessionId: remoteSession.getAgentSessionId.bind(remoteSession) }),\n\t\t\tgetMessages: () => remoteSession.getMessages(),\n\t\t\tisIdle: () => remoteSession.isIdle(),\n\t\t\tprompt: (text) => remoteSession.prompt(text),\n\t\t\t...(remoteSession.steer === undefined ? {} : { steer: remoteSession.steer.bind(remoteSession) }),\n\t\t\tabort: () => remoteSession.abort(),\n\t\t\twaitForIdle: () => remoteSession.waitForIdle(),\n\t\t\tappendEntry: (customType, data) => remoteSession.appendEntry(customType, data),\n\t\t\tgetBranchEntries: () => remoteSession.getBranchEntries(),\n\t\t\t...(remoteGetActiveTools === undefined ? {} : { getActiveTools: () => remoteGetActiveTools() }),\n\t\t\t...(remoteSession.getModel === undefined ? {} : { getModel: () => remoteSession.getModel?.() }),\n\t\t};\n\t\treturn {\n\t\t\tsession,\n\t\t\trun: (prompt, signal) => host.run(prompt, signal),\n\t\t\tcancel: (reason) => host.cancel(reason),\n\t\t\tdispose: async () => {\n\t\t\t\trunner.liveTasks.delete(taskId);\n\t\t\t\ttaskRunnerAdapters.delete(taskId);\n\t\t\t\tif (runner.liveTasks.size === 0) runner.idleSince = clock();\n\t\t\t\tawait host.dispose();\n\t\t\t},\n\t\t};\n\t};\n\n\tconst sweep = (): void => {\n\t\tif (closed) return;\n\t\tpruneDeadRunners();\n\t\tconst now = clock();\n\t\tfor (const runner of runners) {\n\t\t\t// pending > 0 = create 仍在途(真实 runner 的 spawn+握手+会话创建可达\n\t\t\t// 数秒):在途创建是活跃工作,不是空闲——sweep 不得打点也不得回收,\n\t\t\t// 否则激进 idleRunnerTimeoutMs 下 create 中途通道被杀(live 用例实证)。\n\t\t\tif (!runner.alive || runner.pending > 0 || runner.liveTasks.size > 0) continue;\n\t\t\tif (runner.idleSince === 0) runner.idleSince = now;\n\t\t\tif (now - runner.idleSince < options.idleRunnerTimeoutMs) continue;\n\t\t\t// 空闲超时回收:退出前必须无活跃任务(不迁移),dead 标记阻止后续分配。\n\t\t\trunner.alive = false;\n\t\t\tdiagnostic(\"subagent runner idle exit\");\n\t\t\ttry {\n\t\t\t\trunner.handle.kill();\n\t\t\t} catch {\n\t\t\t\t// 进程已退出:忽略\n\t\t\t}\n\t\t}\n\t};\n\n\tconst adapter: SubagentRunnerPoolV1 = {\n\t\t/** D-044 §5.6:转发到承载该任务的 runner 远程 adapter。 */\n\t\tonTaskProgress(taskId: string, listener: (message: string | undefined, seq: number) => void): () => void {\n\t\t\tconst runnerAdapter = taskRunnerAdapters.get(taskId) as\n\t\t\t\t| (AgentTaskAdapter & { onTaskProgress?: (id: string, cb: typeof listener) => () => void })\n\t\t\t\t| undefined;\n\t\t\treturn runnerAdapter?.onTaskProgress?.(taskId, listener) ?? (() => {});\n\t\t},\n\t\tasync create(request: AgentTaskCreateRequest): Promise<AgentTaskHost> {\n\t\t\tif (closed) throw new SubagentRunnerPoolError(\"subagent_pool_closed\", \"subagent runner pool is closed\");\n\t\t\tpruneDeadRunners();\n\t\t\t// 放置循环:并发 create 共享同一次 spawn,因此拿到共享结果后必须**重新**挑选\n\t\t\t// 有空槽的 runner(新 spawn 的那个同样有槽位上限),容量耗尽才报错。\n\t\t\tlet runner = pickRunner();\n\t\t\twhile (runner === undefined) {\n\t\t\t\tif (runners.filter((candidate) => candidate.alive).length >= options.maxRunnerProcesses) {\n\t\t\t\t\tthrow new SubagentRunnerPoolError(\n\t\t\t\t\t\t\"subagent_runner_capacity\",\n\t\t\t\t\t\t`no runner capacity: ${options.maxRunnerProcesses} runner(s) at ${options.maxSessionsPerRunner} session(s) each`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tawait spawnRunner();\n\t\t\t\tshutdownController.signal.throwIfAborted();\n\t\t\t\trunner = pickRunner();\n\t\t\t}\n\t\t\ttry {\n\t\t\t\treturn await createOnRunner(runner, request);\n\t\t\t} catch (error) {\n\t\t\t\tif (error instanceof SubagentTaskProtocolError && error.code === \"runner_lost\") {\n\t\t\t\t\tshutdownController.signal.throwIfAborted();\n\t\t\t\t\t// 分配期失败无副作用:换一个 runner 重试一次;无可用槽位且不能扩则上抛容量错误。\n\t\t\t\t\tmarkDead(runner, error);\n\t\t\t\t\tlet fallback = pickRunner();\n\t\t\t\t\twhile (fallback === undefined) {\n\t\t\t\t\t\tif (runners.filter((candidate) => candidate.alive).length >= options.maxRunnerProcesses) {\n\t\t\t\t\t\t\tthrow new SubagentRunnerPoolError(\"subagent_runner_lost\", \"runner lost and no spare capacity\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawait spawnRunner();\n\t\t\t\t\t\tshutdownController.signal.throwIfAborted();\n\t\t\t\t\t\t// 共享 spawn 的空槽可能已被另一恢复或普通创建预留,必须重新挑选。\n\t\t\t\t\t\tfallback = pickRunner();\n\t\t\t\t\t}\n\t\t\t\t\tif (fallback !== runner) return createOnRunner(fallback, request);\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t},\n\t\tsweep,\n\t\tstats(): SubagentRunnerPoolStatsV1 {\n\t\t\treturn {\n\t\t\t\trunners: runners.length,\n\t\t\t\taliveRunners: runners.filter((runner) => runner.alive).length,\n\t\t\t\tliveTasks: runners.reduce((total, runner) => total + runner.liveTasks.size, 0),\n\t\t\t\tspawnedTotal,\n\t\t\t};\n\t\t},\n\t\tasync shutdown(): Promise<void> {\n\t\t\tif (closed) return;\n\t\t\tclosed = true;\n\t\t\tshutdownController.abort(\n\t\t\t\tnew SubagentRunnerPoolError(\"subagent_pool_closed\", \"subagent runner pool is closed\"),\n\t\t\t);\n\t\t\tif (sweepTimer !== undefined) clearInterval(sweepTimer);\n\t\t\t// 正在 spawn 的句柄归 spawnRunner 回收;握手可取消,已登记 runner 在下面释放。\n\t\t\tif (pendingSpawnHandle !== undefined && spawnInFlight !== undefined) await Promise.allSettled([spawnInFlight]);\n\t\t\tconst alive = runners.filter((runner) => runner.alive);\n\t\t\tawait Promise.allSettled(alive.map((runner) => runner.adapter.dispose?.()));\n\t\t\tfor (const runner of alive) {\n\t\t\t\trunner.alive = false;\n\t\t\t\ttry {\n\t\t\t\t\trunner.handle.kill();\n\t\t\t\t} catch {\n\t\t\t\t\t// 已退出:忽略\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t};\n\n\tif (options.idleCheckIntervalMs !== undefined && options.idleCheckIntervalMs > 0) {\n\t\tsweepTimer = setInterval(sweep, options.idleCheckIntervalMs);\n\t\tsweepTimer.unref?.();\n\t}\n\n\treturn adapter;\n}\n"]}
1
+ {"version":3,"file":"runner-pool.d.ts","sourceRoot":"","sources":["../src/runner-pool.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,gBAAgB,EAAqD,MAAM,yBAAyB,CAAC;AACnH,OAAO,EAEN,KAAK,qBAAqB,EAE1B,MAAM,gCAAgC,CAAC;AAGxC,MAAM,WAAW,2BAA2B;IAC3C,sDAA4B;IAC5B,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAC;IACxC,6DAA6B;IAC7B,MAAM,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;IAC/C,gEAA0B;IAC1B,IAAI,IAAI,IAAI,CAAC;CACb;AAED,MAAM,WAAW,2BAA2B;IAC3C,WAAW,IAAI,OAAO,CAAC,2BAA2B,CAAC,GAAG,2BAA2B,CAAC;IAClF,kBAAkB,EAAE,MAAM,CAAC;IAC3B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,mBAAmB,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,uGAAqD;IACrD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,8CAA0B;IAC1B,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;IACrB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,YAAY,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,yBAAyB;IACzC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,oBAAqB,SAAQ,gBAAgB;IAC7D,+FAAsE;IACtE,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACzG,qFAAiC;IACjC,KAAK,IAAI,IAAI,CAAC;IACd,KAAK,IAAI,yBAAyB,CAAC;IACnC,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,qBAAa,uBAAwB,SAAQ,KAAK;IACjD,QAAQ,CAAC,IAAI,EACV,0BAA0B,GAC1B,sBAAsB,GACtB,sBAAsB,GACtB,8BAA8B,CAAC;IAElC,YACC,IAAI,EACD,0BAA0B,GAC1B,sBAAsB,GACtB,sBAAsB,GACtB,8BAA8B,EACjC,OAAO,EAAE,MAAM,EAKf;CACD;AAKD;;;;GAIG;AACH,wBAAgB,mCAAmC,CAAC,OAAO,EAAE;IAC5D,kBAAkB,EAAE,MAAM,CAAC;IAC3B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B,GAAG,IAAI,CAkCP;AA6ED,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,2BAA2B,GAAG,oBAAoB,CAyPzG","sourcesContent":["/**\n * 池化 runner adapter(D-045 / pooled 切片增量 2)。\n *\n * `AgentTaskAdapter` 的 runner 池实现:按 `maxSessionsPerRunner` 选\"空闲槽位最多\"\n * 的 runner(确定性);无空闲槽位且未达 `maxRunnerProcesses` 时按需 spawn;runner\n * 空闲超时退出(不迁移任务);进程死亡时在途任务以 `runner_lost` 结算(D-044 §4.3\n * 投影 failed + reasonCode subagent_runner_lost)。\n *\n * 分层约束:调度器(SubagentRuntime)继续持有队列与准入;本层只提供\"容量 + 进程\n * 生命周期\"。宿主接线必须保证 `maxConcurrentSubagents <= maxRunnerProcesses *\n * maxSessionsPerRunner`,否则超出容量的准入会以 `subagent_runner_capacity` 失败。\n * spawn 由 `spawnRunner` 注入:生产侧经 stdio 派生子进程,测试注入进程内 fake。\n *\n * 迁自宿主 `src/plugins/subagent-task/runner-pool.ts`(D-075 S4 第三批);公共契约改自\n * plugin-sdk,wire 协议类型改自 @agent-forge/protocol(宿主侧同名模块已下沉该包)。\n */\nimport { randomUUID } from \"node:crypto\";\nimport type { AgentTaskAdapter, AgentTaskCreateRequest, AgentTaskHost, SessionAPI } from \"@agent-forge/plugin-sdk\";\nimport {\n\tSUBAGENT_TASK_PROTOCOL_VERSION,\n\ttype SubagentTaskChannelV1,\n\tSubagentTaskProtocolError,\n} from \"@agent-forge/protocol/subagent\";\nimport { createRemoteAgentTaskAdapter } from \"./remote-adapter.ts\";\n\nexport interface SubagentRunnerSpawnHandleV1 {\n\t/** 与 runner 进程对话的通道(父侧)。 */\n\treadonly channel: SubagentTaskChannelV1;\n\t/** 进程退出通知:error 非空表示异常退出。 */\n\tonExit(handler: (error?: Error) => void): void;\n\t/** 请求进程退出(空闲回收与池关闭使用)。 */\n\tkill(): void;\n}\n\nexport interface SubagentRunnerPoolOptionsV1 {\n\tspawnRunner(): Promise<SubagentRunnerSpawnHandleV1> | SubagentRunnerSpawnHandleV1;\n\tmaxRunnerProcesses: number;\n\tmaxSessionsPerRunner: number;\n\tidleRunnerTimeoutMs: number;\n\t/**\n\t * 宿主全局并发(调度器 maxConcurrentSubagents)。提供时参与 fail-fast 校验:\n\t * 必须 <= maxRunnerProcesses * maxSessionsPerRunner,否则超出容量的准入会以容量错误失败。\n\t */\n\tmaxConcurrentSubagents?: number;\n\t/** 就绪握手超时(默认 5000ms):runner 未在期限内应答 hello 视为启动失败。 */\n\thandshakeTimeoutMs?: number;\n\t/** 确定性时钟(默认 Date.now)。 */\n\tclock?: () => number;\n\tidleCheckIntervalMs?: number;\n\tonDiagnostic?(message: string): void;\n}\n\nexport interface SubagentRunnerPoolStatsV1 {\n\treadonly runners: number;\n\treadonly aliveRunners: number;\n\treadonly liveTasks: number;\n\treadonly spawnedTotal: number;\n}\n\nexport interface SubagentRunnerPoolV1 extends AgentTaskAdapter {\n\t/** D-044 §5.6:按 wire taskId(= host.session.getSessionId())订阅运行中进度。 */\n\tonTaskProgress(taskId: string, listener: (message: string | undefined, seq: number) => void): () => void;\n\t/** 空闲回收扫描(宿主心跳调用;也可由内部定时器驱动)。 */\n\tsweep(): void;\n\tstats(): SubagentRunnerPoolStatsV1;\n\tshutdown(): Promise<void>;\n}\n\nexport class SubagentRunnerPoolError extends Error {\n\treadonly code:\n\t\t| \"subagent_runner_capacity\"\n\t\t| \"subagent_runner_lost\"\n\t\t| \"subagent_pool_closed\"\n\t\t| \"subagent_pool_config_invalid\";\n\n\tconstructor(\n\t\tcode:\n\t\t\t| \"subagent_runner_capacity\"\n\t\t\t| \"subagent_runner_lost\"\n\t\t\t| \"subagent_pool_closed\"\n\t\t\t| \"subagent_pool_config_invalid\",\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"SubagentRunnerPoolError\";\n\t\tthis.code = code;\n\t}\n}\n\nconst MAX_RUNNER_PROCESSES = 5;\nconst DEFAULT_HANDSHAKE_TIMEOUT_MS = 5000;\n\n/**\n * 宿主接线 fail-fast 校验(D-044 §8):配置违规在构造期抛出,不静默钳制。\n * `maxRunnerProcesses = 0` 表示不启用 pooled(由宿主决定不构造本 adapter),\n * 因此本校验要求 >= 1。\n */\nexport function validateSubagentRunnerPoolOptionsV1(options: {\n\tmaxRunnerProcesses: number;\n\tmaxSessionsPerRunner: number;\n\tmaxConcurrentSubagents?: number;\n\tidleRunnerTimeoutMs: number;\n\thandshakeTimeoutMs?: number;\n}): void {\n\tconst fail = (message: string): never => {\n\t\tthrow new SubagentRunnerPoolError(\"subagent_pool_config_invalid\", message);\n\t};\n\tif (\n\t\t!Number.isInteger(options.maxRunnerProcesses) ||\n\t\toptions.maxRunnerProcesses < 1 ||\n\t\toptions.maxRunnerProcesses > MAX_RUNNER_PROCESSES\n\t) {\n\t\tfail(\n\t\t\t`maxRunnerProcesses must be an integer between 1 and ${MAX_RUNNER_PROCESSES}, got ${String(options.maxRunnerProcesses)}`,\n\t\t);\n\t}\n\tif (!Number.isInteger(options.maxSessionsPerRunner) || options.maxSessionsPerRunner < 1) {\n\t\tfail(`maxSessionsPerRunner must be a positive integer, got ${String(options.maxSessionsPerRunner)}`);\n\t}\n\tif (typeof options.maxConcurrentSubagents === \"number\") {\n\t\tif (!Number.isInteger(options.maxConcurrentSubagents) || options.maxConcurrentSubagents < 1) {\n\t\t\tfail(`maxConcurrentSubagents must be a positive integer, got ${String(options.maxConcurrentSubagents)}`);\n\t\t}\n\t\tconst capacity = options.maxRunnerProcesses * options.maxSessionsPerRunner;\n\t\tif (options.maxConcurrentSubagents > capacity) {\n\t\t\tfail(`maxConcurrentSubagents (${options.maxConcurrentSubagents}) exceeds runner capacity (${capacity})`);\n\t\t}\n\t}\n\tif (!Number.isInteger(options.idleRunnerTimeoutMs) || options.idleRunnerTimeoutMs < 1) {\n\t\tfail(`idleRunnerTimeoutMs must be a positive integer, got ${String(options.idleRunnerTimeoutMs)}`);\n\t}\n\tif (\n\t\toptions.handshakeTimeoutMs !== undefined &&\n\t\t(!Number.isInteger(options.handshakeTimeoutMs) || options.handshakeTimeoutMs < 1)\n\t) {\n\t\tfail(`handshakeTimeoutMs must be a positive integer, got ${String(options.handshakeTimeoutMs)}`);\n\t}\n}\n\n/**\n * 就绪握手:在把通道交给 remote adapter 之前做一次 hello 往返,确认 runner 存活\n * 且协议版本一致;超时/版本不符视为启动失败(调用方负责 kill)。\n */\nasync function handshakeRunnerV1(\n\tchannel: SubagentTaskChannelV1,\n\ttimeoutMs: number,\n\tsignal: AbortSignal,\n): Promise<void> {\n\tsignal.throwIfAborted();\n\tconst id = `hello:${randomUUID().slice(0, 8)}`;\n\tconst iterator = channel[Symbol.asyncIterator]();\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tconst timeout = new Promise<never>((_resolve, reject) => {\n\t\ttimer = setTimeout(\n\t\t\t() => reject(new SubagentTaskProtocolError(`runner handshake timed out after ${timeoutMs}ms`, \"runner_lost\")),\n\t\t\ttimeoutMs,\n\t\t);\n\t});\n\tlet rejectCancelled!: (reason: unknown) => void;\n\tconst cancelled = new Promise<never>((_resolve, reject) => {\n\t\trejectCancelled = reject;\n\t});\n\tconst onAbort = (): void => rejectCancelled(signal.reason);\n\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\ttry {\n\t\tawait Promise.race([\n\t\t\tchannel.write({ v: SUBAGENT_TASK_PROTOCOL_VERSION, kind: \"request\", id, op: \"hello\" }),\n\t\t\tcancelled,\n\t\t\ttimeout,\n\t\t]);\n\t\tfor (;;) {\n\t\t\tconst nextPromise = iterator.next();\n\t\t\tvoid nextPromise.catch(() => {});\n\t\t\tconst next = await Promise.race([nextPromise, timeout, cancelled]);\n\t\t\tif (next.done) {\n\t\t\t\tthrow new SubagentTaskProtocolError(\"runner closed during handshake\", \"runner_lost\");\n\t\t\t}\n\t\t\tconst env = next.value;\n\t\t\tif (env.kind !== \"response\" || env.id !== id) continue; // 握手期不应有其它信封:忽略等待\n\t\t\tif (!env.ok) {\n\t\t\t\tthrow new SubagentTaskProtocolError(`runner rejected handshake: ${env.message}`, env.code);\n\t\t\t}\n\t\t\tif (env.protocolVersion !== SUBAGENT_TASK_PROTOCOL_VERSION) {\n\t\t\t\tthrow new SubagentTaskProtocolError(\n\t\t\t\t\t`subagent-task protocol mismatch: runner=${String(env.protocolVersion)} host=${SUBAGENT_TASK_PROTOCOL_VERSION}`,\n\t\t\t\t\t\"invalid_request\",\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t} finally {\n\t\tif (timer !== undefined) clearTimeout(timer);\n\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t}\n}\n\ninterface PoolRunner {\n\treadonly adapter: AgentTaskAdapter;\n\treadonly handle: SubagentRunnerSpawnHandleV1;\n\treadonly liveTasks: Set<string>;\n\t/**\n\t * 分配中(adapter.create 已发起、回执未到)的预留槽位。并发 create 必须在等待\n\t * 回执期间就占位,否则同一 runner 会被超发(压测:40 个并发 create 全落在同一个\n\t * 刚 spawn 的 runner 上,扇出从未发生)。\n\t */\n\tpending: number;\n\talive: boolean;\n\tidleSince: number;\n}\n\nfunction slotsUsed(runner: PoolRunner): number {\n\treturn runner.liveTasks.size + runner.pending;\n}\n\nexport function createPooledAgentTaskAdapterV1(options: SubagentRunnerPoolOptionsV1): SubagentRunnerPoolV1 {\n\t// 配置违规在构造期 fail-fast(D-044 §8),不静默钳制。\n\tvalidateSubagentRunnerPoolOptionsV1(options);\n\tconst clock = options.clock ?? Date.now;\n\tconst handshakeTimeoutMs = options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;\n\tconst diagnostic = options.onDiagnostic ?? ((): void => {});\n\tconst runners: PoolRunner[] = [];\n\t/** wire taskId(= host.session.getSessionId())-> 承载它的 runner adapter,用于进度转发。 */\n\tconst taskRunnerAdapters = new Map<string, AgentTaskAdapter>();\n\tlet spawnedTotal = 0;\n\tlet closed = false;\n\tlet sweepTimer: ReturnType<typeof setInterval> | undefined;\n\tlet spawnInFlight: Promise<PoolRunner> | undefined;\n\tlet pendingSpawnHandle: SubagentRunnerSpawnHandleV1 | undefined;\n\tconst shutdownController = new AbortController();\n\n\t/** 摘除已死 runner(句柄与状态都不再需要),避免长寿命池按 spawn 次数增长。 */\n\tconst pruneDeadRunners = (): void => {\n\t\tfor (let index = runners.length - 1; index >= 0; index -= 1) {\n\t\t\tif (!runners[index]?.alive) runners.splice(index, 1);\n\t\t}\n\t};\n\n\tconst markDead = (runner: PoolRunner, error?: Error): void => {\n\t\tif (!runner.alive) return;\n\t\trunner.alive = false;\n\t\trunner.liveTasks.clear();\n\t\tdiagnostic(`subagent runner lost: ${error?.message ?? \"process exited\"}`);\n\t};\n\n\tconst trackRunner = (handle: SubagentRunnerSpawnHandleV1): PoolRunner => {\n\t\tconst runner: PoolRunner = {\n\t\t\tadapter: createRemoteAgentTaskAdapter(handle.channel),\n\t\t\thandle,\n\t\t\tliveTasks: new Set<string>(),\n\t\t\tpending: 0,\n\t\t\talive: true,\n\t\t\tidleSince: clock(),\n\t\t};\n\t\thandle.onExit((error?: Error) => markDead(runner, error));\n\t\trunners.push(runner);\n\t\tspawnedTotal += 1;\n\t\treturn runner;\n\t};\n\n\tconst spawnRunner = async (): Promise<PoolRunner> => {\n\t\tshutdownController.signal.throwIfAborted();\n\t\t// 并发 create 共享同一次 spawn,避免超发进程。\n\t\tspawnInFlight ??= (async () => {\n\t\t\tconst handle = await options.spawnRunner();\n\t\t\tpendingSpawnHandle = handle;\n\t\t\ttry {\n\t\t\t\tshutdownController.signal.throwIfAborted();\n\t\t\t\t// 就绪握手在把通道交给 remote adapter 之前完成(adapter 启动后独占读取)。\n\t\t\t\tawait handshakeRunnerV1(handle.channel, handshakeTimeoutMs, shutdownController.signal);\n\t\t\t\tshutdownController.signal.throwIfAborted();\n\t\t\t} catch (error) {\n\t\t\t\t// 握手失败:进程不可用,回收并上抛(调用方按启动失败处理)。\n\t\t\t\ttry {\n\t\t\t\t\thandle.kill();\n\t\t\t\t} catch {\n\t\t\t\t\t// 已退出:忽略\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\treturn trackRunner(handle);\n\t\t})().finally(() => {\n\t\t\t// 清理在赋值后的微任务执行;同步 spawn 抛错不能把失败 Promise 留在池中。\n\t\t\tspawnInFlight = undefined;\n\t\t\tpendingSpawnHandle = undefined;\n\t\t});\n\t\treturn spawnInFlight;\n\t};\n\n\tconst pickRunner = (): PoolRunner | undefined => {\n\t\tlet best: PoolRunner | undefined;\n\t\tlet bestFree = 0;\n\t\tfor (const runner of runners) {\n\t\t\tif (!runner.alive) continue;\n\t\t\tconst free = options.maxSessionsPerRunner - slotsUsed(runner);\n\t\t\tif (free > bestFree) {\n\t\t\t\tbest = runner;\n\t\t\t\tbestFree = free;\n\t\t\t}\n\t\t}\n\t\treturn best;\n\t};\n\n\tconst createOnRunner = async (runner: PoolRunner, request: AgentTaskCreateRequest): Promise<AgentTaskHost> => {\n\t\t// 首选和恢复 runner 共用准入,异步 spawn 完成不代表池仍可创建任务。\n\t\tshutdownController.signal.throwIfAborted();\n\t\trunner.pending += 1;\n\t\tlet host: AgentTaskHost;\n\t\ttry {\n\t\t\thost = await runner.adapter.create(request);\n\t\t} finally {\n\t\t\trunner.pending -= 1;\n\t\t}\n\t\t// 回执可能晚于 shutdown;关闭后的任务由 runner 回收,不再发布给调度器。\n\t\tshutdownController.signal.throwIfAborted();\n\t\tif (!runner.alive) {\n\t\t\tthrow new SubagentTaskProtocolError(\"subagent runner lost before task publication\", \"runner_lost\");\n\t\t}\n\t\tconst taskId = host.session.getSessionId();\n\t\trunner.liveTasks.add(taskId);\n\t\ttaskRunnerAdapters.set(taskId, runner.adapter);\n\t\trunner.idleSince = 0;\n\t\tconst remoteSession = host.session;\n\t\t// 包装 session:保留远程会话身份与 steer;dispose 时归还槽位。\n\t\t// 事实面(getActiveTools/getModel)必须一并透传:档案核验靠它自证,包装层吞掉\n\t\t// 就等于把\"已核验\"变成\"无法核验\"。\n\t\tconst remoteGetActiveTools = remoteSession.getActiveTools?.bind(remoteSession);\n\t\tconst remoteGetWebSearch = remoteSession.getWebSearch?.bind(remoteSession);\n\t\tconst session: SessionAPI = {\n\t\t\tgetSessionId: () => remoteSession.getSessionId(),\n\t\t\t...(remoteSession.getAgentSessionId === undefined\n\t\t\t\t? {}\n\t\t\t\t: { getAgentSessionId: remoteSession.getAgentSessionId.bind(remoteSession) }),\n\t\t\tgetMessages: () => remoteSession.getMessages(),\n\t\t\tisIdle: () => remoteSession.isIdle(),\n\t\t\tprompt: (text) => remoteSession.prompt(text),\n\t\t\t...(remoteSession.steer === undefined ? {} : { steer: remoteSession.steer.bind(remoteSession) }),\n\t\t\tabort: () => remoteSession.abort(),\n\t\t\twaitForIdle: () => remoteSession.waitForIdle(),\n\t\t\tappendEntry: (customType, data) => remoteSession.appendEntry(customType, data),\n\t\t\tgetBranchEntries: () => remoteSession.getBranchEntries(),\n\t\t\t...(remoteGetActiveTools === undefined ? {} : { getActiveTools: () => remoteGetActiveTools() }),\n\t\t\t...(remoteSession.getModel === undefined ? {} : { getModel: () => remoteSession.getModel?.() }),\n\t\t\t...(remoteGetWebSearch === undefined ? {} : { getWebSearch: () => remoteGetWebSearch() }),\n\t\t};\n\t\treturn {\n\t\t\tsession,\n\t\t\trun: (prompt, signal) => host.run(prompt, signal),\n\t\t\tcancel: (reason) => host.cancel(reason),\n\t\t\tdispose: async () => {\n\t\t\t\trunner.liveTasks.delete(taskId);\n\t\t\t\ttaskRunnerAdapters.delete(taskId);\n\t\t\t\tif (runner.liveTasks.size === 0) runner.idleSince = clock();\n\t\t\t\tawait host.dispose();\n\t\t\t},\n\t\t};\n\t};\n\n\tconst sweep = (): void => {\n\t\tif (closed) return;\n\t\tpruneDeadRunners();\n\t\tconst now = clock();\n\t\tfor (const runner of runners) {\n\t\t\t// pending > 0 = create 仍在途(真实 runner 的 spawn+握手+会话创建可达\n\t\t\t// 数秒):在途创建是活跃工作,不是空闲——sweep 不得打点也不得回收,\n\t\t\t// 否则激进 idleRunnerTimeoutMs 下 create 中途通道被杀(live 用例实证)。\n\t\t\tif (!runner.alive || runner.pending > 0 || runner.liveTasks.size > 0) continue;\n\t\t\tif (runner.idleSince === 0) runner.idleSince = now;\n\t\t\tif (now - runner.idleSince < options.idleRunnerTimeoutMs) continue;\n\t\t\t// 空闲超时回收:退出前必须无活跃任务(不迁移),dead 标记阻止后续分配。\n\t\t\trunner.alive = false;\n\t\t\tdiagnostic(\"subagent runner idle exit\");\n\t\t\ttry {\n\t\t\t\trunner.handle.kill();\n\t\t\t} catch {\n\t\t\t\t// 进程已退出:忽略\n\t\t\t}\n\t\t}\n\t};\n\n\tconst adapter: SubagentRunnerPoolV1 = {\n\t\t/** D-044 §5.6:转发到承载该任务的 runner 远程 adapter。 */\n\t\tonTaskProgress(taskId: string, listener: (message: string | undefined, seq: number) => void): () => void {\n\t\t\tconst runnerAdapter = taskRunnerAdapters.get(taskId) as\n\t\t\t\t| (AgentTaskAdapter & { onTaskProgress?: (id: string, cb: typeof listener) => () => void })\n\t\t\t\t| undefined;\n\t\t\treturn runnerAdapter?.onTaskProgress?.(taskId, listener) ?? (() => {});\n\t\t},\n\t\tasync create(request: AgentTaskCreateRequest): Promise<AgentTaskHost> {\n\t\t\tif (closed) throw new SubagentRunnerPoolError(\"subagent_pool_closed\", \"subagent runner pool is closed\");\n\t\t\tpruneDeadRunners();\n\t\t\t// 放置循环:并发 create 共享同一次 spawn,因此拿到共享结果后必须**重新**挑选\n\t\t\t// 有空槽的 runner(新 spawn 的那个同样有槽位上限),容量耗尽才报错。\n\t\t\tlet runner = pickRunner();\n\t\t\twhile (runner === undefined) {\n\t\t\t\tif (runners.filter((candidate) => candidate.alive).length >= options.maxRunnerProcesses) {\n\t\t\t\t\tthrow new SubagentRunnerPoolError(\n\t\t\t\t\t\t\"subagent_runner_capacity\",\n\t\t\t\t\t\t`no runner capacity: ${options.maxRunnerProcesses} runner(s) at ${options.maxSessionsPerRunner} session(s) each`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tawait spawnRunner();\n\t\t\t\tshutdownController.signal.throwIfAborted();\n\t\t\t\trunner = pickRunner();\n\t\t\t}\n\t\t\ttry {\n\t\t\t\treturn await createOnRunner(runner, request);\n\t\t\t} catch (error) {\n\t\t\t\tif (error instanceof SubagentTaskProtocolError && error.code === \"runner_lost\") {\n\t\t\t\t\tshutdownController.signal.throwIfAborted();\n\t\t\t\t\t// 分配期失败无副作用:换一个 runner 重试一次;无可用槽位且不能扩则上抛容量错误。\n\t\t\t\t\tmarkDead(runner, error);\n\t\t\t\t\tlet fallback = pickRunner();\n\t\t\t\t\twhile (fallback === undefined) {\n\t\t\t\t\t\tif (runners.filter((candidate) => candidate.alive).length >= options.maxRunnerProcesses) {\n\t\t\t\t\t\t\tthrow new SubagentRunnerPoolError(\"subagent_runner_lost\", \"runner lost and no spare capacity\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawait spawnRunner();\n\t\t\t\t\t\tshutdownController.signal.throwIfAborted();\n\t\t\t\t\t\t// 共享 spawn 的空槽可能已被另一恢复或普通创建预留,必须重新挑选。\n\t\t\t\t\t\tfallback = pickRunner();\n\t\t\t\t\t}\n\t\t\t\t\tif (fallback !== runner) return createOnRunner(fallback, request);\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t},\n\t\tsweep,\n\t\tstats(): SubagentRunnerPoolStatsV1 {\n\t\t\treturn {\n\t\t\t\trunners: runners.length,\n\t\t\t\taliveRunners: runners.filter((runner) => runner.alive).length,\n\t\t\t\tliveTasks: runners.reduce((total, runner) => total + runner.liveTasks.size, 0),\n\t\t\t\tspawnedTotal,\n\t\t\t};\n\t\t},\n\t\tasync shutdown(): Promise<void> {\n\t\t\tif (closed) return;\n\t\t\tclosed = true;\n\t\t\tshutdownController.abort(\n\t\t\t\tnew SubagentRunnerPoolError(\"subagent_pool_closed\", \"subagent runner pool is closed\"),\n\t\t\t);\n\t\t\tif (sweepTimer !== undefined) clearInterval(sweepTimer);\n\t\t\t// 正在 spawn 的句柄归 spawnRunner 回收;握手可取消,已登记 runner 在下面释放。\n\t\t\tif (pendingSpawnHandle !== undefined && spawnInFlight !== undefined) await Promise.allSettled([spawnInFlight]);\n\t\t\tconst alive = runners.filter((runner) => runner.alive);\n\t\t\tawait Promise.allSettled(alive.map((runner) => runner.adapter.dispose?.()));\n\t\t\tfor (const runner of alive) {\n\t\t\t\trunner.alive = false;\n\t\t\t\ttry {\n\t\t\t\t\trunner.handle.kill();\n\t\t\t\t} catch {\n\t\t\t\t\t// 已退出:忽略\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t};\n\n\tif (options.idleCheckIntervalMs !== undefined && options.idleCheckIntervalMs > 0) {\n\t\tsweepTimer = setInterval(sweep, options.idleCheckIntervalMs);\n\t\tsweepTimer.unref?.();\n\t}\n\n\treturn adapter;\n}\n"]}
@@ -225,6 +225,7 @@ export function createPooledAgentTaskAdapterV1(options) {
225
225
  // 事实面(getActiveTools/getModel)必须一并透传:档案核验靠它自证,包装层吞掉
226
226
  // 就等于把"已核验"变成"无法核验"。
227
227
  const remoteGetActiveTools = remoteSession.getActiveTools?.bind(remoteSession);
228
+ const remoteGetWebSearch = remoteSession.getWebSearch?.bind(remoteSession);
228
229
  const session = {
229
230
  getSessionId: () => remoteSession.getSessionId(),
230
231
  ...(remoteSession.getAgentSessionId === undefined
@@ -240,6 +241,7 @@ export function createPooledAgentTaskAdapterV1(options) {
240
241
  getBranchEntries: () => remoteSession.getBranchEntries(),
241
242
  ...(remoteGetActiveTools === undefined ? {} : { getActiveTools: () => remoteGetActiveTools() }),
242
243
  ...(remoteSession.getModel === undefined ? {} : { getModel: () => remoteSession.getModel?.() }),
244
+ ...(remoteGetWebSearch === undefined ? {} : { getWebSearch: () => remoteGetWebSearch() }),
243
245
  };
244
246
  return {
245
247
  session,