@ai-agent-forge/plugin-subagent-delegate 0.85.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +41 -0
  2. package/agent-forge.json +11 -0
  3. package/dist/delegate.d.ts +275 -0
  4. package/dist/delegate.d.ts.map +1 -0
  5. package/dist/delegate.js +1376 -0
  6. package/dist/delegate.js.map +1 -0
  7. package/dist/entry.d.ts +28 -0
  8. package/dist/entry.d.ts.map +1 -0
  9. package/dist/entry.js +101 -0
  10. package/dist/entry.js.map +1 -0
  11. package/dist/index.d.ts +29 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +22 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/multi-agent-tasks.d.ts +101 -0
  16. package/dist/multi-agent-tasks.d.ts.map +1 -0
  17. package/dist/multi-agent-tasks.js +178 -0
  18. package/dist/multi-agent-tasks.js.map +1 -0
  19. package/dist/remote-adapter.d.ts +11 -0
  20. package/dist/remote-adapter.d.ts.map +1 -0
  21. package/dist/remote-adapter.js +366 -0
  22. package/dist/remote-adapter.js.map +1 -0
  23. package/dist/runner-pool.d.ts +59 -0
  24. package/dist/runner-pool.d.ts.map +1 -0
  25. package/dist/runner-pool.js +330 -0
  26. package/dist/runner-pool.js.map +1 -0
  27. package/dist/runner-spawn.d.ts +23 -0
  28. package/dist/runner-spawn.d.ts.map +1 -0
  29. package/dist/runner-spawn.js +79 -0
  30. package/dist/runner-spawn.js.map +1 -0
  31. package/dist/self-invocation.d.ts +20 -0
  32. package/dist/self-invocation.d.ts.map +1 -0
  33. package/dist/self-invocation.js +13 -0
  34. package/dist/self-invocation.js.map +1 -0
  35. package/dist/shared-adapter.d.ts +29 -0
  36. package/dist/shared-adapter.d.ts.map +1 -0
  37. package/dist/shared-adapter.js +60 -0
  38. package/dist/shared-adapter.js.map +1 -0
  39. package/dist/subagent-runtime.d.ts +162 -0
  40. package/dist/subagent-runtime.d.ts.map +1 -0
  41. package/dist/subagent-runtime.js +849 -0
  42. package/dist/subagent-runtime.js.map +1 -0
  43. package/package.json +55 -0
  44. package/plugin.json +27 -0
