@ai-agent-forge/plugin-subagent-delegate 0.86.7 → 0.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/delegate.js CHANGED
@@ -67,34 +67,9 @@ export const capabilityManifest = {
67
67
  };
68
68
  // 通信面的宿主接线 `SubagentCommunicationWiringV1`(含信箱消息形状)是 plugin-sdk
69
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
- }
70
+ // 通信面配置键已移除(D-060 裁决 15,2026-10-03):resume/escalate/sibling 曾是
71
+ // 出厂默认全开的显式开关(裁决 13/14 翻转后仅剩"显式 false 关闭"形态),随开关
72
+ // 形态一并退役——功能基本面只有"有"与"没有",配置键出现即装载期失败。
98
73
  /** 把宿主/调度器/信箱的结构化错误映射为工具面的同码委托错误(保留原始 message)。 */
99
74
  function mapCommunicationError(error) {
100
75
  if (error instanceof SubagentMailboxErrorV1) {
@@ -513,6 +488,11 @@ const RESULT_READ_DEFAULT_MESSAGES = 20;
513
488
  const RESULT_READ_MAX_MESSAGES = 50;
514
489
  /** `subagent_result` 的有界等待上限(后台结算收割),默认 0 = 只查一次。 */
515
490
  const RESULT_READ_MAX_WAIT_MS = 120_000;
491
+ /**
492
+ * 每任务随结算投影/信封返回的兄弟流量摘录条数(D-060 裁决 14 父可见性):有界——
493
+ * 这是审计摘录不是消息回灌,全量以信箱审计日志为准。
494
+ */
495
+ const SIBLING_TRAFFIC_EXCERPTS = 8;
516
496
  /** UTF-8 字节数(回读信封如实报告体积)。 */
517
497
  function utf8ByteLength(text) {
518
498
  return Buffer.byteLength(text, "utf8");
@@ -668,14 +648,16 @@ export function createCapability(api, options) {
668
648
  const agents = api.agents;
669
649
  if (!session)
670
650
  throw new Error("subagent-delegate requires the session capability");
671
- // 通信面配置在装载期 fail fast(两个上下文都读同一份 agent-dir 配置文件)。
672
- const communication = readCommunicationConfig(api.config.communication);
651
+ // 通信面配置键已移除(D-060 裁决 15):出现即装载期失败,不留"配置了但没生效"的假象。
652
+ if (api.config.communication !== undefined) {
653
+ throw new Error("subagent-delegate config communication was removed (D-060 ruling 15): communication tools register unconditionally; remove the key");
654
+ }
673
655
  const wiring = options?.communicationWiring;
674
- // 子侧通信面(子→父 / 子↔子):只要宿主接线了 post 通道且配置开启就注册——
656
+ // 子侧通信面(子→父 / 子↔子):只要宿主接线了 post 通道就注册——
675
657
  // 叶子会话(无 agents)只拿这一面;获准自建会话(D-060 S5,agents 在场)两面都要:
676
658
  // 它既能向下委派(parent face),也仍能向自己的父上抛/收兄弟消息(child face)。
677
659
  const childTools = [];
678
- if (wiring?.postEscalate !== undefined && communication.escalate === true) {
660
+ if (wiring?.postEscalate !== undefined) {
679
661
  childTools.push(api.registerTool({
680
662
  name: "subagent_escalate",
681
663
  label: "Escalate to parent",
@@ -703,12 +685,14 @@ export function createCapability(api, options) {
703
685
  },
704
686
  }));
705
687
  }
706
- if (wiring?.sendSibling !== undefined && communication.sibling === true) {
688
+ if (wiring?.sendSibling !== undefined) {
707
689
  childTools.push(api.registerTool({
708
690
  name: "subagent_send",
709
691
  label: "Message a sibling",
710
692
  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.",
693
+ "Cross-family addressing is rejected. This channel is for data and intermediate artifacts only: task " +
694
+ "assignment and result acceptance stay with the parent agent. Sibling messages are visible to the parent " +
695
+ "(audited as excerpts with task results).",
712
696
  parameters: Type.Object({
713
697
  to: Type.String({ description: "Recipient child session id" }),
714
698
  text: Type.String({
@@ -735,7 +719,8 @@ export function createCapability(api, options) {
735
719
  name: "subagent_inbox",
736
720
  label: "Read sibling inbox",
737
721
  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.",
722
+ "consumed by this call; poll it when coordination is expected. The sibling channel carries data and " +
723
+ "intermediate artifacts only — assignment and acceptance stay with the parent, which can audit this traffic.",
739
724
  parameters: Type.Object({
740
725
  limit: Type.Optional(Type.Number({ description: "Max messages to take (1..20, default 10)", minimum: 1, maximum: 20 })),
741
726
  }),
@@ -875,12 +860,12 @@ export function createCapability(api, options) {
875
860
  // 100):对**非 resume** 的每次 submit 计数,超限按 `subagent_child_quota` 拒绝。
876
861
  const maxTotalPerParent = resolveInt(undefined, configInt(api, "maxTotalPerParent", 0), 100, 0, "maxTotalPerParent");
877
862
  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");
863
+ // background 配置键已移除(D-060 裁决 15,2026-10-03):后台委派是功能基本面,
864
+ // 无条件注册(`subagent_run` 的 background 参数与 subagent_cancel / subagent_steer
865
+ // 随之常在);键出现即装载期失败,不留"配置了但没生效"的假象。
866
+ if (api.config.background !== undefined) {
867
+ throw new Error("subagent-delegate config background was removed (D-060 ruling 15): background delegation registers unconditionally; remove the key");
882
868
  }
883
- const backgroundEnabled = api.config.background === true;
884
869
  // 角色目录 + 方案关联在装载期解析:非法文件/非法关联直接失败,不留"配置了但没生效"的假象。
885
870
  const roleCatalog = options?.roles === undefined
886
871
  ? { roles: new Map(), dangling: [], brokenUnassociated: [] }
@@ -935,6 +920,7 @@ export function createCapability(api, options) {
935
920
  parentSessionId: session.getSessionId(),
936
921
  ...(sessionAbortSignal === undefined ? {} : { parentSignal: sessionAbortSignal }),
937
922
  ...(limits === undefined ? {} : { limits }),
923
+ ...(api.logger === undefined ? {} : { logger: api.logger }),
938
924
  });
939
925
  const parameters = Type.Object({
940
926
  prompts: Type.Array(Type.String({
@@ -977,6 +963,14 @@ export function createCapability(api, options) {
977
963
  "Omit for a generic task. Roles frame the task; they do not change the child's model or tools.",
978
964
  })),
979
965
  }),
966
+ // background(D-060 S6)在 properties 内组合:此前的 `{...parameters, background}`
967
+ // 顶层展开把键放在 properties 之外,模型面 schema(anthropic 只取 schema.properties、
968
+ // openai 原样透传)从未真正声明该参数——D-060 裁决 13 翻转后随默认注册修正,
969
+ // 裁决 15 起开关形态退役、参数无条件声明。
970
+ background: Type.Optional(Type.Boolean({
971
+ description: "Submit without waiting: return task tickets immediately and keep working; settled results are " +
972
+ "announced at your next model round and can be read with subagent_result (optionally waiting).",
973
+ })),
980
974
  });
981
975
  const tool = api.registerTool({
982
976
  name: "subagent_run",
@@ -986,21 +980,19 @@ export function createCapability(api, options) {
986
980
  "final-assistant summary come back; ask a child for more detail in its prompt if you need it." +
987
981
  " Optionally pass `context` — file:line anchors and commands already tried and failed are rendered as a " +
988
982
  "header block at the top of each child prompt (pointers only)." +
983
+ // fork-join 推荐模式(D-060 裁决 14):大任务拆并行块 + 最终组合任务,中间产物
984
+ // 留子层、主上下文不膨胀;通知粒度——结算必然回注但只有组合任务的报告需验收。
985
+ " For large splittable work prefer fork-join: launch the parallel chunk tasks with background:true, then submit " +
986
+ "one final assembly task (a separate background call) that gathers the chunks with subagent_result({taskId, waitMs}) " +
987
+ "— intermediate results stay in the child layer and only the assembled final report enters your context. Every " +
988
+ "task's settlement is announced to you (agent-task-settled), but notifications are not per-task acceptances: " +
989
+ "accept and act on (todos/goals/next steps) only the final assembly task's report; other settled lines are " +
990
+ "status only." +
989
991
  (roles.size === 0
990
992
  ? ""
991
993
  : ` Configured subagent roles (pass \`role\`): ${roleSummary}. A role's instructions frame every task in the call; ` +
992
994
  "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
- },
995
+ parameters,
1004
996
  execute: async (first, second, _signal, onUpdate) => {
1005
997
  const input = objectInput(first, second);
1006
998
  const prompts = readPrompts(input.prompts, maxTasksPerCall);
@@ -1008,7 +1000,7 @@ export function createCapability(api, options) {
1008
1000
  // context 注入(设计 §2.3):形状/边界违规在此整体拒绝;两个列表皆空归一化为
1009
1001
  // undefined,后续路径与 context 缺省完全一致。
1010
1002
  const contextBlock = renderDelegateContext(parseDelegateContext(input.context));
1011
- const background = backgroundEnabled && input.background === true;
1003
+ const background = input.background === true;
1012
1004
  // 嵌套深度硬限(退役切片 2,Option A):沿 S2 血缘头回溯本会话层级,已达
1013
1005
  // maxNestedDepth 层时整次调用结构化拒绝——递归的兜底边界(不依赖模型自觉)。
1014
1006
  // resume 不涉深度(S5 裁定:续写既有会话不计新层级,走 subagent_resume)。
@@ -1120,9 +1112,12 @@ export function createCapability(api, options) {
1120
1112
  const record = await handle.drain({ timeoutMs: taskTimeoutMs });
1121
1113
  // escalate 上抛(D-060 S4):随任务投影一起交回父模型(取后即清)。
1122
1114
  const escalations = wiring?.drainEscalations?.(record.sessionId) ?? [];
1115
+ // 兄弟流量摘录(D-060 裁决 14):只读审计面,随同一信封对父可见。
1116
+ const siblingTraffic = wiring?.readSiblingTraffic?.(record.sessionId, SIBLING_TRAFFIC_EXCERPTS) ?? [];
1123
1117
  tasks[item.index] = {
1124
1118
  ...projectRecord(record),
1125
1119
  ...(escalations.length === 0 ? {} : { escalations }),
1120
+ ...(siblingTraffic.length === 0 ? {} : { siblingTraffic }),
1126
1121
  };
1127
1122
  }
1128
1123
  catch (error) {
@@ -1200,6 +1195,7 @@ export function createCapability(api, options) {
1200
1195
  const messages = record.messages.slice(-requested);
1201
1196
  // escalate 上抛(D-060 S4):按子会话 id 取走并随信封返回(取后即清)。
1202
1197
  const escalations = wiring?.drainEscalations?.(record.sessionId) ?? [];
1198
+ const siblingTraffic = wiring?.readSiblingTraffic?.(record.sessionId, SIBLING_TRAFFIC_EXCERPTS) ?? [];
1203
1199
  return {
1204
1200
  found: true,
1205
1201
  source: "record",
@@ -1214,6 +1210,7 @@ export function createCapability(api, options) {
1214
1210
  truncated: record.truncated || messages.length < record.messages.length,
1215
1211
  messages: messages.map((message) => detachedSessionSnapshot(message)),
1216
1212
  ...(escalations.length === 0 ? {} : { escalations }),
1213
+ ...(siblingTraffic.length === 0 ? {} : { siblingTraffic }),
1217
1214
  };
1218
1215
  }
1219
1216
  const reader = agents?.session;
@@ -1225,6 +1222,7 @@ export function createCapability(api, options) {
1225
1222
  return { found: false, source: "session", sessionId };
1226
1223
  const entries = view.entries.slice(-requested);
1227
1224
  const escalations = wiring?.drainEscalations?.(view.sessionId) ?? [];
1225
+ const siblingTraffic = wiring?.readSiblingTraffic?.(view.sessionId, SIBLING_TRAFFIC_EXCERPTS) ?? [];
1228
1226
  return {
1229
1227
  found: true,
1230
1228
  source: "session",
@@ -1236,96 +1234,137 @@ export function createCapability(api, options) {
1236
1234
  truncated: view.truncated || entries.length < view.entryCount,
1237
1235
  messages: entries.map((entry) => detachedSessionSnapshot(entry)),
1238
1236
  ...(escalations.length === 0 ? {} : { escalations }),
1237
+ ...(siblingTraffic.length === 0 ? {} : { siblingTraffic }),
1239
1238
  };
1240
1239
  },
1241
1240
  });
1242
- // steer 不设模型面工具(D-060 S4):`subagent_run` 是阻塞式 drain-to-terminal,
1243
- // 父模型在工具调用期间不存在"运行中的子任务"窗口——暴露即死 affordance(与
1244
- // `deadlineMs` 同一纪律)。父→子 steer 走程序面:agents 桥的 `steer`(运行中
1245
- // 的后台任务)与本调度器的 `steer`/`findBySession` 原语,供编排插件组合。
1246
- // subagent_cancel(S4 补充裁决,2026-10-01):仅 background 委派启用时注册——
1247
- // 前台 drain 下父模型在 subagent_run 阻塞中不存在运行中窗口(与 steer 的 S4
1248
- // 裁决同前提),cancel 只在 background 委派返回票据后才有可达窗口。
1249
- const cancelTool = backgroundEnabled
1250
- ? api.registerTool({
1251
- name: "subagent_cancel",
1252
- label: "Cancel a background subagent",
1253
- description: "Cancel a running background subagent task by its taskId (the ticket returned by subagent_run with " +
1254
- "background: true). Only background delegation tickets are cancellable: a foreground subagent_run call " +
1255
- "blocks until its tasks finish, so it never leaves a running task behind. Terminal statuses (including a " +
1256
- "task cancelled by something else) come back as already_terminal; unknown taskIds as not_found.",
1257
- parameters: Type.Object({
1258
- taskId: Type.String({ description: "Task id from a background subagent_run ticket" }),
1259
- reason: Type.Optional(Type.String({ description: "Short reason recorded on the cancellation fence" })),
1241
+ // steer 的模型面(D-060 裁决 13,2026-10-02 用户裁决):`subagent_steer` 随
1242
+ // background 面注册。原 S4 裁决"steer 不设模型面工具"的前提——前台阻塞 drain 下
1243
+ // 父模型无"运行中窗口"——在 background 默认开后失效(提交即返回票据,运行中窗口
1244
+ // 存在),该论证保留为背景说明。语义对齐主流取证(codex v2 send_message /
1245
+ // ZCode SendMessage 均为模型面排队投递):不打断子代理当前回合,在其下一个模型
1246
+ // 请求前生效。程序面(agents 桥的 steer 与本调度器的 steer/findBySession 原语)
1247
+ // 保持不变,供编排插件组合。
1248
+ // subagent_cancel(S4 补充裁决,2026-10-01):原"仅 background 委派启用时注册"
1249
+ // 的前提(前台 drain 下父模型在 subagent_run 阻塞中不存在运行中窗口)保留为
1250
+ // 背景说明——cancel 只在 background 委派返回票据后才有可达窗口。
1251
+ // 两个工具自 D-060 裁决 15(2026-10-03)起无条件注册(开关形态退役)。
1252
+ const cancelTool = api.registerTool({
1253
+ name: "subagent_cancel",
1254
+ label: "Cancel a background subagent",
1255
+ description: "Cancel a running background subagent task by its taskId (the ticket returned by subagent_run with " +
1256
+ "background: true). Only background delegation tickets are cancellable: a foreground subagent_run call " +
1257
+ "blocks until its tasks finish, so it never leaves a running task behind. Terminal statuses (including a " +
1258
+ "task cancelled by something else) come back as already_terminal; unknown taskIds as not_found.",
1259
+ parameters: Type.Object({
1260
+ taskId: Type.String({ description: "Task id from a background subagent_run ticket" }),
1261
+ reason: Type.Optional(Type.String({ description: "Short reason recorded on the cancellation fence" })),
1262
+ }),
1263
+ execute: async (first, second) => {
1264
+ const input = objectInput(first, second);
1265
+ const taskId = typeof input.taskId === "string" ? input.taskId : "";
1266
+ if (taskId.trim() === "") {
1267
+ throw new SubagentDelegateError("subagent_invalid_input", "subagent_cancel requires a taskId");
1268
+ }
1269
+ const reason = typeof input.reason === "string" && input.reason.trim() !== "" ? input.reason : undefined;
1270
+ const result = await runtime.cancelById(taskId, { owner: capabilityManifest.id, parentSessionId: session.getSessionId() }, reason);
1271
+ return result.outcome === "not_found"
1272
+ ? { outcome: "not_found", taskId }
1273
+ : { outcome: result.outcome, taskId, status: result.status };
1274
+ },
1275
+ });
1276
+ // subagent_steer(D-060 裁决 13):与 cancelTool 同属 background 面——有界引导消息
1277
+ // 投递给运行中的后台子任务(排队语义),需要硬终止时模型仍走 subagent_cancel。
1278
+ // D-060 裁决 15(2026-10-03)起无条件注册(开关形态退役)。
1279
+ const steerTool = api.registerTool({
1280
+ name: "subagent_steer",
1281
+ label: "Steer a background subagent",
1282
+ description: "Deliver one bounded steering message to a running background subagent task by its taskId (the ticket " +
1283
+ "returned by subagent_run with background: true). Queued delivery: it does NOT interrupt the subagent's " +
1284
+ "current turn — it takes effect before the task's next model request. Use it when user feedback or new " +
1285
+ "information should change the subtask's direction. Use subagent_cancel when the task must stop instead.",
1286
+ parameters: Type.Object({
1287
+ taskId: Type.String({ description: "Task id from a background subagent_run ticket" }),
1288
+ text: Type.String({
1289
+ minLength: 1,
1290
+ description: `Bounded steering message (at most ${SUBAGENT_MAILBOX_MAX_MESSAGE_BYTES} utf-8 bytes), delivered before the task's next model request`,
1260
1291
  }),
1261
- execute: async (first, second) => {
1262
- const input = objectInput(first, second);
1263
- const taskId = typeof input.taskId === "string" ? input.taskId : "";
1264
- if (taskId.trim() === "") {
1265
- throw new SubagentDelegateError("subagent_invalid_input", "subagent_cancel requires a taskId");
1266
- }
1267
- const reason = typeof input.reason === "string" && input.reason.trim() !== "" ? input.reason : undefined;
1268
- const result = await runtime.cancelById(taskId, { owner: capabilityManifest.id, parentSessionId: session.getSessionId() }, reason);
1269
- return result.outcome === "not_found"
1270
- ? { outcome: "not_found", taskId }
1271
- : { outcome: result.outcome, taskId, status: result.status };
1272
- },
1273
- })
1274
- : undefined;
1292
+ }),
1293
+ execute: async (first, second) => {
1294
+ const input = objectInput(first, second);
1295
+ const taskId = typeof input.taskId === "string" ? input.taskId : "";
1296
+ const text = typeof input.text === "string" ? input.text : "";
1297
+ if (taskId.trim() === "" || text.trim() === "") {
1298
+ throw new SubagentDelegateError("subagent_invalid_input", "subagent_steer requires a taskId and non-empty text");
1299
+ }
1300
+ if (Buffer.byteLength(text, "utf8") > SUBAGENT_MAILBOX_MAX_MESSAGE_BYTES) {
1301
+ throw new SubagentDelegateError("subagent_message_too_large", `subagent_steer text exceeds ${SUBAGENT_MAILBOX_MAX_MESSAGE_BYTES} utf-8 bytes`);
1302
+ }
1303
+ try {
1304
+ await runtime.steer(taskId, text, {
1305
+ owner: capabilityManifest.id,
1306
+ parentSessionId: session.getSessionId(),
1307
+ });
1308
+ }
1309
+ catch (error) {
1310
+ mapCommunicationError(error);
1311
+ }
1312
+ return { delivered: true, taskId };
1313
+ },
1314
+ });
1275
1315
  /**
1276
- * 父→子续跑(D-060 S4,默认关):对**已终态且已落盘**的子会话按 sessionId 继续
1277
- * 提交一轮任务(同一份 JSONL 续写,多轮子任务)。血缘作用域由宿主会话回读器判定,
1278
- * 活跃任务占用以 `subagent_session_busy` 拒绝。shared 与 pooled 都支持——pooled 下
1316
+ * 父→子续跑(D-060 S4):对**已终态且已落盘**的子会话按 sessionId 继续提交一轮
1317
+ * 任务(同一份 JSONL 续写,多轮子任务)。血缘作用域由宿主会话回读器判定,活跃
1318
+ * 任务占用以 `subagent_session_busy` 拒绝。shared 与 pooled 都支持——pooled 下
1279
1319
  * resume 键随 metadata 过线,runner 打开既有会话文件续写。
1320
+ * D-060 裁决 15(2026-10-03)起无条件注册(开关形态退役)。
1280
1321
  */
1281
- const resumeTool = communication.resume === true
1282
- ? api.registerTool({
1283
- name: "subagent_resume",
1284
- label: "Resume a subagent",
1285
- description: "Continue a finished subagent session by its sessionId: submit one more prompt into the same persisted " +
1286
- "transcript (multi-turn subtask) and get the bounded terminal projection, like subagent_run for one task.",
1287
- parameters: Type.Object({
1288
- sessionId: Type.String({ description: "Child session id of a finished subagent task" }),
1289
- prompt: Type.String({ minLength: 1, description: "Follow-up prompt for the continued session" }),
1290
- }),
1291
- execute: async (first, second) => {
1292
- const input = objectInput(first, second);
1293
- const sessionId = typeof input.sessionId === "string" ? input.sessionId : "";
1294
- const prompt = typeof input.prompt === "string" ? input.prompt : "";
1295
- if (sessionId === "" || prompt.trim() === "") {
1296
- throw new SubagentDelegateError("subagent_invalid_input", "subagent_resume requires sessionId and prompt");
1297
- }
1298
- const reader = agents?.session;
1299
- if (reader === undefined) {
1300
- throw new SubagentDelegateError("subagent_profile_not_applied", "this host exposes no session reader; resume cannot verify the session scope");
1301
- }
1302
- const view = await reader(sessionId);
1303
- if (view === undefined) {
1304
- throw new SubagentDelegateError("subagent_unknown_session", `no child session ${sessionId} under this parent`);
1305
- }
1306
- const requester = { owner: capabilityManifest.id, parentSessionId: session.getSessionId() };
1307
- const active = runtime.findBySession(sessionId, requester);
1308
- if (active !== undefined && !active.settled) {
1309
- throw new SubagentDelegateError("subagent_session_busy", `session ${sessionId} still has an unsettled task (${active.taskId}); steer or wait instead`);
1310
- }
1311
- let handle;
1312
- try {
1313
- handle = await runtime.submit({
1314
- prompt,
1315
- metadata: {
1316
- [AGENT_TASK_PARENT_SESSION_KEY]: session.getSessionId(),
1317
- [AGENT_TASK_RESUME_SESSION_KEY]: sessionId,
1318
- },
1319
- });
1320
- }
1321
- catch (error) {
1322
- throw new SubagentDelegateError("subagent_submit_rejected", errorText(error));
1323
- }
1324
- const record = await handle.drain({ timeoutMs: taskTimeoutMs });
1325
- return { tasks: [projectRecord(record)] };
1326
- },
1327
- })
1328
- : undefined;
1322
+ const resumeTool = api.registerTool({
1323
+ name: "subagent_resume",
1324
+ label: "Resume a subagent",
1325
+ description: "Continue a finished subagent session by its sessionId: submit one more prompt into the same persisted " +
1326
+ "transcript (multi-turn subtask) and get the bounded terminal projection, like subagent_run for one task.",
1327
+ parameters: Type.Object({
1328
+ sessionId: Type.String({ description: "Child session id of a finished subagent task" }),
1329
+ prompt: Type.String({ minLength: 1, description: "Follow-up prompt for the continued session" }),
1330
+ }),
1331
+ execute: async (first, second) => {
1332
+ const input = objectInput(first, second);
1333
+ const sessionId = typeof input.sessionId === "string" ? input.sessionId : "";
1334
+ const prompt = typeof input.prompt === "string" ? input.prompt : "";
1335
+ if (sessionId === "" || prompt.trim() === "") {
1336
+ throw new SubagentDelegateError("subagent_invalid_input", "subagent_resume requires sessionId and prompt");
1337
+ }
1338
+ const reader = agents?.session;
1339
+ if (reader === undefined) {
1340
+ throw new SubagentDelegateError("subagent_profile_not_applied", "this host exposes no session reader; resume cannot verify the session scope");
1341
+ }
1342
+ const view = await reader(sessionId);
1343
+ if (view === undefined) {
1344
+ throw new SubagentDelegateError("subagent_unknown_session", `no child session ${sessionId} under this parent`);
1345
+ }
1346
+ const requester = { owner: capabilityManifest.id, parentSessionId: session.getSessionId() };
1347
+ const active = runtime.findBySession(sessionId, requester);
1348
+ if (active !== undefined && !active.settled) {
1349
+ throw new SubagentDelegateError("subagent_session_busy", `session ${sessionId} still has an unsettled task (${active.taskId}); steer or wait instead`);
1350
+ }
1351
+ let handle;
1352
+ try {
1353
+ handle = await runtime.submit({
1354
+ prompt,
1355
+ metadata: {
1356
+ [AGENT_TASK_PARENT_SESSION_KEY]: session.getSessionId(),
1357
+ [AGENT_TASK_RESUME_SESSION_KEY]: sessionId,
1358
+ },
1359
+ });
1360
+ }
1361
+ catch (error) {
1362
+ throw new SubagentDelegateError("subagent_submit_rejected", errorText(error));
1363
+ }
1364
+ const record = await handle.drain({ timeoutMs: taskTimeoutMs });
1365
+ return { tasks: [projectRecord(record)] };
1366
+ },
1367
+ });
1329
1368
  /**
1330
1369
  * 子会话盘点(D-060 模型面):列出**本会话自己的**子代理会话(最近活动倒序、
1331
1370
  * 有界),并用调度器记录富化活跃/终态信息。宿主注入 `agents.list` 才注册;
@@ -1379,8 +1418,9 @@ export function createCapability(api, options) {
1379
1418
  return [
1380
1419
  tool,
1381
1420
  resultTool,
1382
- ...(cancelTool === undefined ? [] : [cancelTool]),
1383
- ...(resumeTool === undefined ? [] : [resumeTool]),
1421
+ cancelTool,
1422
+ steerTool,
1423
+ resumeTool,
1384
1424
  ...(listTool === undefined ? [] : [listTool]),
1385
1425
  // 子侧通信面(D-060 S4/S5):获准自建会话两面并存(向下委派 + 向父上抛/兄弟消息)。
1386
1426
  ...childTools,