@ai-agent-forge/plugin-subagent-delegate 0.86.7 → 0.88.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-forge.json +1 -1
- package/dist/delegate.d.ts +115 -13
- package/dist/delegate.d.ts.map +1 -1
- package/dist/delegate.js +834 -318
- package/dist/delegate.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/subagent-runtime.d.ts +9 -1
- package/dist/subagent-runtime.d.ts.map +1 -1
- package/dist/subagent-runtime.js +38 -8
- package/dist/subagent-runtime.js.map +1 -1
- package/package.json +3 -5
- package/plugin.json +1 -1
package/dist/delegate.js
CHANGED
|
@@ -67,34 +67,9 @@ export const capabilityManifest = {
|
|
|
67
67
|
};
|
|
68
68
|
// 通信面的宿主接线 `SubagentCommunicationWiringV1`(含信箱消息形状)是 plugin-sdk
|
|
69
69
|
// 公共契约(D-075 S4 第三批迁入 SDK),本包经上方 import 消费并在 index 再导出。
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
* 未知键与错误类型装载期 fail fast,不让"配置了但没生效"的假象存在。
|
|
74
|
-
*/
|
|
75
|
-
function readCommunicationConfig(raw) {
|
|
76
|
-
if (raw === undefined)
|
|
77
|
-
return {};
|
|
78
|
-
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
79
|
-
throw new Error("subagent-delegate config communication must be an object");
|
|
80
|
-
}
|
|
81
|
-
const record = raw;
|
|
82
|
-
const known = COMMUNICATION_FLAG_KEYS;
|
|
83
|
-
const unknown = Object.keys(record).filter((key) => !known.includes(key));
|
|
84
|
-
if (unknown.length > 0) {
|
|
85
|
-
throw new Error(`subagent-delegate config communication has unknown key(s): ${unknown.join(", ")}`);
|
|
86
|
-
}
|
|
87
|
-
const config = {};
|
|
88
|
-
for (const key of COMMUNICATION_FLAG_KEYS) {
|
|
89
|
-
const value = record[key];
|
|
90
|
-
if (value === undefined)
|
|
91
|
-
continue;
|
|
92
|
-
if (value !== true)
|
|
93
|
-
throw new Error(`subagent-delegate config communication.${key} must be true or omitted`);
|
|
94
|
-
config[key] = true;
|
|
95
|
-
}
|
|
96
|
-
return config;
|
|
97
|
-
}
|
|
70
|
+
// 通信面配置键已移除(D-060 裁决 15,2026-10-03):resume/escalate/sibling 曾是
|
|
71
|
+
// 出厂默认全开的显式开关(裁决 13/14 翻转后仅剩"显式 false 关闭"形态),随开关
|
|
72
|
+
// 形态一并退役——功能基本面只有"有"与"没有",配置键出现即装载期失败。
|
|
98
73
|
/** 把宿主/调度器/信箱的结构化错误映射为工具面的同码委托错误(保留原始 message)。 */
|
|
99
74
|
function mapCommunicationError(error) {
|
|
100
75
|
if (error instanceof SubagentMailboxErrorV1) {
|
|
@@ -202,6 +177,157 @@ function validateRoleDefinition(id, value, origin) {
|
|
|
202
177
|
...(parentView === true ? { parentView: true } : {}),
|
|
203
178
|
};
|
|
204
179
|
}
|
|
180
|
+
/** 角色菜单字符数(工具描述里每个角色占 `id — description`;装载期与注册期共用同一公式)。 */
|
|
181
|
+
function roleMenuChars(roles) {
|
|
182
|
+
let total = 0;
|
|
183
|
+
for (const role of roles)
|
|
184
|
+
total += role.id.length + role.description.length + 3;
|
|
185
|
+
return total;
|
|
186
|
+
}
|
|
187
|
+
/** 未知角色 id 的拒绝文本(resolveRole 与角色工具共用,措辞保持稳定)。 */
|
|
188
|
+
function unknownRoleMessage(id, roles) {
|
|
189
|
+
const available = [...roles.keys()].sort();
|
|
190
|
+
return (`unknown subagent role ${JSON.stringify(id)}; available roles: ` +
|
|
191
|
+
(available.length === 0 ? "(none configured)" : available.join(", ")));
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* 角色是否带**收窄档案**(profile 里除基线 version/roleId 外还有 model/thinkingLevel/
|
|
195
|
+
* tools/mode/systemPromptAppend)。`role.profile !== undefined` 不是这个意思:档案基线
|
|
196
|
+
* 恒含 roleId,因此任何角色都带 profile;"带收窄"看的是有没有收窄字段。
|
|
197
|
+
*/
|
|
198
|
+
function hasNarrowingProfile(role) {
|
|
199
|
+
const profile = role.profile;
|
|
200
|
+
return (profile !== undefined &&
|
|
201
|
+
(profile.model !== undefined ||
|
|
202
|
+
profile.thinkingLevel !== undefined ||
|
|
203
|
+
profile.tools !== undefined ||
|
|
204
|
+
profile.mode !== undefined ||
|
|
205
|
+
profile.systemPromptAppend !== undefined));
|
|
206
|
+
}
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// 会话级子代理角色(session-scoped roles,D-081,2026-10-03 用户裁决落地)
|
|
209
|
+
//
|
|
210
|
+
// 主代理可用角色 = 磁盘角色(方案 associations ∩ 目录文件)∪ 会话角色。会话角色由
|
|
211
|
+
// 主代理在会话内用 `subagent_role_set` 注册,随会话文件持久化(下次打开仍在),
|
|
212
|
+
// 不写 `<agentDir>/roles/`、不跨会话共享、子代理不继承。磁盘角色优先:撞名的注册
|
|
213
|
+
// 被结构化拒绝,装载期合并时撞名的会话角色被忽略并 warn。
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
/**
|
|
216
|
+
* 会话角色快照条目的 customType(跨插件契约,workflow 软校验按此字面量读取):
|
|
217
|
+
* data = `{ version: 1, roles: [...] }` **全量快照**、append-only;恢复时反向扫描
|
|
218
|
+
* 最近一条可解析快照(与 goal/plan/todo/workflow 的 loadState 同构)。压缩只影响
|
|
219
|
+
* LLM 上下文投影,`getBranchEntries` 始终返回完整分支,按分支重建因此压缩安全。
|
|
220
|
+
*/
|
|
221
|
+
export const SESSION_ROLE_STATE_ENTRY = "subagent.roles.state";
|
|
222
|
+
/** 运行期角色 → 会话快照条目(平铺收窄字段,读-改-写与角色文件同构)。 */
|
|
223
|
+
export function toSessionRoleRecord(role) {
|
|
224
|
+
const profile = role.profile;
|
|
225
|
+
return {
|
|
226
|
+
id: role.id,
|
|
227
|
+
description: role.description,
|
|
228
|
+
instructions: role.instructions,
|
|
229
|
+
...(profile?.model === undefined ? {} : { model: profile.model }),
|
|
230
|
+
...(profile?.thinkingLevel === undefined ? {} : { thinkingLevel: profile.thinkingLevel }),
|
|
231
|
+
...(profile?.tools === undefined ? {} : { tools: profile.tools }),
|
|
232
|
+
...(profile?.mode === undefined ? {} : { mode: profile.mode }),
|
|
233
|
+
...(profile?.systemPromptAppend === undefined ? {} : { systemPromptAppend: profile.systemPromptAppend }),
|
|
234
|
+
...(role.parentView === true ? { parentView: true } : {}),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* 恢复水合(D-081):从 `getBranchEntries()` 反向扫描最近一条**可解析**的会话角色
|
|
239
|
+
* 快照。快照级损坏(非对象/版本不符/roles 非数组)→ warn 后继续向前找;快照合法但
|
|
240
|
+
* 个别角色非法(id 形状、字段/档案校验不过)→ 丢弃该角色并 warn、保留其余;全部
|
|
241
|
+
* 不可解析 → 空数组。损坏不阻断会话启动。
|
|
242
|
+
*/
|
|
243
|
+
export function readSessionRoles(session, logger) {
|
|
244
|
+
const entries = session?.getBranchEntries() ?? [];
|
|
245
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
246
|
+
const entry = entries[index];
|
|
247
|
+
if (entry?.customType !== SESSION_ROLE_STATE_ENTRY)
|
|
248
|
+
continue;
|
|
249
|
+
const snapshot = entry.data;
|
|
250
|
+
if (!isPlainRecord(snapshot) || snapshot.version !== 1 || !Array.isArray(snapshot.roles)) {
|
|
251
|
+
logger?.warn?.(`session role state entry ${entry.id} is not a readable snapshot (want { version: 1, roles: [...] }); ` +
|
|
252
|
+
"reading the previous snapshot");
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const roles = [];
|
|
256
|
+
for (const [roleIndex, raw] of snapshot.roles.entries()) {
|
|
257
|
+
const origin = `${SESSION_ROLE_STATE_ENTRY}[${roleIndex}]`;
|
|
258
|
+
if (!isPlainRecord(raw) || typeof raw.id !== "string" || !ROLE_ID_PATTERN.test(raw.id)) {
|
|
259
|
+
logger?.warn?.(`${origin} has no valid role id; entry dropped`);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
// id 来自条目本身;其余键原样交给与角色文件同一套校验(未知键照样拒绝)。
|
|
263
|
+
const definition = {};
|
|
264
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
265
|
+
if (key !== "id")
|
|
266
|
+
definition[key] = value;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const role = validateRoleDefinition(raw.id, definition, origin);
|
|
270
|
+
if (role !== undefined)
|
|
271
|
+
roles.push(role);
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
logger?.warn?.(`${origin} (role ${JSON.stringify(raw.id)}) is invalid and was dropped: ${errorText(error)}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return Object.freeze(roles);
|
|
278
|
+
}
|
|
279
|
+
return Object.freeze([]);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* 合并磁盘角色与会话角色(D-081):**磁盘优先**——撞名的会话角色不暴露并 warn(磁盘
|
|
283
|
+
* 角色是用户资产),但其数据仍留在会话文件里;水合按快照顺序把会话角色收进暴露预算
|
|
284
|
+
* (≤16 个、菜单 ≤2000 字符,磁盘+会话合并计算),超出部分不暴露并 warn——历史快照
|
|
285
|
+
* 可能因上限收紧或磁盘角色增加而超限,但不阻断会话启动,也不删除数据。
|
|
286
|
+
*/
|
|
287
|
+
export function mergeSubagentRoles(diskRoles, sessionRoles, logger) {
|
|
288
|
+
// 数据层:快照里的合法会话角色全量保留(同 id 后者覆盖前者,与快照顺序一致)。
|
|
289
|
+
const data = new Map();
|
|
290
|
+
// 视图层:磁盘角色优先,会话角色按快照顺序通过暴露预算。
|
|
291
|
+
const roles = new Map(diskRoles);
|
|
292
|
+
const shadowedByDisk = [];
|
|
293
|
+
const droppedForQuota = [];
|
|
294
|
+
for (const role of sessionRoles) {
|
|
295
|
+
data.set(role.id, role);
|
|
296
|
+
if (diskRoles.has(role.id)) {
|
|
297
|
+
shadowedByDisk.push(role.id);
|
|
298
|
+
logger?.warn?.(`session role "${role.id}" is shadowed by a visible disk role: kept in the session file but not exposed ` +
|
|
299
|
+
"(the disk role wins)");
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const prospectiveChars = roleMenuChars([...roles.values(), role]);
|
|
303
|
+
if (roles.size + 1 > ROLE_COUNT_LIMIT || prospectiveChars > ROLE_MENU_CHARS) {
|
|
304
|
+
droppedForQuota.push(role.id);
|
|
305
|
+
logger?.warn?.(`session role "${role.id}" exceeds the ${ROLE_COUNT_LIMIT}-role / ${ROLE_MENU_CHARS}-char exposure budget: ` +
|
|
306
|
+
"kept in the session file but not exposed");
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
roles.set(role.id, role);
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
sessionRoles: data,
|
|
313
|
+
roles,
|
|
314
|
+
shadowedByDisk: Object.freeze(shadowedByDisk),
|
|
315
|
+
droppedForQuota: Object.freeze(droppedForQuota),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/** 注册点的预算错误文本(undefined = 在预算内):与装载期上限同一常量,附精简建议。 */
|
|
319
|
+
function roleQuotaViolation(roles) {
|
|
320
|
+
if (roles.size > ROLE_COUNT_LIMIT) {
|
|
321
|
+
return (`registering this session role would expose ${roles.size} roles; at most ${ROLE_COUNT_LIMIT} are allowed ` +
|
|
322
|
+
"(the menu rides in the parent's tool description on every request) — remove unused session roles first");
|
|
323
|
+
}
|
|
324
|
+
const chars = roleMenuChars(roles.values());
|
|
325
|
+
if (chars > ROLE_MENU_CHARS) {
|
|
326
|
+
return (`registering this session role would grow the role menu to ${chars} chars; at most ${ROLE_MENU_CHARS} are ` +
|
|
327
|
+
"allowed — shorten descriptions or remove session roles first");
|
|
328
|
+
}
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
205
331
|
/**
|
|
206
332
|
* 解析当前方案的角色目录(**全部来自磁盘**,无编译内定义)。装载期一次性完成,
|
|
207
333
|
* 任何会让"配置了但没生效"的情况都显式失败:
|
|
@@ -268,7 +394,7 @@ export function resolveSubagentRoleCatalog(options) {
|
|
|
268
394
|
throw new Error(`suite associations.roles exposes ${roles.size} roles; at most ${ROLE_COUNT_LIMIT} are allowed — the role menu ` +
|
|
269
395
|
"rides in the parent's tool description on every request, and an unbounded menu degrades the main agent's own context");
|
|
270
396
|
}
|
|
271
|
-
const menuChars =
|
|
397
|
+
const menuChars = roleMenuChars(roles.values());
|
|
272
398
|
if (menuChars > ROLE_MENU_CHARS) {
|
|
273
399
|
throw new Error(`suite associations.roles menu is ${menuChars} chars; at most ${ROLE_MENU_CHARS} are allowed — shorten role ` +
|
|
274
400
|
"descriptions or associate fewer roles (the menu is part of the parent's tool description)");
|
|
@@ -500,9 +626,7 @@ function resolveRole(value, roles) {
|
|
|
500
626
|
}
|
|
501
627
|
const role = roles.get(value);
|
|
502
628
|
if (role === undefined) {
|
|
503
|
-
|
|
504
|
-
throw new SubagentDelegateError("subagent_unknown_role", `unknown subagent role ${JSON.stringify(value)}; available roles: ` +
|
|
505
|
-
(available.length === 0 ? "(none configured)" : available.join(", ")));
|
|
629
|
+
throw new SubagentDelegateError("subagent_unknown_role", unknownRoleMessage(value, roles));
|
|
506
630
|
}
|
|
507
631
|
return role;
|
|
508
632
|
}
|
|
@@ -513,6 +637,11 @@ const RESULT_READ_DEFAULT_MESSAGES = 20;
|
|
|
513
637
|
const RESULT_READ_MAX_MESSAGES = 50;
|
|
514
638
|
/** `subagent_result` 的有界等待上限(后台结算收割),默认 0 = 只查一次。 */
|
|
515
639
|
const RESULT_READ_MAX_WAIT_MS = 120_000;
|
|
640
|
+
/**
|
|
641
|
+
* 每任务随结算投影/信封返回的兄弟流量摘录条数(D-060 裁决 14 父可见性):有界——
|
|
642
|
+
* 这是审计摘录不是消息回灌,全量以信箱审计日志为准。
|
|
643
|
+
*/
|
|
644
|
+
const SIBLING_TRAFFIC_EXCERPTS = 8;
|
|
516
645
|
/** UTF-8 字节数(回读信封如实报告体积)。 */
|
|
517
646
|
function utf8ByteLength(text) {
|
|
518
647
|
return Buffer.byteLength(text, "utf8");
|
|
@@ -546,10 +675,10 @@ function resolveInt(explicit, configured, fallback, min, label) {
|
|
|
546
675
|
* 因此"第二参非 undefined 即输入"的判定会把 context 误当输入(prompts 读成 undefined)。
|
|
547
676
|
* 改按形状判定:第一参是普通对象即取第一参,否则看第二参。
|
|
548
677
|
*/
|
|
549
|
-
function objectInput(first, second) {
|
|
678
|
+
function objectInput(first, second, tool = "subagent_run") {
|
|
550
679
|
const primary = isPlainRecord(first) ? first : second;
|
|
551
680
|
if (!isPlainRecord(primary)) {
|
|
552
|
-
throw new SubagentDelegateError("subagent_invalid_input",
|
|
681
|
+
throw new SubagentDelegateError("subagent_invalid_input", `${tool} input must be an object`);
|
|
553
682
|
}
|
|
554
683
|
return primary;
|
|
555
684
|
}
|
|
@@ -668,14 +797,16 @@ export function createCapability(api, options) {
|
|
|
668
797
|
const agents = api.agents;
|
|
669
798
|
if (!session)
|
|
670
799
|
throw new Error("subagent-delegate requires the session capability");
|
|
671
|
-
//
|
|
672
|
-
|
|
800
|
+
// 通信面配置键已移除(D-060 裁决 15):出现即装载期失败,不留"配置了但没生效"的假象。
|
|
801
|
+
if (api.config.communication !== undefined) {
|
|
802
|
+
throw new Error("subagent-delegate config communication was removed (D-060 ruling 15): communication tools register unconditionally; remove the key");
|
|
803
|
+
}
|
|
673
804
|
const wiring = options?.communicationWiring;
|
|
674
|
-
// 子侧通信面(子→父 / 子↔子):只要宿主接线了 post
|
|
805
|
+
// 子侧通信面(子→父 / 子↔子):只要宿主接线了 post 通道就注册——
|
|
675
806
|
// 叶子会话(无 agents)只拿这一面;获准自建会话(D-060 S5,agents 在场)两面都要:
|
|
676
807
|
// 它既能向下委派(parent face),也仍能向自己的父上抛/收兄弟消息(child face)。
|
|
677
808
|
const childTools = [];
|
|
678
|
-
if (wiring?.postEscalate !== undefined
|
|
809
|
+
if (wiring?.postEscalate !== undefined) {
|
|
679
810
|
childTools.push(api.registerTool({
|
|
680
811
|
name: "subagent_escalate",
|
|
681
812
|
label: "Escalate to parent",
|
|
@@ -703,12 +834,14 @@ export function createCapability(api, options) {
|
|
|
703
834
|
},
|
|
704
835
|
}));
|
|
705
836
|
}
|
|
706
|
-
if (wiring?.sendSibling !== undefined
|
|
837
|
+
if (wiring?.sendSibling !== undefined) {
|
|
707
838
|
childTools.push(api.registerTool({
|
|
708
839
|
name: "subagent_send",
|
|
709
840
|
label: "Message a sibling",
|
|
710
841
|
description: "Send one bounded message to a sibling subagent (a child of the same parent), addressed by its sessionId. " +
|
|
711
|
-
"Cross-family addressing is rejected."
|
|
842
|
+
"Cross-family addressing is rejected. This channel is for data and intermediate artifacts only: task " +
|
|
843
|
+
"assignment and result acceptance stay with the parent agent. Sibling messages are visible to the parent " +
|
|
844
|
+
"(audited as excerpts with task results).",
|
|
712
845
|
parameters: Type.Object({
|
|
713
846
|
to: Type.String({ description: "Recipient child session id" }),
|
|
714
847
|
text: Type.String({
|
|
@@ -735,7 +868,8 @@ export function createCapability(api, options) {
|
|
|
735
868
|
name: "subagent_inbox",
|
|
736
869
|
label: "Read sibling inbox",
|
|
737
870
|
description: "Read and clear the earliest bounded messages your siblings sent you (subagent_send). Messages are " +
|
|
738
|
-
"consumed by this call; poll it when coordination is expected."
|
|
871
|
+
"consumed by this call; poll it when coordination is expected. The sibling channel carries data and " +
|
|
872
|
+
"intermediate artifacts only — assignment and acceptance stay with the parent, which can audit this traffic.",
|
|
739
873
|
parameters: Type.Object({
|
|
740
874
|
limit: Type.Optional(Type.Number({ description: "Max messages to take (1..20, default 10)", minimum: 1, maximum: 20 })),
|
|
741
875
|
}),
|
|
@@ -875,27 +1009,47 @@ export function createCapability(api, options) {
|
|
|
875
1009
|
// 100):对**非 resume** 的每次 submit 计数,超限按 `subagent_child_quota` 拒绝。
|
|
876
1010
|
const maxTotalPerParent = resolveInt(undefined, configInt(api, "maxTotalPerParent", 0), 100, 0, "maxTotalPerParent");
|
|
877
1011
|
let createdSubagentCount = 0;
|
|
878
|
-
//
|
|
879
|
-
//
|
|
880
|
-
|
|
881
|
-
|
|
1012
|
+
// background 配置键已移除(D-060 裁决 15,2026-10-03):后台委派是功能基本面,
|
|
1013
|
+
// 无条件注册(`subagent_run` 的 background 参数与 subagent_cancel / subagent_steer
|
|
1014
|
+
// 随之常在);键出现即装载期失败,不留"配置了但没生效"的假象。
|
|
1015
|
+
if (api.config.background !== undefined) {
|
|
1016
|
+
throw new Error("subagent-delegate config background was removed (D-060 ruling 15): background delegation registers unconditionally; remove the key");
|
|
882
1017
|
}
|
|
883
|
-
const backgroundEnabled = api.config.background === true;
|
|
884
1018
|
// 角色目录 + 方案关联在装载期解析:非法文件/非法关联直接失败,不留"配置了但没生效"的假象。
|
|
885
1019
|
const roleCatalog = options?.roles === undefined
|
|
886
1020
|
? { roles: new Map(), dangling: [], brokenUnassociated: [] }
|
|
887
1021
|
: resolveSubagentRoleCatalog(options.roles);
|
|
888
|
-
const
|
|
1022
|
+
const diskRoles = roleCatalog.roles;
|
|
889
1023
|
for (const id of roleCatalog.dangling) {
|
|
890
1024
|
api.logger?.warn?.(`suite associations.roles lists "${id}" but no role file exists: ${subagentRoleCatalogPath(options?.roles?.agentDir ?? "", id)} (role stays unavailable for this session)`);
|
|
891
1025
|
}
|
|
892
1026
|
for (const id of roleCatalog.brokenUnassociated) {
|
|
893
1027
|
api.logger?.warn?.(`role file for "${id}" is invalid and is not associated by this suite; ignored (fix it or remove the file)`);
|
|
894
1028
|
}
|
|
895
|
-
|
|
1029
|
+
// 会话角色水合(D-081):磁盘目录解析之后从分支快照重建(最近一条可解析快照,
|
|
1030
|
+
// 损坏向前回退并 warn,全部不可解析则为空);磁盘优先、16 个 / 2000 字符菜单
|
|
1031
|
+
// (磁盘+会话合并计算)都是**暴露**预算——未暴露的项保留在数据表里,不删除。
|
|
1032
|
+
const hydratedRoles = mergeSubagentRoles(diskRoles, readSessionRoles(session, api.logger), api.logger);
|
|
1033
|
+
/**
|
|
1034
|
+
* 会话角色数据表 = 快照里的**全部**合法会话角色(含被磁盘遮蔽/未暴露项):
|
|
1035
|
+
* 先 appendEntry 成功再提交,写失败即注册失败(内存与文件不背离)。持久化写它——
|
|
1036
|
+
* 视图收窄(遮蔽/超预算)不抹掉数据。
|
|
1037
|
+
*/
|
|
1038
|
+
const sessionRoles = new Map(hydratedRoles.sessionRoles);
|
|
1039
|
+
/** 可用集 = 磁盘 ∪ 通过暴露预算的会话角色(磁盘优先):resolveRole/菜单/role 枚举只看它。 */
|
|
1040
|
+
const roles = new Map(hydratedRoles.roles);
|
|
1041
|
+
const renderRoleMenu = () => [...roles.values()]
|
|
896
1042
|
.sort((left, right) => (left.id < right.id ? -1 : 1))
|
|
897
1043
|
.map((role) => `${role.id} — ${role.description}`)
|
|
898
1044
|
.join("; ");
|
|
1045
|
+
/** 全量快照落盘(append-only):只存会话角色,绝不把磁盘角色写进条目。 */
|
|
1046
|
+
const persistSessionRoles = (next) => {
|
|
1047
|
+
const snapshot = {
|
|
1048
|
+
version: 1,
|
|
1049
|
+
roles: Object.freeze([...next].map(toSessionRoleRecord)),
|
|
1050
|
+
};
|
|
1051
|
+
session.appendEntry(SESSION_ROLE_STATE_ENTRY, snapshot);
|
|
1052
|
+
};
|
|
899
1053
|
/**
|
|
900
1054
|
* D-044 §5.6:把支持进度面的 adapter 装饰一层,累计"运行中 step"与最近一条
|
|
901
1055
|
* 摘要,供工具 onUpdate 节流上报父 UI(只回计数与摘要,不回正文)。
|
|
@@ -935,50 +1089,14 @@ export function createCapability(api, options) {
|
|
|
935
1089
|
parentSessionId: session.getSessionId(),
|
|
936
1090
|
...(sessionAbortSignal === undefined ? {} : { parentSignal: sessionAbortSignal }),
|
|
937
1091
|
...(limits === undefined ? {} : { limits }),
|
|
1092
|
+
...(api.logger === undefined ? {} : { logger: api.logger }),
|
|
938
1093
|
});
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
description: `1..${maxTasksPerCall} independent prompts run as separate subagent tasks`,
|
|
946
|
-
}),
|
|
947
|
-
context: Type.Optional(Type.Object({
|
|
948
|
-
anchors: Type.Optional(Type.Array(Type.Object({
|
|
949
|
-
path: Type.String({
|
|
950
|
-
minLength: 1,
|
|
951
|
-
maxLength: CONTEXT_PATH_MAX_CHARS,
|
|
952
|
-
description: "File path the child should start from",
|
|
953
|
-
}),
|
|
954
|
-
line: Type.Optional(Type.Integer({ minimum: 1, description: "Line number in path (positive integer)" })),
|
|
955
|
-
note: Type.Optional(Type.String({
|
|
956
|
-
maxLength: CONTEXT_NOTE_MAX_CHARS,
|
|
957
|
-
description: "One-line why-it-matters note",
|
|
958
|
-
})),
|
|
959
|
-
}), { maxItems: CONTEXT_LIST_MAX_ITEMS })),
|
|
960
|
-
failedAttempts: Type.Optional(Type.Array(Type.Object({
|
|
961
|
-
command: Type.String({
|
|
962
|
-
minLength: 1,
|
|
963
|
-
maxLength: CONTEXT_COMMAND_MAX_CHARS,
|
|
964
|
-
description: "Command that was already tried and failed",
|
|
965
|
-
}),
|
|
966
|
-
note: Type.Optional(Type.String({ maxLength: CONTEXT_NOTE_MAX_CHARS, description: "One-line failure note" })),
|
|
967
|
-
}), { maxItems: CONTEXT_LIST_MAX_ITEMS })),
|
|
968
|
-
}, {
|
|
969
|
-
description: "Parent pointers rendered as a header block into each child prompt: file:line anchors and commands " +
|
|
970
|
-
"already tried and failed. Pointers only — children still read the real files.",
|
|
971
|
-
})),
|
|
972
|
-
...(roles.size === 0
|
|
973
|
-
? {}
|
|
974
|
-
: {
|
|
975
|
-
role: Type.Optional(Type.String({
|
|
976
|
-
description: `Named subagent role applied to every prompt in this call; one of: ${[...roles.keys()].sort().join(", ")}. ` +
|
|
977
|
-
"Omit for a generic task. Roles frame the task; they do not change the child's model or tools.",
|
|
978
|
-
})),
|
|
979
|
-
}),
|
|
980
|
-
});
|
|
981
|
-
const tool = api.registerTool({
|
|
1094
|
+
/**
|
|
1095
|
+
* `subagent_run` 的定义按**当前角色表**现场构建(D-081 即时生效):set/remove 之后
|
|
1096
|
+
* 以同 owner 同名重注册(runtime 语义 = 更新)让新菜单与 `role` 参数枚举在父会话的
|
|
1097
|
+
* 下一次模型请求即可见,不需要重启或新建会话。
|
|
1098
|
+
*/
|
|
1099
|
+
const buildRunToolDefinition = () => ({
|
|
982
1100
|
name: "subagent_run",
|
|
983
1101
|
label: "Run subagents",
|
|
984
1102
|
description: `Delegate 1..${maxTasksPerCall} independent subagent tasks in one call and return their bounded terminal results. ` +
|
|
@@ -986,161 +1104,509 @@ export function createCapability(api, options) {
|
|
|
986
1104
|
"final-assistant summary come back; ask a child for more detail in its prompt if you need it." +
|
|
987
1105
|
" Optionally pass `context` — file:line anchors and commands already tried and failed are rendered as a " +
|
|
988
1106
|
"header block at the top of each child prompt (pointers only)." +
|
|
1107
|
+
// fork-join 推荐模式(D-060 裁决 14):大任务拆并行块 + 最终组合任务,中间产物
|
|
1108
|
+
// 留子层、主上下文不膨胀;通知粒度——结算必然回注但只有组合任务的报告需验收。
|
|
1109
|
+
" For large splittable work prefer fork-join: launch the parallel chunk tasks with background:true, then submit " +
|
|
1110
|
+
"one final assembly task (a separate background call) that gathers the chunks with subagent_result({taskId, waitMs}) " +
|
|
1111
|
+
"— intermediate results stay in the child layer and only the assembled final report enters your context. Every " +
|
|
1112
|
+
"task's settlement is announced to you (agent-task-settled), but notifications are not per-task acceptances: " +
|
|
1113
|
+
"accept and act on (todos/goals/next steps) only the final assembly task's report; other settled lines are " +
|
|
1114
|
+
"status only." +
|
|
989
1115
|
(roles.size === 0
|
|
990
1116
|
? ""
|
|
991
|
-
: ` Configured subagent roles (pass \`role\`): ${
|
|
992
|
-
"
|
|
993
|
-
parameters: {
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1117
|
+
: ` Configured subagent roles (pass \`role\`): ${renderRoleMenu()}. A role's instructions frame every task in the call; ` +
|
|
1118
|
+
"a role with a narrowing profile also applies its tools/mode/model/thinkingLevel to the child, verified after creation."),
|
|
1119
|
+
parameters: Type.Object({
|
|
1120
|
+
prompts: Type.Array(Type.String({
|
|
1121
|
+
minLength: 1,
|
|
1122
|
+
description: "Self-contained prompt for one subagent task; the child session sees no parent history",
|
|
1123
|
+
}), {
|
|
1124
|
+
minItems: 1,
|
|
1125
|
+
description: `1..${maxTasksPerCall} independent prompts run as separate subagent tasks`,
|
|
1126
|
+
}),
|
|
1127
|
+
context: Type.Optional(Type.Object({
|
|
1128
|
+
anchors: Type.Optional(Type.Array(Type.Object({
|
|
1129
|
+
path: Type.String({
|
|
1130
|
+
minLength: 1,
|
|
1131
|
+
maxLength: CONTEXT_PATH_MAX_CHARS,
|
|
1132
|
+
description: "File path the child should start from",
|
|
1133
|
+
}),
|
|
1134
|
+
line: Type.Optional(Type.Integer({ minimum: 1, description: "Line number in path (positive integer)" })),
|
|
1135
|
+
note: Type.Optional(Type.String({
|
|
1136
|
+
maxLength: CONTEXT_NOTE_MAX_CHARS,
|
|
1137
|
+
description: "One-line why-it-matters note",
|
|
1138
|
+
})),
|
|
1139
|
+
}), { maxItems: CONTEXT_LIST_MAX_ITEMS })),
|
|
1140
|
+
failedAttempts: Type.Optional(Type.Array(Type.Object({
|
|
1141
|
+
command: Type.String({
|
|
1142
|
+
minLength: 1,
|
|
1143
|
+
maxLength: CONTEXT_COMMAND_MAX_CHARS,
|
|
1144
|
+
description: "Command that was already tried and failed",
|
|
1145
|
+
}),
|
|
1146
|
+
note: Type.Optional(Type.String({ maxLength: CONTEXT_NOTE_MAX_CHARS, description: "One-line failure note" })),
|
|
1147
|
+
}), { maxItems: CONTEXT_LIST_MAX_ITEMS })),
|
|
1148
|
+
}, {
|
|
1149
|
+
description: "Parent pointers rendered as a header block into each child prompt: file:line anchors and commands " +
|
|
1150
|
+
"already tried and failed. Pointers only — children still read the real files.",
|
|
1151
|
+
})),
|
|
1152
|
+
...(roles.size === 0
|
|
1153
|
+
? {}
|
|
1154
|
+
: {
|
|
1155
|
+
role: Type.Optional(Type.String({
|
|
1156
|
+
description: `Named subagent role applied to every prompt in this call; one of: ${[...roles.keys()].sort().join(", ")}. ` +
|
|
1157
|
+
"Omit for a generic task. Roles frame the task; a role may also narrow the child's " +
|
|
1158
|
+
"tools/mode/model (verified after creation).",
|
|
1000
1159
|
})),
|
|
1160
|
+
}),
|
|
1161
|
+
// background(D-060 S6)在 properties 内组合:此前的 `{...parameters, background}`
|
|
1162
|
+
// 顶层展开把键放在 properties 之外,模型面 schema(anthropic 只取 schema.properties、
|
|
1163
|
+
// openai 原样透传)从未真正声明该参数——D-060 裁决 13 翻转后随默认注册修正,
|
|
1164
|
+
// 裁决 15 起开关形态退役、参数无条件声明。
|
|
1165
|
+
background: Type.Optional(Type.Boolean({
|
|
1166
|
+
description: "Submit without waiting: return task tickets immediately and keep working; settled results are " +
|
|
1167
|
+
"announced at your next model round and can be read with subagent_result (optionally waiting).",
|
|
1168
|
+
})),
|
|
1169
|
+
}),
|
|
1170
|
+
execute: executeSubagentRun,
|
|
1171
|
+
});
|
|
1172
|
+
const executeSubagentRun = async (first, second, _signal, onUpdate) => {
|
|
1173
|
+
const input = objectInput(first, second);
|
|
1174
|
+
const prompts = readPrompts(input.prompts, maxTasksPerCall);
|
|
1175
|
+
const role = resolveRole(input.role, roles);
|
|
1176
|
+
// context 注入(设计 §2.3):形状/边界违规在此整体拒绝;两个列表皆空归一化为
|
|
1177
|
+
// undefined,后续路径与 context 缺省完全一致。
|
|
1178
|
+
const contextBlock = renderDelegateContext(parseDelegateContext(input.context));
|
|
1179
|
+
const background = input.background === true;
|
|
1180
|
+
// 嵌套深度硬限(退役切片 2,Option A):沿 S2 血缘头回溯本会话层级,已达
|
|
1181
|
+
// maxNestedDepth 层时整次调用结构化拒绝——递归的兜底边界(不依赖模型自觉)。
|
|
1182
|
+
// resume 不涉深度(S5 裁定:续写既有会话不计新层级,走 subagent_resume)。
|
|
1183
|
+
// agentDir 单源(D-075 S4 第三批):显式 options 优先,缺省回落 api.host.agentDir。
|
|
1184
|
+
const agentDir = options?.agentDir ?? api.host.agentDir ?? "";
|
|
1185
|
+
if (agentDir !== "") {
|
|
1186
|
+
const depth = lineageDepth(session.getSessionId(), agentDir, maxNestedDepth + 2);
|
|
1187
|
+
if (depth !== undefined && depth + 1 > maxNestedDepth) {
|
|
1188
|
+
throw new SubagentDelegateError("subagent_depth_exceeded", `this session already sits at nested depth ${depth}; delegation beyond maxNestedDepth ${maxNestedDepth} is refused`);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
// submit 是同步抛出契约(预算/队列/已释放),逐条捕获为条目级 rejected;
|
|
1192
|
+
// 全部提交完成后再逐条 drain,任务因此并行执行。
|
|
1193
|
+
const pending = [];
|
|
1194
|
+
const tasks = new Array(prompts.length).fill(undefined);
|
|
1195
|
+
// 回合级血缘(第 3 期第二批):提交动作发生在父回合工具阶段,此时 correlationId
|
|
1196
|
+
// 有值;getter 缺席或返回 undefined 时不写键(行为回退)。仅 subagent_run 新建
|
|
1197
|
+
// 路径携带——resume 打开既有会话、头定格,写键即"要求了但不生效"。
|
|
1198
|
+
const turnCorrelationId = options?.getTurnCorrelationId?.();
|
|
1199
|
+
for (let index = 0; index < prompts.length; index += 1) {
|
|
1200
|
+
const prompt = prompts[index];
|
|
1201
|
+
// 总量硬限(承载退役切片 2,Option A):本会话累计创建数达上限时按结构化
|
|
1202
|
+
// 码拒绝该条目(同批其余条目继续)。深度硬限在 execute 开头沿 S2 血缘头
|
|
1203
|
+
// 整调用拒绝(见上),不在此重复。
|
|
1204
|
+
if (createdSubagentCount >= maxTotalPerParent) {
|
|
1205
|
+
tasks[index] = {
|
|
1206
|
+
taskId: "",
|
|
1207
|
+
status: "rejected",
|
|
1208
|
+
reasonCode: "subagent_child_quota",
|
|
1209
|
+
reason: `this session already created ${createdSubagentCount} child sessions (cap ${maxTotalPerParent})`,
|
|
1210
|
+
messageCount: 0,
|
|
1211
|
+
};
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
createdSubagentCount += 1;
|
|
1215
|
+
// 首条 prompt 组装(角色框定 + 可选父投影);context 头部块(有则)在最上、
|
|
1216
|
+
// 与后续内容空一行,无块时与现状逐字节一致。字节数仍走 submit 侧 maxPromptBytes
|
|
1217
|
+
// 预算,超限按预算拒绝而不是静默截断。
|
|
1218
|
+
const composed = role === undefined
|
|
1219
|
+
? prompt
|
|
1220
|
+
: role.parentView === true
|
|
1221
|
+
? `${composeRolePrompt(role, prompt)}\n\n${composeParentView(session, session.getSessionId()) ?? ""}`.trim()
|
|
1222
|
+
: composeRolePrompt(role, prompt);
|
|
1223
|
+
try {
|
|
1224
|
+
pending.push({
|
|
1225
|
+
index,
|
|
1226
|
+
handle: runtime.submit({
|
|
1227
|
+
prompt: contextBlock === undefined ? composed : `${contextBlock}\n\n${composed}`,
|
|
1228
|
+
// 血缘(D-060 S1):父会话 id 随 metadata 过线,宿主写进子会话文件头。
|
|
1229
|
+
// 角色存在时 child profile 恒定携带(档案基线含 roleId,无收窄字段的角色
|
|
1230
|
+
// 同样有血缘——/agents 与 subagent_list/subagent_result 的角色血缘对全部
|
|
1231
|
+
// 角色生效);`role.profile ?? { version: 1, roleId }` 是防御兜底——对
|
|
1232
|
+
// validateRoleDefinition 产出的角色不可达;回归测试钉住该行为。
|
|
1233
|
+
metadata: {
|
|
1234
|
+
[AGENT_TASK_PARENT_SESSION_KEY]: session.getSessionId(),
|
|
1235
|
+
...(turnCorrelationId === undefined ? {} : { [AGENT_TASK_PARENT_TRACE_KEY]: turnCorrelationId }),
|
|
1236
|
+
...(role === undefined
|
|
1237
|
+
? {}
|
|
1238
|
+
: {
|
|
1239
|
+
[AGENT_TASK_CHILD_PROFILE_KEY]: role.profile ?? { version: 1, roleId: role.id },
|
|
1240
|
+
}),
|
|
1241
|
+
},
|
|
1242
|
+
}),
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
catch (error) {
|
|
1246
|
+
tasks[index] = {
|
|
1247
|
+
taskId: "",
|
|
1248
|
+
status: "rejected",
|
|
1249
|
+
reasonCode: reasonCodeOf(error),
|
|
1250
|
+
reason: errorText(error),
|
|
1251
|
+
messageCount: 0,
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
// 后台委派(D-060 S6,配置门默认关):提交即返回票据,不 drain——任务挂在
|
|
1256
|
+
// 调度器上继续跑(会话级生命周期,父栅栏/会话销毁统一回收)。每个后台任务
|
|
1257
|
+
// 挂一个结算观察器:结算一行有界 JSON 进宿主 sink,随父的下一次模型轮次注入。
|
|
1258
|
+
if (background === true) {
|
|
1259
|
+
const tickets = [];
|
|
1260
|
+
for (const item of pending) {
|
|
1261
|
+
try {
|
|
1262
|
+
const handle = await item.handle;
|
|
1263
|
+
tickets.push({ taskId: handle.id });
|
|
1264
|
+
const sink = options?.backgroundDigestSink;
|
|
1265
|
+
if (sink !== undefined) {
|
|
1266
|
+
void handle
|
|
1267
|
+
.result()
|
|
1268
|
+
.then((record) => {
|
|
1269
|
+
sink(JSON.stringify({
|
|
1270
|
+
taskId: record.taskId,
|
|
1271
|
+
sessionId: record.sessionId,
|
|
1272
|
+
status: record.status,
|
|
1273
|
+
...(record.reasonCode === undefined ? {} : { reasonCode: record.reasonCode }),
|
|
1274
|
+
}));
|
|
1275
|
+
})
|
|
1276
|
+
.catch(() => {
|
|
1277
|
+
// 摘要观察失败不影响任务本身(终态已在调度器落库可查)。
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1001
1280
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
execute: async (first, second, _signal, onUpdate) => {
|
|
1005
|
-
const input = objectInput(first, second);
|
|
1006
|
-
const prompts = readPrompts(input.prompts, maxTasksPerCall);
|
|
1007
|
-
const role = resolveRole(input.role, roles);
|
|
1008
|
-
// context 注入(设计 §2.3):形状/边界违规在此整体拒绝;两个列表皆空归一化为
|
|
1009
|
-
// undefined,后续路径与 context 缺省完全一致。
|
|
1010
|
-
const contextBlock = renderDelegateContext(parseDelegateContext(input.context));
|
|
1011
|
-
const background = backgroundEnabled && input.background === true;
|
|
1012
|
-
// 嵌套深度硬限(退役切片 2,Option A):沿 S2 血缘头回溯本会话层级,已达
|
|
1013
|
-
// maxNestedDepth 层时整次调用结构化拒绝——递归的兜底边界(不依赖模型自觉)。
|
|
1014
|
-
// resume 不涉深度(S5 裁定:续写既有会话不计新层级,走 subagent_resume)。
|
|
1015
|
-
// agentDir 单源(D-075 S4 第三批):显式 options 优先,缺省回落 api.host.agentDir。
|
|
1016
|
-
const agentDir = options?.agentDir ?? api.host.agentDir ?? "";
|
|
1017
|
-
if (agentDir !== "") {
|
|
1018
|
-
const depth = lineageDepth(session.getSessionId(), agentDir, maxNestedDepth + 2);
|
|
1019
|
-
if (depth !== undefined && depth + 1 > maxNestedDepth) {
|
|
1020
|
-
throw new SubagentDelegateError("subagent_depth_exceeded", `this session already sits at nested depth ${depth}; delegation beyond maxNestedDepth ${maxNestedDepth} is refused`);
|
|
1281
|
+
catch {
|
|
1282
|
+
// submit 同步抛错已在上面捕获为条目级 rejected——后台票据只收成功提交。
|
|
1021
1283
|
}
|
|
1022
1284
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
//
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1285
|
+
return { background: true, tasks: tickets };
|
|
1286
|
+
}
|
|
1287
|
+
let completed = tasks.filter((task) => task !== undefined).length;
|
|
1288
|
+
for (const item of pending) {
|
|
1289
|
+
let taskId = "";
|
|
1290
|
+
try {
|
|
1291
|
+
const handle = await item.handle;
|
|
1292
|
+
taskId = handle.id;
|
|
1293
|
+
const record = await handle.drain({ timeoutMs: taskTimeoutMs });
|
|
1294
|
+
// escalate 上抛(D-060 S4):随任务投影一起交回父模型(取后即清)。
|
|
1295
|
+
const escalations = wiring?.drainEscalations?.(record.sessionId) ?? [];
|
|
1296
|
+
// 兄弟流量摘录(D-060 裁决 14):只读审计面,随同一信封对父可见。
|
|
1297
|
+
const siblingTraffic = wiring?.readSiblingTraffic?.(record.sessionId, SIBLING_TRAFFIC_EXCERPTS) ?? [];
|
|
1298
|
+
tasks[item.index] = {
|
|
1299
|
+
...projectRecord(record),
|
|
1300
|
+
...(escalations.length === 0 ? {} : { escalations }),
|
|
1301
|
+
...(siblingTraffic.length === 0 ? {} : { siblingTraffic }),
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
catch (error) {
|
|
1305
|
+
// drain 抛错(如持久化失败):保留原始 message,不静默吞掉。
|
|
1306
|
+
tasks[item.index] = { taskId, status: "failed", reason: errorText(error), messageCount: 0 };
|
|
1307
|
+
}
|
|
1308
|
+
completed += 1;
|
|
1309
|
+
// D-044 §5.6:进度随任务完成节流上报(累计 step 数 + 最近一条回合摘要);
|
|
1310
|
+
// 只回摘要,不回模型正文。
|
|
1311
|
+
const progressSuffix = progressState.steps === 0
|
|
1312
|
+
? ""
|
|
1313
|
+
: ` · steps ${progressState.steps}${progressState.last === undefined ? "" : ` · ${progressState.last}`}`;
|
|
1314
|
+
onUpdate?.({
|
|
1315
|
+
content: [{ type: "text", text: `subagent ${completed}/${prompts.length}${progressSuffix}` }],
|
|
1316
|
+
details: {},
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
return { tasks: tasks.filter((task) => task !== undefined) };
|
|
1320
|
+
};
|
|
1321
|
+
/** `subagent_run` 的注册句柄:set/remove 后刷新替换,释放时以最新句柄为准。 */
|
|
1322
|
+
const runTool = { current: api.registerTool(buildRunToolDefinition()) };
|
|
1323
|
+
/** 同 owner 同名重注册 = 更新(旧句柄 dispose 因表项已被接手而失效)。 */
|
|
1324
|
+
const refreshRunTool = () => {
|
|
1325
|
+
runTool.current = api.registerTool(buildRunToolDefinition());
|
|
1326
|
+
};
|
|
1327
|
+
/**
|
|
1328
|
+
* 会话角色三工具(D-081):注册/列出/删除。注册是"先落盘再提交内存"——持久化条目
|
|
1329
|
+
* 写在会话文件里(customType `subagent.roles.state`),下次打开同一会话仍在;会话
|
|
1330
|
+
* 角色不写 `<agentDir>/roles/`、不跨会话共享、子代理不继承。三工具未声明 readonlySafe,
|
|
1331
|
+
* readonly 会话沿用既有"未声明即不激活"语义,不做特殊分支。
|
|
1332
|
+
*/
|
|
1333
|
+
const roleSetTool = api.registerTool({
|
|
1334
|
+
name: "subagent_role_set",
|
|
1335
|
+
label: "Register a session subagent role",
|
|
1336
|
+
description: "Register or update a session-scoped subagent role and use it by name in subagent_run. The role is persisted " +
|
|
1337
|
+
"in this session file, so it is still available when this session is opened again; it is not written to the " +
|
|
1338
|
+
"shared roles directory, not shared with other sessions, and not inherited by child subagents. Use it when the " +
|
|
1339
|
+
"built-in roles are too coarse for the task at hand. id, description and instructions are required; the optional " +
|
|
1340
|
+
"fields narrow the child the same way a role file does (tools/mode/model/thinkingLevel/systemPromptAppend) and " +
|
|
1341
|
+
"parentView adds a bounded parent projection. Same id updates that session role; the change is visible to the " +
|
|
1342
|
+
"next subagent_run call without restarting anything.",
|
|
1343
|
+
parameters: Type.Object({
|
|
1344
|
+
id: Type.String({
|
|
1345
|
+
pattern: "^[a-z0-9][a-z0-9_-]{0,31}$",
|
|
1346
|
+
description: "Lowercase role id (letters, digits, - and _, at most 32 chars)",
|
|
1347
|
+
}),
|
|
1348
|
+
description: Type.String({
|
|
1349
|
+
minLength: 1,
|
|
1350
|
+
maxLength: ROLE_DESCRIPTION_CHARS,
|
|
1351
|
+
description: `One-line role description shown in the subagent_run role menu (at most ${ROLE_DESCRIPTION_CHARS} chars)`,
|
|
1352
|
+
}),
|
|
1353
|
+
instructions: Type.String({
|
|
1354
|
+
minLength: 1,
|
|
1355
|
+
maxLength: ROLE_INSTRUCTIONS_CHARS,
|
|
1356
|
+
description: `Role instructions framing every task in a call (at most ${ROLE_INSTRUCTIONS_CHARS} chars)`,
|
|
1357
|
+
}),
|
|
1358
|
+
model: Type.Optional(Type.String({
|
|
1359
|
+
description: "Narrow the child to this model id (must resolve within this session's scoped models)",
|
|
1360
|
+
})),
|
|
1361
|
+
thinkingLevel: Type.Optional(Type.String({
|
|
1362
|
+
description: "Narrow the child thinking level: minimal | low | medium | high | xhigh | max",
|
|
1363
|
+
})),
|
|
1364
|
+
tools: Type.Optional(Type.Array(Type.String(), {
|
|
1365
|
+
description: "Tool allowlist intersected with the inherited tool set ([] gives the child no tools)",
|
|
1366
|
+
})),
|
|
1367
|
+
mode: Type.Optional(Type.Union([Type.Literal("readonly"), Type.Literal("full-control")], {
|
|
1368
|
+
description: "Narrow-only session mode: readonly never widens a readonly parent",
|
|
1369
|
+
})),
|
|
1370
|
+
systemPromptAppend: Type.Optional(Type.String({ description: "Text appended to the child system prompt (never replaces inherited content)" })),
|
|
1371
|
+
parentView: Type.Optional(Type.Boolean({
|
|
1372
|
+
description: "Give the child a bounded projection of the parent goal/tools (parent history stays hidden)",
|
|
1373
|
+
})),
|
|
1374
|
+
}),
|
|
1375
|
+
execute: async (first, second) => {
|
|
1376
|
+
const input = objectInput(first, second, "subagent_role_set");
|
|
1377
|
+
const id = input.id;
|
|
1378
|
+
if (typeof id !== "string" || !ROLE_ID_PATTERN.test(id)) {
|
|
1379
|
+
throw new SubagentDelegateError("subagent_invalid_input", `subagent_role_set id must match ${String(ROLE_ID_PATTERN)} (lowercase letters, digits, "-" and "_", ` +
|
|
1380
|
+
`at most 32 chars), got ${JSON.stringify(id)}`);
|
|
1381
|
+
}
|
|
1382
|
+
for (const key of Object.keys(input)) {
|
|
1383
|
+
if (key === "id" || ROLE_KNOWN_KEYS.includes(key))
|
|
1044
1384
|
continue;
|
|
1385
|
+
throw new SubagentDelegateError("subagent_invalid_input", `subagent_role_set has unknown parameter ${JSON.stringify(key)} (known: id, ${ROLE_KNOWN_KEYS.join(", ")})`);
|
|
1386
|
+
}
|
|
1387
|
+
const definition = {};
|
|
1388
|
+
for (const key of ROLE_KNOWN_KEYS) {
|
|
1389
|
+
if (input[key] !== undefined)
|
|
1390
|
+
definition[key] = input[key];
|
|
1391
|
+
}
|
|
1392
|
+
let role;
|
|
1393
|
+
try {
|
|
1394
|
+
role = validateRoleDefinition(id, definition, `subagent_role_set "${id}"`);
|
|
1395
|
+
}
|
|
1396
|
+
catch (error) {
|
|
1397
|
+
throw new SubagentDelegateError("subagent_invalid_input", errorText(error));
|
|
1398
|
+
}
|
|
1399
|
+
if (role === undefined) {
|
|
1400
|
+
throw new SubagentDelegateError("subagent_invalid_input", `subagent_role_set could not build role ${JSON.stringify(id)}`);
|
|
1401
|
+
}
|
|
1402
|
+
// 磁盘角色是用户资产:与**当前可见**磁盘角色撞名的新注册结构化拒绝,不隐式改写
|
|
1403
|
+
// 其含义。已有会话数据条目(装载期被磁盘遮蔽的影子角色)走正常 upsert 路径:
|
|
1404
|
+
// 更新的是会话文件里的数据,视图仍由磁盘角色当值。
|
|
1405
|
+
if (diskRoles.has(id) && !sessionRoles.has(id)) {
|
|
1406
|
+
throw new SubagentDelegateError("subagent_role_conflict", `subagent role ${JSON.stringify(id)} is a disk role declared by this suite; a session role cannot shadow it — ` +
|
|
1407
|
+
"pick another id or edit the role file");
|
|
1408
|
+
}
|
|
1409
|
+
// 预算检查(暴露预算:可用集 + 新角色,磁盘+会话合并计算):超限结构化拒绝并提示精简。
|
|
1410
|
+
const prospective = new Map(roles);
|
|
1411
|
+
prospective.set(id, role);
|
|
1412
|
+
const quota = roleQuotaViolation(prospective);
|
|
1413
|
+
if (quota !== undefined)
|
|
1414
|
+
throw new SubagentDelegateError("subagent_role_quota", quota);
|
|
1415
|
+
// 写入顺序:先 appendEntry(失败原样抛出,注册即失败),成功后才提交内存——
|
|
1416
|
+
// 内存与文件不背离。快照是 append-only 全量形态,写的是**数据表**(含未暴露项,
|
|
1417
|
+
// 视图收窄不抹掉数据)。
|
|
1418
|
+
const status = sessionRoles.has(id) ? "updated" : "created";
|
|
1419
|
+
const nextSessionRoles = new Map(sessionRoles);
|
|
1420
|
+
nextSessionRoles.set(id, role);
|
|
1421
|
+
persistSessionRoles(nextSessionRoles.values());
|
|
1422
|
+
sessionRoles.set(id, role);
|
|
1423
|
+
// 视图提交:磁盘角色优先——影子 id 只更新数据,不改变可用集。
|
|
1424
|
+
if (!diskRoles.has(id))
|
|
1425
|
+
roles.set(id, role);
|
|
1426
|
+
refreshRunTool();
|
|
1427
|
+
return { id, status, roles: [...roles.keys()].sort() };
|
|
1428
|
+
},
|
|
1429
|
+
});
|
|
1430
|
+
const roleListTool = api.registerTool({
|
|
1431
|
+
name: "subagent_role_list",
|
|
1432
|
+
label: "List subagent roles",
|
|
1433
|
+
description: "List the subagent roles available in this session: id, origin (disk = shared role file, session = registered " +
|
|
1434
|
+
"here with subagent_role_set), description, and whether the role narrows the child (hasProfile) or enables the " +
|
|
1435
|
+
"bounded parent projection (hasParentView). Pass an id to get one role's full definition, including its " +
|
|
1436
|
+
"instructions and narrowing fields, to read, modify and re-register with subagent_role_set.",
|
|
1437
|
+
parameters: Type.Object({
|
|
1438
|
+
id: Type.Optional(Type.String({ description: "Role id; omit to list every available role" })),
|
|
1439
|
+
}),
|
|
1440
|
+
execute: async (first, second) => {
|
|
1441
|
+
const input = objectInput(first, second, "subagent_role_list");
|
|
1442
|
+
const requested = input.id;
|
|
1443
|
+
if (requested !== undefined && (typeof requested !== "string" || requested.trim() === "")) {
|
|
1444
|
+
throw new SubagentDelegateError("subagent_invalid_input", "subagent_role_list id must be a non-empty string when present");
|
|
1445
|
+
}
|
|
1446
|
+
const originOf = (roleId) => (diskRoles.has(roleId) ? "disk" : "session");
|
|
1447
|
+
if (requested === undefined) {
|
|
1448
|
+
const entries = [...roles.values()]
|
|
1449
|
+
.sort((left, right) => (left.id < right.id ? -1 : 1))
|
|
1450
|
+
.map((role) => ({
|
|
1451
|
+
id: role.id,
|
|
1452
|
+
origin: originOf(role.id),
|
|
1453
|
+
description: role.description,
|
|
1454
|
+
hasProfile: hasNarrowingProfile(role),
|
|
1455
|
+
hasParentView: role.parentView === true,
|
|
1456
|
+
}));
|
|
1457
|
+
return { roles: entries };
|
|
1458
|
+
}
|
|
1459
|
+
const role = roles.get(requested);
|
|
1460
|
+
if (role === undefined) {
|
|
1461
|
+
throw new SubagentDelegateError("subagent_unknown_role", unknownRoleMessage(requested, roles));
|
|
1462
|
+
}
|
|
1463
|
+
return { role: { ...toSessionRoleRecord(role), origin: originOf(role.id) } };
|
|
1464
|
+
},
|
|
1465
|
+
});
|
|
1466
|
+
const roleRemoveTool = api.registerTool({
|
|
1467
|
+
name: "subagent_role_remove",
|
|
1468
|
+
label: "Remove a session subagent role",
|
|
1469
|
+
description: "Remove one session-scoped subagent role (one registered with subagent_role_set) from this session. The removal " +
|
|
1470
|
+
"is persisted in the session file and takes effect for the next subagent_run call. Disk roles cannot be removed " +
|
|
1471
|
+
"here — they live in the shared roles directory and are changed by editing their file; such a call is rejected " +
|
|
1472
|
+
"with a structured error.",
|
|
1473
|
+
parameters: Type.Object({
|
|
1474
|
+
id: Type.String({ description: "Session role id to remove" }),
|
|
1475
|
+
}),
|
|
1476
|
+
execute: async (first, second) => {
|
|
1477
|
+
const input = objectInput(first, second, "subagent_role_remove");
|
|
1478
|
+
const id = input.id;
|
|
1479
|
+
if (typeof id !== "string" || id.trim() === "") {
|
|
1480
|
+
throw new SubagentDelegateError("subagent_invalid_input", "subagent_role_remove requires a role id");
|
|
1481
|
+
}
|
|
1482
|
+
// 会话数据条目优先:被磁盘遮蔽的影子角色也能删(删的是会话文件里的数据,
|
|
1483
|
+
// 磁盘角色本身不受影响);没有会话数据时才按"纯磁盘角色"结构化拒绝。
|
|
1484
|
+
if (!sessionRoles.has(id)) {
|
|
1485
|
+
if (diskRoles.has(id)) {
|
|
1486
|
+
throw new SubagentDelegateError("subagent_role_conflict", `subagent role ${JSON.stringify(id)} is a disk role; disk roles are changed by editing their file, ` +
|
|
1487
|
+
"not removed from a session");
|
|
1045
1488
|
}
|
|
1046
|
-
|
|
1047
|
-
// 首条 prompt 组装(角色框定 + 可选父投影);context 头部块(有则)在最上、
|
|
1048
|
-
// 与后续内容空一行,无块时与现状逐字节一致。字节数仍走 submit 侧 maxPromptBytes
|
|
1049
|
-
// 预算,超限按预算拒绝而不是静默截断。
|
|
1050
|
-
const composed = role === undefined
|
|
1051
|
-
? prompt
|
|
1052
|
-
: role.parentView === true
|
|
1053
|
-
? `${composeRolePrompt(role, prompt)}\n\n${composeParentView(session, session.getSessionId()) ?? ""}`.trim()
|
|
1054
|
-
: composeRolePrompt(role, prompt);
|
|
1055
|
-
try {
|
|
1056
|
-
pending.push({
|
|
1057
|
-
index,
|
|
1058
|
-
handle: runtime.submit({
|
|
1059
|
-
prompt: contextBlock === undefined ? composed : `${contextBlock}\n\n${composed}`,
|
|
1060
|
-
// 血缘(D-060 S1):父会话 id 随 metadata 过线,宿主写进子会话文件头;
|
|
1061
|
-
// 角色档案(若角色定义带收窄)一并带上。
|
|
1062
|
-
metadata: {
|
|
1063
|
-
[AGENT_TASK_PARENT_SESSION_KEY]: session.getSessionId(),
|
|
1064
|
-
...(turnCorrelationId === undefined
|
|
1065
|
-
? {}
|
|
1066
|
-
: { [AGENT_TASK_PARENT_TRACE_KEY]: turnCorrelationId }),
|
|
1067
|
-
...(role?.profile === undefined ? {} : { [AGENT_TASK_CHILD_PROFILE_KEY]: role.profile }),
|
|
1068
|
-
},
|
|
1069
|
-
}),
|
|
1070
|
-
});
|
|
1071
|
-
}
|
|
1072
|
-
catch (error) {
|
|
1073
|
-
tasks[index] = {
|
|
1074
|
-
taskId: "",
|
|
1075
|
-
status: "rejected",
|
|
1076
|
-
reasonCode: reasonCodeOf(error),
|
|
1077
|
-
reason: errorText(error),
|
|
1078
|
-
messageCount: 0,
|
|
1079
|
-
};
|
|
1080
|
-
}
|
|
1489
|
+
throw new SubagentDelegateError("subagent_unknown_role", unknownRoleMessage(id, roles));
|
|
1081
1490
|
}
|
|
1082
|
-
//
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1491
|
+
// 同样的写入顺序:先全量快照落盘,成功后才提交内存与刷新工具面。
|
|
1492
|
+
const nextSessionRoles = new Map(sessionRoles);
|
|
1493
|
+
nextSessionRoles.delete(id);
|
|
1494
|
+
persistSessionRoles(nextSessionRoles.values());
|
|
1495
|
+
sessionRoles.delete(id);
|
|
1496
|
+
// 视图提交:磁盘角色优先——删掉影子数据后磁盘角色仍是可用集的当值项。
|
|
1497
|
+
if (!diskRoles.has(id))
|
|
1498
|
+
roles.delete(id);
|
|
1499
|
+
refreshRunTool();
|
|
1500
|
+
return { id, status: "removed", roles: [...roles.keys()].sort() };
|
|
1501
|
+
},
|
|
1502
|
+
});
|
|
1503
|
+
/**
|
|
1504
|
+
* `/roles` 视图(D-081 用户面):数据源只有上面的三个闭包表(实时值)——可用集
|
|
1505
|
+
* `roles`(按来源分区,磁盘 `diskRoles` 优先)、数据表 `sessionRoles`(诊断里
|
|
1506
|
+
* "数据仍在会话文件里"的判据)、水合诊断 `hydratedRoles` 与目录解析诊断
|
|
1507
|
+
* `roleCatalog.dangling`。分区/诊断行仅在非空时出现,列表按 id 排序。
|
|
1508
|
+
*/
|
|
1509
|
+
const renderRolesOverview = () => {
|
|
1510
|
+
// roles.agentDir 是角色目录(<agentDir>/roles/)的来源;没有它就没有磁盘角色面,
|
|
1511
|
+
// 路径提示也就无从谈起(未见关联的会话不引导用户去丢文件)。
|
|
1512
|
+
const roleAgentDir = options?.roles?.agentDir;
|
|
1513
|
+
const sorted = [...roles.values()].sort((left, right) => (left.id < right.id ? -1 : 1));
|
|
1514
|
+
const disk = sorted.filter((role) => diskRoles.has(role.id));
|
|
1515
|
+
const session = sorted.filter((role) => !diskRoles.has(role.id));
|
|
1516
|
+
const lines = [];
|
|
1517
|
+
if (sorted.length === 0) {
|
|
1518
|
+
lines.push("No subagent roles are available in this session. Register one with subagent_role_set" +
|
|
1519
|
+
(roleAgentDir === undefined
|
|
1520
|
+
? ""
|
|
1521
|
+
: `, or drop a role file into ${join(roleAgentDir, ROLE_CATALOG_DIR)}/ and associate its id in the suite.`));
|
|
1522
|
+
}
|
|
1523
|
+
else {
|
|
1524
|
+
lines.push(`Subagent roles available in this session: ${sorted.length}`);
|
|
1525
|
+
if (disk.length > 0 || session.length > 0)
|
|
1526
|
+
lines.push("");
|
|
1527
|
+
if (disk.length > 0) {
|
|
1528
|
+
lines.push("disk:");
|
|
1529
|
+
for (const role of disk)
|
|
1530
|
+
lines.push(` ${role.id} — ${role.description}`);
|
|
1531
|
+
}
|
|
1532
|
+
if (session.length > 0) {
|
|
1533
|
+
lines.push("session:");
|
|
1534
|
+
for (const role of session)
|
|
1535
|
+
lines.push(` ${role.id} — ${role.description}`);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
// 诊断区(仅非空时显示):影子/超预算条目按数据表过滤——已被 remove 的旧诊断
|
|
1539
|
+
// 不再声称"数据仍保留";悬空关联只列 id,有角色目录时附逐 id 修复提示。
|
|
1540
|
+
const shadowed = [...hydratedRoles.shadowedByDisk].filter((id) => sessionRoles.has(id)).sort();
|
|
1541
|
+
const overBudget = [...hydratedRoles.droppedForQuota].filter((id) => sessionRoles.has(id)).sort();
|
|
1542
|
+
const dangling = [...roleCatalog.dangling].sort();
|
|
1543
|
+
if (shadowed.length > 0 || overBudget.length > 0) {
|
|
1544
|
+
lines.push("", "Not exposed (data kept in this session file):");
|
|
1545
|
+
if (shadowed.length > 0)
|
|
1546
|
+
lines.push(` shadowed by a visible disk role: ${shadowed.join(", ")}`);
|
|
1547
|
+
if (overBudget.length > 0) {
|
|
1548
|
+
lines.push(` over the menu budget (${ROLE_COUNT_LIMIT} roles / ${ROLE_MENU_CHARS} chars): ${overBudget.join(", ")}`);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
if (dangling.length > 0) {
|
|
1552
|
+
lines.push("", `Suite associations without a role file (unavailable): ${dangling.join(", ")}`);
|
|
1553
|
+
if (roleAgentDir !== undefined) {
|
|
1554
|
+
for (const id of dangling) {
|
|
1555
|
+
lines.push(` create ${subagentRoleCatalogPath(roleAgentDir, id)} or drop the association`);
|
|
1111
1556
|
}
|
|
1112
|
-
return { background: true, tasks: tickets };
|
|
1113
1557
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1558
|
+
}
|
|
1559
|
+
return lines.join("\n");
|
|
1560
|
+
};
|
|
1561
|
+
/** `/roles <id>` 视图:单角色完整定义(收窄字段存在才显示,parentView 仅 true 显示)。 */
|
|
1562
|
+
const renderRoleDefinitionView = (role) => {
|
|
1563
|
+
const lines = [
|
|
1564
|
+
`id: ${role.id}`,
|
|
1565
|
+
`origin: ${diskRoles.has(role.id) ? "disk" : "session"}`,
|
|
1566
|
+
`description: ${role.description}`,
|
|
1567
|
+
"instructions:",
|
|
1568
|
+
role.instructions,
|
|
1569
|
+
];
|
|
1570
|
+
const profile = role.profile;
|
|
1571
|
+
if (profile?.model !== undefined)
|
|
1572
|
+
lines.push(`model: ${profile.model}`);
|
|
1573
|
+
if (profile?.thinkingLevel !== undefined)
|
|
1574
|
+
lines.push(`thinkingLevel: ${profile.thinkingLevel}`);
|
|
1575
|
+
if (profile?.tools !== undefined)
|
|
1576
|
+
lines.push(`tools: ${profile.tools.join(", ")}`);
|
|
1577
|
+
if (profile?.mode !== undefined)
|
|
1578
|
+
lines.push(`mode: ${profile.mode}`);
|
|
1579
|
+
if (profile?.systemPromptAppend !== undefined)
|
|
1580
|
+
lines.push("systemPromptAppend:", profile.systemPromptAppend);
|
|
1581
|
+
if (role.parentView === true)
|
|
1582
|
+
lines.push("parentView: true");
|
|
1583
|
+
return lines.join("\n");
|
|
1584
|
+
};
|
|
1585
|
+
/**
|
|
1586
|
+
* `/roles` 斜杠命令(D-081 用户面):与角色三工具同一注册分支(委派面存在才注册),
|
|
1587
|
+
* 句柄随插件 dispose。只读视图——不改状态、不引入新配置键;未知 id 是正常结果
|
|
1588
|
+
* (error + 可用 id 列表),任何异常都收敛为 `{ status: "error" }`,命令不崩宿主。
|
|
1589
|
+
*/
|
|
1590
|
+
const rolesCommand = api.registerCommand({
|
|
1591
|
+
name: "roles",
|
|
1592
|
+
description: "List the subagent roles available in this session (disk + session), or show one role's full definition",
|
|
1593
|
+
argumentHint: "[id]",
|
|
1594
|
+
execute: async (args) => {
|
|
1595
|
+
try {
|
|
1596
|
+
const requested = args.trim();
|
|
1597
|
+
if (requested === "") {
|
|
1598
|
+
return { status: "success", message: renderRolesOverview() };
|
|
1127
1599
|
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1600
|
+
const role = roles.get(requested);
|
|
1601
|
+
if (role === undefined) {
|
|
1602
|
+
return { status: "error", message: unknownRoleMessage(requested, roles) };
|
|
1131
1603
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
: ` · steps ${progressState.steps}${progressState.last === undefined ? "" : ` · ${progressState.last}`}`;
|
|
1138
|
-
onUpdate?.({
|
|
1139
|
-
content: [{ type: "text", text: `subagent ${completed}/${prompts.length}${progressSuffix}` }],
|
|
1140
|
-
details: {},
|
|
1141
|
-
});
|
|
1604
|
+
return { status: "success", message: renderRoleDefinitionView(role) };
|
|
1605
|
+
}
|
|
1606
|
+
catch (error) {
|
|
1607
|
+
// A slash command must not crash the host (memory /promote precedent).
|
|
1608
|
+
return { status: "error", message: errorText(error) };
|
|
1142
1609
|
}
|
|
1143
|
-
return { tasks: tasks.filter((task) => task !== undefined) };
|
|
1144
1610
|
},
|
|
1145
1611
|
});
|
|
1146
1612
|
/**
|
|
@@ -1200,6 +1666,7 @@ export function createCapability(api, options) {
|
|
|
1200
1666
|
const messages = record.messages.slice(-requested);
|
|
1201
1667
|
// escalate 上抛(D-060 S4):按子会话 id 取走并随信封返回(取后即清)。
|
|
1202
1668
|
const escalations = wiring?.drainEscalations?.(record.sessionId) ?? [];
|
|
1669
|
+
const siblingTraffic = wiring?.readSiblingTraffic?.(record.sessionId, SIBLING_TRAFFIC_EXCERPTS) ?? [];
|
|
1203
1670
|
return {
|
|
1204
1671
|
found: true,
|
|
1205
1672
|
source: "record",
|
|
@@ -1214,6 +1681,7 @@ export function createCapability(api, options) {
|
|
|
1214
1681
|
truncated: record.truncated || messages.length < record.messages.length,
|
|
1215
1682
|
messages: messages.map((message) => detachedSessionSnapshot(message)),
|
|
1216
1683
|
...(escalations.length === 0 ? {} : { escalations }),
|
|
1684
|
+
...(siblingTraffic.length === 0 ? {} : { siblingTraffic }),
|
|
1217
1685
|
};
|
|
1218
1686
|
}
|
|
1219
1687
|
const reader = agents?.session;
|
|
@@ -1225,6 +1693,7 @@ export function createCapability(api, options) {
|
|
|
1225
1693
|
return { found: false, source: "session", sessionId };
|
|
1226
1694
|
const entries = view.entries.slice(-requested);
|
|
1227
1695
|
const escalations = wiring?.drainEscalations?.(view.sessionId) ?? [];
|
|
1696
|
+
const siblingTraffic = wiring?.readSiblingTraffic?.(view.sessionId, SIBLING_TRAFFIC_EXCERPTS) ?? [];
|
|
1228
1697
|
return {
|
|
1229
1698
|
found: true,
|
|
1230
1699
|
source: "session",
|
|
@@ -1236,96 +1705,137 @@ export function createCapability(api, options) {
|
|
|
1236
1705
|
truncated: view.truncated || entries.length < view.entryCount,
|
|
1237
1706
|
messages: entries.map((entry) => detachedSessionSnapshot(entry)),
|
|
1238
1707
|
...(escalations.length === 0 ? {} : { escalations }),
|
|
1708
|
+
...(siblingTraffic.length === 0 ? {} : { siblingTraffic }),
|
|
1239
1709
|
};
|
|
1240
1710
|
},
|
|
1241
1711
|
});
|
|
1242
|
-
// steer
|
|
1243
|
-
//
|
|
1244
|
-
//
|
|
1245
|
-
//
|
|
1246
|
-
//
|
|
1247
|
-
//
|
|
1248
|
-
//
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1712
|
+
// steer 的模型面(D-060 裁决 13,2026-10-02 用户裁决):`subagent_steer` 随
|
|
1713
|
+
// background 面注册。原 S4 裁决"steer 不设模型面工具"的前提——前台阻塞 drain 下
|
|
1714
|
+
// 父模型无"运行中窗口"——在 background 默认开后失效(提交即返回票据,运行中窗口
|
|
1715
|
+
// 存在),该论证保留为背景说明。语义对齐主流取证(codex v2 send_message /
|
|
1716
|
+
// ZCode SendMessage 均为模型面排队投递):不打断子代理当前回合,在其下一个模型
|
|
1717
|
+
// 请求前生效。程序面(agents 桥的 steer 与本调度器的 steer/findBySession 原语)
|
|
1718
|
+
// 保持不变,供编排插件组合。
|
|
1719
|
+
// subagent_cancel(S4 补充裁决,2026-10-01):原"仅 background 委派启用时注册"
|
|
1720
|
+
// 的前提(前台 drain 下父模型在 subagent_run 阻塞中不存在运行中窗口)保留为
|
|
1721
|
+
// 背景说明——cancel 只在 background 委派返回票据后才有可达窗口。
|
|
1722
|
+
// 两个工具自 D-060 裁决 15(2026-10-03)起无条件注册(开关形态退役)。
|
|
1723
|
+
const cancelTool = api.registerTool({
|
|
1724
|
+
name: "subagent_cancel",
|
|
1725
|
+
label: "Cancel a background subagent",
|
|
1726
|
+
description: "Cancel a running background subagent task by its taskId (the ticket returned by subagent_run with " +
|
|
1727
|
+
"background: true). Only background delegation tickets are cancellable: a foreground subagent_run call " +
|
|
1728
|
+
"blocks until its tasks finish, so it never leaves a running task behind. Terminal statuses (including a " +
|
|
1729
|
+
"task cancelled by something else) come back as already_terminal; unknown taskIds as not_found.",
|
|
1730
|
+
parameters: Type.Object({
|
|
1731
|
+
taskId: Type.String({ description: "Task id from a background subagent_run ticket" }),
|
|
1732
|
+
reason: Type.Optional(Type.String({ description: "Short reason recorded on the cancellation fence" })),
|
|
1733
|
+
}),
|
|
1734
|
+
execute: async (first, second) => {
|
|
1735
|
+
const input = objectInput(first, second);
|
|
1736
|
+
const taskId = typeof input.taskId === "string" ? input.taskId : "";
|
|
1737
|
+
if (taskId.trim() === "") {
|
|
1738
|
+
throw new SubagentDelegateError("subagent_invalid_input", "subagent_cancel requires a taskId");
|
|
1739
|
+
}
|
|
1740
|
+
const reason = typeof input.reason === "string" && input.reason.trim() !== "" ? input.reason : undefined;
|
|
1741
|
+
const result = await runtime.cancelById(taskId, { owner: capabilityManifest.id, parentSessionId: session.getSessionId() }, reason);
|
|
1742
|
+
return result.outcome === "not_found"
|
|
1743
|
+
? { outcome: "not_found", taskId }
|
|
1744
|
+
: { outcome: result.outcome, taskId, status: result.status };
|
|
1745
|
+
},
|
|
1746
|
+
});
|
|
1747
|
+
// subagent_steer(D-060 裁决 13):与 cancelTool 同属 background 面——有界引导消息
|
|
1748
|
+
// 投递给运行中的后台子任务(排队语义),需要硬终止时模型仍走 subagent_cancel。
|
|
1749
|
+
// D-060 裁决 15(2026-10-03)起无条件注册(开关形态退役)。
|
|
1750
|
+
const steerTool = api.registerTool({
|
|
1751
|
+
name: "subagent_steer",
|
|
1752
|
+
label: "Steer a background subagent",
|
|
1753
|
+
description: "Deliver one bounded steering message to a running background subagent task by its taskId (the ticket " +
|
|
1754
|
+
"returned by subagent_run with background: true). Queued delivery: it does NOT interrupt the subagent's " +
|
|
1755
|
+
"current turn — it takes effect before the task's next model request. Use it when user feedback or new " +
|
|
1756
|
+
"information should change the subtask's direction. Use subagent_cancel when the task must stop instead.",
|
|
1757
|
+
parameters: Type.Object({
|
|
1758
|
+
taskId: Type.String({ description: "Task id from a background subagent_run ticket" }),
|
|
1759
|
+
text: Type.String({
|
|
1760
|
+
minLength: 1,
|
|
1761
|
+
description: `Bounded steering message (at most ${SUBAGENT_MAILBOX_MAX_MESSAGE_BYTES} utf-8 bytes), delivered before the task's next model request`,
|
|
1260
1762
|
}),
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1763
|
+
}),
|
|
1764
|
+
execute: async (first, second) => {
|
|
1765
|
+
const input = objectInput(first, second);
|
|
1766
|
+
const taskId = typeof input.taskId === "string" ? input.taskId : "";
|
|
1767
|
+
const text = typeof input.text === "string" ? input.text : "";
|
|
1768
|
+
if (taskId.trim() === "" || text.trim() === "") {
|
|
1769
|
+
throw new SubagentDelegateError("subagent_invalid_input", "subagent_steer requires a taskId and non-empty text");
|
|
1770
|
+
}
|
|
1771
|
+
if (Buffer.byteLength(text, "utf8") > SUBAGENT_MAILBOX_MAX_MESSAGE_BYTES) {
|
|
1772
|
+
throw new SubagentDelegateError("subagent_message_too_large", `subagent_steer text exceeds ${SUBAGENT_MAILBOX_MAX_MESSAGE_BYTES} utf-8 bytes`);
|
|
1773
|
+
}
|
|
1774
|
+
try {
|
|
1775
|
+
await runtime.steer(taskId, text, {
|
|
1776
|
+
owner: capabilityManifest.id,
|
|
1777
|
+
parentSessionId: session.getSessionId(),
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
catch (error) {
|
|
1781
|
+
mapCommunicationError(error);
|
|
1782
|
+
}
|
|
1783
|
+
return { delivered: true, taskId };
|
|
1784
|
+
},
|
|
1785
|
+
});
|
|
1275
1786
|
/**
|
|
1276
|
-
* 父→子续跑(D-060 S4
|
|
1277
|
-
*
|
|
1278
|
-
*
|
|
1787
|
+
* 父→子续跑(D-060 S4):对**已终态且已落盘**的子会话按 sessionId 继续提交一轮
|
|
1788
|
+
* 任务(同一份 JSONL 续写,多轮子任务)。血缘作用域由宿主会话回读器判定,活跃
|
|
1789
|
+
* 任务占用以 `subagent_session_busy` 拒绝。shared 与 pooled 都支持——pooled 下
|
|
1279
1790
|
* resume 键随 metadata 过线,runner 打开既有会话文件续写。
|
|
1791
|
+
* D-060 裁决 15(2026-10-03)起无条件注册(开关形态退役)。
|
|
1280
1792
|
*/
|
|
1281
|
-
const resumeTool =
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
})
|
|
1328
|
-
: undefined;
|
|
1793
|
+
const resumeTool = api.registerTool({
|
|
1794
|
+
name: "subagent_resume",
|
|
1795
|
+
label: "Resume a subagent",
|
|
1796
|
+
description: "Continue a finished subagent session by its sessionId: submit one more prompt into the same persisted " +
|
|
1797
|
+
"transcript (multi-turn subtask) and get the bounded terminal projection, like subagent_run for one task.",
|
|
1798
|
+
parameters: Type.Object({
|
|
1799
|
+
sessionId: Type.String({ description: "Child session id of a finished subagent task" }),
|
|
1800
|
+
prompt: Type.String({ minLength: 1, description: "Follow-up prompt for the continued session" }),
|
|
1801
|
+
}),
|
|
1802
|
+
execute: async (first, second) => {
|
|
1803
|
+
const input = objectInput(first, second);
|
|
1804
|
+
const sessionId = typeof input.sessionId === "string" ? input.sessionId : "";
|
|
1805
|
+
const prompt = typeof input.prompt === "string" ? input.prompt : "";
|
|
1806
|
+
if (sessionId === "" || prompt.trim() === "") {
|
|
1807
|
+
throw new SubagentDelegateError("subagent_invalid_input", "subagent_resume requires sessionId and prompt");
|
|
1808
|
+
}
|
|
1809
|
+
const reader = agents?.session;
|
|
1810
|
+
if (reader === undefined) {
|
|
1811
|
+
throw new SubagentDelegateError("subagent_profile_not_applied", "this host exposes no session reader; resume cannot verify the session scope");
|
|
1812
|
+
}
|
|
1813
|
+
const view = await reader(sessionId);
|
|
1814
|
+
if (view === undefined) {
|
|
1815
|
+
throw new SubagentDelegateError("subagent_unknown_session", `no child session ${sessionId} under this parent`);
|
|
1816
|
+
}
|
|
1817
|
+
const requester = { owner: capabilityManifest.id, parentSessionId: session.getSessionId() };
|
|
1818
|
+
const active = runtime.findBySession(sessionId, requester);
|
|
1819
|
+
if (active !== undefined && !active.settled) {
|
|
1820
|
+
throw new SubagentDelegateError("subagent_session_busy", `session ${sessionId} still has an unsettled task (${active.taskId}); steer or wait instead`);
|
|
1821
|
+
}
|
|
1822
|
+
let handle;
|
|
1823
|
+
try {
|
|
1824
|
+
handle = await runtime.submit({
|
|
1825
|
+
prompt,
|
|
1826
|
+
metadata: {
|
|
1827
|
+
[AGENT_TASK_PARENT_SESSION_KEY]: session.getSessionId(),
|
|
1828
|
+
[AGENT_TASK_RESUME_SESSION_KEY]: sessionId,
|
|
1829
|
+
},
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
catch (error) {
|
|
1833
|
+
throw new SubagentDelegateError("subagent_submit_rejected", errorText(error));
|
|
1834
|
+
}
|
|
1835
|
+
const record = await handle.drain({ timeoutMs: taskTimeoutMs });
|
|
1836
|
+
return { tasks: [projectRecord(record)] };
|
|
1837
|
+
},
|
|
1838
|
+
});
|
|
1329
1839
|
/**
|
|
1330
1840
|
* 子会话盘点(D-060 模型面):列出**本会话自己的**子代理会话(最近活动倒序、
|
|
1331
1841
|
* 有界),并用调度器记录富化活跃/终态信息。宿主注入 `agents.list` 才注册;
|
|
@@ -1377,10 +1887,16 @@ export function createCapability(api, options) {
|
|
|
1377
1887
|
},
|
|
1378
1888
|
});
|
|
1379
1889
|
return [
|
|
1380
|
-
|
|
1890
|
+
// 刷新句柄:释放时以最新一次 subagent_run 注册为准(旧句柄 dispose 因表项被接手而失效)。
|
|
1891
|
+
createDisposableRegistration("subagent-delegate.tool.subagent_run", () => void runTool.current.dispose(), `registration:${capabilityManifest.id}:refreshable:tool:subagent_run`),
|
|
1381
1892
|
resultTool,
|
|
1382
|
-
|
|
1383
|
-
|
|
1893
|
+
cancelTool,
|
|
1894
|
+
steerTool,
|
|
1895
|
+
resumeTool,
|
|
1896
|
+
roleSetTool,
|
|
1897
|
+
roleListTool,
|
|
1898
|
+
roleRemoveTool,
|
|
1899
|
+
rolesCommand,
|
|
1384
1900
|
...(listTool === undefined ? [] : [listTool]),
|
|
1385
1901
|
// 子侧通信面(D-060 S4/S5):获准自建会话两面并存(向下委派 + 向父上抛/兄弟消息)。
|
|
1386
1902
|
...childTools,
|