@@ -0,0 +1,1376 @@
1
+ /**
2
+ * First-party builtin subagent-delegate capability: the first production
3
+ * orchestrator over the plugin-level `SubagentRuntime` (D-044).
4
+ *
5
+ * The capability consumes only the public face — `session`, `agents`,
6
+ * `sessionAbortSignal` and `tools` — and owns exactly one scheduler instance
7
+ * per plugin load. It adds no scheduling policy of its own: queue, admission,
8
+ * budgets, terminal states and the parent cancel fence all stay in the
9
+ * scheduler; this module is the model-facing `subagent_run` tool plus a bounded
10
+ * result projection (status / reason codes / message counts / short summary).
11
+ * Raw child messages never enter the tool result (context budget spec).
12
+ *
13
+ * The scheduler's deadline fence is tick-driven by design (no hidden timers),
14
+ * and this consumer has no tick driver of its own, so the tool exposes no
15
+ * `deadlineMs` parameter: a bound that nothing enforces would be a false
16
+ * affordance. Wait bounds are `taskTimeoutMs` (drain) and the host's
17
+ * `limits.drainTimeoutMs` inside the scheduler.
18
+ *
19
+ * 迁自宿主 `src/capabilities/subagent-delegate.ts`(D-075 S4 第三批拆包)。除 import
20
+ * 来源外保持宿主行为:公共契约(agent-task 键/档案、通信接线、血缘读取、快照)改自
21
+ * plugin-sdk;信箱类型/错误改自 @agent-forge/protocol;调度器与 runner 模块改包内
22
+ * 相对路径。三处语义收敛见 `SubagentDelegateCapabilityOptions` 与 createCapability
23
+ * 内的迁移注记(agentDir 单源、runnerEntryAuto 驱动自动池化、lineage 深度经 SDK 读取)。
24
+ */
25
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
26
+ import { join } from "node:path";
27
+ import { AGENT_TASK_CHILD_PROFILE_KEY, AGENT_TASK_PARENT_SESSION_KEY, AGENT_TASK_PARENT_TRACE_KEY, AGENT_TASK_RESUME_SESSION_KEY, createDisposableRegistration, detachedSessionSnapshot, EXPERIMENTAL_PUBLIC_API_VERSION, parseAgentTaskChildProfileV1, } from "@agent-forge/plugin-sdk";
28
+ import { readAgentSessionLineage } from "@agent-forge/plugin-sdk/session-lineage";
29
+ import { SUBAGENT_MAILBOX_MAX_MESSAGE_BYTES, SubagentMailboxErrorV1, subscribeTaskProgressV1, } from "@agent-forge/protocol/subagent";
30
+ import { Type } from "typebox";
31
+ import { createPooledAgentTaskAdapterV1, } from "./runner-pool.js";
32
+ import { createNodeSubagentRunnerSpawnerV1 } from "./runner-spawn.js";
33
+ import { resolveSelfInvocationV1 } from "./self-invocation.js";
34
+ import { createAgentTaskApiAdapterV1 } from "./shared-adapter.js";
35
+ import { createSubagentRuntime, SubagentRuntimeError, } from "./subagent-runtime.js";
36
+ export const capabilityManifest = {
37
+ id: "agent-forge.builtin.subagent-delegate",
38
+ version: "0.1.0",
39
+ apiVersion: EXPERIMENTAL_PUBLIC_API_VERSION,
40
+ // `agents` 是可选能力:runner 的叶子会话刻意关闭 agent tasks(enableAgentTasks: false),
41
+ // 此时本能力"不适用"而非"加载失败",缺失时静默不注册工具,避免每个叶子会话刷拒绝警告。
42
+ requiredCapabilities: ["session", "tools"],
43
+ optionalCapabilities: ["agents"],
44
+ /**
45
+ * 用户可配置默认值(D-044 §6 四层归属的 Profile/capability 配置层):
46
+ * 经 agent-dir 配置通道 `<agentDir>/capabilities/builtin.subagent-delegate.json`
47
+ * 浅合并覆盖;宿主显式 options 优先级最高;非法值在装载时 fail fast。
48
+ */
49
+ config: {
50
+ maxConcurrentSubagents: 8,
51
+ maxQueueDepth: 200,
52
+ maxPromptBytes: 65536,
53
+ maxMetadataBytes: 16384,
54
+ maxResultBytes: 524288,
55
+ maxMessages: 2000,
56
+ drainTimeoutMs: 30000,
57
+ maxRetainedRecords: 256,
58
+ maxTasksPerCall: 4,
59
+ taskTimeoutMs: 300000,
60
+ // pooled 模式(D-045):0 = 关闭(默认,shared 形态,零额外进程)。
61
+ runnerMaxProcesses: 0,
62
+ runnerMaxSessionsPerRunner: 8,
63
+ runnerIdleTimeoutMs: 300000,
64
+ runnerSweepIntervalMs: 60000,
65
+ runnerEntryAuto: false,
66
+ },
67
+ };
68
+ // 通信面的宿主接线 `SubagentCommunicationWiringV1`(含信箱消息形状)是 plugin-sdk
69
+ // 公共契约(D-075 S4 第三批迁入 SDK),本包经上方 import 消费并在 index 再导出。
70
+ const COMMUNICATION_FLAG_KEYS = ["resume", "escalate", "sibling"];
71
+ /**
72
+ * 配置通道解析(`api.config.communication`):缺省对象 = 全关;字段只接受 `true` 或省略,
73
+ * 未知键与错误类型装载期 fail fast,不让"配置了但没生效"的假象存在。
74
+ */
75
+ function readCommunicationConfig(raw) {
76
+ if (raw === undefined)
77
+ return {};
78
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
79
+ throw new Error("subagent-delegate config communication must be an object");
80
+ }
81
+ const record = raw;
82
+ const known = COMMUNICATION_FLAG_KEYS;
83
+ const unknown = Object.keys(record).filter((key) => !known.includes(key));
84
+ if (unknown.length > 0) {
85
+ throw new Error(`subagent-delegate config communication has unknown key(s): ${unknown.join(", ")}`);
86
+ }
87
+ const config = {};
88
+ for (const key of COMMUNICATION_FLAG_KEYS) {
89
+ const value = record[key];
90
+ if (value === undefined)
91
+ continue;
92
+ if (value !== true)
93
+ throw new Error(`subagent-delegate config communication.${key} must be true or omitted`);
94
+ config[key] = true;
95
+ }
96
+ return config;
97
+ }
98
+ /** 把宿主/调度器/信箱的结构化错误映射为工具面的同码委托错误(保留原始 message)。 */
99
+ function mapCommunicationError(error) {
100
+ if (error instanceof SubagentMailboxErrorV1) {
101
+ throw new SubagentDelegateError(error.code, error.message);
102
+ }
103
+ if (error instanceof SubagentRuntimeError &&
104
+ (error.code === "subagent_unknown_task" ||
105
+ error.code === "subagent_not_running" ||
106
+ error.code === "subagent_steer_unsupported")) {
107
+ throw new SubagentDelegateError(error.code, error.message);
108
+ }
109
+ throw error;
110
+ }
111
+ export class SubagentDelegateError extends Error {
112
+ code;
113
+ constructor(code, message) {
114
+ super(message);
115
+ this.name = "SubagentDelegateError";
116
+ this.code = code;
117
+ }
118
+ }
119
+ /**
120
+ * 出厂种子(仅用于首次落盘,不是运行期定义):默认方案关联 explore / reviewer,
121
+ * 因此新装环境开箱即用;用户改文件即生效,删文件则该关联成为 dangling 诊断。
122
+ */
123
+ const FACTORY_ROLE_SEEDS = Object.freeze([
124
+ Object.freeze({
125
+ id: "explore",
126
+ description: "Read-only explorer: locates code and reports file:line evidence",
127
+ instructions: "You are an exploration subagent. Find the relevant code, configuration or documentation and report concrete " +
128
+ "evidence as file:line references with a one-line note per hit. Do not modify any file and do not run commands " +
129
+ "with side effects. Answer with findings first, then the smallest set of next steps.",
130
+ }),
131
+ Object.freeze({
132
+ id: "reviewer",
133
+ description: "Code reviewer: reads a diff or change set and reports findings and risks",
134
+ instructions: "You are a code-review subagent. Read the change set you are given (diff, files or description) and report " +
135
+ "findings ordered by severity: correctness first, then contract/regression risk, then clarity. For each finding " +
136
+ "give the location, why it is wrong or risky, and the smallest fix. Do not modify files. State explicitly when " +
137
+ "you found nothing in a category instead of inventing issues.",
138
+ }),
139
+ ]);
140
+ const ROLE_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,31}$/;
141
+ const ROLE_DESCRIPTION_CHARS = 200;
142
+ const ROLE_INSTRUCTIONS_CHARS = 8000;
143
+ /** 角色数量与"菜单"总长上限:菜单是主代理工具描述的一部分,必须有界。 */
144
+ const ROLE_COUNT_LIMIT = 16;
145
+ const ROLE_MENU_CHARS = 2000;
146
+ const ROLE_KNOWN_KEYS = [
147
+ "description",
148
+ "instructions",
149
+ "model",
150
+ "thinkingLevel",
151
+ "tools",
152
+ "mode",
153
+ "systemPromptAppend",
154
+ "parentView",
155
+ ];
156
+ /** 用户角色目录(agentDir 下),与 `<agentDir>/capabilities/` 并列。 */
157
+ const ROLE_CATALOG_DIR = "roles";
158
+ function validateRoleDefinition(id, value, origin) {
159
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
160
+ throw new Error(`${origin} must be an object with description and instructions`);
161
+ }
162
+ const fields = value;
163
+ for (const key of Object.keys(fields)) {
164
+ if (!ROLE_KNOWN_KEYS.includes(key)) {
165
+ throw new Error(`${origin}.${key} is not a recognized role field (known: ${ROLE_KNOWN_KEYS.join(", ")})`);
166
+ }
167
+ }
168
+ const description = fields.description;
169
+ if (typeof description !== "string" || description.trim() === "" || description.length > ROLE_DESCRIPTION_CHARS) {
170
+ throw new Error(`${origin}.description must be a non-empty string of at most ${ROLE_DESCRIPTION_CHARS} chars, got ` +
171
+ JSON.stringify(description));
172
+ }
173
+ const instructions = fields.instructions;
174
+ if (typeof instructions !== "string" ||
175
+ instructions.trim() === "" ||
176
+ instructions.length > ROLE_INSTRUCTIONS_CHARS) {
177
+ throw new Error(`${origin}.instructions must be a non-empty string of at most ${ROLE_INSTRUCTIONS_CHARS} chars`);
178
+ }
179
+ // 档案字段交给公共解析器校验(类型/范围/未知键都在那里拦),错误文本带上文件来源。
180
+ const profileInput = { version: 1, roleId: id };
181
+ for (const key of ["model", "thinkingLevel", "tools", "mode", "systemPromptAppend"]) {
182
+ if (fields[key] !== undefined)
183
+ profileInput[key] = fields[key];
184
+ }
185
+ const hasProfile = Object.keys(profileInput).length > 1;
186
+ let profile;
187
+ if (hasProfile) {
188
+ const parsed = parseAgentTaskChildProfileV1(profileInput);
189
+ if (!parsed.ok)
190
+ throw new Error(`${origin}: ${parsed.message}`);
191
+ profile = parsed.profile;
192
+ }
193
+ const parentView = fields.parentView;
194
+ if (parentView !== undefined && typeof parentView !== "boolean") {
195
+ throw new Error(`${origin}.parentView must be a boolean`);
196
+ }
197
+ return {
198
+ id,
199
+ description,
200
+ instructions,
201
+ ...(profile === undefined ? {} : { profile }),
202
+ ...(parentView === true ? { parentView: true } : {}),
203
+ };
204
+ }
205
+ /**
206
+ * 解析当前方案的角色目录(**全部来自磁盘**,无编译内定义)。装载期一次性完成,
207
+ * 任何会让"配置了但没生效"的情况都显式失败:
208
+ * - 被关联 + 文件存在但非法 → 抛错(指名文件);
209
+ * - 被关联但目录里没有该 id → dangling(诊断,角色不可见),调用该名走 `subagent_unknown_role`;
210
+ * - 未被关联的坏文件只进诊断:别的方案不该被一个用不到的文件拖垮;
211
+ * - 暴露集受 16 个 / 2000 字符上限约束(菜单是主代理工具描述的一部分)。
212
+ */
213
+ export function resolveSubagentRoleCatalog(options) {
214
+ const associated = [...new Set(options.associations)];
215
+ const roles = new Map();
216
+ const dangling = [];
217
+ const brokenUnassociated = [];
218
+ const catalogDir = join(options.agentDir, ROLE_CATALOG_DIR);
219
+ let entries = [];
220
+ try {
221
+ entries = readdirSync(catalogDir).filter((name) => name.endsWith(".json"));
222
+ }
223
+ catch {
224
+ // 目录不存在(未种子化/未配置)是正常状态。
225
+ entries = [];
226
+ }
227
+ for (const entry of entries) {
228
+ const id = entry.slice(0, -".json".length);
229
+ if (!ROLE_ID_PATTERN.test(id))
230
+ continue;
231
+ const origin = `${catalogDir}/${entry}`;
232
+ const isAssociated = associated.includes(id);
233
+ let parsed;
234
+ try {
235
+ parsed = JSON.parse(readFileSync(join(catalogDir, entry), "utf8"));
236
+ }
237
+ catch (error) {
238
+ if (isAssociated) {
239
+ throw new Error(`subagent role "${id}" is associated by this suite but ${origin} is unreadable`, {
240
+ cause: error,
241
+ });
242
+ }
243
+ brokenUnassociated.push(id);
244
+ continue;
245
+ }
246
+ let role;
247
+ try {
248
+ role = validateRoleDefinition(id, parsed, origin);
249
+ }
250
+ catch (error) {
251
+ if (isAssociated)
252
+ throw error;
253
+ brokenUnassociated.push(id);
254
+ continue;
255
+ }
256
+ if (isAssociated && role !== undefined)
257
+ roles.set(id, role);
258
+ }
259
+ for (const id of associated) {
260
+ if (!ROLE_ID_PATTERN.test(id)) {
261
+ throw new Error(`suite associations.roles entry ${JSON.stringify(id)} is not a valid role id (expect lowercase [a-z0-9_-], ` +
262
+ "max 32 chars)");
263
+ }
264
+ if (!roles.has(id))
265
+ dangling.push(id);
266
+ }
267
+ if (roles.size > ROLE_COUNT_LIMIT) {
268
+ throw new Error(`suite associations.roles exposes ${roles.size} roles; at most ${ROLE_COUNT_LIMIT} are allowed — the role menu ` +
269
+ "rides in the parent's tool description on every request, and an unbounded menu degrades the main agent's own context");
270
+ }
271
+ const menuChars = [...roles.values()].reduce((total, role) => total + role.id.length + role.description.length + 3, 0);
272
+ if (menuChars > ROLE_MENU_CHARS) {
273
+ throw new Error(`suite associations.roles menu is ${menuChars} chars; at most ${ROLE_MENU_CHARS} are allowed — shorten role ` +
274
+ "descriptions or associate fewer roles (the menu is part of the parent's tool description)");
275
+ }
276
+ return { roles, dangling: Object.freeze(dangling), brokenUnassociated: Object.freeze(brokenUnassociated) };
277
+ }
278
+ /** 角色文件名(`<agentDir>/roles/<id>.json`),供种子与目录与文档共用一处。 */
279
+ export function subagentRoleCatalogPath(agentDir, id) {
280
+ return join(agentDir, ROLE_CATALOG_DIR, `${id}.json`);
281
+ }
282
+ /**
283
+ * 出厂种子落盘(仅补缺,绝不覆盖既有文件):默认方案关联 explore/reviewer,
284
+ * 因此新环境开箱即用;这两个文件与用户自己写的角色完全同权——改文件即生效,
285
+ * 删文件使关联成为 dangling 诊断。引擎运行期从不回退到编译内的角色定义。
286
+ */
287
+ export function seedFactoryRoleCatalog(agentDir) {
288
+ const dir = join(agentDir, ROLE_CATALOG_DIR);
289
+ try {
290
+ mkdirSync(dir, { recursive: true });
291
+ }
292
+ catch {
293
+ return; // 不可写(只读 agentDir):角色可选,不阻断启动。
294
+ }
295
+ for (const role of FACTORY_ROLE_SEEDS) {
296
+ const path = subagentRoleCatalogPath(agentDir, role.id);
297
+ if (existsSync(path))
298
+ continue;
299
+ try {
300
+ writeFileSync(path, `${JSON.stringify({ description: role.description, instructions: role.instructions }, null, 2)}\n`, {
301
+ encoding: "utf8",
302
+ flag: "wx",
303
+ });
304
+ }
305
+ catch {
306
+ // 并发种子化或只读目录:已在磁盘上或不需要,保持现状。
307
+ }
308
+ }
309
+ }
310
+ /** 父投影上限:目标文本与工具面名单都取小值(这是"背景",不是父上下文回灌)。 */
311
+ const PARENT_VIEW_GOAL_CHARS = 2000;
312
+ const PARENT_VIEW_TOOL_LIMIT = 40;
313
+ /** 从父会话消息里取最后一条用户文本(有界),作为"父当前目标"。 */
314
+ function lastUserText(messages, limit) {
315
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
316
+ const message = messages[index];
317
+ if (typeof message !== "object" || message === null)
318
+ continue;
319
+ const record = message;
320
+ if (record.role !== "user")
321
+ continue;
322
+ const raw = typeof record.content === "string"
323
+ ? record.content
324
+ : Array.isArray(record.content)
325
+ ? record.content.map((part) => part.text ?? "").join("")
326
+ : "";
327
+ const trimmed = raw.trim();
328
+ if (trimmed === "")
329
+ continue;
330
+ return trimmed.length <= limit ? trimmed : `${trimmed.slice(0, limit)}…`;
331
+ }
332
+ return undefined;
333
+ }
334
+ /**
335
+ * 子读父(D-060 §4):父会话的**有界投影**——sessionId、当前目标(最后一条用户文本,截断)、
336
+ * 工具面名单。默认关闭;开启后拼在角色指令与任务之间。父历史全文永不进入子会话。
337
+ */
338
+ function composeParentView(session, parentSessionId) {
339
+ const goal = session === undefined ? undefined : lastUserText(session.getMessages(), PARENT_VIEW_GOAL_CHARS);
340
+ const tools = session?.getActiveTools?.() ?? [];
341
+ const toolList = tools.slice(0, PARENT_VIEW_TOOL_LIMIT).join(", ");
342
+ return [
343
+ "Parent context (bounded projection; the parent's full history is not available to you):",
344
+ `- parent session: ${parentSessionId}`,
345
+ ...(goal === undefined ? [] : [`- current goal: ${goal}`]),
346
+ ...(toolList === "" ? [] : [`- parent tools: ${toolList}`]),
347
+ ].join("\n");
348
+ }
349
+ /**
350
+ * 角色框定:角色指令在上、任务在下,任务原文原样保留(子会话看不到父会话历史,
351
+ * 因此这段文本是它唯一的"我是谁 + 做什么"来源)。组装后的字节数仍走 submit 侧
352
+ * maxPromptBytes 预算,超限按预算拒绝而不是静默截断。
353
+ */
354
+ function composeRolePrompt(role, prompt) {
355
+ return `${role.instructions}\n\nTask:\n${prompt}`;
356
+ }
357
+ /** context 硬边界(设计 §2.3):两个列表 ≤10 项、note ≤200 字符、path/command ≤500 字符。 */
358
+ const CONTEXT_LIST_MAX_ITEMS = 10;
359
+ const CONTEXT_NOTE_MAX_CHARS = 200;
360
+ const CONTEXT_PATH_MAX_CHARS = 500;
361
+ const CONTEXT_COMMAND_MAX_CHARS = 500;
362
+ function parseContextNote(value, origin) {
363
+ if (value === undefined)
364
+ return undefined;
365
+ if (typeof value !== "string" || value.length > CONTEXT_NOTE_MAX_CHARS) {
366
+ throw new SubagentDelegateError("subagent_invalid_input", `${origin} must be a string of at most ${CONTEXT_NOTE_MAX_CHARS} chars`);
367
+ }
368
+ return value === "" ? undefined : value;
369
+ }
370
+ function parseContextList(value, label, parseEntry) {
371
+ if (!Array.isArray(value)) {
372
+ throw new SubagentDelegateError("subagent_invalid_input", `subagent_run context.${label} must be an array`);
373
+ }
374
+ if (value.length > CONTEXT_LIST_MAX_ITEMS) {
375
+ throw new SubagentDelegateError("subagent_invalid_input", `subagent_run context.${label} accepts at most ${CONTEXT_LIST_MAX_ITEMS} entries; received ${value.length}`);
376
+ }
377
+ return value.map((entry, index) => {
378
+ const origin = `subagent_run context.${label}[${index}]`;
379
+ if (!isPlainRecord(entry)) {
380
+ throw new SubagentDelegateError("subagent_invalid_input", `${origin} must be an object`);
381
+ }
382
+ return parseEntry(entry, origin);
383
+ });
384
+ }
385
+ /**
386
+ * 解析工具入参里的 `context`(形状/边界违规都以 `subagent_invalid_input` 整体拒绝);
387
+ * 两个列表皆空时归一化为 undefined——与 context 缺省同一路径,整块不渲染。
388
+ */
389
+ function parseDelegateContext(value) {
390
+ if (value === undefined)
391
+ return undefined;
392
+ if (!isPlainRecord(value)) {
393
+ throw new SubagentDelegateError("subagent_invalid_input", "subagent_run context must be an object");
394
+ }
395
+ const anchors = value.anchors === undefined
396
+ ? undefined
397
+ : parseContextList(value.anchors, "anchors", (entry, origin) => {
398
+ const path = entry.path;
399
+ if (typeof path !== "string" || path === "" || path.length > CONTEXT_PATH_MAX_CHARS) {
400
+ throw new SubagentDelegateError("subagent_invalid_input", `${origin}.path must be a non-empty string of at most ${CONTEXT_PATH_MAX_CHARS} chars`);
401
+ }
402
+ const rawLine = entry.line;
403
+ const line = rawLine === undefined
404
+ ? undefined
405
+ : (() => {
406
+ if (typeof rawLine !== "number" || !Number.isInteger(rawLine) || rawLine < 1) {
407
+ throw new SubagentDelegateError("subagent_invalid_input", `${origin}.line must be a positive integer`);
408
+ }
409
+ return rawLine;
410
+ })();
411
+ const note = parseContextNote(entry.note, `${origin}.note`);
412
+ return {
413
+ path,
414
+ ...(line === undefined ? {} : { line }),
415
+ ...(note === undefined ? {} : { note }),
416
+ };
417
+ });
418
+ const failedAttempts = value.failedAttempts === undefined
419
+ ? undefined
420
+ : parseContextList(value.failedAttempts, "failedAttempts", (entry, origin) => {
421
+ const command = entry.command;
422
+ if (typeof command !== "string" || command === "" || command.length > CONTEXT_COMMAND_MAX_CHARS) {
423
+ throw new SubagentDelegateError("subagent_invalid_input", `${origin}.command must be a non-empty string of at most ${CONTEXT_COMMAND_MAX_CHARS} chars`);
424
+ }
425
+ const note = parseContextNote(entry.note, `${origin}.note`);
426
+ return { command, ...(note === undefined ? {} : { note }) };
427
+ });
428
+ if ((anchors?.length ?? 0) === 0 && (failedAttempts?.length ?? 0) === 0)
429
+ return undefined;
430
+ return {
431
+ ...(anchors === undefined ? {} : { anchors }),
432
+ ...(failedAttempts === undefined ? {} : { failedAttempts }),
433
+ };
434
+ }
435
+ /**
436
+ * 把 context 确定性渲染为子会话首条 prompt 的头部块(模板逐字,设计 §2.3):line
437
+ * 缺省只输出 path、note 缺省省略 " — <note>"、某列表为空不渲染对应小节、两个列表
438
+ * 皆空(context 已在解析侧归一化)返回 undefined——子会话 prompt 与现状逐字节一致。
439
+ */
440
+ function renderDelegateContext(context) {
441
+ if (context === undefined)
442
+ return undefined;
443
+ const lines = ["[parent context anchors — may be stale; verify against current files]"];
444
+ const anchors = context.anchors ?? [];
445
+ if (anchors.length > 0) {
446
+ lines.push("Key locations:");
447
+ for (const anchor of anchors) {
448
+ const location = anchor.line === undefined ? anchor.path : `${anchor.path}:${anchor.line}`;
449
+ lines.push(`- ${location}${anchor.note === undefined ? "" : ` — ${anchor.note}`}`);
450
+ }
451
+ }
452
+ const failedAttempts = context.failedAttempts ?? [];
453
+ if (failedAttempts.length > 0) {
454
+ lines.push("Tried and failed:");
455
+ for (const attempt of failedAttempts) {
456
+ lines.push(`- \`${attempt.command}\`${attempt.note === undefined ? "" : ` — ${attempt.note}`}`);
457
+ }
458
+ }
459
+ return lines.length === 1 ? undefined : lines.join("\n");
460
+ }
461
+ /**
462
+ * 核验档案是否真的落到了子会话上:工具面必须是请求白名单的子集,模型必须等于请求值,
463
+ * 模式请求 `readonly` 时必须已是只读。宿主不暴露可核验事实(getActiveTools/getModel)
464
+ * 也算未落地——"要求了但无法证明"与"没要求"对上层是同一种风险。
465
+ */
466
+ function verifyChildProfileApplied(host, requested) {
467
+ const parsed = parseAgentTaskChildProfileV1(requested);
468
+ if (!parsed.ok)
469
+ return `child task profile is invalid: ${parsed.message}`;
470
+ const profile = parsed.profile;
471
+ const session = host.session;
472
+ if (profile.tools !== undefined || profile.mode === "readonly") {
473
+ const active = session.getActiveTools?.();
474
+ if (active === undefined) {
475
+ return "host does not expose getActiveTools(); the requested tool narrowing cannot be verified";
476
+ }
477
+ if (profile.tools !== undefined) {
478
+ const extra = active.filter((name) => !profile.tools?.includes(name));
479
+ if (extra.length > 0) {
480
+ return `child session kept tools outside the requested allowlist: ${extra.join(", ")}`;
481
+ }
482
+ }
483
+ }
484
+ if (profile.model !== undefined) {
485
+ const model = session.getModel?.();
486
+ if (model === undefined) {
487
+ return "host does not expose getModel(); the requested model cannot be verified";
488
+ }
489
+ if (model.id !== profile.model) {
490
+ return `child session is on model ${JSON.stringify(model.id)} instead of the requested ${JSON.stringify(profile.model)}`;
491
+ }
492
+ }
493
+ return undefined;
494
+ }
495
+ function resolveRole(value, roles) {
496
+ if (value === undefined || value === null)
497
+ return undefined;
498
+ if (typeof value !== "string" || value.trim() === "") {
499
+ throw new SubagentDelegateError("subagent_invalid_input", "subagent_run role must be a non-empty string");
500
+ }
501
+ const role = roles.get(value);
502
+ if (role === undefined) {
503
+ const available = [...roles.keys()].sort();
504
+ throw new SubagentDelegateError("subagent_unknown_role", `unknown subagent role ${JSON.stringify(value)}; available roles: ` +
505
+ (available.length === 0 ? "(none configured)" : available.join(", ")));
506
+ }
507
+ return role;
508
+ }
509
+ /** 单条 summary 的字符上限(上下文预算:只回摘要,不回 messages 原文)。 */
510
+ const SUMMARY_CHARS = 200;
511
+ /** `subagent_result` 默认/上限返回条数(有界回读,绝不默认回灌全文)。 */
512
+ const RESULT_READ_DEFAULT_MESSAGES = 20;
513
+ const RESULT_READ_MAX_MESSAGES = 50;
514
+ /** `subagent_result` 的有界等待上限(后台结算收割),默认 0 = 只查一次。 */
515
+ const RESULT_READ_MAX_WAIT_MS = 120_000;
516
+ /** UTF-8 字节数(回读信封如实报告体积)。 */
517
+ function utf8ByteLength(text) {
518
+ return Buffer.byteLength(text, "utf8");
519
+ }
520
+ const DEFAULT_MAX_TASKS_PER_CALL = 4;
521
+ const DEFAULT_TASK_TIMEOUT_MS = 300_000;
522
+ /**
523
+ * 配置通道读取(manifest 默认 + agent-dir 文件合并后的 `api.config`):
524
+ * 非整数/低于下限即 fail fast 并指名键,不让错误配置静默生效。
525
+ */
526
+ function configInt(api, key, min) {
527
+ const value = api.config[key];
528
+ if (value === undefined)
529
+ return undefined;
530
+ if (typeof value !== "number" || !Number.isInteger(value) || value < min) {
531
+ throw new Error(`subagent-delegate config ${key} must be an integer >= ${min}, got ${JSON.stringify(value)}`);
532
+ }
533
+ return value;
534
+ }
535
+ /** options > config > 默认,最终值仍需通过同一套下限校验。 */
536
+ function resolveInt(explicit, configured, fallback, min, label) {
537
+ const value = explicit ?? configured ?? fallback;
538
+ if (!Number.isInteger(value) || value < min) {
539
+ throw new Error(`subagent-delegate ${label} must be an integer >= ${min}, got ${JSON.stringify(value)}`);
540
+ }
541
+ return value;
542
+ }
543
+ /**
544
+ * 宽松调用兼容(对齐 goal/plan/todo-set):标准签名的第二参为 params,直接调用时第一参即 params。
545
+ * 生产装配向 runtime invoke 面注入 toolExecutionContext 作为 execute 的第二参(公共契约),
546
+ * 因此"第二参非 undefined 即输入"的判定会把 context 误当输入(prompts 读成 undefined)。
547
+ * 改按形状判定:第一参是普通对象即取第一参,否则看第二参。
548
+ */
549
+ function objectInput(first, second) {
550
+ const primary = isPlainRecord(first) ? first : second;
551
+ if (!isPlainRecord(primary)) {
552
+ throw new SubagentDelegateError("subagent_invalid_input", "subagent_run input must be an object");
553
+ }
554
+ return primary;
555
+ }
556
+ function isPlainRecord(value) {
557
+ return typeof value === "object" && value !== null && !Array.isArray(value);
558
+ }
559
+ function readPrompts(value, maxTasksPerCall) {
560
+ if (!Array.isArray(value) || value.length === 0) {
561
+ throw new SubagentDelegateError("subagent_invalid_input", "subagent_run requires a non-empty prompts array");
562
+ }
563
+ const prompts = [];
564
+ for (const entry of value) {
565
+ if (typeof entry !== "string" || entry.trim() === "") {
566
+ throw new SubagentDelegateError("subagent_invalid_input", "subagent_run prompts must be non-empty strings");
567
+ }
568
+ prompts.push(entry);
569
+ }
570
+ if (prompts.length > maxTasksPerCall) {
571
+ throw new SubagentDelegateError("subagent_batch_too_large", `subagent_run accepts at most ${maxTasksPerCall} prompts per call; received ${prompts.length}`);
572
+ }
573
+ return prompts;
574
+ }
575
+ function errorText(error) {
576
+ if (error instanceof Error)
577
+ return error.message;
578
+ if (typeof error === "string")
579
+ return error;
580
+ try {
581
+ const serialized = JSON.stringify(error);
582
+ return serialized === undefined ? String(error) : serialized;
583
+ }
584
+ catch {
585
+ return String(error);
586
+ }
587
+ }
588
+ /** 结构化原因码:优先取错误自带的 code(调度器/适配器的结构化错误),否则表达为 submit 拒绝。 */
589
+ function reasonCodeOf(error) {
590
+ if (error && typeof error === "object" && typeof error.code === "string") {
591
+ return error.code;
592
+ }
593
+ return "subagent_submit_rejected";
594
+ }
595
+ /** 文本块抽取:content 为字符串或 text 块数组;无文本返回 undefined。 */
596
+ function messageText(content) {
597
+ if (typeof content === "string")
598
+ return content === "" ? undefined : content;
599
+ if (!Array.isArray(content))
600
+ return undefined;
601
+ const parts = [];
602
+ for (const item of content) {
603
+ if (!item || typeof item !== "object")
604
+ continue;
605
+ const block = item;
606
+ if (block.type !== "text" || typeof block.text !== "string")
607
+ continue;
608
+ parts.push(block.text);
609
+ }
610
+ const text = parts.join("");
611
+ return text === "" ? undefined : text;
612
+ }
613
+ /** 最后一条带文本的 assistant 消息 → 有界 summary;找不到(或全无文本)省略。 */
614
+ function assistantSummary(messages) {
615
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
616
+ const message = messages[index];
617
+ if (!message || typeof message !== "object")
618
+ continue;
619
+ const record = message;
620
+ if (record.role !== "assistant" && record.type !== "assistant")
621
+ continue;
622
+ const text = messageText(record.content);
623
+ if (text === undefined)
624
+ continue;
625
+ return text.length <= SUMMARY_CHARS ? text : text.slice(0, SUMMARY_CHARS);
626
+ }
627
+ return undefined;
628
+ }
629
+ function projectRecord(record) {
630
+ const summary = assistantSummary(record.messages);
631
+ return {
632
+ taskId: record.taskId,
633
+ status: record.status,
634
+ ...(record.reasonCode === undefined ? {} : { reasonCode: record.reasonCode }),
635
+ ...(record.reason === undefined ? {} : { reason: record.reason }),
636
+ ...(record.truncated ? { truncated: true } : {}),
637
+ messageCount: record.messages.length,
638
+ ...(record.sessionId === undefined || record.sessionId === "" ? {} : { sessionId: record.sessionId }),
639
+ ...(summary === undefined ? {} : { summary }),
640
+ };
641
+ }
642
+ /**
643
+ * 血缘深度(承载退役切片 2,Option A):沿 S2 落盘头行的 `parentAgentSession` 上行,
644
+ * 返回本会话的祖先层数。root(无头行/非落盘会话)返回 undefined——不设限。
645
+ * 最多上行走 `maxWalk` 步(有界,防环按步数截断)。
646
+ *
647
+ * 迁移注记(D-075 S4 第三批):宿主实现的逐头回读(`readAgentSession` 循环)改为
648
+ * plugin-sdk 的 `readAgentSessionLineage`——同一落盘契约的只读首行实现。语义对齐点:
649
+ * `ancestors` 只含**可解析**的祖先,而宿主 depth 对"父指针存在但父会话未落盘"的
650
+ * 最后一条边也计数(root 在内存里、文件不可达是常态),因此 dangling 边补 +1;
651
+ * `truncated`(走满 maxWalk 步仍有可解析父)时 ancestors.length 与宿主的 maxWalk
652
+ * 返回值一致。
653
+ */
654
+ function lineageDepth(sessionId, agentDir, maxWalk) {
655
+ const lineage = readAgentSessionLineage(agentDir, sessionId, maxWalk);
656
+ if (lineage.self === undefined)
657
+ return undefined;
658
+ const last = lineage.ancestors.at(-1) ?? lineage.self;
659
+ const danglingParent = last.parentAgentSession !== undefined && last.parentAgentSession !== last.sessionId;
660
+ return lineage.ancestors.length + (danglingParent && !lineage.truncated ? 1 : 0);
661
+ }
662
+ /**
663
+ * 第一方编排 capability:一个调度器实例 + `subagent_run` 工具 + 释放登记。
664
+ * 只消费公共 API(session/agents/sessionAbortSignal/tools),不 import core 内部私有模块。
665
+ */
666
+ export function createCapability(api, options) {
667
+ const session = api.session;
668
+ const agents = api.agents;
669
+ if (!session)
670
+ throw new Error("subagent-delegate requires the session capability");
671
+ // 通信面配置在装载期 fail fast(两个上下文都读同一份 agent-dir 配置文件)。
672
+ const communication = readCommunicationConfig(api.config.communication);
673
+ const wiring = options?.communicationWiring;
674
+ // 子侧通信面(子→父 / 子↔子):只要宿主接线了 post 通道且配置开启就注册——
675
+ // 叶子会话(无 agents)只拿这一面;获准自建会话(D-060 S5,agents 在场)两面都要:
676
+ // 它既能向下委派(parent face),也仍能向自己的父上抛/收兄弟消息(child face)。
677
+ const childTools = [];
678
+ if (wiring?.postEscalate !== undefined && communication.escalate === true) {
679
+ childTools.push(api.registerTool({
680
+ name: "subagent_escalate",
681
+ label: "Escalate to parent",
682
+ description: "Send one bounded message up to your parent agent. It does NOT interrupt the parent; it is delivered " +
683
+ "with the parent's next task result. Use it for a blocker or a key finding the task result alone would lose.",
684
+ parameters: Type.Object({
685
+ text: Type.String({
686
+ minLength: 1,
687
+ description: `Message to the parent (at most ${SUBAGENT_MAILBOX_MAX_MESSAGE_BYTES} utf-8 bytes)`,
688
+ }),
689
+ }),
690
+ execute: async (first, second) => {
691
+ const input = objectInput(first, second);
692
+ const text = typeof input.text === "string" ? input.text : "";
693
+ if (text.trim() === "") {
694
+ throw new SubagentDelegateError("subagent_invalid_input", "subagent_escalate requires non-empty text");
695
+ }
696
+ try {
697
+ wiring.postEscalate?.(text);
698
+ }
699
+ catch (error) {
700
+ mapCommunicationError(error);
701
+ }
702
+ return { delivered: true, note: "the parent reads this with its next task result" };
703
+ },
704
+ }));
705
+ }
706
+ if (wiring?.sendSibling !== undefined && communication.sibling === true) {
707
+ childTools.push(api.registerTool({
708
+ name: "subagent_send",
709
+ label: "Message a sibling",
710
+ description: "Send one bounded message to a sibling subagent (a child of the same parent), addressed by its sessionId. " +
711
+ "Cross-family addressing is rejected.",
712
+ parameters: Type.Object({
713
+ to: Type.String({ description: "Recipient child session id" }),
714
+ text: Type.String({
715
+ minLength: 1,
716
+ description: `Message (at most ${SUBAGENT_MAILBOX_MAX_MESSAGE_BYTES} utf-8 bytes)`,
717
+ }),
718
+ }),
719
+ execute: async (first, second) => {
720
+ const input = objectInput(first, second);
721
+ const to = typeof input.to === "string" ? input.to : "";
722
+ const text = typeof input.text === "string" ? input.text : "";
723
+ if (to === "" || text.trim() === "") {
724
+ throw new SubagentDelegateError("subagent_invalid_input", "subagent_send requires to and non-empty text");
725
+ }
726
+ try {
727
+ wiring.sendSibling?.(to, text);
728
+ }
729
+ catch (error) {
730
+ mapCommunicationError(error);
731
+ }
732
+ return { delivered: true };
733
+ },
734
+ }), api.registerTool({
735
+ name: "subagent_inbox",
736
+ label: "Read sibling inbox",
737
+ description: "Read and clear the earliest bounded messages your siblings sent you (subagent_send). Messages are " +
738
+ "consumed by this call; poll it when coordination is expected.",
739
+ parameters: Type.Object({
740
+ limit: Type.Optional(Type.Number({ description: "Max messages to take (1..20, default 10)", minimum: 1, maximum: 20 })),
741
+ }),
742
+ execute: async (first, second) => {
743
+ const input = objectInput(first, second);
744
+ const requested = input.limit === undefined ? 10 : Number(input.limit);
745
+ if (!Number.isInteger(requested) || requested < 1 || requested > 20) {
746
+ throw new SubagentDelegateError("subagent_invalid_input", "subagent_inbox limit must be an integer in 1..20");
747
+ }
748
+ const messages = wiring.drainInbox?.(requested) ?? [];
749
+ return { messages };
750
+ },
751
+ }));
752
+ }
753
+ // 调度器限额:options.limits 显式覆盖优先;否则由配置通道逐键取值,缺省回落到调度器默认。
754
+ const configuredLimits = {
755
+ maxConcurrentSubagents: configInt(api, "maxConcurrentSubagents", 1),
756
+ maxQueueDepth: configInt(api, "maxQueueDepth", 1),
757
+ maxPromptBytes: configInt(api, "maxPromptBytes", 1),
758
+ maxMetadataBytes: configInt(api, "maxMetadataBytes", 1),
759
+ maxResultBytes: configInt(api, "maxResultBytes", 1),
760
+ maxMessages: configInt(api, "maxMessages", 1),
761
+ drainTimeoutMs: configInt(api, "drainTimeoutMs", 1),
762
+ maxRetainedRecords: configInt(api, "maxRetainedRecords", 1),
763
+ };
764
+ const limits = options?.limits ??
765
+ (Object.values(configuredLimits).every((value) => value === undefined)
766
+ ? undefined
767
+ : configuredLimits);
768
+ const sessionAbortSignal = api.sessionAbortSignal;
769
+ /**
770
+ * adapter 选择(优先级):宿主显式 adapter > 配置驱动的 pooled runner 池 > 共享模式(裸 primitive)。
771
+ * pooled 关闭时零额外进程;启用时 entryPath 必须显式给出(不猜安装形态),缺失即装载失败。
772
+ * 退役切片 1(生产默认 pooled,D-075 S4 第三批收敛):自动池化旗标不再由宿主 options
773
+ * 注入,改由既有配置键 `runnerEntryAuto: true` 驱动——未显式配置 `runnerMaxProcesses`/
774
+ * `runnerEntryPath` 且未注入 spawner 时,若 self-invocation 可解析则自动以池化装配
775
+ * (默认 2 个 runner、每池 8 会话);不可解析则保持共享模式。
776
+ */
777
+ const pool = (() => {
778
+ const configuredMaxProcesses = configInt(api, "runnerMaxProcesses", 0);
779
+ let runnerMaxProcesses = resolveInt(undefined, configuredMaxProcesses, 0, 0, "runnerMaxProcesses");
780
+ const selfInvocation = resolveSelfInvocationV1();
781
+ const autoPool = runnerMaxProcesses === 0 &&
782
+ api.config.runnerEntryAuto === true &&
783
+ options?.runnerSpawner === undefined &&
784
+ typeof api.config.runnerEntryPath !== "string";
785
+ if (autoPool && selfInvocation !== undefined)
786
+ runnerMaxProcesses = 2;
787
+ if (runnerMaxProcesses === 0)
788
+ return undefined;
789
+ // runner 配置存在即做一致性校验(即使宿主注入了 adapter,错误配置也不得静默):
790
+ // 非法值/缺失入口/容量不覆盖并发都在装载期暴露。
791
+ const maxSessionsPerRunner = resolveInt(undefined, configInt(api, "runnerMaxSessionsPerRunner", 1), 8, 1, "runnerMaxSessionsPerRunner");
792
+ const idleRunnerTimeoutMs = resolveInt(undefined, configInt(api, "runnerIdleTimeoutMs", 1), 300_000, 1, "runnerIdleTimeoutMs");
793
+ const idleCheckIntervalMs = resolveInt(undefined, configInt(api, "runnerSweepIntervalMs", 1), 60_000, 1, "runnerSweepIntervalMs");
794
+ // 有效并发取"已配并发"或调度器默认值:容量校验必须在未显式配置并发时也生效,
795
+ // 否则池化容量小于默认并发(8)会被静默容忍,直到运行期才以 capacity 错误暴露。
796
+ const effectiveMaxConcurrent = options?.limits?.maxConcurrentSubagents ?? limits?.maxConcurrentSubagents ?? 8;
797
+ // spawn 工厂优先用注入;否则要求配置显式给出 runner 入口(装载期校验,不推迟到首次 spawn)。
798
+ let spawnRunner = options?.runnerSpawner;
799
+ if (spawnRunner === undefined) {
800
+ const entryPath = api.config.runnerEntryPath;
801
+ if (typeof entryPath === "string" && entryPath.trim() !== "") {
802
+ const nodeArgs = Array.isArray(api.config.runnerNodeArgs)
803
+ ? api.config.runnerNodeArgs.filter((value) => typeof value === "string")
804
+ : entryPath.endsWith(".ts")
805
+ ? ["--experimental-strip-types"]
806
+ : [];
807
+ const spawner = createNodeSubagentRunnerSpawnerV1({ entryPath, nodeArgs });
808
+ spawnRunner = () => spawner();
809
+ }
810
+ else if (autoPool) {
811
+ // 退役切片 1:CLI 宿主的自动池化——self-invocation 已在上方预检可解析。
812
+ const self = selfInvocation;
813
+ const nodeArgs = Array.isArray(api.config.runnerNodeArgs)
814
+ ? api.config.runnerNodeArgs.filter((value) => typeof value === "string")
815
+ : [...self.nodeArgs];
816
+ const spawner = createNodeSubagentRunnerSpawnerV1({ entryPath: self.entryPath, nodeArgs });
817
+ spawnRunner = () => spawner();
818
+ }
819
+ else if (api.config.runnerEntryAuto === true) {
820
+ // 显式开启的零配置路径:把当前进程的启动方式当作 runner 入口(见 self-invocation)。
821
+ const self = resolveSelfInvocationV1();
822
+ if (self === undefined) {
823
+ throw new Error("subagent-delegate config runnerEntryAuto is enabled but the current process has no script entry " +
824
+ "(compiled binaries must set runnerEntryPath explicitly)");
825
+ }
826
+ const nodeArgs = Array.isArray(api.config.runnerNodeArgs)
827
+ ? api.config.runnerNodeArgs.filter((value) => typeof value === "string")
828
+ : [...self.nodeArgs];
829
+ const spawner = createNodeSubagentRunnerSpawnerV1({ entryPath: self.entryPath, nodeArgs });
830
+ spawnRunner = () => spawner();
831
+ }
832
+ else {
833
+ throw new Error("subagent-delegate config runnerEntryPath is required when runnerMaxProcesses >= 1 " +
834
+ "(or enable runnerEntryAuto to reuse this process's own entry)");
835
+ }
836
+ }
837
+ if (options?.adapter !== undefined)
838
+ return undefined;
839
+ // 开发态 runner(TS 引导的 CLI)冷启动可能超过默认 5s 握手预算,宿主可配置放宽。
840
+ const handshakeTimeoutMs = configInt(api, "runnerHandshakeTimeoutMs", 1);
841
+ return createPooledAgentTaskAdapterV1({
842
+ spawnRunner,
843
+ maxRunnerProcesses: runnerMaxProcesses,
844
+ maxSessionsPerRunner,
845
+ idleRunnerTimeoutMs,
846
+ idleCheckIntervalMs,
847
+ maxConcurrentSubagents: effectiveMaxConcurrent,
848
+ ...(handshakeTimeoutMs === undefined ? {} : { handshakeTimeoutMs }),
849
+ onDiagnostic: (message) => {
850
+ api.logger?.debug?.(`subagent-delegate runner: ${message}`);
851
+ },
852
+ });
853
+ })();
854
+ // 桥转换只在 agents 暴露任务面(create)时成立(承载退役切片 2:reader-only 宿主的
855
+ // agents 只承载回读,不承载委派)。父面 = 宿主 adapter > 配置/自动池 > 桥。
856
+ const baseAdapter = options?.adapter ?? pool ?? (agents?.create === undefined ? undefined : createAgentTaskApiAdapterV1(agents));
857
+ // 门控(承载退役):父面 = 池或宿主 adapter 任一在即可,不再依赖 agents 能力——
858
+ // runner 叶子带自动池化(退役切片 1)因此同样获得委派父面(嵌套委派,深度硬限兜底)。
859
+ // 无任何 adapter 时委派不适用:只保留子侧通信面(fail-closed)。
860
+ if (baseAdapter === undefined) {
861
+ if (childTools.length === 0) {
862
+ api.logger?.debug?.("subagent-delegate: no runner pool and no host adapter; delegation tool stays unregistered");
863
+ return [];
864
+ }
865
+ return childTools;
866
+ }
867
+ // 配置错误在装载时 fail fast,不让 0/负数静默地拒绝每一次调用或永不超时。
868
+ const maxTasksPerCall = resolveInt(options?.maxTasksPerCall, configInt(api, "maxTasksPerCall", 1), DEFAULT_MAX_TASKS_PER_CALL, 1, "maxTasksPerCall");
869
+ const taskTimeoutMs = resolveInt(options?.taskTimeoutMs, configInt(api, "taskTimeoutMs", 1), DEFAULT_TASK_TIMEOUT_MS, 1, "taskTimeoutMs");
870
+ // 嵌套深度硬限(承载退役切片 2,Option A):runner 叶子带自动池化后同样获得委派
871
+ // 父面,递归边界由血缘深度兜底——本会话已是第 maxNestedDepth 层时再委派按
872
+ // `subagent_depth_exceeded` 结构化拒绝。与 S5 shared 侧的深度上限同一默认(2 层)。
873
+ const maxNestedDepth = resolveInt(undefined, configInt(api, "maxNestedDepth", 1), 2, 1, "maxNestedDepth");
874
+ // 每父级累计可创建的子代理总量(承载退役切片 2,Option A;与 S5 shared 侧同一默认
875
+ // 100):对**非 resume** 的每次 submit 计数,超限按 `subagent_child_quota` 拒绝。
876
+ const maxTotalPerParent = resolveInt(undefined, configInt(api, "maxTotalPerParent", 0), 100, 0, "maxTotalPerParent");
877
+ let createdSubagentCount = 0;
878
+ // 后台委派开关(D-060 S6):默认关;显式 `true` 才给 `subagent_run` 加 `background`
879
+ // 参数(提交即返回票据,不阻塞父回合;结算摘要经宿主 sink 进父的下一次模型轮次)。
880
+ if (api.config.background !== undefined && api.config.background !== true) {
881
+ throw new Error("subagent-delegate config background must be true or omitted");
882
+ }
883
+ const backgroundEnabled = api.config.background === true;
884
+ // 角色目录 + 方案关联在装载期解析:非法文件/非法关联直接失败,不留"配置了但没生效"的假象。
885
+ const roleCatalog = options?.roles === undefined
886
+ ? { roles: new Map(), dangling: [], brokenUnassociated: [] }
887
+ : resolveSubagentRoleCatalog(options.roles);
888
+ const roles = roleCatalog.roles;
889
+ for (const id of roleCatalog.dangling) {
890
+ api.logger?.warn?.(`suite associations.roles lists "${id}" but no role file exists: ${subagentRoleCatalogPath(options?.roles?.agentDir ?? "", id)} (role stays unavailable for this session)`);
891
+ }
892
+ for (const id of roleCatalog.brokenUnassociated) {
893
+ api.logger?.warn?.(`role file for "${id}" is invalid and is not associated by this suite; ignored (fix it or remove the file)`);
894
+ }
895
+ const roleSummary = [...roles.values()]
896
+ .sort((left, right) => (left.id < right.id ? -1 : 1))
897
+ .map((role) => `${role.id} — ${role.description}`)
898
+ .join("; ");
899
+ /**
900
+ * D-044 §5.6:把支持进度面的 adapter 装饰一层,累计"运行中 step"与最近一条
901
+ * 摘要,供工具 onUpdate 节流上报父 UI(只回计数与摘要,不回正文)。
902
+ */
903
+ const progressState = { steps: 0, last: undefined };
904
+ const progressUnsubscribes = new Set();
905
+ const adapter = {
906
+ create: async (request) => {
907
+ const requestedProfile = request.metadata?.[AGENT_TASK_CHILD_PROFILE_KEY];
908
+ const host = await baseAdapter.create(request);
909
+ // 档案核验:宿主必须把收窄落到了子会话上,且要能自证(getActiveTools/getModel)。
910
+ // 核验失败 = 上层以为的"只读评审者"没有生效,因此这里就地失败并回收该子会话,
911
+ // 而不是让它带着比预期更大的权限去跑。
912
+ if (requestedProfile !== undefined) {
913
+ const violation = verifyChildProfileApplied(host, requestedProfile);
914
+ if (violation !== undefined) {
915
+ await host.dispose().catch(() => { });
916
+ throw new SubagentDelegateError("subagent_profile_not_applied", violation);
917
+ }
918
+ }
919
+ const taskId = host.session.getSessionId();
920
+ const unsubscribe = subscribeTaskProgressV1(baseAdapter, taskId, (message) => {
921
+ progressState.steps += 1;
922
+ if (message !== undefined)
923
+ progressState.last = message;
924
+ });
925
+ progressUnsubscribes.add(unsubscribe);
926
+ return host;
927
+ },
928
+ ...(baseAdapter.dispose === undefined
929
+ ? {}
930
+ : { dispose: async () => void (await baseAdapter.dispose?.()) }),
931
+ };
932
+ const runtime = createSubagentRuntime({
933
+ adapter,
934
+ owner: capabilityManifest.id,
935
+ parentSessionId: session.getSessionId(),
936
+ ...(sessionAbortSignal === undefined ? {} : { parentSignal: sessionAbortSignal }),
937
+ ...(limits === undefined ? {} : { limits }),
938
+ });
939
+ const parameters = Type.Object({
940
+ prompts: Type.Array(Type.String({
941
+ minLength: 1,
942
+ description: "Self-contained prompt for one subagent task; the child session sees no parent history",
943
+ }), {
944
+ minItems: 1,
945
+ description: `1..${maxTasksPerCall} independent prompts run as separate subagent tasks`,
946
+ }),
947
+ context: Type.Optional(Type.Object({
948
+ anchors: Type.Optional(Type.Array(Type.Object({
949
+ path: Type.String({
950
+ minLength: 1,
951
+ maxLength: CONTEXT_PATH_MAX_CHARS,
952
+ description: "File path the child should start from",
953
+ }),
954
+ line: Type.Optional(Type.Integer({ minimum: 1, description: "Line number in path (positive integer)" })),
955
+ note: Type.Optional(Type.String({
956
+ maxLength: CONTEXT_NOTE_MAX_CHARS,
957
+ description: "One-line why-it-matters note",
958
+ })),
959
+ }), { maxItems: CONTEXT_LIST_MAX_ITEMS })),
960
+ failedAttempts: Type.Optional(Type.Array(Type.Object({
961
+ command: Type.String({
962
+ minLength: 1,
963
+ maxLength: CONTEXT_COMMAND_MAX_CHARS,
964
+ description: "Command that was already tried and failed",
965
+ }),
966
+ note: Type.Optional(Type.String({ maxLength: CONTEXT_NOTE_MAX_CHARS, description: "One-line failure note" })),
967
+ }), { maxItems: CONTEXT_LIST_MAX_ITEMS })),
968
+ }, {
969
+ description: "Parent pointers rendered as a header block into each child prompt: file:line anchors and commands " +
970
+ "already tried and failed. Pointers only — children still read the real files.",
971
+ })),
972
+ ...(roles.size === 0
973
+ ? {}
974
+ : {
975
+ role: Type.Optional(Type.String({
976
+ description: `Named subagent role applied to every prompt in this call; one of: ${[...roles.keys()].sort().join(", ")}. ` +
977
+ "Omit for a generic task. Roles frame the task; they do not change the child's model or tools.",
978
+ })),
979
+ }),
980
+ });
981
+ const tool = api.registerTool({
982
+ name: "subagent_run",
983
+ label: "Run subagents",
984
+ description: `Delegate 1..${maxTasksPerCall} independent subagent tasks in one call and return their bounded terminal results. ` +
985
+ "Each prompt runs in its own isolated child session. Only status, reason codes, message counts and a shortened " +
986
+ "final-assistant summary come back; ask a child for more detail in its prompt if you need it." +
987
+ " Optionally pass `context` — file:line anchors and commands already tried and failed are rendered as a " +
988
+ "header block at the top of each child prompt (pointers only)." +
989
+ (roles.size === 0
990
+ ? ""
991
+ : ` Configured subagent roles (pass \`role\`): ${roleSummary}. A role's instructions frame every task in the call; ` +
992
+ "child model and tools stay inherited from this session."),
993
+ parameters: {
994
+ ...parameters,
995
+ ...(backgroundEnabled
996
+ ? {
997
+ background: Type.Optional(Type.Boolean({
998
+ description: "Submit without waiting: return task tickets immediately and keep working; settled results are " +
999
+ "announced at your next model round and can be read with subagent_result (optionally waiting).",
1000
+ })),
1001
+ }
1002
+ : {}),
1003
+ },
1004
+ execute: async (first, second, _signal, onUpdate) => {
1005
+ const input = objectInput(first, second);
1006
+ const prompts = readPrompts(input.prompts, maxTasksPerCall);
1007
+ const role = resolveRole(input.role, roles);
1008
+ // context 注入(设计 §2.3):形状/边界违规在此整体拒绝;两个列表皆空归一化为
1009
+ // undefined,后续路径与 context 缺省完全一致。
1010
+ const contextBlock = renderDelegateContext(parseDelegateContext(input.context));
1011
+ const background = backgroundEnabled && input.background === true;
1012
+ // 嵌套深度硬限(退役切片 2,Option A):沿 S2 血缘头回溯本会话层级,已达
1013
+ // maxNestedDepth 层时整次调用结构化拒绝——递归的兜底边界(不依赖模型自觉)。
1014
+ // resume 不涉深度(S5 裁定:续写既有会话不计新层级,走 subagent_resume)。
1015
+ // agentDir 单源(D-075 S4 第三批):显式 options 优先,缺省回落 api.host.agentDir。
1016
+ const agentDir = options?.agentDir ?? api.host.agentDir ?? "";
1017
+ if (agentDir !== "") {
1018
+ const depth = lineageDepth(session.getSessionId(), agentDir, maxNestedDepth + 2);
1019
+ if (depth !== undefined && depth + 1 > maxNestedDepth) {
1020
+ throw new SubagentDelegateError("subagent_depth_exceeded", `this session already sits at nested depth ${depth}; delegation beyond maxNestedDepth ${maxNestedDepth} is refused`);
1021
+ }
1022
+ }
1023
+ // submit 是同步抛出契约(预算/队列/已释放),逐条捕获为条目级 rejected;
1024
+ // 全部提交完成后再逐条 drain,任务因此并行执行。
1025
+ const pending = [];
1026
+ const tasks = new Array(prompts.length).fill(undefined);
1027
+ // 回合级血缘(第 3 期第二批):提交动作发生在父回合工具阶段,此时 correlationId
1028
+ // 有值;getter 缺席或返回 undefined 时不写键(行为回退)。仅 subagent_run 新建
1029
+ // 路径携带——resume 打开既有会话、头定格,写键即"要求了但不生效"。
1030
+ const turnCorrelationId = options?.getTurnCorrelationId?.();
1031
+ for (let index = 0; index < prompts.length; index += 1) {
1032
+ const prompt = prompts[index];
1033
+ // 总量硬限(承载退役切片 2,Option A):本会话累计创建数达上限时按结构化
1034
+ // 码拒绝该条目(同批其余条目继续)。深度硬限在 execute 开头沿 S2 血缘头
1035
+ // 整调用拒绝(见上),不在此重复。
1036
+ if (createdSubagentCount >= maxTotalPerParent) {
1037
+ tasks[index] = {
1038
+ taskId: "",
1039
+ status: "rejected",
1040
+ reasonCode: "subagent_child_quota",
1041
+ reason: `this session already created ${createdSubagentCount} child sessions (cap ${maxTotalPerParent})`,
1042
+ messageCount: 0,
1043
+ };
1044
+ continue;
1045
+ }
1046
+ createdSubagentCount += 1;
1047
+ // 首条 prompt 组装(角色框定 + 可选父投影);context 头部块(有则)在最上、
1048
+ // 与后续内容空一行,无块时与现状逐字节一致。字节数仍走 submit 侧 maxPromptBytes
1049
+ // 预算,超限按预算拒绝而不是静默截断。
1050
+ const composed = role === undefined
1051
+ ? prompt
1052
+ : role.parentView === true
1053
+ ? `${composeRolePrompt(role, prompt)}\n\n${composeParentView(session, session.getSessionId()) ?? ""}`.trim()
1054
+ : composeRolePrompt(role, prompt);
1055
+ try {
1056
+ pending.push({
1057
+ index,
1058
+ handle: runtime.submit({
1059
+ prompt: contextBlock === undefined ? composed : `${contextBlock}\n\n${composed}`,
1060
+ // 血缘(D-060 S1):父会话 id 随 metadata 过线,宿主写进子会话文件头;
1061
+ // 角色档案(若角色定义带收窄)一并带上。
1062
+ metadata: {
1063
+ [AGENT_TASK_PARENT_SESSION_KEY]: session.getSessionId(),
1064
+ ...(turnCorrelationId === undefined
1065
+ ? {}
1066
+ : { [AGENT_TASK_PARENT_TRACE_KEY]: turnCorrelationId }),
1067
+ ...(role?.profile === undefined ? {} : { [AGENT_TASK_CHILD_PROFILE_KEY]: role.profile }),
1068
+ },
1069
+ }),
1070
+ });
1071
+ }
1072
+ catch (error) {
1073
+ tasks[index] = {
1074
+ taskId: "",
1075
+ status: "rejected",
1076
+ reasonCode: reasonCodeOf(error),
1077
+ reason: errorText(error),
1078
+ messageCount: 0,
1079
+ };
1080
+ }
1081
+ }
1082
+ // 后台委派(D-060 S6,配置门默认关):提交即返回票据,不 drain——任务挂在
1083
+ // 调度器上继续跑(会话级生命周期,父栅栏/会话销毁统一回收)。每个后台任务
1084
+ // 挂一个结算观察器:结算一行有界 JSON 进宿主 sink,随父的下一次模型轮次注入。
1085
+ if (background === true) {
1086
+ const tickets = [];
1087
+ for (const item of pending) {
1088
+ try {
1089
+ const handle = await item.handle;
1090
+ tickets.push({ taskId: handle.id });
1091
+ const sink = options?.backgroundDigestSink;
1092
+ if (sink !== undefined) {
1093
+ void handle
1094
+ .result()
1095
+ .then((record) => {
1096
+ sink(JSON.stringify({
1097
+ taskId: record.taskId,
1098
+ sessionId: record.sessionId,
1099
+ status: record.status,
1100
+ ...(record.reasonCode === undefined ? {} : { reasonCode: record.reasonCode }),
1101
+ }));
1102
+ })
1103
+ .catch(() => {
1104
+ // 摘要观察失败不影响任务本身(终态已在调度器落库可查)。
1105
+ });
1106
+ }
1107
+ }
1108
+ catch {
1109
+ // submit 同步抛错已在上面捕获为条目级 rejected——后台票据只收成功提交。
1110
+ }
1111
+ }
1112
+ return { background: true, tasks: tickets };
1113
+ }
1114
+ let completed = tasks.filter((task) => task !== undefined).length;
1115
+ for (const item of pending) {
1116
+ let taskId = "";
1117
+ try {
1118
+ const handle = await item.handle;
1119
+ taskId = handle.id;
1120
+ const record = await handle.drain({ timeoutMs: taskTimeoutMs });
1121
+ // escalate 上抛(D-060 S4):随任务投影一起交回父模型(取后即清)。
1122
+ const escalations = wiring?.drainEscalations?.(record.sessionId) ?? [];
1123
+ tasks[item.index] = {
1124
+ ...projectRecord(record),
1125
+ ...(escalations.length === 0 ? {} : { escalations }),
1126
+ };
1127
+ }
1128
+ catch (error) {
1129
+ // drain 抛错(如持久化失败):保留原始 message,不静默吞掉。
1130
+ tasks[item.index] = { taskId, status: "failed", reason: errorText(error), messageCount: 0 };
1131
+ }
1132
+ completed += 1;
1133
+ // D-044 §5.6:进度随任务完成节流上报(累计 step 数 + 最近一条回合摘要);
1134
+ // 只回摘要,不回模型正文。
1135
+ const progressSuffix = progressState.steps === 0
1136
+ ? ""
1137
+ : ` · steps ${progressState.steps}${progressState.last === undefined ? "" : ` · ${progressState.last}`}`;
1138
+ onUpdate?.({
1139
+ content: [{ type: "text", text: `subagent ${completed}/${prompts.length}${progressSuffix}` }],
1140
+ details: {},
1141
+ });
1142
+ }
1143
+ return { tasks: tasks.filter((task) => task !== undefined) };
1144
+ },
1145
+ });
1146
+ /**
1147
+ * 只读回读工具(D-060 S3):按 `taskId` 或 `sessionId` 取回**有界**的子会话片段。
1148
+ * - `taskId` → 调度记录(内存/durable,owner+parent 作用域);
1149
+ * - `sessionId` → 持久化 transcript(宿主注入的会话回读器;未注入时结构化报错)。
1150
+ * 返回默认只给最后 N 条(可调),并如实报告条数/字节/截断——绝不默认回灌全文。
1151
+ */
1152
+ const resultTool = api.registerTool({
1153
+ name: "subagent_result",
1154
+ label: "Read subagent result",
1155
+ description: "Read back a bounded slice of a subagent's session: pass the taskId from subagent_run, or the child's sessionId " +
1156
+ "to read its persisted transcript. Returns at most `maxMessages` trailing entries (default 20) plus counts, " +
1157
+ "byte size and a truncated flag; it never returns the whole transcript by default.",
1158
+ parameters: Type.Object({
1159
+ taskId: Type.Optional(Type.String({ description: "Task id from subagent_run (scheduler record read)" })),
1160
+ sessionId: Type.Optional(Type.String({
1161
+ description: "Child session id (persisted transcript read; requires a host session reader)",
1162
+ })),
1163
+ maxMessages: Type.Optional(Type.Number({ description: "Trailing entries to return (1..50, default 20)", minimum: 1, maximum: 50 })),
1164
+ waitMs: Type.Optional(Type.Number({
1165
+ description: "Bounded wait for a background task to settle, in ms (0..120000, default 0 = check once; " +
1166
+ "unsettled tasks come back with knownTask/taskStatus instead of the transcript)",
1167
+ minimum: 0,
1168
+ maximum: 120000,
1169
+ })),
1170
+ }),
1171
+ execute: async (first, second) => {
1172
+ const input = objectInput(first, second);
1173
+ const taskId = typeof input.taskId === "string" && input.taskId.trim() !== "" ? input.taskId : undefined;
1174
+ const sessionId = typeof input.sessionId === "string" && input.sessionId.trim() !== "" ? input.sessionId : undefined;
1175
+ const requested = input.maxMessages === undefined ? RESULT_READ_DEFAULT_MESSAGES : Number(input.maxMessages);
1176
+ if (!Number.isInteger(requested) || requested < 1 || requested > RESULT_READ_MAX_MESSAGES) {
1177
+ throw new SubagentDelegateError("subagent_invalid_input", `subagent_result maxMessages must be an integer in 1..${RESULT_READ_MAX_MESSAGES}`);
1178
+ }
1179
+ const waitMs = input.waitMs === undefined ? 0 : Number(input.waitMs);
1180
+ if (!Number.isInteger(waitMs) || waitMs < 0 || waitMs > RESULT_READ_MAX_WAIT_MS) {
1181
+ throw new SubagentDelegateError("subagent_invalid_input", `subagent_result waitMs must be an integer in 0..${RESULT_READ_MAX_WAIT_MS}`);
1182
+ }
1183
+ if (taskId === undefined && sessionId === undefined) {
1184
+ throw new SubagentDelegateError("subagent_invalid_input", "subagent_result requires taskId (scheduler record) or sessionId (persisted transcript)");
1185
+ }
1186
+ if (taskId !== undefined) {
1187
+ const requester = {
1188
+ owner: capabilityManifest.id,
1189
+ parentSessionId: session.getSessionId(),
1190
+ };
1191
+ // 后台委派(D-060 S6):waitMs > 0 时有界等待结算;否则查一次。
1192
+ const record = waitMs > 0 ? await runtime.wait(taskId, waitMs, requester) : await runtime.result(taskId, requester);
1193
+ if (record === undefined) {
1194
+ // 未结算与不存在分开表达:已知任务回当前状态,模型可稍后再取。
1195
+ const known = runtime.peek(taskId, requester);
1196
+ return known === undefined
1197
+ ? { found: false, source: "record", taskId }
1198
+ : { found: false, source: "record", taskId, knownTask: true, taskStatus: known.status };
1199
+ }
1200
+ const messages = record.messages.slice(-requested);
1201
+ // escalate 上抛(D-060 S4):按子会话 id 取走并随信封返回(取后即清)。
1202
+ const escalations = wiring?.drainEscalations?.(record.sessionId) ?? [];
1203
+ return {
1204
+ found: true,
1205
+ source: "record",
1206
+ taskId: record.taskId,
1207
+ sessionId: record.sessionId,
1208
+ status: record.status,
1209
+ ...(record.reasonCode === undefined ? {} : { reasonCode: record.reasonCode }),
1210
+ ...(record.reason === undefined ? {} : { reason: record.reason }),
1211
+ messageCount: record.messages.length,
1212
+ returnedMessages: messages.length,
1213
+ bytes: utf8ByteLength(JSON.stringify(record.messages)),
1214
+ truncated: record.truncated || messages.length < record.messages.length,
1215
+ messages: messages.map((message) => detachedSessionSnapshot(message)),
1216
+ ...(escalations.length === 0 ? {} : { escalations }),
1217
+ };
1218
+ }
1219
+ const reader = agents?.session;
1220
+ if (reader === undefined) {
1221
+ throw new SubagentDelegateError("subagent_profile_not_applied", "this host exposes no session reader; pass taskId instead (or run on a host that injects one)");
1222
+ }
1223
+ const view = await reader(sessionId);
1224
+ if (view === undefined)
1225
+ return { found: false, source: "session", sessionId };
1226
+ const entries = view.entries.slice(-requested);
1227
+ const escalations = wiring?.drainEscalations?.(view.sessionId) ?? [];
1228
+ return {
1229
+ found: true,
1230
+ source: "session",
1231
+ sessionId: view.sessionId,
1232
+ ...(view.agentRole === undefined ? {} : { roleId: view.agentRole }),
1233
+ entryCount: view.entryCount,
1234
+ returnedMessages: entries.length,
1235
+ bytes: view.bytes,
1236
+ truncated: view.truncated || entries.length < view.entryCount,
1237
+ messages: entries.map((entry) => detachedSessionSnapshot(entry)),
1238
+ ...(escalations.length === 0 ? {} : { escalations }),
1239
+ };
1240
+ },
1241
+ });
1242
+ // steer 不设模型面工具(D-060 S4):`subagent_run` 是阻塞式 drain-to-terminal,
1243
+ // 父模型在工具调用期间不存在"运行中的子任务"窗口——暴露即死 affordance(与
1244
+ // `deadlineMs` 同一纪律)。父→子 steer 走程序面:agents 桥的 `steer`(运行中
1245
+ // 的后台任务)与本调度器的 `steer`/`findBySession` 原语,供编排插件组合。
1246
+ /**
1247
+ * 父→子续跑(D-060 S4,默认关):对**已终态且已落盘**的子会话按 sessionId 继续
1248
+ * 提交一轮任务(同一份 JSONL 续写,多轮子任务)。血缘作用域由宿主会话回读器判定,
1249
+ * 活跃任务占用以 `subagent_session_busy` 拒绝。shared 与 pooled 都支持——pooled 下
1250
+ * resume 键随 metadata 过线,runner 打开既有会话文件续写。
1251
+ */
1252
+ const resumeTool = communication.resume === true
1253
+ ? api.registerTool({
1254
+ name: "subagent_resume",
1255
+ label: "Resume a subagent",
1256
+ description: "Continue a finished subagent session by its sessionId: submit one more prompt into the same persisted " +
1257
+ "transcript (multi-turn subtask) and get the bounded terminal projection, like subagent_run for one task.",
1258
+ parameters: Type.Object({
1259
+ sessionId: Type.String({ description: "Child session id of a finished subagent task" }),
1260
+ prompt: Type.String({ minLength: 1, description: "Follow-up prompt for the continued session" }),
1261
+ }),
1262
+ execute: async (first, second) => {
1263
+ const input = objectInput(first, second);
1264
+ const sessionId = typeof input.sessionId === "string" ? input.sessionId : "";
1265
+ const prompt = typeof input.prompt === "string" ? input.prompt : "";
1266
+ if (sessionId === "" || prompt.trim() === "") {
1267
+ throw new SubagentDelegateError("subagent_invalid_input", "subagent_resume requires sessionId and prompt");
1268
+ }
1269
+ const reader = agents?.session;
1270
+ if (reader === undefined) {
1271
+ throw new SubagentDelegateError("subagent_profile_not_applied", "this host exposes no session reader; resume cannot verify the session scope");
1272
+ }
1273
+ const view = await reader(sessionId);
1274
+ if (view === undefined) {
1275
+ throw new SubagentDelegateError("subagent_unknown_session", `no child session ${sessionId} under this parent`);
1276
+ }
1277
+ const requester = { owner: capabilityManifest.id, parentSessionId: session.getSessionId() };
1278
+ const active = runtime.findBySession(sessionId, requester);
1279
+ if (active !== undefined && !active.settled) {
1280
+ throw new SubagentDelegateError("subagent_session_busy", `session ${sessionId} still has an unsettled task (${active.taskId}); steer or wait instead`);
1281
+ }
1282
+ let handle;
1283
+ try {
1284
+ handle = await runtime.submit({
1285
+ prompt,
1286
+ metadata: {
1287
+ [AGENT_TASK_PARENT_SESSION_KEY]: session.getSessionId(),
1288
+ [AGENT_TASK_RESUME_SESSION_KEY]: sessionId,
1289
+ },
1290
+ });
1291
+ }
1292
+ catch (error) {
1293
+ throw new SubagentDelegateError("subagent_submit_rejected", errorText(error));
1294
+ }
1295
+ const record = await handle.drain({ timeoutMs: taskTimeoutMs });
1296
+ return { tasks: [projectRecord(record)] };
1297
+ },
1298
+ })
1299
+ : undefined;
1300
+ /**
1301
+ * 子会话盘点(D-060 模型面):列出**本会话自己的**子代理会话(最近活动倒序、
1302
+ * 有界),并用调度器记录富化活跃/终态信息。宿主注入 `agents.list` 才注册;
1303
+ * 读的是落盘目录,父会话跨进程重启后仍能找回自己的子代理。返回的 sessionId
1304
+ * 可直接用于 `subagent_result` / `subagent_resume`。
1305
+ */
1306
+ const listTool = agents?.list === undefined
1307
+ ? undefined
1308
+ : api.registerTool({
1309
+ name: "subagent_list",
1310
+ label: "List subagent sessions",
1311
+ description: "List this session's own subagent sessions (newest activity first) with role, timestamps and transcript " +
1312
+ "size; tasks still in flight are marked active, settled ones carry their terminal status. Read-only. " +
1313
+ "Use a listed sessionId with subagent_result or subagent_resume.",
1314
+ parameters: Type.Object({
1315
+ limit: Type.Optional(Type.Number({ description: "Max entries to return (1..50, default 20)", minimum: 1, maximum: 50 })),
1316
+ }),
1317
+ execute: async (first, second) => {
1318
+ const input = objectInput(first, second);
1319
+ const requested = input.limit === undefined ? 20 : Number(input.limit);
1320
+ if (!Number.isInteger(requested) || requested < 1 || requested > 50) {
1321
+ throw new SubagentDelegateError("subagent_invalid_input", "subagent_list limit must be an integer in 1..50");
1322
+ }
1323
+ const summaries = (await agents.list?.()) ?? [];
1324
+ const requester = { owner: capabilityManifest.id, parentSessionId: session.getSessionId() };
1325
+ const sessions = [];
1326
+ for (const summary of summaries.slice(0, requested)) {
1327
+ const live = runtime.findBySession(summary.sessionId, requester);
1328
+ if (live !== undefined && !live.settled) {
1329
+ sessions.push({ ...summary, active: true });
1330
+ continue;
1331
+ }
1332
+ if (live !== undefined) {
1333
+ const record = await runtime.result(live.taskId, requester);
1334
+ if (record !== undefined) {
1335
+ sessions.push({
1336
+ ...summary,
1337
+ status: record.status,
1338
+ ...(record.reasonCode === undefined ? {} : { reasonCode: record.reasonCode }),
1339
+ ...(record.reason === undefined ? {} : { reason: record.reason }),
1340
+ });
1341
+ continue;
1342
+ }
1343
+ }
1344
+ // 无内存记录(父重启前的旧子会话):只回摘要,transcript 仍在盘上。
1345
+ sessions.push(summary);
1346
+ }
1347
+ return { count: sessions.length, sessions };
1348
+ },
1349
+ });
1350
+ return [
1351
+ tool,
1352
+ resultTool,
1353
+ ...(resumeTool === undefined ? [] : [resumeTool]),
1354
+ ...(listTool === undefined ? [] : [listTool]),
1355
+ // 子侧通信面(D-060 S4/S5):获准自建会话两面并存(向下委派 + 向父上抛/兄弟消息)。
1356
+ ...childTools,
1357
+ createDisposableRegistration("subagent-delegate.runtime",
1358
+ // 调度器释放是有界的 async 过程(cancel fence + 有界 drain),但 embedded
1359
+ // builtin 走同步 loader(loadCapabilityPluginSync -> disposePluginSync 拒绝
1360
+ // 异步 cleanup),因此登记处同步发起、观察 promise,不返回 Promise。
1361
+ () => {
1362
+ for (const unsubscribe of progressUnsubscribes)
1363
+ unsubscribe();
1364
+ progressUnsubscribes.clear();
1365
+ // 先释放调度器(任务终态落定),再回收 runner 池(先释放任务再杀进程)。
1366
+ void runtime
1367
+ .dispose()
1368
+ .then(() => pool?.shutdown())
1369
+ .catch(() => {
1370
+ // 释放失败的收尾错误已由 runtime 内部按任务暴露;同步 cleanup 路径
1371
+ // 无法再抛,这里只保证不产生未处理的 rejection。
1372
+ });
1373
+ }, `registration:${capabilityManifest.id}:runtime`),
1374
+ ];
1375
+ }
1376
+ //# sourceMappingURL=delegate.js.map