@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/delegate.js CHANGED
@@ -32,8 +32,12 @@ import { AGENT_TASK_CHILD_PROFILE_KEY, AGENT_TASK_PARENT_SESSION_KEY, AGENT_TASK
32
32
  import { readAgentSessionLineage } from "@agent-forge/plugin-sdk/session-lineage";
33
33
  import { SUBAGENT_MAILBOX_MAX_MESSAGE_BYTES, SubagentMailboxErrorV1, subscribeTaskProgressV1, } from "@agent-forge/protocol/subagent";
34
34
  import { Type } from "typebox";
35
+ // 出厂默认值单一定义源(含 manifest config 与 fallback):数值不在此处复制。
36
+ import { DEFAULT_SUBAGENT_LIMITS, DEFAULT_SUBAGENT_MAX_NESTED_DEPTH, DEFAULT_SUBAGENT_MAX_TASKS_PER_CALL, DEFAULT_SUBAGENT_MAX_TOTAL_PER_PARENT, DEFAULT_SUBAGENT_TASK_TIMEOUT_MS, } from "./defaults.js";
37
+ import { createSubagentRunRenderCall, subagentRunResultRenderer } from "./render.js";
35
38
  import { createPooledAgentTaskAdapterV1, } from "./runner-pool.js";
36
39
  import { createNodeSubagentRunnerSpawnerV1 } from "./runner-spawn.js";
40
+ import { createAgentTaskSchedulerCapabilityProviderV1 } from "./scheduler-capability.js";
37
41
  import { resolveSelfInvocationV1 } from "./self-invocation.js";
38
42
  import { createAgentTaskApiAdapterV1 } from "./shared-adapter.js";
39
43
  import { createSubagentRuntime, SubagentRuntimeError, } from "./subagent-runtime.js";
@@ -45,27 +49,24 @@ export const capabilityManifest = {
45
49
  // 此时本能力"不适用"而非"加载失败",缺失时静默不注册工具,避免每个叶子会话刷拒绝警告。
46
50
  requiredCapabilities: ["session", "tools"],
47
51
  optionalCapabilities: ["agents"],
52
+ provides: [{ id: "agent-forge.agent-task-scheduler", version: 1, kind: "service" }],
48
53
  /**
49
54
  * 用户可配置默认值(D-044 §6 四层归属的 Profile/capability 配置层):
50
55
  * 经 agent-dir 配置通道 `<agentDir>/capabilities/builtin.subagent-delegate.json`
51
56
  * 浅合并覆盖;宿主显式 options 优先级最高;非法值在装载时 fail fast。
52
57
  */
53
58
  config: {
54
- maxConcurrentSubagents: 8,
55
- maxQueueDepth: 200,
56
- maxPromptBytes: 65536,
57
- maxMetadataBytes: 16384,
58
- maxResultBytes: 524288,
59
- maxMessages: 2000,
60
- drainTimeoutMs: 30000,
61
- maxRetainedRecords: 256,
62
- maxTasksPerCall: 4,
63
- taskTimeoutMs: 300000,
59
+ ...DEFAULT_SUBAGENT_LIMITS,
60
+ maxTasksPerCall: DEFAULT_SUBAGENT_MAX_TASKS_PER_CALL,
61
+ taskTimeoutMs: DEFAULT_SUBAGENT_TASK_TIMEOUT_MS,
62
+ maxTotalPerParent: DEFAULT_SUBAGENT_MAX_TOTAL_PER_PARENT,
63
+ maxNestedDepth: DEFAULT_SUBAGENT_MAX_NESTED_DEPTH,
64
64
  // pooled 模式(D-045):0 = 关闭(默认,shared 形态,零额外进程)。
65
+ // 每 runner 12 会话与默认并发 12 对齐:runnerMaxProcesses=1 即可覆盖默认并发。
65
66
  runnerMaxProcesses: 0,
66
- runnerMaxSessionsPerRunner: 8,
67
- runnerIdleTimeoutMs: 300000,
68
- runnerSweepIntervalMs: 60000,
67
+ runnerMaxSessionsPerRunner: 12,
68
+ runnerIdleTimeoutMs: 300_000,
69
+ runnerSweepIntervalMs: 60_000,
69
70
  runnerEntryAuto: false,
70
71
  },
71
72
  };
@@ -98,13 +99,11 @@ export class SubagentDelegateError extends Error {
98
99
  /**
99
100
  * 出厂种子(仅用于首次落盘,不是运行期定义):默认方案关联 explore / reviewer /
100
101
  * design-proposer / visual-reviewer,因此新装环境开箱即用;用户改文件即生效,删文件则该关联
101
- * 成为 dangling 诊断。design-proposer / visual-reviewer 是视觉保底角色(2026-10-04 用户裁决
102
- * "术有专攻、角色隔离",见 docs/plans/视觉保底角色设计注记.md):readonly + 只读工具档案收窄,
103
- * 结构上不能写代码、不能改文件;指令内嵌保底资产(失败模式清单 + 兜底纪律 + 评审维度与
104
- * 动线)。失败模式与裁决顺序两角色逐字共享;兜底纪律在提案侧是正向结构约束、在评审侧是
105
- * 同尺反向指认(同语义、镜像措辞),保证提案与评审用同一把尺。兜底只收抽象纪律,不写死具体
106
- * 颜色或样式——底色深浅、色相、渐变、容器等具体选择由主题与风格宣言推导(2026-10-08
107
- * 用户裁决"美的定义去个人化",见 docs/plans/study/视觉美学通用原理.md §9-§11)。
102
+ * 成为 dangling 诊断。design-proposer / visual-reviewer 共用形式、内容与情境之间恰当而有表现力的关系
103
+ * 这一工作定义,并以六项项目归纳方法组织判断;根据意图、约束与可观察效果取舍。提案角色据此提出方向
104
+ * 与可检查表现,评审角色据此检查实际呈现并按证据判断。两者职责与指令各自落实,不要求逐字共享失败模式
105
+ * 或裁决顺序。角色仍为 readonly + 只读工具档案,种子仅用于首次落盘;结构上不能写代码、不能改文件,
106
+ * 用户改文件即生效。
108
107
  */
109
108
  const FACTORY_ROLE_SEEDS = Object.freeze([
110
109
  Object.freeze({
@@ -125,57 +124,50 @@ const FACTORY_ROLE_SEEDS = Object.freeze([
125
124
  Object.freeze({
126
125
  id: "design-proposer",
127
126
  description: "设计提案者:只读产出视觉方向提案与验收清单;不写实现代码,不自我判定观感",
128
- instructions: `你是设计提案者,只做视觉设计方向的提案,术有专攻:功能正确性归编码角色,最终视觉判定权在视觉审核者。你不写实现代码、不修改任何文件,也不以"我自己觉得可以"替代评审。
127
+ instructions: `你是设计提案者,负责提出视觉方向;功能正确性由编码角色负责,最终视觉判断由视觉审核者负责。你不写实现代码、不修改文件,也不以自我观感代替评审。
129
128
 
130
- 【输入】需求描述、主题与载体、约束(品牌、受众、内容)。缺信息就列为假设,不要虚构需求。
129
+ 【输入】从用户需求、内容、受众、媒介与约束中提炼表达目的和文化气质。缺少信息时标为待确认或明示假设,不把个人偏好设为所有用户的默认。
131
130
 
132
- 【产出契约】按以下六节输出:
133
- 1. 风格宣言:用 1-2 个词命名设计方向,再用 3-5 句话覆盖空间与形式、色彩与材质、构图与平衡。观念先行,后面的具体选择必须能从宣言推导。
134
- 2. 配色角色表:按 背景、主色、强调色、中性阶(语义色按需)列出每个角色的定位与面积占比,并用相对档位表达明度与饱和度(如"高明度低饱和"或"深底中高对比");底色深浅与色相由主题推导,浅色、深色皆合法,不预设。不必给具体色值,给色值时注明仅示意。需要层次区分时用主色的深浅变体,不新增色相。
135
- 3. 布局与层级策略:每个画面只允许一个第一焦点;重要性只用四个杠杆编码(尺寸、字重与深浅、位置、隔离度);分组首选邻近与间距,容器是备选手段,采用时须成体系、同类同待遇;间距值成阶梯。
136
- 4. 文案密度预算:主动砍字,给每个区域的文字量上限(如"标题不超过 12 字、正文每块不超过 3 行")。把"堆描述"当作必须对抗的默认倾向。
137
- 5. 验收清单:交给视觉审核者的可检查清单,逐条映射评审维度(层级、焦点、对齐、间距、密度、对比、一致性、平衡、色彩秩序、动线、收口),每条写成可判真假的陈述。
138
- 6. 明确不做项:列出本轮刻意回避的选择及理由;想加新元素时先问"如何让已有的更好"——反加法本能,默认做减法。
131
+ 【工作方法】美是形式、内容与情境之间恰当而有表现力的关系。先表达意图,再组织关系,结合情境取舍,最后观察效果。以下六原理是可选用的方法,不要求使用中国符号、固定风格、特定色相或质感:以意统形(表达意图指导形式);虚实相生(显露与保留协同,必需信息不能隐藏);疏密有致(密处有组织、疏处有作用,不强求低密度);气韵贯通(用节奏、呼应、动势维系整体气质);变化有度(按目的安排统一与变化,有意的微差可以形成层次);因境制宜(根据需求、受众、内容、媒介与约束取舍)。有理由不等于效果成立,形式须在提案中说明如何服务意图。不要承诺设计方法会在所有情境稳定产出美。
139
132
 
140
- 【失败模式(逐条自查,普适的丑)】平=无层级、无入口;挤=无留白、密度超载;花=色彩/字体/形状来源过多、各自抢戏;歪=该对齐的没对齐;噪=无理由的装饰变化;脏=低对比灰、浑浊配色、透明滥用。
133
+ 【提案结构】按六节组织提案,写清表达意图、元素关系、情境取舍和可检查的表现:
134
+ 1. 风格宣言:用 1-2 个词命名方向,再用 3-5 句话说明表达目的、受众感受与文化气质,以及空间形式、色彩材质、构图节奏如何共同承载它。
135
+ 2. 配色角色表:按背景、主色、强调色、中性阶(语义色按需)说明角色、作用及相对明度与饱和度。色彩和质感依表达意图、内容与约束决定;不预设色相或数量。需要色相变化时说明它带来的表达或语义作用,给出的色值注明仅示意。
136
+ 3. 布局与层级策略:说明主要信息及视觉入口如何对应表达目的;允许一个或多个焦点,按内容关系组织层级。可组合尺寸、字重、颜色、位置、隔离等信号;分组可使用邻近、间距或容器,并说明关系是否清晰。间距与节奏服务整体组织,不要求固定阶梯。
137
+ 4. 文案密度预算:说明信息取舍、各区域的信息量和密度如何服务内容与阅读;需要删减、保留或集中信息时说明理由,不默认砍字或追求低密度。
138
+ 5. 验收清单:交给视觉审核者的可检查清单,保留层级、焦点、对齐、间距、密度、对比、一致性、平衡、色彩秩序、动线、收口 11 个维度;按本次意图与约束写成可核对的表现,不把偏好写成普遍要求。
139
+ 6. 明确不做项:列出本轮不采用的做法及其理由;加、减或保留元素都根据目的与效果取舍。
141
140
 
142
- 【兜底纪律】保底不限定风格与具体选择——底色深浅、色相、渐变、容器、装饰本身无对错,被约束的是选择的结构。选择有理由:任何颜色、效果、容器、装饰须服务层级或语义,且能从风格宣言推导;色相由主题、品牌与内容推导,不采用无主题依据的"默认安全色"(哪个色相都不例外),说不出理由的选择不做。色彩有角色:主导/次级/强调/中性各司其职,大面积低差异、小面积高对比;层级与可读性由明度差承载,灰度检查须成立;色相数量克制(3 个以内起步,增加须有语义理由)。分组有结构:邻近、间距、字重是第一分组手段,容器是备选且同类同待遇。密度有预算:文字量主动砍、每区域设上限;信息密度越高,装饰越退后。来源有纪律:字体族不超过 2 个,图标一套风格,阴影与描边成体系。
143
-
144
- 【通用原理】统一中的变化:全是秩序则无聊,全是变化则混乱。重复造统一,对比造趣味,每个对比必须有理由;对比要单纯——用大小对比就只用大小;要么一致,要么明显不同,微差最伤质感。
145
-
146
- 【裁决顺序】可读/功能 > 层级 > 一致性 > 美感偏好。与需求冲突时以需求为准,并在提案中写明取舍。`,
141
+ 【判断纪律】这些是观察线索,不是自动判错规则:平、挤、花、歪、噪、脏以及层级、密度、色彩、对齐等仅在具体元素和关系影响表达目的、必要信息可读性或用户约束时指明。颜色、材质、容器、留白、疏密和构图不因风格偏好本身被排除。有理由不等于效果成立,实际呈现仍须支持意图;表达意图不能豁免用户硬约束或必要信息的可读性。`,
147
142
  profile: Object.freeze({ version: 1, tools: Object.freeze(["read"]), mode: "readonly" }),
148
143
  }),
149
144
  Object.freeze({
150
145
  id: "visual-reviewer",
151
- description: "视觉审核者:只读评审渲染图与 TUI 文本帧,按维度证据化指认问题;不审代码功能,不修改文件",
152
- instructions: `你是视觉审核者,只读评审渲染出来的视觉产物(截图、渲染图、TUI 文本帧日志)。你与写代码的角色彼此隔离:不审代码功能正确性,不修改任何文件;判定只对画面负责,不因实现者是谁而放行或苛责。
146
+ description: "视觉审核者:只读检查实际可观察的视觉呈现,按证据支持的维度判断;不审代码功能,不修改文件",
147
+ instructions: `你是视觉审核者,只读检查任务中实际可观察的视觉呈现。美是形式、内容与情境之间恰当而有表现力的关系;它不是科学评分或跨文化唯一答案。根据用户需求、内容、受众、媒介与约束理解表达目的和文化气质,不把个人偏好设为所有用户的默认;证据支持什么判断什么。不审代码功能正确性,不修改任何文件。
148
+
149
+ 【方法】先表达意图,再组织关系,结合情境取舍,最后观察效果。以下六原理是可按意图选用的组织方法,不要求使用中国符号、固定风格、特定色相或质感:以意统形(表达意图指导形式);虚实相生(显露与保留协同,必需信息不能隐藏);疏密有致(密处有组织、疏处有作用,不强求低密度);气韵贯通(节奏、呼应、动势维系整体气质);变化有度(统一与变化依目的取舍,有意的微差可形成层次);因境制宜(按用户需求、内容、受众、媒介与约束取舍)。对照已有意图和约束检查这些关系如何体现在呈现中;实际效果是否支持表达意图,应由可观察证据判断,有理由不等于效果成立。
153
150
 
154
- 【输入】待审产物(渲染图截图,或 TUI 文本帧日志)的文件路径与任务背景;若有设计提案,一并提供作为对照基准。约束:
155
- - 只评审真实读到的内容。图像路径缺失、文件不存在、或当前会话的模型无法真实读取图像内容时,明确报告"无法执行视觉评审"及原因——禁止凭想象评审。
156
- - 多张同类产物(多屏、多页)除逐张结论外,追加跨张一致性检查(间距、配色、字级、组件待遇)。
151
+ 【六原理的审核动作】对照已有意图与用户约束检查:以意统形,观察形式是否支持表达目的;虚实相生,观察显露与保留是否协作、必要信息是否可见;疏密有致,观察信息密度和留白是否共同组织内容;气韵贯通,观察节奏、呼应、动势是否通过具体元素关系维系气质;变化有度,观察统一与变化是否服务目的,有意的微差可形成层次;因境制宜,观察选择是否适合需求、受众、内容、媒介与约束。
157
152
 
158
- 【评审动线(按序执行,先整体后局部)】
159
- 1. 一瞥测试:第一眼落在哪里?落点应唯一且是最重要的元素;不是则层级失败。
160
- 2. 眯眼测试:模糊细节后画面是否还剩清晰的结构团块(大/中/小、深/中/浅);只剩均匀一片即"平"。
161
- 3. 灰度检查:去掉颜色后层级与可读性是否仍成立;不成立即色彩在代偿结构缺陷。
162
- 4. 维度走查:按下方维度逐项检查。
163
- 5. 细节收口:错位、光学对齐、边缘溢出、"差一点"问题。
153
+ 【输入】待审视觉呈现及任务背景;如有设计提案或验收清单,一并作为本次对照。只评审实际读到并能观察的内容。多份同类产物逐份检查,并比较其一致性(如边距、色彩、字级及组件待遇)。证据缺失、无法访问或无法解读时,如实说明具体原因;没有任何可观察证据时,总评为 Unverified,并报告"无法执行视觉评审"。
164
154
 
165
- 【评审维度】层级(重要性是否被可见编码);焦点(第一眼落点是否唯一且正确);对齐(是否落到共同边缘或轴线);间距(组内距小于组间距、间距成阶梯);密度(信息量与文字密度是否超载);对比(差异是否充分、是否有理由);一致性(同类元素是否同待遇);平衡(视觉重量分布是否有意);色彩秩序(色相数量、角色分配、明度结构);动线(扫视顺序是否与重要性一致);收口(像素级细节)。
155
+ 【观察动线(按序执行,先整体后局部)】
156
+ 1. 意图与入口:先回看表达目的与内容重点,再观察一个或多个视觉入口是否帮助理解;不能仅因焦点不唯一判错。
157
+ 2. 关系观察:略去局部细节后,观察主要关系、层次、疏密与节奏是否支持表达意图;不把均匀、低密度或某种构图单独判错。
158
+ 3. 灰度观察:需要判断色彩是否承担层级作用时,可借灰度观察层次;它是辅助证据,不替代完整呈现,也不以结果单独判错。
159
+ 4. 维度走查:根据任务目标与验收要求检查必要维度。
160
+ 5. 细节观察:检查对齐、间距、边缘与细节是否符合本次媒介、意图和约束。
166
161
 
167
- 【失败模式(命中即问题)】平=无层级无入口;挤=贴边、无呼吸、密度超载;花=色彩/字体/形状来源过多;歪=对齐不成体系;噪=无理由的装饰变化;脏=低对比灰、浑浊配色、透明滥用。
162
+ 【评审维度】按本次意图与约束解释:层级(信息关系是否支持理解与目的);焦点(视觉入口是否对应重点,允许多个);对齐(对齐关系是否有意且连贯);间距(疏密是否组织内容关系与节奏);密度(信息量是否适合目的、内容与阅读);对比(差异是否服务表达且不相互冲突);一致性(重复与变化是否形成有意关系);平衡(视觉重量是否支持表达);色彩秩序(色彩角色、关系及其表达作用,不按固定色数判错);动线(视线关系是否帮助理解);收口(细节是否影响本次表达或约束)。
168
163
 
169
- 【兜底纪律(指认依据)】不因具体颜色或样式本身指认——深底、浅底、渐变、任一色相、卡片、色条都不是问题;判定落在结构与理由上:色彩无角色分配即"花";效果不服务层级或语义、说不出来由即"噪";明度差撑不起可读性即"脏";分组可靠近却套容器、或同类容器不同待遇即"噪/花";密度超载即"挤";来源不成体系(字体族、图标风格、阴影描边混杂)即"花"。有设计提案时先对照提案:宣言承诺的选择必须在画面兑现,宣言之外临时发挥的效果按上述纪律指认。
164
+ 容器可以服务层级、语义、交互、情绪、气质或文化表达;不能仅因邻近也能分组就判错。观察容器是否确实支持本次表达目的;只有缺少可观察作用且造成额外视觉负担,或同类容器待遇无理由不一致时,才指认问题。这些是观察线索,不是自动判错规则:诸如平、挤、花、歪、噪、脏,只有在具体元素与关系损害表达意图、必要信息可读性或用户约束时才报告。效果可以服务功能、理解、情感、气质或文化意味,不限于层级与语义。
170
165
 
171
- 【TUI 文本帧评审(介质变体)】待审产物是终端界面的文本帧(如镜像日志)时,评审动线与维度照用,但只判文本可辨的版式维度——对齐(列与框线是否成列)、截断(行尾省略号/溢出)、密度(留白行与信息行的节奏)、层级(标题/正文/辅助文本的标记差异)、一致性(同类界面的边距与对齐口径);文本帧不携带色彩,色彩秩序维度跳过并注明。判定依据不足(帧数太少、缺少同类界面对照)照实标 Unverified。
166
+ 六原理的运用本身不是验收项。主观改进建议不能冒充违规;“缺少气韵”等抽象评价,必须指出具体元素、关系与目标证据。艺术意图不能豁免必要信息可读性或用户硬约束。必要信息不可读或违反用户明确约束时,按实际证据判定问题。
172
167
 
173
- 【输出协议】先给总结论 pass 或 fail(任一维度被违反即 fail),再逐条列问题,每条包含:维度、位置(哪个元素)、为什么(违反了什么)、最小改法。规范:
174
- - 只报告能说出具体问题的条目;禁止"感觉不好、不够精致"这类空判,禁止发明像素值,禁止泛泛的美化建议。
175
- - 无问题的维度不必逐条表扬,但结尾列出本次已检查的维度清单。
176
- - 图像过小看不清或信息不足的项标 Unverified,不猜测。
168
+ 【证据与评审范围】先按任务目标与已有验收清单确定必要检查的维度和范围。必要项不能因证据缺失改标不适用;只有任务目标本身不要求的维度才可标不适用并说明理由。必要信息缺失时对应项为 Unverified;结论只覆盖实际检查范围,不能外推为完整视觉已通过。已发现问题但其他必要项仍未知时,总评为 fail,并保留未知项。
177
169
 
178
- 【裁决顺序】可读/功能 > 层级 > 一致性 > 美感偏好。冲突时高层胜出:新颖但读不清=fail;朴素但层级清晰=pass。`,
170
+ 【输出协议】总结论条件:已证实违反评审要求的问题 → fail;无已知违反但必要检查证据不足 → Unverified(无法确认);必要检查完成且无违反 → pass。逐条列出已证实问题,每条包含:维度、位置(哪个元素)、为什么(违反了什么)、最小改法。必要时列出 Unverified 项;结尾列出本次已检查的维度。禁止虚构像素测量。pass 仅说明本次要求范围通过,不代表普遍的美或保证用户喜欢。`,
179
171
  profile: Object.freeze({ version: 1, tools: Object.freeze(["read"]), mode: "readonly" }),
180
172
  }),
181
173
  ]);
@@ -191,7 +183,9 @@ const ROLE_KNOWN_KEYS = [
191
183
  "model",
192
184
  "thinkingLevel",
193
185
  "tools",
186
+ "writeScope",
194
187
  "mode",
188
+ "webSearch",
195
189
  "systemPromptAppend",
196
190
  "parentView",
197
191
  ];
@@ -220,7 +214,15 @@ function validateRoleDefinition(id, value, origin) {
220
214
  }
221
215
  // 档案字段交给公共解析器校验(类型/范围/未知键都在那里拦),错误文本带上文件来源。
222
216
  const profileInput = { version: 1, roleId: id };
223
- for (const key of ["model", "thinkingLevel", "tools", "mode", "systemPromptAppend"]) {
217
+ for (const key of [
218
+ "model",
219
+ "thinkingLevel",
220
+ "tools",
221
+ "writeScope",
222
+ "mode",
223
+ "webSearch",
224
+ "systemPromptAppend",
225
+ ]) {
224
226
  if (fields[key] !== undefined)
225
227
  profileInput[key] = fields[key];
226
228
  }
@@ -259,7 +261,7 @@ function unknownRoleMessage(id, roles) {
259
261
  }
260
262
  /**
261
263
  * 角色是否带**收窄档案**(profile 里除基线 version/roleId 外还有 model/thinkingLevel/
262
- * tools/mode/systemPromptAppend)。`role.profile !== undefined` 不是这个意思:档案基线
264
+ * tools/writeScope/mode/systemPromptAppend)。`role.profile !== undefined` 不是这个意思:档案基线
263
265
  * 恒含 roleId,因此任何角色都带 profile;"带收窄"看的是有没有收窄字段。
264
266
  */
265
267
  function hasNarrowingProfile(role) {
@@ -268,14 +270,16 @@ function hasNarrowingProfile(role) {
268
270
  (profile.model !== undefined ||
269
271
  profile.thinkingLevel !== undefined ||
270
272
  profile.tools !== undefined ||
273
+ profile.writeScope !== undefined ||
271
274
  profile.mode !== undefined ||
275
+ profile.webSearch !== undefined ||
272
276
  profile.systemPromptAppend !== undefined));
273
277
  }
274
278
  // ---------------------------------------------------------------------------
275
279
  // 会话级子代理角色(session-scoped roles,D-081,2026-10-03 用户裁决落地)
276
280
  //
277
281
  // 主代理可用角色 = 磁盘角色(方案 associations ∩ 目录文件)∪ 会话角色。会话角色由
278
- // 主代理在会话内用 `subagent_role_set` 注册,随会话文件持久化(下次打开仍在),
282
+ // 主代理在会话内用 `subagent_role`(type `set`)注册,随会话文件持久化(下次打开仍在),
279
283
  // 不写 `<agentDir>/roles/`、不跨会话共享、子代理不继承。磁盘角色优先:撞名的注册
280
284
  // 被结构化拒绝,装载期合并时撞名的会话角色被忽略并 warn。
281
285
  // ---------------------------------------------------------------------------
@@ -296,7 +300,9 @@ export function toSessionRoleRecord(role) {
296
300
  ...(profile?.model === undefined ? {} : { model: profile.model }),
297
301
  ...(profile?.thinkingLevel === undefined ? {} : { thinkingLevel: profile.thinkingLevel }),
298
302
  ...(profile?.tools === undefined ? {} : { tools: profile.tools }),
303
+ ...(profile?.writeScope === undefined ? {} : { writeScope: profile.writeScope }),
299
304
  ...(profile?.mode === undefined ? {} : { mode: profile.mode }),
305
+ ...(profile?.webSearch === undefined ? {} : { webSearch: profile.webSearch }),
300
306
  ...(profile?.systemPromptAppend === undefined ? {} : { systemPromptAppend: profile.systemPromptAppend }),
301
307
  ...(role.parentView === true ? { parentView: true } : {}),
302
308
  };
@@ -491,7 +497,15 @@ export function seedFactoryRoleCatalog(agentDir) {
491
497
  continue;
492
498
  // 文件形状与用户手写角色一致(平铺键):description/instructions + 可选档案键。
493
499
  const file = { description: role.description, instructions: role.instructions };
494
- for (const key of ["model", "thinkingLevel", "tools", "mode", "systemPromptAppend"]) {
500
+ for (const key of [
501
+ "model",
502
+ "thinkingLevel",
503
+ "tools",
504
+ "writeScope",
505
+ "mode",
506
+ "webSearch",
507
+ "systemPromptAppend",
508
+ ]) {
495
509
  const value = role.profile?.[key];
496
510
  if (value !== undefined)
497
511
  file[key] = value;
@@ -692,6 +706,15 @@ function verifyChildProfileApplied(host, requested) {
692
706
  return `child session is on model ${JSON.stringify(model.id)} instead of the requested ${JSON.stringify(profile.model)}`;
693
707
  }
694
708
  }
709
+ if (profile.webSearch !== undefined) {
710
+ const actual = session.getWebSearch?.();
711
+ if (actual === undefined) {
712
+ return "host does not expose getWebSearch(); the requested web search mode cannot be verified";
713
+ }
714
+ if (actual !== profile.webSearch) {
715
+ return `child session web search is ${JSON.stringify(actual)} instead of the requested ${JSON.stringify(profile.webSearch)}`;
716
+ }
717
+ }
695
718
  return undefined;
696
719
  }
697
720
  function resolveRole(value, roles) {
@@ -708,10 +731,10 @@ function resolveRole(value, roles) {
708
731
  }
709
732
  /** 单条 summary 的字符上限(上下文预算:只回摘要,不回 messages 原文)。 */
710
733
  const SUMMARY_CHARS = 200;
711
- /** `subagent_result` 默认/上限返回条数(有界回读,绝不默认回灌全文)。 */
734
+ /** `subagent_task`(type `result`)默认/上限返回条数(有界回读,绝不默认回灌全文)。 */
712
735
  const RESULT_READ_DEFAULT_MESSAGES = 20;
713
736
  const RESULT_READ_MAX_MESSAGES = 50;
714
- /** `subagent_result` 的有界等待上限(后台结算收割),默认 0 = 只查一次。 */
737
+ /** `subagent_task`(type `result`)的有界等待上限(后台结算收割),默认 0 = 只查一次。 */
715
738
  const RESULT_READ_MAX_WAIT_MS = 120_000;
716
739
  /**
717
740
  * 每任务随结算投影/信封返回的兄弟流量摘录条数(D-060 裁决 14 父可见性):有界——
@@ -722,8 +745,8 @@ const SIBLING_TRAFFIC_EXCERPTS = 8;
722
745
  function utf8ByteLength(text) {
723
746
  return Buffer.byteLength(text, "utf8");
724
747
  }
725
- const DEFAULT_MAX_TASKS_PER_CALL = 4;
726
- const DEFAULT_TASK_TIMEOUT_MS = 300_000;
748
+ const DEFAULT_MAX_TASKS_PER_CALL = DEFAULT_SUBAGENT_MAX_TASKS_PER_CALL;
749
+ const DEFAULT_TASK_TIMEOUT_MS = DEFAULT_SUBAGENT_TASK_TIMEOUT_MS;
727
750
  /**
728
751
  * 后台委派缺省死线(60 分钟,出厂默认登记见 docs/design/产品默认值清单.md):挂死
729
752
  * 后台任务的兜底寿命。`subagent_run background:true` 提交即带 `deadlineAt`;工具参数
@@ -774,6 +797,58 @@ function objectInput(first, second, tool = "subagent_run") {
774
797
  function isPlainRecord(value) {
775
798
  return typeof value === "object" && value !== null && !Array.isArray(value);
776
799
  }
800
+ /**
801
+ * Merge per-operation parameter schemas into one all-optional property map.
802
+ *
803
+ * A merged tool's schema cannot reuse the operation schemas verbatim: their
804
+ * `required` lists would leak across types (cancel's required `taskId` would
805
+ * make it required for every type). The dispatcher validates the required set
806
+ * per type instead; constraints and descriptions survive.
807
+ */
808
+ function optionalParams(...maps) {
809
+ const merged = {};
810
+ for (const map of maps) {
811
+ if (map === undefined)
812
+ continue;
813
+ for (const [key, schema] of Object.entries(map))
814
+ merged[key] = Type.Optional(schema);
815
+ }
816
+ return merged;
817
+ }
818
+ /**
819
+ * Keep a tool's definition local without registering it: merged tools
820
+ * (`subagent_task` / `subagent_message` / `subagent_role`) register one entry
821
+ * and dispatch to these specs, instead of each operation owning a top-level
822
+ * tool name (2026-10-21 user ruling: one tool per large category).
823
+ */
824
+ function toolSpec(spec) {
825
+ return spec;
826
+ }
827
+ /** Drop `type` before delegating to an operation that validates its own keys. */
828
+ function withoutDiscriminator(input) {
829
+ const { type: _type, ...rest } = input;
830
+ return rest;
831
+ }
832
+ /** Read and validate a merged tool's `type` discriminator. */
833
+ function readDiscriminator(input, tool, allowed) {
834
+ const value = input.type;
835
+ if (typeof value !== "string" || !allowed.includes(value)) {
836
+ throw new SubagentDelegateError("subagent_invalid_input", `${tool}.type must be one of: ${allowed.join(", ")}`);
837
+ }
838
+ return value;
839
+ }
840
+ /**
841
+ * Reject parameters that belong to a different operation of the same merged
842
+ * tool. Fail-closed on purpose: silently ignoring `taskId` on `type:"list"`
843
+ * would look like it filtered by that task.
844
+ */
845
+ function rejectForeignParams(input, tool, type, allowed) {
846
+ for (const key of Object.keys(input)) {
847
+ if (key === "type" || allowed.includes(key))
848
+ continue;
849
+ throw new SubagentDelegateError("subagent_invalid_input", `${tool} type "${type}" does not accept ${JSON.stringify(key)}`);
850
+ }
851
+ }
777
852
  function readPrompts(value, maxTasksPerCall) {
778
853
  if (!Array.isArray(value) || value.length === 0) {
779
854
  throw new SubagentDelegateError("subagent_invalid_input", "subagent_run requires a non-empty prompts array");
@@ -855,6 +930,8 @@ function projectRecord(record) {
855
930
  messageCount: record.messages.length,
856
931
  ...(record.sessionId === undefined || record.sessionId === "" ? {} : { sessionId: record.sessionId }),
857
932
  ...(summary === undefined ? {} : { summary }),
933
+ ...(record.taskOutcome === undefined ? {} : { taskOutcome: record.taskOutcome }),
934
+ ...(record.refusalReason === undefined ? {} : { refusalReason: record.refusalReason }),
858
935
  };
859
936
  }
860
937
  /**
@@ -895,9 +972,13 @@ export function createCapability(api, options) {
895
972
  // 叶子会话(无 agents)只拿这一面;获准自建会话(D-060 S5,agents 在场)两面都要:
896
973
  // 它既能向下委派(parent face),也仍能向自己的父上抛/收兄弟消息(child face)。
897
974
  const childTools = [];
898
- if (wiring?.postEscalate !== undefined) {
899
- childTools.push(api.registerTool({
900
- name: "subagent_escalate",
975
+ // 子侧通信面合并为单工具 `subagent_message`(2026-10-21 用户裁决:大类型一个工具,
976
+ // 用 type + 参数区分)。可用 type 取决于宿主接线:只接 post 通道的叶子会话只有
977
+ // escalate;接了兄弟通道的会话有 send/inbox。
978
+ const escalateSpec = wiring?.postEscalate === undefined
979
+ ? undefined
980
+ : toolSpec({
981
+ name: "subagent_message.escalate",
901
982
  label: "Escalate to parent",
902
983
  description: "Send one bounded message up to your parent agent. It does NOT interrupt the parent; it is delivered " +
903
984
  "with the parent's next task result. Use it for a blocker or a key finding the task result alone would lose.",
@@ -908,10 +989,10 @@ export function createCapability(api, options) {
908
989
  }),
909
990
  }),
910
991
  execute: async (first, second) => {
911
- const input = objectInput(first, second);
992
+ const input = objectInput(first, second, 'subagent_message type "escalate"');
912
993
  const text = typeof input.text === "string" ? input.text : "";
913
994
  if (text.trim() === "") {
914
- throw new SubagentDelegateError("subagent_invalid_input", "subagent_escalate requires non-empty text");
995
+ throw new SubagentDelegateError("subagent_invalid_input", `subagent_message type "escalate" requires non-empty text`);
915
996
  }
916
997
  try {
917
998
  wiring.postEscalate?.(text);
@@ -921,11 +1002,44 @@ export function createCapability(api, options) {
921
1002
  }
922
1003
  return { delivered: true, note: "the parent reads this with its next task result" };
923
1004
  },
924
- }));
925
- }
926
- if (wiring?.sendSibling !== undefined) {
927
- childTools.push(api.registerTool({
928
- name: "subagent_send",
1005
+ });
1006
+ // 任务级拒绝出口:子代理声明"活着但不做这个任务"的结构化终局声明。不调用
1007
+ // postEscalate、不触发任何取消/结算(子侧触不到父 runtime)——调度器在 settle 时
1008
+ // 从 transcript 里的本调用记录检测(取最后一条),随自然成功结算附 taskOutcome。
1009
+ // 随 subagent_message 工具的既有注册条件走,不新增独立注册开关。
1010
+ // 缺参报错必须教形状:只说"拒了什么"模型无法自纠,会重试同错直至放弃,
1011
+ // refusalReason 随之丢失。入参字段名随 subagent_message 面内 "text" 惯例——
1012
+ // 真实模型四轮取证:schema 含 reason 时模型仍按 text 先验传参(运行时设计 §4.4)。
1013
+ const refuseInputError = () => new SubagentDelegateError("subagent_invalid_input", 'subagent_message type "refuse" requires {"type":"refuse","text":"<why>"} — the refusal explanation goes in "text" like your other messages');
1014
+ const refuseSpec = toolSpec({
1015
+ name: "subagent_message.refuse",
1016
+ label: "Refuse this task",
1017
+ description: "Declare this task refused: you are alive but will not do this task (missing access, out of scope, or in " +
1018
+ "conflict with your instructions). The refusal is recorded as the terminal declaration for the task — the " +
1019
+ 'parent reads it as taskOutcome "refused" with your reason. Do not use it to report partial progress; ' +
1020
+ "escalate is for that.",
1021
+ parameters: Type.Object({
1022
+ text: Type.String({
1023
+ minLength: 1,
1024
+ description: "Why this task is refused; recorded verbatim as the task's refusalReason for the parent",
1025
+ }),
1026
+ }),
1027
+ execute: async (first, second) => {
1028
+ const input = objectInput(first, second, 'subagent_message type "refuse"');
1029
+ const text = typeof input.text === "string" ? input.text : "";
1030
+ if (text.trim() === "") {
1031
+ throw refuseInputError();
1032
+ }
1033
+ return {
1034
+ refused: true,
1035
+ note: "Your refusal has been recorded as the terminal declaration for this task. End your turn now and do not call any other tools.",
1036
+ };
1037
+ },
1038
+ });
1039
+ const sendSpec = wiring?.sendSibling === undefined
1040
+ ? undefined
1041
+ : toolSpec({
1042
+ name: "subagent_message.send",
929
1043
  label: "Message a sibling",
930
1044
  description: "Send one bounded message to a sibling subagent (a child of the same parent), addressed by its sessionId. " +
931
1045
  "Cross-family addressing is rejected. This channel is for data and intermediate artifacts only: task " +
@@ -939,11 +1053,11 @@ export function createCapability(api, options) {
939
1053
  }),
940
1054
  }),
941
1055
  execute: async (first, second) => {
942
- const input = objectInput(first, second);
1056
+ const input = objectInput(first, second, 'subagent_message type "send"');
943
1057
  const to = typeof input.to === "string" ? input.to : "";
944
1058
  const text = typeof input.text === "string" ? input.text : "";
945
1059
  if (to === "" || text.trim() === "") {
946
- throw new SubagentDelegateError("subagent_invalid_input", "subagent_send requires to and non-empty text");
1060
+ throw new SubagentDelegateError("subagent_invalid_input", `subagent_message type "send" requires to and non-empty text`);
947
1061
  }
948
1062
  try {
949
1063
  wiring.sendSibling?.(to, text);
@@ -953,24 +1067,83 @@ export function createCapability(api, options) {
953
1067
  }
954
1068
  return { delivered: true };
955
1069
  },
956
- }), api.registerTool({
957
- name: "subagent_inbox",
1070
+ });
1071
+ const inboxSpec = wiring?.sendSibling === undefined
1072
+ ? undefined
1073
+ : toolSpec({
1074
+ name: "subagent_message.inbox",
958
1075
  label: "Read sibling inbox",
959
- description: "Read and clear the earliest bounded messages your siblings sent you (subagent_send). Messages are " +
960
- "consumed by this call; poll it when coordination is expected. The sibling channel carries data and " +
961
- "intermediate artifacts only — assignment and acceptance stay with the parent, which can audit this traffic.",
1076
+ description: 'Read and clear the earliest bounded messages your siblings sent you (subagent_message type "send"). ' +
1077
+ "Messages are consumed by this call; poll it when coordination is expected. The sibling channel carries " +
1078
+ "data and intermediate artifacts only — assignment and acceptance stay with the parent, which can audit " +
1079
+ "this traffic.",
962
1080
  parameters: Type.Object({
963
1081
  limit: Type.Optional(Type.Number({ description: "Max messages to take (1..20, default 10)", minimum: 1, maximum: 20 })),
964
1082
  }),
965
1083
  execute: async (first, second) => {
966
- const input = objectInput(first, second);
1084
+ const input = objectInput(first, second, 'subagent_message type "inbox"');
967
1085
  const requested = input.limit === undefined ? 10 : Number(input.limit);
968
1086
  if (!Number.isInteger(requested) || requested < 1 || requested > 20) {
969
- throw new SubagentDelegateError("subagent_invalid_input", "subagent_inbox limit must be an integer in 1..20");
1087
+ throw new SubagentDelegateError("subagent_invalid_input", `subagent_message type "inbox" limit must be an integer in 1..20`);
970
1088
  }
971
1089
  const messages = wiring.drainInbox?.(requested) ?? [];
972
1090
  return { messages };
973
1091
  },
1092
+ });
1093
+ if (escalateSpec !== undefined || sendSpec !== undefined) {
1094
+ const messageOps = [
1095
+ ...(escalateSpec === undefined ? [] : ["escalate"]),
1096
+ ...(sendSpec === undefined ? [] : ["send", "inbox"]),
1097
+ "refuse",
1098
+ ];
1099
+ childTools.push(api.registerTool({
1100
+ name: "subagent_message",
1101
+ label: "Message a subagent",
1102
+ description: "One bounded messaging surface. " +
1103
+ [
1104
+ ...(escalateSpec === undefined
1105
+ ? []
1106
+ : ['type="escalate": send a message up to your parent (delivered with its next task result)']),
1107
+ ...(sendSpec === undefined
1108
+ ? []
1109
+ : [
1110
+ 'type="send": send data or an intermediate artifact to a sibling subagent by sessionId',
1111
+ 'type="inbox": read and clear messages your siblings sent you',
1112
+ ]),
1113
+ 'type="refuse": declare this task refused with an explanation in "text" (a terminal declaration for the task; the parent sees taskOutcome "refused" — end your turn right after)',
1114
+ ].join("; ") +
1115
+ "; task assignment and result acceptance stay with the parent agent, which can audit sibling traffic.",
1116
+ parameters: Type.Object({
1117
+ type: Type.Union(messageOps.map((op) => Type.Literal(op)), { description: `Operation: ${messageOps.join(" | ")}` }),
1118
+ ...optionalParams(escalateSpec?.parameters.properties, sendSpec?.parameters.properties, inboxSpec?.parameters.properties,
1119
+ // refuse 无条件创建,属性总是合入:type 进了 messageOps 而 schema 漏合
1120
+ // = 模型被 schema 约束永远发不出该 type 的合法参数(教形状也救不回)。
1121
+ refuseSpec.parameters.properties),
1122
+ }),
1123
+ execute: async (first, second) => {
1124
+ const input = objectInput(first, second, "subagent_message");
1125
+ const type = readDiscriminator(input, "subagent_message", messageOps);
1126
+ if (type === "escalate") {
1127
+ rejectForeignParams(input, "subagent_message", type, ["text"]);
1128
+ return escalateSpec?.execute(withoutDiscriminator(input), second);
1129
+ }
1130
+ if (type === "send") {
1131
+ rejectForeignParams(input, "subagent_message", type, ["to", "text"]);
1132
+ return sendSpec?.execute(withoutDiscriminator(input), second);
1133
+ }
1134
+ if (type === "refuse") {
1135
+ // text 前置校验先于外参拒绝(教形状优先于 fail-closed):缺 text 时先给出
1136
+ // 可自纠的形状;合法 text 带外来参数(如旧先验的 reason)仍走
1137
+ // rejectForeignParams,不放松。
1138
+ const text = input.text;
1139
+ if (typeof text !== "string" || text.trim() === "")
1140
+ throw refuseInputError();
1141
+ rejectForeignParams(input, "subagent_message", type, ["text"]);
1142
+ return refuseSpec.execute(withoutDiscriminator(input), second);
1143
+ }
1144
+ rejectForeignParams(input, "subagent_message", type, ["limit"]);
1145
+ return inboxSpec?.execute(withoutDiscriminator(input), second);
1146
+ },
974
1147
  }));
975
1148
  }
976
1149
  // 调度器限额:options.limits 显式覆盖优先;否则由配置通道逐键取值,缺省回落到调度器默认。
@@ -995,7 +1168,7 @@ export function createCapability(api, options) {
995
1168
  * 退役切片 1(生产默认 pooled,D-075 S4 第三批收敛):自动池化旗标不再由宿主 options
996
1169
  * 注入,改由既有配置键 `runnerEntryAuto: true` 驱动——未显式配置 `runnerMaxProcesses`/
997
1170
  * `runnerEntryPath` 且未注入 spawner 时,若 self-invocation 可解析则自动以池化装配
998
- * (默认 2 个 runner、每池 8 会话);不可解析则保持共享模式。
1171
+ * (默认 2 个 runner、每池 12 会话);不可解析则保持共享模式。
999
1172
  */
1000
1173
  const pool = (() => {
1001
1174
  const configuredMaxProcesses = configInt(api, "runnerMaxProcesses", 0);
@@ -1015,8 +1188,10 @@ export function createCapability(api, options) {
1015
1188
  const idleRunnerTimeoutMs = resolveInt(undefined, configInt(api, "runnerIdleTimeoutMs", 1), 300_000, 1, "runnerIdleTimeoutMs");
1016
1189
  const idleCheckIntervalMs = resolveInt(undefined, configInt(api, "runnerSweepIntervalMs", 1), 60_000, 1, "runnerSweepIntervalMs");
1017
1190
  // 有效并发取"已配并发"或调度器默认值:容量校验必须在未显式配置并发时也生效,
1018
- // 否则池化容量小于默认并发(8)会被静默容忍,直到运行期才以 capacity 错误暴露。
1019
- const effectiveMaxConcurrent = options?.limits?.maxConcurrentSubagents ?? limits?.maxConcurrentSubagents ?? 8;
1191
+ // 否则池化容量小于默认并发(12)会被静默容忍,直到运行期才以 capacity 错误暴露。
1192
+ const effectiveMaxConcurrent = options?.limits?.maxConcurrentSubagents ??
1193
+ limits?.maxConcurrentSubagents ??
1194
+ DEFAULT_SUBAGENT_LIMITS.maxConcurrentSubagents;
1020
1195
  // spawn 工厂优先用注入;否则要求配置显式给出 runner 入口(装载期校验,不推迟到首次 spawn)。
1021
1196
  let spawnRunner = options?.runnerSpawner;
1022
1197
  if (spawnRunner === undefined) {
@@ -1092,14 +1267,16 @@ export function createCapability(api, options) {
1092
1267
  const taskTimeoutMs = resolveInt(options?.taskTimeoutMs, configInt(api, "taskTimeoutMs", 1), DEFAULT_TASK_TIMEOUT_MS, 1, "taskTimeoutMs");
1093
1268
  // 嵌套深度硬限(承载退役切片 2,Option A):runner 叶子带自动池化后同样获得委派
1094
1269
  // 父面,递归边界由血缘深度兜底——本会话已是第 maxNestedDepth 层时再委派按
1095
- // `subagent_depth_exceeded` 结构化拒绝。与 S5 shared 侧的深度上限同一默认(2 层)。
1096
- const maxNestedDepth = resolveInt(undefined, configInt(api, "maxNestedDepth", 1), 2, 1, "maxNestedDepth");
1270
+ // `subagent_depth_exceeded` 结构化拒绝。与 S5 shared 侧的深度上限同一默认
1271
+ // (DEFAULT_SUBAGENT_MAX_NESTED_DEPTH = 2 层)。
1272
+ const maxNestedDepth = resolveInt(undefined, configInt(api, "maxNestedDepth", 1), DEFAULT_SUBAGENT_MAX_NESTED_DEPTH, 1, "maxNestedDepth");
1097
1273
  // 每父级累计可创建的子代理总量(承载退役切片 2,Option A;与 S5 shared 侧同一默认
1098
- // 100):对**非 resume** 的每次 submit 计数,超限按 `subagent_child_quota` 拒绝。
1099
- const maxTotalPerParent = resolveInt(undefined, configInt(api, "maxTotalPerParent", 0), 100, 0, "maxTotalPerParent");
1274
+ // DEFAULT_SUBAGENT_MAX_TOTAL_PER_PARENT):对**非 resume** 的每次 submit 计数,
1275
+ // 超限按 `subagent_child_quota` 拒绝。
1276
+ const maxTotalPerParent = resolveInt(undefined, configInt(api, "maxTotalPerParent", 0), DEFAULT_SUBAGENT_MAX_TOTAL_PER_PARENT, 0, "maxTotalPerParent");
1100
1277
  let createdSubagentCount = 0;
1101
1278
  // background 配置键已移除(D-060 裁决 15,2026-10-03):后台委派是功能基本面,
1102
- // 无条件注册(`subagent_run` 的 background 参数与 subagent_cancel / subagent_steer
1279
+ // 无条件注册(`subagent_run` 的 background 参数与 `subagent_task` 的 cancel / steer
1103
1280
  // 随之常在);键出现即装载期失败,不留"配置了但没生效"的假象。
1104
1281
  if (api.config.background !== undefined) {
1105
1282
  throw new Error("subagent-delegate config background was removed (D-060 ruling 15): background delegation registers unconditionally; remove the key");
@@ -1180,6 +1357,15 @@ export function createCapability(api, options) {
1180
1357
  // runtime 的 results 契约);缺席 = 不持久化(回读 undefined,既有语义)。
1181
1358
  ...(options?.results === undefined ? {} : { results: options.results }),
1182
1359
  });
1360
+ // P1b(联网检索设计 §5.6):把调度器的 one-shot 面经能力目录暴露为公共能力
1361
+ // `agent-forge.agent-task-scheduler@1`(消费方经目录调用)。requester
1362
+ // 作用域绑定本会话(owner + 当前父会话 id),调用方不可传。registration 放在返回
1363
+ // 数组的 runtime 项之后:逆序 dispose 时先摘能力、再释放调度器。
1364
+ const schedulerRegistration = createAgentTaskSchedulerCapabilityProviderV1({
1365
+ runtime,
1366
+ owner: capabilityManifest.id,
1367
+ parentSessionId: session.getSessionId(),
1368
+ })(api);
1183
1369
  // ---------------------------------------------------------------------------
1184
1370
  // 生产 tick 驱动(设计 §5.7):调度器的死线强制点是显式 tick(),本消费者挂载唯一
1185
1371
  // 的墙钟驱动——存在带未决死线的在途后台任务时才启动 ticker(惰性启停),每跳只调
@@ -1237,6 +1423,10 @@ export function createCapability(api, options) {
1237
1423
  const buildRunToolDefinition = () => ({
1238
1424
  name: "subagent_run",
1239
1425
  label: "Run subagents",
1426
+ // 折叠态一行人类可读摘要,结果 JSON 不进转录(2026-10-19 用户裁决);
1427
+ // 结构性组件、无 TUI import(goal/plan/alignment 同一边界)。
1428
+ renderCall: createSubagentRunRenderCall((roleId) => roles.get(roleId)?.description),
1429
+ renderResult: subagentRunResultRenderer,
1240
1430
  description: `Delegate 1..${maxTasksPerCall} independent subagent tasks in one call and return their bounded terminal results. ` +
1241
1431
  "Each prompt runs in its own isolated child session. Only status, reason codes, message counts and a shortened " +
1242
1432
  "final-assistant summary come back; ask a child for more detail in its prompt if you need it." +
@@ -1245,18 +1435,24 @@ export function createCapability(api, options) {
1245
1435
  // fork-join 推荐模式(D-060 裁决 14):大任务拆并行块 + 最终组合任务,中间产物
1246
1436
  // 留子层、主上下文不膨胀;通知粒度——结算必然回注但只有组合任务的报告需验收。
1247
1437
  " For large splittable work prefer fork-join: launch the parallel chunk tasks with background:true, then submit " +
1248
- "one final assembly task (a separate background call) that gathers the chunks with subagent_result({taskId, waitMs}) " +
1438
+ 'one final assembly task (a separate background call) that gathers the chunks with subagent_task({type: "result", taskId, waitMs}) ' +
1249
1439
  "— intermediate results stay in the child layer and only the assembled final report enters your context. Every " +
1250
1440
  "task's settlement is announced to you (agent-task-settled), but notifications are not per-task acceptances: " +
1251
1441
  "accept and act on (todos/goals/next steps) only the final assembly task's report; other settled lines are " +
1252
1442
  "status only." +
1443
+ // 任务级拒绝:子代理可能声明不做该任务;投影 taskOutcome "refused" 即终局声明,
1444
+ // 父侧不得原样重派,先补条件或上报,细节经 result 回读 refusalReason。
1445
+ ' A child may declare a task-level refusal via subagent_message (type "refuse"): when a settlement ' +
1446
+ 'projection carries taskOutcome "refused", that refusal is final — do not re-dispatch the same task ' +
1447
+ "unchanged; narrow the scope or provide what was missing, or report to the user, and read refusalReason " +
1448
+ 'via subagent_task (type "result").' +
1253
1449
  // 死线兜底(设计 §5.7):后台任务缺省 60 分钟死线,挂死任务到点被强制 timed_out。
1254
1450
  " Background tasks carry a watchdog deadline by default (60 minutes; override per call with `deadlineMs`): " +
1255
1451
  "a task still running when its deadline passes is force-settled timed_out." +
1256
1452
  (roles.size === 0
1257
1453
  ? ""
1258
1454
  : ` Configured subagent roles (pass \`role\`): ${renderRoleMenu()}. A role's instructions frame every task in the call; ` +
1259
- "a role with a narrowing profile also applies its tools/mode/model/thinkingLevel to the child, verified after creation."),
1455
+ "a role with a narrowing profile also applies its tools/writeScope/mode/model/thinkingLevel to the child, verified after creation."),
1260
1456
  parameters: Type.Object({
1261
1457
  prompts: Type.Array(Type.String({
1262
1458
  minLength: 1,
@@ -1288,15 +1484,14 @@ export function createCapability(api, options) {
1288
1484
  }), { maxItems: CONTEXT_LIST_MAX_ITEMS })),
1289
1485
  }, {
1290
1486
  description: "Parent pointers rendered as a header block into each child prompt: file:line anchors and commands " +
1291
- "already tried and failed. Pointers only — children still read the real files.",
1487
+ "already tried and failed.",
1292
1488
  })),
1293
1489
  ...(roles.size === 0
1294
1490
  ? {}
1295
1491
  : {
1296
1492
  role: Type.Optional(Type.String({
1297
1493
  description: `Named subagent role applied to every prompt in this call; one of: ${[...roles.keys()].sort().join(", ")}. ` +
1298
- "Omit for a generic task. Roles frame the task; a role may also narrow the child's " +
1299
- "tools/mode/model (verified after creation).",
1494
+ "Omit for a generic task.",
1300
1495
  })),
1301
1496
  }),
1302
1497
  // background(D-060 S6)在 properties 内组合:此前的 `{...parameters, background}`
@@ -1305,15 +1500,14 @@ export function createCapability(api, options) {
1305
1500
  // 裁决 15 起开关形态退役、参数无条件声明。
1306
1501
  background: Type.Optional(Type.Boolean({
1307
1502
  description: "Submit without waiting: return task tickets immediately and keep working; settled results are " +
1308
- "announced at your next model round and can be read with subagent_result (optionally waiting).",
1503
+ 'announced at your next model round and can be read with subagent_task (type "result", optionally waiting).',
1309
1504
  })),
1310
1505
  // 死线兜底(设计 §5.7):仅后台合法——前台任务已有父侧 drain 预算,两种边界
1311
1506
  // 不混用;前台携带按 invalid_input 拒绝,不留"配了但不生效"的假象。
1312
1507
  deadlineMs: Type.Optional(Type.Integer({
1313
1508
  minimum: 1,
1314
1509
  description: "Watchdog deadline for this call's background tasks, in ms (positive integer; requires background: true). " +
1315
- `Default ${DEFAULT_BACKGROUND_TASK_DEADLINE_MS} (60 minutes). A background task still running when its ` +
1316
- "deadline passes is force-settled timed_out (reason deadline_passed).",
1510
+ `Default ${DEFAULT_BACKGROUND_TASK_DEADLINE_MS} (60 minutes).`,
1317
1511
  })),
1318
1512
  }),
1319
1513
  execute: executeSubagentRun,
@@ -1340,7 +1534,7 @@ export function createCapability(api, options) {
1340
1534
  const backgroundDeadlineMs = background === true ? (rawDeadlineMs ?? DEFAULT_BACKGROUND_TASK_DEADLINE_MS) : undefined;
1341
1535
  // 嵌套深度硬限(退役切片 2,Option A):沿 S2 血缘头回溯本会话层级,已达
1342
1536
  // maxNestedDepth 层时整次调用结构化拒绝——递归的兜底边界(不依赖模型自觉)。
1343
- // resume 不涉深度(S5 裁定:续写既有会话不计新层级,走 subagent_resume)。
1537
+ // resume 不涉深度(S5 裁定:续写既有会话不计新层级,走 `subagent_task` type `resume`)。
1344
1538
  // agentDir 单源(D-075 S4 第三批):显式 options 优先,缺省回落 api.host.agentDir。
1345
1539
  const agentDir = options?.agentDir ?? api.host.agentDir ?? "";
1346
1540
  if (agentDir !== "") {
@@ -1391,7 +1585,7 @@ export function createCapability(api, options) {
1391
1585
  prompt: contextBlock === undefined ? composed : `${contextBlock}\n\n${composed}`,
1392
1586
  // 血缘(D-060 S1):父会话 id 随 metadata 过线,宿主写进子会话文件头。
1393
1587
  // 角色存在时 child profile 恒定携带(档案基线含 roleId,无收窄字段的角色
1394
- // 同样有血缘——/agents 与 subagent_list/subagent_result 的角色血缘对全部
1588
+ // 同样有血缘——/agents 与 `subagent_task` 的 list/result 角色血缘对全部
1395
1589
  // 角色生效);`role.profile ?? { version: 1, roleId }` 是防御兜底——对
1396
1590
  // validateRoleDefinition 产出的角色不可达;回归测试钉住该行为。
1397
1591
  metadata: {
@@ -1499,19 +1693,19 @@ export function createCapability(api, options) {
1499
1693
  runTool.current = api.registerTool(buildRunToolDefinition());
1500
1694
  };
1501
1695
  /**
1502
- * 会话角色三工具(D-081):注册/列出/删除。注册是"先落盘再提交内存"——持久化条目
1696
+ * 会话角色(D-081,现合并为 `subagent_role`):注册/列出/删除。注册是"先落盘再提交内存"——持久化条目
1503
1697
  * 写在会话文件里(customType `subagent.roles.state`),下次打开同一会话仍在;会话
1504
- * 角色不写 `<agentDir>/roles/`、不跨会话共享、子代理不继承。三工具未声明 readonlySafe,
1698
+ * 角色不写 `<agentDir>/roles/`、不跨会话共享、子代理不继承。角色工具未声明 readonlySafe,
1505
1699
  * readonly 会话沿用既有"未声明即不激活"语义,不做特殊分支。
1506
1700
  */
1507
- const roleSetTool = api.registerTool({
1508
- name: "subagent_role_set",
1701
+ const roleSetSpec = toolSpec({
1702
+ name: "subagent_role.set",
1509
1703
  label: "Register a session subagent role",
1510
1704
  description: "Register or update a session-scoped subagent role and use it by name in subagent_run. The role is persisted " +
1511
1705
  "in this session file, so it is still available when this session is opened again; it is not written to the " +
1512
1706
  "shared roles directory, not shared with other sessions, and not inherited by child subagents. Use it when the " +
1513
1707
  "built-in roles are too coarse for the task at hand. id, description and instructions are required; the optional " +
1514
- "fields narrow the child the same way a role file does (tools/mode/model/thinkingLevel/systemPromptAppend) and " +
1708
+ "fields narrow the child the same way a role file does (tools/writeScope/mode/webSearch/model/thinkingLevel/systemPromptAppend) and " +
1515
1709
  "parentView adds a bounded parent projection. Same id updates that session role; the change is visible to the " +
1516
1710
  "next subagent_run call without restarting anything.",
1517
1711
  parameters: Type.Object({
@@ -1538,25 +1732,31 @@ export function createCapability(api, options) {
1538
1732
  tools: Type.Optional(Type.Array(Type.String(), {
1539
1733
  description: "Tool allowlist intersected with the inherited tool set ([] gives the child no tools)",
1540
1734
  })),
1735
+ writeScope: Type.Optional(Type.Array(Type.String(), {
1736
+ description: "Relative write-path globs for edit/write (POSIX separators; [] denies all writes; absent = unrestricted)",
1737
+ })),
1541
1738
  mode: Type.Optional(Type.Union([Type.Literal("readonly"), Type.Literal("full-control")], {
1542
1739
  description: "Narrow-only session mode: readonly never widens a readonly parent",
1543
1740
  })),
1741
+ webSearch: Type.Optional(Type.Union([Type.Literal("off"), Type.Literal("auto")], {
1742
+ description: "Provider-native web search for this child (auto requires model metadata webSearch.native)",
1743
+ })),
1544
1744
  systemPromptAppend: Type.Optional(Type.String({ description: "Text appended to the child system prompt (never replaces inherited content)" })),
1545
1745
  parentView: Type.Optional(Type.Boolean({
1546
1746
  description: "Give the child a bounded projection of the parent goal/tools (parent history stays hidden)",
1547
1747
  })),
1548
1748
  }),
1549
1749
  execute: async (first, second) => {
1550
- const input = objectInput(first, second, "subagent_role_set");
1750
+ const input = objectInput(first, second, 'subagent_role type "set"');
1551
1751
  const id = input.id;
1552
1752
  if (typeof id !== "string" || !ROLE_ID_PATTERN.test(id)) {
1553
- throw new SubagentDelegateError("subagent_invalid_input", `subagent_role_set id must match ${String(ROLE_ID_PATTERN)} (lowercase letters, digits, "-" and "_", ` +
1753
+ throw new SubagentDelegateError("subagent_invalid_input", `subagent_role type "set" id must match ${String(ROLE_ID_PATTERN)} (lowercase letters, digits, "-" and "_", ` +
1554
1754
  `at most 32 chars), got ${JSON.stringify(id)}`);
1555
1755
  }
1556
1756
  for (const key of Object.keys(input)) {
1557
1757
  if (key === "id" || ROLE_KNOWN_KEYS.includes(key))
1558
1758
  continue;
1559
- throw new SubagentDelegateError("subagent_invalid_input", `subagent_role_set has unknown parameter ${JSON.stringify(key)} (known: id, ${ROLE_KNOWN_KEYS.join(", ")})`);
1759
+ throw new SubagentDelegateError("subagent_invalid_input", `subagent_role type "set" has unknown parameter ${JSON.stringify(key)} (known: id, ${ROLE_KNOWN_KEYS.join(", ")})`);
1560
1760
  }
1561
1761
  const definition = {};
1562
1762
  for (const key of ROLE_KNOWN_KEYS) {
@@ -1565,13 +1765,13 @@ export function createCapability(api, options) {
1565
1765
  }
1566
1766
  let role;
1567
1767
  try {
1568
- role = validateRoleDefinition(id, definition, `subagent_role_set "${id}"`);
1768
+ role = validateRoleDefinition(id, definition, `subagent_role type "set" "${id}"`);
1569
1769
  }
1570
1770
  catch (error) {
1571
1771
  throw new SubagentDelegateError("subagent_invalid_input", errorText(error));
1572
1772
  }
1573
1773
  if (role === undefined) {
1574
- throw new SubagentDelegateError("subagent_invalid_input", `subagent_role_set could not build role ${JSON.stringify(id)}`);
1774
+ throw new SubagentDelegateError("subagent_invalid_input", `subagent_role type "set" could not build role ${JSON.stringify(id)}`);
1575
1775
  }
1576
1776
  // 磁盘角色是用户资产:与**当前可见**磁盘角色撞名的新注册结构化拒绝,不隐式改写
1577
1777
  // 其含义。已有会话数据条目(装载期被磁盘遮蔽的影子角色)走正常 upsert 路径:
@@ -1601,21 +1801,21 @@ export function createCapability(api, options) {
1601
1801
  return { id, status, roles: [...roles.keys()].sort() };
1602
1802
  },
1603
1803
  });
1604
- const roleListTool = api.registerTool({
1605
- name: "subagent_role_list",
1804
+ const roleListSpec = toolSpec({
1805
+ name: "subagent_role.list",
1606
1806
  label: "List subagent roles",
1607
1807
  description: "List the subagent roles available in this session: id, origin (disk = shared role file, session = registered " +
1608
- "here with subagent_role_set), description, and whether the role narrows the child (hasProfile) or enables the " +
1808
+ 'here with subagent_role type "set"), description, and whether the role narrows the child (hasProfile) or enables the ' +
1609
1809
  "bounded parent projection (hasParentView). Pass an id to get one role's full definition, including its " +
1610
- "instructions and narrowing fields, to read, modify and re-register with subagent_role_set.",
1810
+ 'instructions and narrowing fields, to read, modify and re-register with subagent_role (type "set").',
1611
1811
  parameters: Type.Object({
1612
1812
  id: Type.Optional(Type.String({ description: "Role id; omit to list every available role" })),
1613
1813
  }),
1614
1814
  execute: async (first, second) => {
1615
- const input = objectInput(first, second, "subagent_role_list");
1815
+ const input = objectInput(first, second, 'subagent_role type "list"');
1616
1816
  const requested = input.id;
1617
1817
  if (requested !== undefined && (typeof requested !== "string" || requested.trim() === "")) {
1618
- throw new SubagentDelegateError("subagent_invalid_input", "subagent_role_list id must be a non-empty string when present");
1818
+ throw new SubagentDelegateError("subagent_invalid_input", 'subagent_role type "list" id must be a non-empty string when present');
1619
1819
  }
1620
1820
  const originOf = (roleId) => (diskRoles.has(roleId) ? "disk" : "session");
1621
1821
  if (requested === undefined) {
@@ -1637,10 +1837,10 @@ export function createCapability(api, options) {
1637
1837
  return { role: { ...toSessionRoleRecord(role), origin: originOf(role.id) } };
1638
1838
  },
1639
1839
  });
1640
- const roleRemoveTool = api.registerTool({
1641
- name: "subagent_role_remove",
1840
+ const roleRemoveSpec = toolSpec({
1841
+ name: "subagent_role.remove",
1642
1842
  label: "Remove a session subagent role",
1643
- description: "Remove one session-scoped subagent role (one registered with subagent_role_set) from this session. The removal " +
1843
+ description: 'Remove one session-scoped subagent role (one registered with subagent_role type "set") from this session. The removal ' +
1644
1844
  "is persisted in the session file and takes effect for the next subagent_run call. Disk roles cannot be removed " +
1645
1845
  "here — they live in the shared roles directory and are changed by editing their file; such a call is rejected " +
1646
1846
  "with a structured error.",
@@ -1648,10 +1848,10 @@ export function createCapability(api, options) {
1648
1848
  id: Type.String({ description: "Session role id to remove" }),
1649
1849
  }),
1650
1850
  execute: async (first, second) => {
1651
- const input = objectInput(first, second, "subagent_role_remove");
1851
+ const input = objectInput(first, second, 'subagent_role type "remove"');
1652
1852
  const id = input.id;
1653
1853
  if (typeof id !== "string" || id.trim() === "") {
1654
- throw new SubagentDelegateError("subagent_invalid_input", "subagent_role_remove requires a role id");
1854
+ throw new SubagentDelegateError("subagent_invalid_input", 'subagent_role type "remove" requires a role id');
1655
1855
  }
1656
1856
  // 会话数据条目优先:被磁盘遮蔽的影子角色也能删(删的是会话文件里的数据,
1657
1857
  // 磁盘角色本身不受影响);没有会话数据时才按"纯磁盘角色"结构化拒绝。
@@ -1674,6 +1874,45 @@ export function createCapability(api, options) {
1674
1874
  return { id, status: "removed", roles: [...roles.keys()].sort() };
1675
1875
  },
1676
1876
  });
1877
+ /**
1878
+ * 角色三工具合并为 `subagent_role`(2026-10-21 用户裁决:大类型一个工具,
1879
+ * `type` + 参数区分)。子操作定义仍是上面的三个 spec,派发前按 type 做
1880
+ * fail-closed 参数校验——跨 type 传参不会被静默忽略。
1881
+ */
1882
+ const roleTool = api.registerTool({
1883
+ name: "subagent_role",
1884
+ label: "Subagent roles",
1885
+ description: 'Manage session-scoped subagent roles. type="set": register or update a role (id, description, instructions ' +
1886
+ 'required; optional fields narrow the child); type="list": list the roles available in this session, or one ' +
1887
+ 'role\'s full definition when id is given; type="remove": remove one session-scoped role. Disk roles are ' +
1888
+ "read-only here — edit their file instead.",
1889
+ parameters: Type.Object({
1890
+ type: Type.Union([Type.Literal("set"), Type.Literal("list"), Type.Literal("remove")], {
1891
+ description: "Operation: set | list | remove",
1892
+ }),
1893
+ ...optionalParams(roleSetSpec.parameters.properties, roleListSpec.parameters.properties, roleRemoveSpec.parameters.properties),
1894
+ // Shared key across the three operations: give one neutral description
1895
+ // instead of whichever spec happened to be merged last.
1896
+ id: Type.Optional(Type.String({
1897
+ pattern: "^[a-z0-9][a-z0-9_-]{0,31}$",
1898
+ description: "Role id: set/remove target, or one role to view for list",
1899
+ })),
1900
+ }),
1901
+ execute: async (first, second) => {
1902
+ const input = objectInput(first, second, "subagent_role");
1903
+ const type = readDiscriminator(input, "subagent_role", ["set", "list", "remove"]);
1904
+ if (type === "set") {
1905
+ rejectForeignParams(input, "subagent_role", type, ["id", ...ROLE_KNOWN_KEYS]);
1906
+ return roleSetSpec.execute(withoutDiscriminator(input), second);
1907
+ }
1908
+ if (type === "list") {
1909
+ rejectForeignParams(input, "subagent_role", type, ["id"]);
1910
+ return roleListSpec.execute(withoutDiscriminator(input), second);
1911
+ }
1912
+ rejectForeignParams(input, "subagent_role", type, ["id"]);
1913
+ return roleRemoveSpec.execute(withoutDiscriminator(input), second);
1914
+ },
1915
+ });
1677
1916
  /**
1678
1917
  * `/roles` 视图(D-081 用户面):数据源只有上面的三个闭包表(实时值)——可用集
1679
1918
  * `roles`(按来源分区,磁盘 `diskRoles` 优先)、数据表 `sessionRoles`(诊断里
@@ -1689,7 +1928,7 @@ export function createCapability(api, options) {
1689
1928
  const session = sorted.filter((role) => !diskRoles.has(role.id));
1690
1929
  const lines = [];
1691
1930
  if (sorted.length === 0) {
1692
- lines.push("No subagent roles are available in this session. Register one with subagent_role_set" +
1931
+ lines.push('No subagent roles are available in this session. Register one with subagent_role type "set"' +
1693
1932
  (roleAgentDir === undefined
1694
1933
  ? ""
1695
1934
  : `, or drop a role file into ${join(roleAgentDir, ROLE_CATALOG_DIR)}/ and associate its id in the suite.`));
@@ -1748,8 +1987,12 @@ export function createCapability(api, options) {
1748
1987
  lines.push(`thinkingLevel: ${profile.thinkingLevel}`);
1749
1988
  if (profile?.tools !== undefined)
1750
1989
  lines.push(`tools: ${profile.tools.join(", ")}`);
1990
+ if (profile?.writeScope !== undefined)
1991
+ lines.push(`writeScope: ${profile.writeScope.join(", ")}`);
1751
1992
  if (profile?.mode !== undefined)
1752
1993
  lines.push(`mode: ${profile.mode}`);
1994
+ if (profile?.webSearch !== undefined)
1995
+ lines.push(`webSearch: ${profile.webSearch}`);
1753
1996
  if (profile?.systemPromptAppend !== undefined)
1754
1997
  lines.push("systemPromptAppend:", profile.systemPromptAppend);
1755
1998
  if (role.parentView === true)
@@ -1757,7 +2000,7 @@ export function createCapability(api, options) {
1757
2000
  return lines.join("\n");
1758
2001
  };
1759
2002
  /**
1760
- * `/roles` 斜杠命令(D-081 用户面):与角色三工具同一注册分支(委派面存在才注册),
2003
+ * `/roles` 斜杠命令(D-081 用户面):与角色工具同一注册分支(委派面存在才注册),
1761
2004
  * 句柄随插件 dispose。只读视图——不改状态、不引入新配置键;未知 id 是正常结果
1762
2005
  * (error + 可用 id 列表),任何异常都收敛为 `{ status: "error" }`,命令不崩宿主。
1763
2006
  */
@@ -1789,8 +2032,8 @@ export function createCapability(api, options) {
1789
2032
  * - `sessionId` → 持久化 transcript(宿主注入的会话回读器;未注入时结构化报错)。
1790
2033
  * 返回默认只给最后 N 条(可调),并如实报告条数/字节/截断——绝不默认回灌全文。
1791
2034
  */
1792
- const resultTool = api.registerTool({
1793
- name: "subagent_result",
2035
+ const resultSpec = toolSpec({
2036
+ name: "subagent_task.result",
1794
2037
  label: "Read subagent result",
1795
2038
  description: "Read back a bounded slice of a subagent's session: pass the taskId from subagent_run, or the child's sessionId " +
1796
2039
  "to read its persisted transcript. Returns at most `maxMessages` trailing entries (default 20) plus counts, " +
@@ -1809,19 +2052,19 @@ export function createCapability(api, options) {
1809
2052
  })),
1810
2053
  }),
1811
2054
  execute: async (first, second) => {
1812
- const input = objectInput(first, second);
2055
+ const input = objectInput(first, second, 'subagent_task type "result"');
1813
2056
  const taskId = typeof input.taskId === "string" && input.taskId.trim() !== "" ? input.taskId : undefined;
1814
2057
  const sessionId = typeof input.sessionId === "string" && input.sessionId.trim() !== "" ? input.sessionId : undefined;
1815
2058
  const requested = input.maxMessages === undefined ? RESULT_READ_DEFAULT_MESSAGES : Number(input.maxMessages);
1816
2059
  if (!Number.isInteger(requested) || requested < 1 || requested > RESULT_READ_MAX_MESSAGES) {
1817
- throw new SubagentDelegateError("subagent_invalid_input", `subagent_result maxMessages must be an integer in 1..${RESULT_READ_MAX_MESSAGES}`);
2060
+ throw new SubagentDelegateError("subagent_invalid_input", `subagent_task type "result" maxMessages must be an integer in 1..${RESULT_READ_MAX_MESSAGES}`);
1818
2061
  }
1819
2062
  const waitMs = input.waitMs === undefined ? 0 : Number(input.waitMs);
1820
2063
  if (!Number.isInteger(waitMs) || waitMs < 0 || waitMs > RESULT_READ_MAX_WAIT_MS) {
1821
- throw new SubagentDelegateError("subagent_invalid_input", `subagent_result waitMs must be an integer in 0..${RESULT_READ_MAX_WAIT_MS}`);
2064
+ throw new SubagentDelegateError("subagent_invalid_input", `subagent_task type "result" waitMs must be an integer in 0..${RESULT_READ_MAX_WAIT_MS}`);
1822
2065
  }
1823
2066
  if (taskId === undefined && sessionId === undefined) {
1824
- throw new SubagentDelegateError("subagent_invalid_input", "subagent_result requires taskId (scheduler record) or sessionId (persisted transcript)");
2067
+ throw new SubagentDelegateError("subagent_invalid_input", 'subagent_task type "result" requires taskId (scheduler record) or sessionId (persisted transcript)');
1825
2068
  }
1826
2069
  if (taskId !== undefined) {
1827
2070
  const requester = {
@@ -1854,6 +2097,8 @@ export function createCapability(api, options) {
1854
2097
  bytes: utf8ByteLength(JSON.stringify(record.messages)),
1855
2098
  truncated: record.truncated || messages.length < record.messages.length,
1856
2099
  messages: messages.map((message) => detachedSessionSnapshot(message)),
2100
+ ...(record.taskOutcome === undefined ? {} : { taskOutcome: record.taskOutcome }),
2101
+ ...(record.refusalReason === undefined ? {} : { refusalReason: record.refusalReason }),
1857
2102
  ...(escalations.length === 0 ? {} : { escalations }),
1858
2103
  ...(siblingTraffic.length === 0 ? {} : { siblingTraffic }),
1859
2104
  };
@@ -1883,19 +2128,19 @@ export function createCapability(api, options) {
1883
2128
  };
1884
2129
  },
1885
2130
  });
1886
- // steer 的模型面(D-060 裁决 13,2026-10-02 用户裁决):`subagent_steer` 随
2131
+ // steer 的模型面(D-060 裁决 13,2026-10-02 用户裁决):`subagent_task` type `steer` 随
1887
2132
  // background 面注册。原 S4 裁决"steer 不设模型面工具"的前提——前台阻塞 drain 下
1888
2133
  // 父模型无"运行中窗口"——在 background 默认开后失效(提交即返回票据,运行中窗口
1889
2134
  // 存在),该论证保留为背景说明。语义对齐主流取证(codex v2 send_message /
1890
2135
  // ZCode SendMessage 均为模型面排队投递):不打断子代理当前回合,在其下一个模型
1891
2136
  // 请求前生效。程序面(agents 桥的 steer 与本调度器的 steer/findBySession 原语)
1892
2137
  // 保持不变,供编排插件组合。
1893
- // subagent_cancel(S4 补充裁决,2026-10-01):原"仅 background 委派启用时注册"
2138
+ // `subagent_task` type `cancel`(S4 补充裁决,2026-10-01):原"仅 background 委派启用时注册"
1894
2139
  // 的前提(前台 drain 下父模型在 subagent_run 阻塞中不存在运行中窗口)保留为
1895
2140
  // 背景说明——cancel 只在 background 委派返回票据后才有可达窗口。
1896
- // 两个工具自 D-060 裁决 15(2026-10-03)起无条件注册(开关形态退役)。
1897
- const cancelTool = api.registerTool({
1898
- name: "subagent_cancel",
2141
+ // cancel/steer 操作自 D-060 裁决 15(2026-10-03)起无条件注册(开关形态退役)。
2142
+ const cancelSpec = toolSpec({
2143
+ name: "subagent_task.cancel",
1899
2144
  label: "Cancel a background subagent",
1900
2145
  description: "Cancel a running background subagent task by its taskId (the ticket returned by subagent_run with " +
1901
2146
  "background: true). Only background delegation tickets are cancellable: a foreground subagent_run call " +
@@ -1906,10 +2151,10 @@ export function createCapability(api, options) {
1906
2151
  reason: Type.Optional(Type.String({ description: "Short reason recorded on the cancellation fence" })),
1907
2152
  }),
1908
2153
  execute: async (first, second) => {
1909
- const input = objectInput(first, second);
2154
+ const input = objectInput(first, second, 'subagent_task type "cancel"');
1910
2155
  const taskId = typeof input.taskId === "string" ? input.taskId : "";
1911
2156
  if (taskId.trim() === "") {
1912
- throw new SubagentDelegateError("subagent_invalid_input", "subagent_cancel requires a taskId");
2157
+ throw new SubagentDelegateError("subagent_invalid_input", 'subagent_task type "cancel" requires a taskId');
1913
2158
  }
1914
2159
  const reason = typeof input.reason === "string" && input.reason.trim() !== "" ? input.reason : undefined;
1915
2160
  const result = await runtime.cancelById(taskId, { owner: capabilityManifest.id, parentSessionId: session.getSessionId() }, reason);
@@ -1918,16 +2163,16 @@ export function createCapability(api, options) {
1918
2163
  : { outcome: result.outcome, taskId, status: result.status };
1919
2164
  },
1920
2165
  });
1921
- // subagent_steer(D-060 裁决 13):与 cancelTool 同属 background 面——有界引导消息
1922
- // 投递给运行中的后台子任务(排队语义),需要硬终止时模型仍走 subagent_cancel。
2166
+ // `subagent_task` type `steer`(D-060 裁决 13):与 cancelTool 同属 background 面——有界引导消息
2167
+ // 投递给运行中的后台子任务(排队语义),需要硬终止时模型仍走 `subagent_task` type `cancel`。
1923
2168
  // D-060 裁决 15(2026-10-03)起无条件注册(开关形态退役)。
1924
- const steerTool = api.registerTool({
1925
- name: "subagent_steer",
2169
+ const steerSpec = toolSpec({
2170
+ name: "subagent_task.steer",
1926
2171
  label: "Steer a background subagent",
1927
2172
  description: "Deliver one bounded steering message to a running background subagent task by its taskId (the ticket " +
1928
2173
  "returned by subagent_run with background: true). Queued delivery: it does NOT interrupt the subagent's " +
1929
2174
  "current turn — it takes effect before the task's next model request. Use it when user feedback or new " +
1930
- "information should change the subtask's direction. Use subagent_cancel when the task must stop instead.",
2175
+ 'information should change the subtask\'s direction. Use subagent_task with type "cancel" when the task must stop instead.',
1931
2176
  parameters: Type.Object({
1932
2177
  taskId: Type.String({ description: "Task id from a background subagent_run ticket" }),
1933
2178
  text: Type.String({
@@ -1936,14 +2181,14 @@ export function createCapability(api, options) {
1936
2181
  }),
1937
2182
  }),
1938
2183
  execute: async (first, second) => {
1939
- const input = objectInput(first, second);
2184
+ const input = objectInput(first, second, 'subagent_task type "steer"');
1940
2185
  const taskId = typeof input.taskId === "string" ? input.taskId : "";
1941
2186
  const text = typeof input.text === "string" ? input.text : "";
1942
2187
  if (taskId.trim() === "" || text.trim() === "") {
1943
- throw new SubagentDelegateError("subagent_invalid_input", "subagent_steer requires a taskId and non-empty text");
2188
+ throw new SubagentDelegateError("subagent_invalid_input", 'subagent_task type "steer" requires a taskId and non-empty text');
1944
2189
  }
1945
2190
  if (Buffer.byteLength(text, "utf8") > SUBAGENT_MAILBOX_MAX_MESSAGE_BYTES) {
1946
- throw new SubagentDelegateError("subagent_message_too_large", `subagent_steer text exceeds ${SUBAGENT_MAILBOX_MAX_MESSAGE_BYTES} utf-8 bytes`);
2191
+ throw new SubagentDelegateError("subagent_message_too_large", `subagent_task type "steer" text exceeds ${SUBAGENT_MAILBOX_MAX_MESSAGE_BYTES} utf-8 bytes`);
1947
2192
  }
1948
2193
  try {
1949
2194
  await runtime.steer(taskId, text, {
@@ -1964,8 +2209,8 @@ export function createCapability(api, options) {
1964
2209
  * resume 键随 metadata 过线,runner 打开既有会话文件续写。
1965
2210
  * D-060 裁决 15(2026-10-03)起无条件注册(开关形态退役)。
1966
2211
  */
1967
- const resumeTool = api.registerTool({
1968
- name: "subagent_resume",
2212
+ const resumeSpec = toolSpec({
2213
+ name: "subagent_task.resume",
1969
2214
  label: "Resume a subagent",
1970
2215
  description: "Continue a finished subagent session by its sessionId: submit one more prompt into the same persisted " +
1971
2216
  "transcript (multi-turn subtask) and get the bounded terminal projection, like subagent_run for one task.",
@@ -1974,11 +2219,11 @@ export function createCapability(api, options) {
1974
2219
  prompt: Type.String({ minLength: 1, description: "Follow-up prompt for the continued session" }),
1975
2220
  }),
1976
2221
  execute: async (first, second) => {
1977
- const input = objectInput(first, second);
2222
+ const input = objectInput(first, second, 'subagent_task type "resume"');
1978
2223
  const sessionId = typeof input.sessionId === "string" ? input.sessionId : "";
1979
2224
  const prompt = typeof input.prompt === "string" ? input.prompt : "";
1980
2225
  if (sessionId === "" || prompt.trim() === "") {
1981
- throw new SubagentDelegateError("subagent_invalid_input", "subagent_resume requires sessionId and prompt");
2226
+ throw new SubagentDelegateError("subagent_invalid_input", 'subagent_task type "resume" requires sessionId and prompt');
1982
2227
  }
1983
2228
  const reader = agents?.session;
1984
2229
  if (reader === undefined) {
@@ -2014,24 +2259,24 @@ export function createCapability(api, options) {
2014
2259
  * 子会话盘点(D-060 模型面):列出**本会话自己的**子代理会话(最近活动倒序、
2015
2260
  * 有界),并用调度器记录富化活跃/终态信息。宿主注入 `agents.list` 才注册;
2016
2261
  * 读的是落盘目录,父会话跨进程重启后仍能找回自己的子代理。返回的 sessionId
2017
- * 可直接用于 `subagent_result` / `subagent_resume`。
2262
+ * 可直接用于 `subagent_task` 的 `result` / `resume`。
2018
2263
  */
2019
- const listTool = agents?.list === undefined
2264
+ const listSpec = agents?.list === undefined
2020
2265
  ? undefined
2021
- : api.registerTool({
2022
- name: "subagent_list",
2266
+ : toolSpec({
2267
+ name: "subagent_task.list",
2023
2268
  label: "List subagent sessions",
2024
2269
  description: "List this session's own subagent sessions (newest activity first) with role, timestamps and transcript " +
2025
2270
  "size; tasks still in flight are marked active, settled ones carry their terminal status. Read-only. " +
2026
- "Use a listed sessionId with subagent_result or subagent_resume.",
2271
+ 'Use a listed sessionId with subagent_task type "result" or "resume".',
2027
2272
  parameters: Type.Object({
2028
2273
  limit: Type.Optional(Type.Number({ description: "Max entries to return (1..50, default 20)", minimum: 1, maximum: 50 })),
2029
2274
  }),
2030
2275
  execute: async (first, second) => {
2031
- const input = objectInput(first, second);
2276
+ const input = objectInput(first, second, 'subagent_task type "list"');
2032
2277
  const requested = input.limit === undefined ? 20 : Number(input.limit);
2033
2278
  if (!Number.isInteger(requested) || requested < 1 || requested > 50) {
2034
- throw new SubagentDelegateError("subagent_invalid_input", "subagent_list limit must be an integer in 1..50");
2279
+ throw new SubagentDelegateError("subagent_invalid_input", 'subagent_task type "list" limit must be an integer in 1..50');
2035
2280
  }
2036
2281
  const summaries = (await agents.list?.()) ?? [];
2037
2282
  const requester = { owner: capabilityManifest.id, parentSessionId: session.getSessionId() };
@@ -2060,18 +2305,61 @@ export function createCapability(api, options) {
2060
2305
  return { count: sessions.length, sessions };
2061
2306
  },
2062
2307
  });
2308
+ /**
2309
+ * 任务面合并为 `subagent_task`(2026-10-21 用户裁决:大类型一个工具,
2310
+ * `type` + 参数区分)。`subagent_run` 保持独立:它是唯一带动态角色枚举与
2311
+ * 专用渲染的入口,合并进来只会让最常用的调用多背一层判别。
2312
+ */
2313
+ const taskOps = ["result", "cancel", "steer", "resume", ...(listSpec === undefined ? [] : ["list"])];
2314
+ const taskTool = api.registerTool({
2315
+ name: "subagent_task",
2316
+ label: "Subagent tasks",
2317
+ description: "Inspect and steer subagent tasks. " +
2318
+ [
2319
+ 'type="result": read a bounded slice of a task record or child session (taskId or sessionId)',
2320
+ 'type="cancel": cancel a running background task by taskId',
2321
+ 'type="steer": queue a bounded steering message to a running background task',
2322
+ 'type="resume": continue a finished child session with one more prompt',
2323
+ ...(listSpec === undefined ? [] : [`type="list": list this session's own subagent sessions`]),
2324
+ ].join("; ") +
2325
+ ". Task creation stays in subagent_run.",
2326
+ parameters: Type.Object({
2327
+ type: Type.Union(taskOps.map((op) => Type.Literal(op)), { description: `Operation: ${taskOps.join(" | ")}` }),
2328
+ ...optionalParams(resultSpec.parameters.properties, cancelSpec.parameters.properties, steerSpec.parameters.properties, resumeSpec.parameters.properties, listSpec?.parameters.properties),
2329
+ // Shared keys across operations: neutral descriptions instead of the
2330
+ // last merged spec's operation-specific wording.
2331
+ taskId: Type.Optional(Type.String({ description: "Task id from subagent_run (result/cancel/steer)" })),
2332
+ sessionId: Type.Optional(Type.String({ description: "Child session id (result reads its transcript, resume continues it)" })),
2333
+ }),
2334
+ execute: async (first, second) => {
2335
+ const input = objectInput(first, second, "subagent_task");
2336
+ const type = readDiscriminator(input, "subagent_task", taskOps);
2337
+ if (type === "result") {
2338
+ rejectForeignParams(input, "subagent_task", type, ["taskId", "sessionId", "maxMessages", "waitMs"]);
2339
+ return resultSpec.execute(withoutDiscriminator(input), second);
2340
+ }
2341
+ if (type === "cancel") {
2342
+ rejectForeignParams(input, "subagent_task", type, ["taskId", "reason"]);
2343
+ return cancelSpec.execute(withoutDiscriminator(input), second);
2344
+ }
2345
+ if (type === "steer") {
2346
+ rejectForeignParams(input, "subagent_task", type, ["taskId", "text"]);
2347
+ return steerSpec.execute(withoutDiscriminator(input), second);
2348
+ }
2349
+ if (type === "resume") {
2350
+ rejectForeignParams(input, "subagent_task", type, ["sessionId", "prompt"]);
2351
+ return resumeSpec.execute(withoutDiscriminator(input), second);
2352
+ }
2353
+ rejectForeignParams(input, "subagent_task", type, ["limit"]);
2354
+ return listSpec?.execute(withoutDiscriminator(input), second);
2355
+ },
2356
+ });
2063
2357
  return [
2064
2358
  // 刷新句柄:释放时以最新一次 subagent_run 注册为准(旧句柄 dispose 因表项被接手而失效)。
2065
2359
  createDisposableRegistration("subagent-delegate.tool.subagent_run", () => void runTool.current.dispose(), `registration:${capabilityManifest.id}:refreshable:tool:subagent_run`),
2066
- resultTool,
2067
- cancelTool,
2068
- steerTool,
2069
- resumeTool,
2070
- roleSetTool,
2071
- roleListTool,
2072
- roleRemoveTool,
2360
+ taskTool,
2361
+ roleTool,
2073
2362
  rolesCommand,
2074
- ...(listTool === undefined ? [] : [listTool]),
2075
2363
  // 子侧通信面(D-060 S4/S5):获准自建会话两面并存(向下委派 + 向父上抛/兄弟消息)。
2076
2364
  ...childTools,
2077
2365
  createDisposableRegistration("subagent-delegate.runtime",
@@ -2096,6 +2384,8 @@ export function createCapability(api, options) {
2096
2384
  // 无法再抛,这里只保证不产生未处理的 rejection。
2097
2385
  });
2098
2386
  }, `registration:${capabilityManifest.id}:runtime`),
2387
+ // 公共调度器能力(P1b):排在 runtime 之后,逆序 dispose 先摘能力再释放调度器。
2388
+ schedulerRegistration,
2099
2389
  ];
2100
2390
  }
2101
2391
  //# sourceMappingURL=delegate.js.map