@ai-agent-forge/plugin-subagent-delegate 0.88.14 → 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/MANUAL.md +19 -23
- package/README.md +59 -54
- package/agent-forge.json +1 -1
- package/dist/defaults.d.ts +30 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +30 -0
- package/dist/defaults.js.map +1 -0
- package/dist/delegate.d.ts +37 -17
- package/dist/delegate.d.ts.map +1 -1
- package/dist/delegate.js +453 -163
- package/dist/delegate.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/remote-adapter.d.ts.map +1 -1
- package/dist/remote-adapter.js +5 -0
- package/dist/remote-adapter.js.map +1 -1
- package/dist/render.d.ts +67 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +325 -0
- package/dist/render.js.map +1 -0
- package/dist/runner-pool.d.ts.map +1 -1
- package/dist/runner-pool.js +2 -0
- package/dist/runner-pool.js.map +1 -1
- package/dist/scheduler-capability.d.ts +39 -0
- package/dist/scheduler-capability.d.ts.map +1 -0
- package/dist/scheduler-capability.js +266 -0
- package/dist/scheduler-capability.js.map +1 -0
- package/dist/subagent-runtime.d.ts +17 -3
- package/dist/subagent-runtime.d.ts.map +1 -1
- package/dist/subagent-runtime.js +181 -8
- package/dist/subagent-runtime.js.map +1 -1
- package/package.json +3 -4
- package/plugin.json +9 -7
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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:
|
|
67
|
-
runnerIdleTimeoutMs:
|
|
68
|
-
runnerSweepIntervalMs:
|
|
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
|
|
102
|
-
*
|
|
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
|
-
|
|
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: "
|
|
152
|
-
instructions:
|
|
146
|
+
description: "视觉审核者:只读检查实际可观察的视觉呈现,按证据支持的维度判断;不审代码功能,不修改文件",
|
|
147
|
+
instructions: `你是视觉审核者,只读检查任务中实际可观察的视觉呈现。美是形式、内容与情境之间恰当而有表现力的关系;它不是科学评分或跨文化唯一答案。根据用户需求、内容、受众、媒介与约束理解表达目的和文化气质,不把个人偏好设为所有用户的默认;证据支持什么判断什么。不审代码功能正确性,不修改任何文件。
|
|
148
|
+
|
|
149
|
+
【方法】先表达意图,再组织关系,结合情境取舍,最后观察效果。以下六原理是可按意图选用的组织方法,不要求使用中国符号、固定风格、特定色相或质感:以意统形(表达意图指导形式);虚实相生(显露与保留协同,必需信息不能隐藏);疏密有致(密处有组织、疏处有作用,不强求低密度);气韵贯通(节奏、呼应、动势维系整体气质);变化有度(统一与变化依目的取舍,有意的微差可形成层次);因境制宜(按用户需求、内容、受众、媒介与约束取舍)。对照已有意图和约束检查这些关系如何体现在呈现中;实际效果是否支持表达意图,应由可观察证据判断,有理由不等于效果成立。
|
|
153
150
|
|
|
154
|
-
|
|
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
|
-
|
|
166
|
+
六原理的运用本身不是验收项。主观改进建议不能冒充违规;“缺少气韵”等抽象评价,必须指出具体元素、关系与目标证据。艺术意图不能豁免必要信息可读性或用户硬约束。必要信息不可读或违反用户明确约束时,按实际证据判定问题。
|
|
172
167
|
|
|
173
|
-
|
|
174
|
-
- 只报告能说出具体问题的条目;禁止"感觉不好、不够精致"这类空判,禁止发明像素值,禁止泛泛的美化建议。
|
|
175
|
-
- 无问题的维度不必逐条表扬,但结尾列出本次已检查的维度清单。
|
|
176
|
-
- 图像过小看不清或信息不足的项标 Unverified,不猜测。
|
|
168
|
+
【证据与评审范围】先按任务目标与已有验收清单确定必要检查的维度和范围。必要项不能因证据缺失改标不适用;只有任务目标本身不要求的维度才可标不适用并说明理由。必要信息缺失时对应项为 Unverified;结论只覆盖实际检查范围,不能外推为完整视觉已通过。已发现问题但其他必要项仍未知时,总评为 fail,并保留未知项。
|
|
177
169
|
|
|
178
|
-
|
|
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 [
|
|
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
|
-
// 主代理在会话内用 `
|
|
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 [
|
|
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
|
-
/** `
|
|
734
|
+
/** `subagent_task`(type `result`)默认/上限返回条数(有界回读,绝不默认回灌全文)。 */
|
|
712
735
|
const RESULT_READ_DEFAULT_MESSAGES = 20;
|
|
713
736
|
const RESULT_READ_MAX_MESSAGES = 50;
|
|
714
|
-
/** `
|
|
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 =
|
|
726
|
-
const DEFAULT_TASK_TIMEOUT_MS =
|
|
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
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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", "
|
|
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
|
-
|
|
927
|
-
|
|
928
|
-
|
|
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", "
|
|
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
|
-
})
|
|
957
|
-
|
|
1070
|
+
});
|
|
1071
|
+
const inboxSpec = wiring?.sendSibling === undefined
|
|
1072
|
+
? undefined
|
|
1073
|
+
: toolSpec({
|
|
1074
|
+
name: "subagent_message.inbox",
|
|
958
1075
|
label: "Read sibling inbox",
|
|
959
|
-
description:
|
|
960
|
-
"consumed by this call; poll it when coordination is expected. The sibling channel carries
|
|
961
|
-
"intermediate artifacts only — assignment and acceptance stay with the parent, which can audit
|
|
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", "
|
|
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、每池
|
|
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
|
-
// 否则池化容量小于默认并发(
|
|
1019
|
-
const effectiveMaxConcurrent = options?.limits?.maxConcurrentSubagents ??
|
|
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
|
|
1096
|
-
|
|
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
|
-
//
|
|
1099
|
-
|
|
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 参数与
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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)
|
|
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 裁定:续写既有会话不计新层级,走
|
|
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 与
|
|
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
|
-
*
|
|
1696
|
+
* 会话角色(D-081,现合并为 `subagent_role`):注册/列出/删除。注册是"先落盘再提交内存"——持久化条目
|
|
1503
1697
|
* 写在会话文件里(customType `subagent.roles.state`),下次打开同一会话仍在;会话
|
|
1504
|
-
* 角色不写 `<agentDir>/roles
|
|
1698
|
+
* 角色不写 `<agentDir>/roles/`、不跨会话共享、子代理不继承。角色工具未声明 readonlySafe,
|
|
1505
1699
|
* readonly 会话沿用既有"未声明即不激活"语义,不做特殊分支。
|
|
1506
1700
|
*/
|
|
1507
|
-
const
|
|
1508
|
-
name: "
|
|
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, "
|
|
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", `
|
|
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", `
|
|
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, `
|
|
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", `
|
|
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
|
|
1605
|
-
name: "
|
|
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
|
-
|
|
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
|
-
|
|
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, "
|
|
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", "
|
|
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
|
|
1641
|
-
name: "
|
|
1840
|
+
const roleRemoveSpec = toolSpec({
|
|
1841
|
+
name: "subagent_role.remove",
|
|
1642
1842
|
label: "Remove a session subagent role",
|
|
1643
|
-
description:
|
|
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, "
|
|
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", "
|
|
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(
|
|
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
|
|
1793
|
-
name: "
|
|
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", `
|
|
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", `
|
|
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", "
|
|
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 用户裁决):`
|
|
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
|
-
//
|
|
2138
|
+
// `subagent_task` type `cancel`(S4 补充裁决,2026-10-01):原"仅 background 委派启用时注册"
|
|
1894
2139
|
// 的前提(前台 drain 下父模型在 subagent_run 阻塞中不存在运行中窗口)保留为
|
|
1895
2140
|
// 背景说明——cancel 只在 background 委派返回票据后才有可达窗口。
|
|
1896
|
-
//
|
|
1897
|
-
const
|
|
1898
|
-
name: "
|
|
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", "
|
|
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
|
-
//
|
|
1922
|
-
// 投递给运行中的后台子任务(排队语义),需要硬终止时模型仍走
|
|
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
|
|
1925
|
-
name: "
|
|
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
|
-
|
|
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", "
|
|
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", `
|
|
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
|
|
1968
|
-
name: "
|
|
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", "
|
|
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
|
-
* 可直接用于 `
|
|
2262
|
+
* 可直接用于 `subagent_task` 的 `result` / `resume`。
|
|
2018
2263
|
*/
|
|
2019
|
-
const
|
|
2264
|
+
const listSpec = agents?.list === undefined
|
|
2020
2265
|
? undefined
|
|
2021
|
-
:
|
|
2022
|
-
name: "
|
|
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
|
-
|
|
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", "
|
|
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
|
-
|
|
2067
|
-
|
|
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
|