@ai-agent-forge/plugin-subagent-delegate 0.85.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/agent-forge.json +11 -0
- package/dist/delegate.d.ts +275 -0
- package/dist/delegate.d.ts.map +1 -0
- package/dist/delegate.js +1376 -0
- package/dist/delegate.js.map +1 -0
- package/dist/entry.d.ts +28 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +101 -0
- package/dist/entry.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/multi-agent-tasks.d.ts +101 -0
- package/dist/multi-agent-tasks.d.ts.map +1 -0
- package/dist/multi-agent-tasks.js +178 -0
- package/dist/multi-agent-tasks.js.map +1 -0
- package/dist/remote-adapter.d.ts +11 -0
- package/dist/remote-adapter.d.ts.map +1 -0
- package/dist/remote-adapter.js +366 -0
- package/dist/remote-adapter.js.map +1 -0
- package/dist/runner-pool.d.ts +59 -0
- package/dist/runner-pool.d.ts.map +1 -0
- package/dist/runner-pool.js +330 -0
- package/dist/runner-pool.js.map +1 -0
- package/dist/runner-spawn.d.ts +23 -0
- package/dist/runner-spawn.d.ts.map +1 -0
- package/dist/runner-spawn.js +79 -0
- package/dist/runner-spawn.js.map +1 -0
- package/dist/self-invocation.d.ts +20 -0
- package/dist/self-invocation.d.ts.map +1 -0
- package/dist/self-invocation.js +13 -0
- package/dist/self-invocation.js.map +1 -0
- package/dist/shared-adapter.d.ts +29 -0
- package/dist/shared-adapter.d.ts.map +1 -0
- package/dist/shared-adapter.js +60 -0
- package/dist/shared-adapter.js.map +1 -0
- package/dist/subagent-runtime.d.ts +162 -0
- package/dist/subagent-runtime.d.ts.map +1 -0
- package/dist/subagent-runtime.js +849 -0
- package/dist/subagent-runtime.js.map +1 -0
- package/package.json +55 -0
- package/plugin.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @agent-forge/plugin-subagent-delegate
|
|
2
|
+
|
|
3
|
+
First-party subagent-delegate capability plugin for Agent Forge: the
|
|
4
|
+
`subagent_run` / `subagent_result` (and optionally `subagent_resume` /
|
|
5
|
+
`subagent_list` / communication tools) model face over the plugin-level
|
|
6
|
+
`SubagentRuntime` scheduler, extracted from the host's embedded builtin
|
|
7
|
+
capability (D-075 S4, third batch).
|
|
8
|
+
|
|
9
|
+
## Library face vs plugin entry
|
|
10
|
+
|
|
11
|
+
- `createSubagentDelegateCapability(api, options?)` — explicit-options
|
|
12
|
+
factory for embedders and tests. Adapter/limits/runner spawner/roles are
|
|
13
|
+
injected explicitly.
|
|
14
|
+
- `./dist/entry.js` (default export) — the manifest-loaded plugin factory. It
|
|
15
|
+
maps host services onto the same options:
|
|
16
|
+
- `api.host.communication` → `communicationWiring`
|
|
17
|
+
- `api.host.turnContext.backgroundDigestSink` / `getTurnCorrelationId`
|
|
18
|
+
- `api.host.agentDir` + `api.host.roleAssociations` → role catalog (after an
|
|
19
|
+
idempotent factory seed of `<agentDir>/roles/`)
|
|
20
|
+
- `api.host.agentDir` → the nested-depth lineage gate source
|
|
21
|
+
- auto-pooling is driven by the existing config key `runnerEntryAuto`
|
|
22
|
+
- Config channel: hosts declaring `api.host.agentDir` get
|
|
23
|
+
`<agentDir>/capabilities/builtin.subagent-delegate.json` merged over the
|
|
24
|
+
manifest config (missing file = unconfigured; unreadable/invalid JSON fails
|
|
25
|
+
the plugin load).
|
|
26
|
+
|
|
27
|
+
## Bundled modules
|
|
28
|
+
|
|
29
|
+
The scheduler (`subagent-runtime`), multi-agent task registry
|
|
30
|
+
(`multi-agent-tasks`), runner pool / spawner, remote (wire) and shared
|
|
31
|
+
(in-process) adapters, and the self-invocation probe all live in this package
|
|
32
|
+
and are re-exported from the index. The wire protocol, stdio channel, task
|
|
33
|
+
server, progress face, and mailbox come from `@agent-forge/protocol`;
|
|
34
|
+
agent-task contracts, durable service types, and session lineage reading come
|
|
35
|
+
from `@agent-forge/plugin-sdk`.
|
|
36
|
+
|
|
37
|
+
## Capabilities
|
|
38
|
+
|
|
39
|
+
Requires `session` + `tools`; `agents` is optional (leaf sessions register
|
|
40
|
+
their child communication face only). Provides the
|
|
41
|
+
`subagent-delegate.agent-task` service (v1).
|
package/agent-forge.json
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { type AgentTaskAdapter, type AgentTaskChildProfileV1, type CapabilityAPI, type DisposableRegistration, type SubagentCommunicationWiringV1 } from "@agent-forge/plugin-sdk";
|
|
2
|
+
import { type SubagentRunnerSpawnHandleV1 } from "./runner-pool.ts";
|
|
3
|
+
import { type SubagentRuntimeLimitsV1, type SubagentRuntimeStatus } from "./subagent-runtime.ts";
|
|
4
|
+
export declare const capabilityManifest: {
|
|
5
|
+
readonly id: "agent-forge.builtin.subagent-delegate";
|
|
6
|
+
readonly version: "0.1.0";
|
|
7
|
+
readonly apiVersion: "1-draft";
|
|
8
|
+
readonly requiredCapabilities: readonly ["session", "tools"];
|
|
9
|
+
readonly optionalCapabilities: readonly ["agents"];
|
|
10
|
+
/**
|
|
11
|
+
* 用户可配置默认值(D-044 §6 四层归属的 Profile/capability 配置层):
|
|
12
|
+
* 经 agent-dir 配置通道 `<agentDir>/capabilities/builtin.subagent-delegate.json`
|
|
13
|
+
* 浅合并覆盖;宿主显式 options 优先级最高;非法值在装载时 fail fast。
|
|
14
|
+
*/
|
|
15
|
+
readonly config: {
|
|
16
|
+
readonly maxConcurrentSubagents: 8;
|
|
17
|
+
readonly maxQueueDepth: 200;
|
|
18
|
+
readonly maxPromptBytes: 65536;
|
|
19
|
+
readonly maxMetadataBytes: 16384;
|
|
20
|
+
readonly maxResultBytes: 524288;
|
|
21
|
+
readonly maxMessages: 2000;
|
|
22
|
+
readonly drainTimeoutMs: 30000;
|
|
23
|
+
readonly maxRetainedRecords: 256;
|
|
24
|
+
readonly maxTasksPerCall: 4;
|
|
25
|
+
readonly taskTimeoutMs: 300000;
|
|
26
|
+
readonly runnerMaxProcesses: 0;
|
|
27
|
+
readonly runnerMaxSessionsPerRunner: 8;
|
|
28
|
+
readonly runnerIdleTimeoutMs: 300000;
|
|
29
|
+
readonly runnerSweepIntervalMs: 60000;
|
|
30
|
+
readonly runnerEntryAuto: false;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
export interface SubagentDelegateCapabilityOptions {
|
|
34
|
+
/** 宿主可注入调度器 adapter(如 pooled runner adapter);优先级高于配置驱动的 pooled 装配与共享模式。 */
|
|
35
|
+
adapter?: AgentTaskAdapter;
|
|
36
|
+
limits?: SubagentRuntimeLimitsV1;
|
|
37
|
+
/** 单次工具调用的并行任务上限(默认 4)与单任务 drain 超时(默认 300000ms)。 */
|
|
38
|
+
maxTasksPerCall?: number;
|
|
39
|
+
taskTimeoutMs?: number;
|
|
40
|
+
/**
|
|
41
|
+
* 测试/宿主注入的 runner spawn 工厂(优先级高于 `runnerEntryPath` 配置):
|
|
42
|
+
* 生产默认由 `createNodeSubagentRunnerSpawnerV1` 派生真实进程,测试注入进程内 fake。
|
|
43
|
+
*/
|
|
44
|
+
runnerSpawner?: () => SubagentRunnerSpawnHandleV1;
|
|
45
|
+
/**
|
|
46
|
+
* 角色目录与方案关联(角色只来自磁盘 `<agentDir>/roles/<id>.json`,见
|
|
47
|
+
* {@link resolveSubagentRoleCatalog})。库面调用方(测试/第三方)显式注入;
|
|
48
|
+
* 插件 entry 由 `api.host.roleAssociations` + `api.host.agentDir` 组装。
|
|
49
|
+
* 未提供时该会话**不暴露任何角色**(不是"回退到内置"——引擎没有内置角色)。
|
|
50
|
+
*/
|
|
51
|
+
roles?: {
|
|
52
|
+
readonly agentDir: string;
|
|
53
|
+
readonly associations: readonly string[];
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* 通信面宿主接线(D-060 S4):父装配域注入 `drainEscalations`(取子→父上抛),
|
|
57
|
+
* 子装配域注入 `postEscalate`/`sendSibling`/`drainInbox`(发上抛/发兄弟/取收件箱,
|
|
58
|
+
* 身份与血缘都由宿主绑定校验)。未接线时通信工具一律不注册。
|
|
59
|
+
*/
|
|
60
|
+
communicationWiring?: SubagentCommunicationWiringV1;
|
|
61
|
+
/**
|
|
62
|
+
* agentDir(承载退役切片 2):嵌套深度硬限沿 S2 落盘头行回溯血缘,需要 agentDir
|
|
63
|
+
* 定位会话目录。库面显式注入优先;缺省回落到 `api.host.agentDir`(插件 entry 的
|
|
64
|
+
* 单源通道,D-075 S4 第三批收敛——原 `roles.agentDir` 双通道已退役);两者都不在
|
|
65
|
+
* 时深度门不生效(配额仍在)。
|
|
66
|
+
*/
|
|
67
|
+
agentDir?: string;
|
|
68
|
+
/**
|
|
69
|
+
* 后台委派结算摘要 sink(D-060 S6):`background` 提交的任务每结算一个,宿主收到
|
|
70
|
+
* 一条有界 JSON 行(taskId/sessionId/status/reasonCode),由宿主注入父的下一次
|
|
71
|
+
* 模型轮次。未接线时后台照常可跑,只是没有推送感知。
|
|
72
|
+
*/
|
|
73
|
+
backgroundDigestSink?: (line: string) => void;
|
|
74
|
+
/**
|
|
75
|
+
* 委派提交时刻的回合 correlationId(第 3 期第二批):background 委派的提交动作
|
|
76
|
+
* 发生在父回合工具阶段、此时有值。getter 缺席或返回 undefined 时不写键,行为回退。
|
|
77
|
+
*/
|
|
78
|
+
getTurnCorrelationId?: () => string | undefined;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 通信面的方向开关(D-060 S4,**默认全关**):`api.config.communication` 里逐项显式
|
|
82
|
+
* `true` 才开启。开启项只决定工具注册,作用域与有界性由宿主接线与信箱硬上限保证。
|
|
83
|
+
*/
|
|
84
|
+
export interface SubagentCommunicationConfigV1 {
|
|
85
|
+
/** 父→子:对已终态且已落盘的子会话按 sessionId 续跑(`subagent_resume`)。steer 的程序面不经此开关。 */
|
|
86
|
+
readonly resume?: true;
|
|
87
|
+
/** 子→父:有界上抛消息(`subagent_escalate`),落在父下一次工具结果,不抢占父。 */
|
|
88
|
+
readonly escalate?: true;
|
|
89
|
+
/** 子↔子:同父兄弟互发有界消息(`subagent_send`/`subagent_inbox`)。 */
|
|
90
|
+
readonly sibling?: true;
|
|
91
|
+
}
|
|
92
|
+
/** 结构化错误码:调用形状错误、批上限错误与未知角色分开表达,便于调用方分支。 */
|
|
93
|
+
export type SubagentDelegateErrorCodesV1 = "subagent_invalid_input" | "subagent_batch_too_large" | "subagent_unknown_role"
|
|
94
|
+
/** 宿主未落地或无法自证档案收窄:宁可失败,也不让"只读评审者"变成一句空话。 */
|
|
95
|
+
| "subagent_profile_not_applied" | "subagent_unknown_task" | "subagent_unknown_session" | "subagent_not_running" | "subagent_steer_unsupported" | "subagent_session_busy" | "subagent_message_too_large" | "subagent_escalate_quota" | "subagent_inbox_quota"
|
|
96
|
+
/** 嵌套委派深度超限(提交侧执行;与 S5 shared 侧同一语义)。 */
|
|
97
|
+
| "subagent_depth_exceeded"
|
|
98
|
+
/** 本会话累计创建的子代理达到每父级总量上限(提交侧执行)。 */
|
|
99
|
+
| "subagent_child_quota"
|
|
100
|
+
/** submit 同步拒绝且无结构化 code 时的本地兜底码(与投影 reasonCode 同名)。 */
|
|
101
|
+
| "subagent_submit_rejected";
|
|
102
|
+
export declare class SubagentDelegateError extends Error {
|
|
103
|
+
readonly code: SubagentDelegateErrorCodesV1;
|
|
104
|
+
constructor(code: SubagentDelegateErrorCodesV1, message: string);
|
|
105
|
+
}
|
|
106
|
+
/** 终态投影:调度器权威状态词汇 + 本地 submit 拒绝("rejected")。 */
|
|
107
|
+
export type SubagentDelegateTaskStatusV1 = SubagentRuntimeStatus | "rejected";
|
|
108
|
+
export interface SubagentDelegateTaskProjectionV1 {
|
|
109
|
+
/** 调度器任务 id;submit 同步抛错的 rejected 条目尚未生成 id,回空串。 */
|
|
110
|
+
readonly taskId: string;
|
|
111
|
+
readonly status: SubagentDelegateTaskStatusV1;
|
|
112
|
+
/**
|
|
113
|
+
* 结构化原因码(如 `subagent_queue_full`);无则省略。调度器/适配器抛出的错误
|
|
114
|
+
* 无结构化 code 时用本地兜底码 `subagent_submit_rejected`。
|
|
115
|
+
*/
|
|
116
|
+
readonly reasonCode?: string;
|
|
117
|
+
/** 失败/取消的原始原因文本。 */
|
|
118
|
+
readonly reason?: string;
|
|
119
|
+
/** 调度器 settle 截断过消息快照时为 true;未截断时省略。 */
|
|
120
|
+
readonly truncated?: true;
|
|
121
|
+
/** 有界消息快照的条数(rejected/failed 无记录时为 0)。 */
|
|
122
|
+
readonly messageCount: number;
|
|
123
|
+
/** 子会话自身的 sessionId(D-060 S1):可按它在会话目录里定位这份 transcript。 */
|
|
124
|
+
readonly sessionId?: string;
|
|
125
|
+
/** 最后一条 assistant 文本的前 200 字符;找不到则省略。 */
|
|
126
|
+
readonly summary?: string;
|
|
127
|
+
/**
|
|
128
|
+
* 子会话 escalate 上抛的有界消息(D-060 S4):落在父侧下一次工具结果里(这里或
|
|
129
|
+
* `subagent_result`),父回合不被打断;无积压时省略。
|
|
130
|
+
*/
|
|
131
|
+
readonly escalations?: readonly string[];
|
|
132
|
+
}
|
|
133
|
+
export interface SubagentDelegateResultV1 {
|
|
134
|
+
readonly tasks: readonly SubagentDelegateTaskProjectionV1[];
|
|
135
|
+
}
|
|
136
|
+
/** 后台委派票据(D-060 S6):提交即返回,结果稍后经 subagent_result / 摘要注入获得。 */
|
|
137
|
+
export interface SubagentDelegateBackgroundTaskV1 {
|
|
138
|
+
readonly taskId: string;
|
|
139
|
+
}
|
|
140
|
+
export interface SubagentDelegateBackgroundResultV1 {
|
|
141
|
+
readonly background: true;
|
|
142
|
+
readonly tasks: readonly SubagentDelegateBackgroundTaskV1[];
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 角色(persona):子代理侧身份,主代理按名调用。
|
|
146
|
+
*
|
|
147
|
+
* **定义只在磁盘上**(与 skill/MCP/插件同构,不搞内置特权):
|
|
148
|
+
* - 共享目录 `<agentDir>/roles/<id>.json` → `{ "description": …, "instructions": … }`;
|
|
149
|
+
* - 可见性由**当前方案**决定:只有 `suite.associations.roles` 关联到的 id 才出现在
|
|
150
|
+
* `subagent_run` 的菜单里;没关联的角色对主代理不可见(不报错)。
|
|
151
|
+
* - 工厂只做**种子落盘**({@link seedFactoryRoleCatalog},首次缺文件时写入 explore/reviewer
|
|
152
|
+
* 两个普通文件,可改可删),默认方案关联它们,因此开箱即用;引擎从不回退到编译内的定义。
|
|
153
|
+
*
|
|
154
|
+
* 角色今天**确定性地决定子任务的任务框定**(instructions 组装进送交子会话的提示词),
|
|
155
|
+
* 但**不改变子会话的模型与工具面**——后者需要"子任务档案"这一公共契约(见
|
|
156
|
+
* docs/design/子代理运行时设计.md §13)。因此角色文件里出现 `model`/`tools`/`readonly`
|
|
157
|
+
* 等键时在装载期直接报错,而不是静默忽略让用户以为"只读评审者不会写文件"。
|
|
158
|
+
*/
|
|
159
|
+
export interface SubagentDelegateRoleV1 {
|
|
160
|
+
readonly id: string;
|
|
161
|
+
/** 给主代理看的一句话说明(工具描述里列出),不送进子会话提示词。 */
|
|
162
|
+
readonly description: string;
|
|
163
|
+
/** 送进子会话提示词的角色指令(系统提示词由子会话继承父会话,不在这里重复)。 */
|
|
164
|
+
readonly instructions: string;
|
|
165
|
+
/**
|
|
166
|
+
* 子任务档案(收窄语义,公共契约 AgentTaskChildProfileV1):模型 / 思考级别 /
|
|
167
|
+
* 工具面 / 会话模式 / 追加系统提示词。子会话创建时由宿主落地,**创建后核验**;
|
|
168
|
+
* 宿主不落地或无法核验时该条目失败(`subagent_profile_not_applied`),绝不静默忽略。
|
|
169
|
+
* shared 与 pooled 两种形态都落地全部字段——见设计文档 §6。
|
|
170
|
+
*/
|
|
171
|
+
readonly profile?: AgentTaskChildProfileV1;
|
|
172
|
+
/**
|
|
173
|
+
* 子读父的有界投影(D-060 §4,默认关闭):开启时把父会话的**目标**(最后一条用户文本,
|
|
174
|
+
* ≤2KB)、父 sessionId 与父工具面名单拼进子任务提示词。只给目标不给历史——子会话拿不到
|
|
175
|
+
* 父上下文全文。
|
|
176
|
+
*/
|
|
177
|
+
readonly parentView?: boolean;
|
|
178
|
+
}
|
|
179
|
+
/** 解析结果:暴露集(按方案关联过滤后)+ 可诊断的异常,装载与诊断共用同一实现。 */
|
|
180
|
+
export interface SubagentRoleCatalogResolutionV1 {
|
|
181
|
+
/** 当前方案可见的角色(内置定义 ∪ 用户目录,再按 associations 过滤)。 */
|
|
182
|
+
readonly roles: ReadonlyMap<string, SubagentDelegateRoleV1>;
|
|
183
|
+
/** 被方案关联、但内置与用户目录都没有定义的角色 id(诊断,不阻断)。 */
|
|
184
|
+
readonly dangling: readonly string[];
|
|
185
|
+
/** 未被关联、但用户文件非法的角色文件:只在被关联时才致命,这里记录用于诊断。 */
|
|
186
|
+
readonly brokenUnassociated: readonly string[];
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* 解析当前方案的角色目录(**全部来自磁盘**,无编译内定义)。装载期一次性完成,
|
|
190
|
+
* 任何会让"配置了但没生效"的情况都显式失败:
|
|
191
|
+
* - 被关联 + 文件存在但非法 → 抛错(指名文件);
|
|
192
|
+
* - 被关联但目录里没有该 id → dangling(诊断,角色不可见),调用该名走 `subagent_unknown_role`;
|
|
193
|
+
* - 未被关联的坏文件只进诊断:别的方案不该被一个用不到的文件拖垮;
|
|
194
|
+
* - 暴露集受 16 个 / 2000 字符上限约束(菜单是主代理工具描述的一部分)。
|
|
195
|
+
*/
|
|
196
|
+
export declare function resolveSubagentRoleCatalog(options: {
|
|
197
|
+
readonly agentDir: string;
|
|
198
|
+
readonly associations: readonly string[];
|
|
199
|
+
}): SubagentRoleCatalogResolutionV1;
|
|
200
|
+
/** 角色文件名(`<agentDir>/roles/<id>.json`),供种子与目录与文档共用一处。 */
|
|
201
|
+
export declare function subagentRoleCatalogPath(agentDir: string, id: string): string;
|
|
202
|
+
/**
|
|
203
|
+
* 出厂种子落盘(仅补缺,绝不覆盖既有文件):默认方案关联 explore/reviewer,
|
|
204
|
+
* 因此新环境开箱即用;这两个文件与用户自己写的角色完全同权——改文件即生效,
|
|
205
|
+
* 删文件使关联成为 dangling 诊断。引擎运行期从不回退到编译内的角色定义。
|
|
206
|
+
*/
|
|
207
|
+
export declare function seedFactoryRoleCatalog(agentDir: string): void;
|
|
208
|
+
/**
|
|
209
|
+
* 父→子指针注入(错误教训与召回质量护栏设计 §2.3):可选结构化 `context`(文件:行
|
|
210
|
+
* 锚点、已试失败命令清单),确定性渲染进子会话首条 prompt 头部。只传路标不传内容:
|
|
211
|
+
* 子代理仍自行读取真实文件,指针过期不产生错误事实。硬边界超限走 schema 校验失败
|
|
212
|
+
* 整体拒绝——拒绝优于静默截断:父代理必须知道子代理没拿到完整锚点。
|
|
213
|
+
*/
|
|
214
|
+
export interface SubagentDelegateContextAnchorV1 {
|
|
215
|
+
readonly path: string;
|
|
216
|
+
readonly line?: number;
|
|
217
|
+
readonly note?: string;
|
|
218
|
+
}
|
|
219
|
+
export interface SubagentDelegateContextFailedAttemptV1 {
|
|
220
|
+
readonly command: string;
|
|
221
|
+
readonly note?: string;
|
|
222
|
+
}
|
|
223
|
+
export interface SubagentDelegateContextV1 {
|
|
224
|
+
readonly anchors?: readonly SubagentDelegateContextAnchorV1[];
|
|
225
|
+
readonly failedAttempts?: readonly SubagentDelegateContextFailedAttemptV1[];
|
|
226
|
+
}
|
|
227
|
+
/** `subagent_result` 的返回形状:两个来源(调度记录 / 持久化 transcript)共用一个有界信封。 */
|
|
228
|
+
export interface SubagentResultReadResultV1 {
|
|
229
|
+
readonly found: boolean;
|
|
230
|
+
readonly source: "record" | "session";
|
|
231
|
+
readonly taskId?: string;
|
|
232
|
+
readonly sessionId?: string;
|
|
233
|
+
readonly status?: string;
|
|
234
|
+
readonly reasonCode?: string;
|
|
235
|
+
readonly reason?: string;
|
|
236
|
+
readonly roleId?: string;
|
|
237
|
+
/** 来源里的总条数(截断前)。 */
|
|
238
|
+
readonly messageCount?: number;
|
|
239
|
+
readonly entryCount?: number;
|
|
240
|
+
readonly returnedMessages?: number;
|
|
241
|
+
/** 整份来源的 UTF-8 字节数。 */
|
|
242
|
+
readonly bytes?: number;
|
|
243
|
+
readonly truncated?: boolean;
|
|
244
|
+
readonly messages?: readonly unknown[];
|
|
245
|
+
/** 该子会话 escalate 上抛的有界消息(取后即清);无积压时省略。 */
|
|
246
|
+
readonly escalations?: readonly string[];
|
|
247
|
+
/** taskId 已知但尚未结算(后台委派):稍后再取或带 waitMs 等待。 */
|
|
248
|
+
readonly knownTask?: true;
|
|
249
|
+
/** 未结算任务的当前调度器状态(queued/admitted/running)。 */
|
|
250
|
+
readonly taskStatus?: string;
|
|
251
|
+
}
|
|
252
|
+
/** `subagent_list` 的条目:store 摘要 + 调度器状态富化(有则附)。 */
|
|
253
|
+
export interface SubagentSessionListEntryV1 {
|
|
254
|
+
readonly sessionId: string;
|
|
255
|
+
readonly agentRole?: string;
|
|
256
|
+
readonly createdAt?: string;
|
|
257
|
+
readonly modifiedAt: number;
|
|
258
|
+
readonly bytes: number;
|
|
259
|
+
/** 本调度器里仍有未结算任务时为 true。 */
|
|
260
|
+
readonly active?: true;
|
|
261
|
+
/** 终态记录可查时为调度器权威状态词汇。 */
|
|
262
|
+
readonly status?: SubagentRuntimeStatus;
|
|
263
|
+
readonly reasonCode?: string;
|
|
264
|
+
readonly reason?: string;
|
|
265
|
+
}
|
|
266
|
+
export interface SubagentSessionListResultV1 {
|
|
267
|
+
readonly count: number;
|
|
268
|
+
readonly sessions: readonly SubagentSessionListEntryV1[];
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* 第一方编排 capability:一个调度器实例 + `subagent_run` 工具 + 释放登记。
|
|
272
|
+
* 只消费公共 API(session/agents/sessionAbortSignal/tools),不 import core 内部私有模块。
|
|
273
|
+
*/
|
|
274
|
+
export declare function createCapability(api: CapabilityAPI, options?: SubagentDelegateCapabilityOptions): DisposableRegistration[];
|
|
275
|
+
//# sourceMappingURL=delegate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delegate.d.ts","sourceRoot":"","sources":["../src/delegate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * First-party builtin subagent-delegate capability: the first production\n * orchestrator over the plugin-level `SubagentRuntime` (D-044).\n *\n * The capability consumes only the public face — `session`, `agents`,\n * `sessionAbortSignal` and `tools` — and owns exactly one scheduler instance\n * per plugin load. It adds no scheduling policy of its own: queue, admission,\n * budgets, terminal states and the parent cancel fence all stay in the\n * scheduler; this module is the model-facing `subagent_run` tool plus a bounded\n * result projection (status / reason codes / message counts / short summary).\n * Raw child messages never enter the tool result (context budget spec).\n *\n * The scheduler's deadline fence is tick-driven by design (no hidden timers),\n * and this consumer has no tick driver of its own, so the tool exposes no\n * `deadlineMs` parameter: a bound that nothing enforces would be a false\n * affordance. Wait bounds are `taskTimeoutMs` (drain) and the host's\n * `limits.drainTimeoutMs` inside the scheduler.\n *\n * 迁自宿主 `src/capabilities/subagent-delegate.ts`(D-075 S4 第三批拆包)。除 import\n * 来源外保持宿主行为:公共契约(agent-task 键/档案、通信接线、血缘读取、快照)改自\n * plugin-sdk;信箱类型/错误改自 @agent-forge/protocol;调度器与 runner 模块改包内\n * 相对路径。三处语义收敛见 `SubagentDelegateCapabilityOptions` 与 createCapability\n * 内的迁移注记(agentDir 单源、runnerEntryAuto 驱动自动池化、lineage 深度经 SDK 读取)。\n */\nimport { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport {\n\tAGENT_TASK_CHILD_PROFILE_KEY,\n\tAGENT_TASK_PARENT_SESSION_KEY,\n\tAGENT_TASK_PARENT_TRACE_KEY,\n\tAGENT_TASK_RESUME_SESSION_KEY,\n\ttype AgentTaskAdapter,\n\ttype AgentTaskChildProfileV1,\n\ttype CapabilityAPI,\n\tcreateDisposableRegistration,\n\ttype DisposableRegistration,\n\tdetachedSessionSnapshot,\n\tEXPERIMENTAL_PUBLIC_API_VERSION,\n\tparseAgentTaskChildProfileV1,\n\ttype SubagentCommunicationWiringV1,\n} from \"@agent-forge/plugin-sdk\";\nimport { readAgentSessionLineage } from \"@agent-forge/plugin-sdk/session-lineage\";\nimport {\n\tSUBAGENT_MAILBOX_MAX_MESSAGE_BYTES,\n\tSubagentMailboxErrorV1,\n\tsubscribeTaskProgressV1,\n} from \"@agent-forge/protocol/subagent\";\nimport { Type } from \"typebox\";\nimport {\n\tcreatePooledAgentTaskAdapterV1,\n\ttype SubagentRunnerPoolV1,\n\ttype SubagentRunnerSpawnHandleV1,\n} from \"./runner-pool.ts\";\nimport { createNodeSubagentRunnerSpawnerV1 } from \"./runner-spawn.ts\";\nimport { resolveSelfInvocationV1, type SelfInvocationV1 } from \"./self-invocation.ts\";\nimport { createAgentTaskApiAdapterV1 } from \"./shared-adapter.ts\";\nimport {\n\tcreateSubagentRuntime,\n\ttype SchedulerTaskRecordV1,\n\tSubagentRuntimeError,\n\ttype SubagentRuntimeLimitsV1,\n\ttype SubagentRuntimeStatus,\n\ttype SubagentRuntimeV1,\n\ttype SubagentTaskHandleV1,\n} from \"./subagent-runtime.ts\";\n\nexport const capabilityManifest = {\n\tid: \"agent-forge.builtin.subagent-delegate\",\n\tversion: \"0.1.0\",\n\tapiVersion: EXPERIMENTAL_PUBLIC_API_VERSION,\n\t// `agents` 是可选能力:runner 的叶子会话刻意关闭 agent tasks(enableAgentTasks: false),\n\t// 此时本能力\"不适用\"而非\"加载失败\",缺失时静默不注册工具,避免每个叶子会话刷拒绝警告。\n\trequiredCapabilities: [\"session\", \"tools\"],\n\toptionalCapabilities: [\"agents\"],\n\t/**\n\t * 用户可配置默认值(D-044 §6 四层归属的 Profile/capability 配置层):\n\t * 经 agent-dir 配置通道 `<agentDir>/capabilities/builtin.subagent-delegate.json`\n\t * 浅合并覆盖;宿主显式 options 优先级最高;非法值在装载时 fail fast。\n\t */\n\tconfig: {\n\t\tmaxConcurrentSubagents: 8,\n\t\tmaxQueueDepth: 200,\n\t\tmaxPromptBytes: 65536,\n\t\tmaxMetadataBytes: 16384,\n\t\tmaxResultBytes: 524288,\n\t\tmaxMessages: 2000,\n\t\tdrainTimeoutMs: 30000,\n\t\tmaxRetainedRecords: 256,\n\t\tmaxTasksPerCall: 4,\n\t\ttaskTimeoutMs: 300000,\n\t\t// pooled 模式(D-045):0 = 关闭(默认,shared 形态,零额外进程)。\n\t\trunnerMaxProcesses: 0,\n\t\trunnerMaxSessionsPerRunner: 8,\n\t\trunnerIdleTimeoutMs: 300000,\n\t\trunnerSweepIntervalMs: 60000,\n\t\trunnerEntryAuto: false,\n\t},\n} as const;\n\nexport interface SubagentDelegateCapabilityOptions {\n\t/** 宿主可注入调度器 adapter(如 pooled runner adapter);优先级高于配置驱动的 pooled 装配与共享模式。 */\n\tadapter?: AgentTaskAdapter;\n\tlimits?: SubagentRuntimeLimitsV1;\n\t/** 单次工具调用的并行任务上限(默认 4)与单任务 drain 超时(默认 300000ms)。 */\n\tmaxTasksPerCall?: number;\n\ttaskTimeoutMs?: number;\n\t/**\n\t * 测试/宿主注入的 runner spawn 工厂(优先级高于 `runnerEntryPath` 配置):\n\t * 生产默认由 `createNodeSubagentRunnerSpawnerV1` 派生真实进程,测试注入进程内 fake。\n\t */\n\trunnerSpawner?: () => SubagentRunnerSpawnHandleV1;\n\t/**\n\t * 角色目录与方案关联(角色只来自磁盘 `<agentDir>/roles/<id>.json`,见\n\t * {@link resolveSubagentRoleCatalog})。库面调用方(测试/第三方)显式注入;\n\t * 插件 entry 由 `api.host.roleAssociations` + `api.host.agentDir` 组装。\n\t * 未提供时该会话**不暴露任何角色**(不是\"回退到内置\"——引擎没有内置角色)。\n\t */\n\troles?: { readonly agentDir: string; readonly associations: readonly string[] };\n\t/**\n\t * 通信面宿主接线(D-060 S4):父装配域注入 `drainEscalations`(取子→父上抛),\n\t * 子装配域注入 `postEscalate`/`sendSibling`/`drainInbox`(发上抛/发兄弟/取收件箱,\n\t * 身份与血缘都由宿主绑定校验)。未接线时通信工具一律不注册。\n\t */\n\tcommunicationWiring?: SubagentCommunicationWiringV1;\n\t/**\n\t * agentDir(承载退役切片 2):嵌套深度硬限沿 S2 落盘头行回溯血缘,需要 agentDir\n\t * 定位会话目录。库面显式注入优先;缺省回落到 `api.host.agentDir`(插件 entry 的\n\t * 单源通道,D-075 S4 第三批收敛——原 `roles.agentDir` 双通道已退役);两者都不在\n\t * 时深度门不生效(配额仍在)。\n\t */\n\tagentDir?: string;\n\t/**\n\t * 后台委派结算摘要 sink(D-060 S6):`background` 提交的任务每结算一个,宿主收到\n\t * 一条有界 JSON 行(taskId/sessionId/status/reasonCode),由宿主注入父的下一次\n\t * 模型轮次。未接线时后台照常可跑,只是没有推送感知。\n\t */\n\tbackgroundDigestSink?: (line: string) => void;\n\t/**\n\t * 委派提交时刻的回合 correlationId(第 3 期第二批):background 委派的提交动作\n\t * 发生在父回合工具阶段、此时有值。getter 缺席或返回 undefined 时不写键,行为回退。\n\t */\n\tgetTurnCorrelationId?: () => string | undefined;\n}\n\n/**\n * 通信面的方向开关(D-060 S4,**默认全关**):`api.config.communication` 里逐项显式\n * `true` 才开启。开启项只决定工具注册,作用域与有界性由宿主接线与信箱硬上限保证。\n */\nexport interface SubagentCommunicationConfigV1 {\n\t/** 父→子:对已终态且已落盘的子会话按 sessionId 续跑(`subagent_resume`)。steer 的程序面不经此开关。 */\n\treadonly resume?: true;\n\t/** 子→父:有界上抛消息(`subagent_escalate`),落在父下一次工具结果,不抢占父。 */\n\treadonly escalate?: true;\n\t/** 子↔子:同父兄弟互发有界消息(`subagent_send`/`subagent_inbox`)。 */\n\treadonly sibling?: true;\n}\n\n// 通信面的宿主接线 `SubagentCommunicationWiringV1`(含信箱消息形状)是 plugin-sdk\n// 公共契约(D-075 S4 第三批迁入 SDK),本包经上方 import 消费并在 index 再导出。\n\nconst COMMUNICATION_FLAG_KEYS = [\"resume\", \"escalate\", \"sibling\"] as const;\n\n/**\n * 配置通道解析(`api.config.communication`):缺省对象 = 全关;字段只接受 `true` 或省略,\n * 未知键与错误类型装载期 fail fast,不让\"配置了但没生效\"的假象存在。\n */\nfunction readCommunicationConfig(raw: unknown): SubagentCommunicationConfigV1 {\n\tif (raw === undefined) return {};\n\tif (raw === null || typeof raw !== \"object\" || Array.isArray(raw)) {\n\t\tthrow new Error(\"subagent-delegate config communication must be an object\");\n\t}\n\tconst record = raw as Record<string, unknown>;\n\tconst known = COMMUNICATION_FLAG_KEYS as readonly string[];\n\tconst unknown = Object.keys(record).filter((key) => !known.includes(key));\n\tif (unknown.length > 0) {\n\t\tthrow new Error(`subagent-delegate config communication has unknown key(s): ${unknown.join(\", \")}`);\n\t}\n\tconst config: Record<string, true> = {};\n\tfor (const key of COMMUNICATION_FLAG_KEYS) {\n\t\tconst value = record[key];\n\t\tif (value === undefined) continue;\n\t\tif (value !== true) throw new Error(`subagent-delegate config communication.${key} must be true or omitted`);\n\t\tconfig[key] = true;\n\t}\n\treturn config as SubagentCommunicationConfigV1;\n}\n\n/** 把宿主/调度器/信箱的结构化错误映射为工具面的同码委托错误(保留原始 message)。 */\nfunction mapCommunicationError(error: unknown): never {\n\tif (error instanceof SubagentMailboxErrorV1) {\n\t\tthrow new SubagentDelegateError(error.code, error.message);\n\t}\n\tif (\n\t\terror instanceof SubagentRuntimeError &&\n\t\t(error.code === \"subagent_unknown_task\" ||\n\t\t\terror.code === \"subagent_not_running\" ||\n\t\t\terror.code === \"subagent_steer_unsupported\")\n\t) {\n\t\tthrow new SubagentDelegateError(error.code, error.message);\n\t}\n\tthrow error as Error;\n}\n\n/** 结构化错误码:调用形状错误、批上限错误与未知角色分开表达,便于调用方分支。 */\nexport type SubagentDelegateErrorCodesV1 =\n\t| \"subagent_invalid_input\"\n\t| \"subagent_batch_too_large\"\n\t| \"subagent_unknown_role\"\n\t/** 宿主未落地或无法自证档案收窄:宁可失败,也不让\"只读评审者\"变成一句空话。 */\n\t| \"subagent_profile_not_applied\"\n\t// S4 通信面:目标定位、运行态与池化边界分开表达。\n\t| \"subagent_unknown_task\"\n\t| \"subagent_unknown_session\"\n\t| \"subagent_not_running\"\n\t| \"subagent_steer_unsupported\"\n\t| \"subagent_session_busy\"\n\t| \"subagent_message_too_large\"\n\t| \"subagent_escalate_quota\"\n\t| \"subagent_inbox_quota\"\n\t/** 嵌套委派深度超限(提交侧执行;与 S5 shared 侧同一语义)。 */\n\t| \"subagent_depth_exceeded\"\n\t/** 本会话累计创建的子代理达到每父级总量上限(提交侧执行)。 */\n\t| \"subagent_child_quota\"\n\t/** submit 同步拒绝且无结构化 code 时的本地兜底码(与投影 reasonCode 同名)。 */\n\t| \"subagent_submit_rejected\";\n\nexport class SubagentDelegateError extends Error {\n\treadonly code: SubagentDelegateErrorCodesV1;\n\n\tconstructor(code: SubagentDelegateErrorCodesV1, message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"SubagentDelegateError\";\n\t\tthis.code = code;\n\t}\n}\n\n/** 终态投影:调度器权威状态词汇 + 本地 submit 拒绝(\"rejected\")。 */\nexport type SubagentDelegateTaskStatusV1 = SubagentRuntimeStatus | \"rejected\";\n\nexport interface SubagentDelegateTaskProjectionV1 {\n\t/** 调度器任务 id;submit 同步抛错的 rejected 条目尚未生成 id,回空串。 */\n\treadonly taskId: string;\n\treadonly status: SubagentDelegateTaskStatusV1;\n\t/**\n\t * 结构化原因码(如 `subagent_queue_full`);无则省略。调度器/适配器抛出的错误\n\t * 无结构化 code 时用本地兜底码 `subagent_submit_rejected`。\n\t */\n\treadonly reasonCode?: string;\n\t/** 失败/取消的原始原因文本。 */\n\treadonly reason?: string;\n\t/** 调度器 settle 截断过消息快照时为 true;未截断时省略。 */\n\treadonly truncated?: true;\n\t/** 有界消息快照的条数(rejected/failed 无记录时为 0)。 */\n\treadonly messageCount: number;\n\t/** 子会话自身的 sessionId(D-060 S1):可按它在会话目录里定位这份 transcript。 */\n\treadonly sessionId?: string;\n\t/** 最后一条 assistant 文本的前 200 字符;找不到则省略。 */\n\treadonly summary?: string;\n\t/**\n\t * 子会话 escalate 上抛的有界消息(D-060 S4):落在父侧下一次工具结果里(这里或\n\t * `subagent_result`),父回合不被打断;无积压时省略。\n\t */\n\treadonly escalations?: readonly string[];\n}\n\nexport interface SubagentDelegateResultV1 {\n\treadonly tasks: readonly SubagentDelegateTaskProjectionV1[];\n}\n\n/** 后台委派票据(D-060 S6):提交即返回,结果稍后经 subagent_result / 摘要注入获得。 */\nexport interface SubagentDelegateBackgroundTaskV1 {\n\treadonly taskId: string;\n}\n\nexport interface SubagentDelegateBackgroundResultV1 {\n\treadonly background: true;\n\treadonly tasks: readonly SubagentDelegateBackgroundTaskV1[];\n}\n\n/**\n * 角色(persona):子代理侧身份,主代理按名调用。\n *\n * **定义只在磁盘上**(与 skill/MCP/插件同构,不搞内置特权):\n * - 共享目录 `<agentDir>/roles/<id>.json` → `{ \"description\": …, \"instructions\": … }`;\n * - 可见性由**当前方案**决定:只有 `suite.associations.roles` 关联到的 id 才出现在\n * `subagent_run` 的菜单里;没关联的角色对主代理不可见(不报错)。\n * - 工厂只做**种子落盘**({@link seedFactoryRoleCatalog},首次缺文件时写入 explore/reviewer\n * 两个普通文件,可改可删),默认方案关联它们,因此开箱即用;引擎从不回退到编译内的定义。\n *\n * 角色今天**确定性地决定子任务的任务框定**(instructions 组装进送交子会话的提示词),\n * 但**不改变子会话的模型与工具面**——后者需要\"子任务档案\"这一公共契约(见\n * docs/design/子代理运行时设计.md §13)。因此角色文件里出现 `model`/`tools`/`readonly`\n * 等键时在装载期直接报错,而不是静默忽略让用户以为\"只读评审者不会写文件\"。\n */\nexport interface SubagentDelegateRoleV1 {\n\treadonly id: string;\n\t/** 给主代理看的一句话说明(工具描述里列出),不送进子会话提示词。 */\n\treadonly description: string;\n\t/** 送进子会话提示词的角色指令(系统提示词由子会话继承父会话,不在这里重复)。 */\n\treadonly instructions: string;\n\t/**\n\t * 子任务档案(收窄语义,公共契约 AgentTaskChildProfileV1):模型 / 思考级别 /\n\t * 工具面 / 会话模式 / 追加系统提示词。子会话创建时由宿主落地,**创建后核验**;\n\t * 宿主不落地或无法核验时该条目失败(`subagent_profile_not_applied`),绝不静默忽略。\n\t * shared 与 pooled 两种形态都落地全部字段——见设计文档 §6。\n\t */\n\treadonly profile?: AgentTaskChildProfileV1;\n\t/**\n\t * 子读父的有界投影(D-060 §4,默认关闭):开启时把父会话的**目标**(最后一条用户文本,\n\t * ≤2KB)、父 sessionId 与父工具面名单拼进子任务提示词。只给目标不给历史——子会话拿不到\n\t * 父上下文全文。\n\t */\n\treadonly parentView?: boolean;\n}\n\n/**\n * 出厂种子(仅用于首次落盘,不是运行期定义):默认方案关联 explore / reviewer,\n * 因此新装环境开箱即用;用户改文件即生效,删文件则该关联成为 dangling 诊断。\n */\nconst FACTORY_ROLE_SEEDS: readonly SubagentDelegateRoleV1[] = Object.freeze([\n\tObject.freeze({\n\t\tid: \"explore\",\n\t\tdescription: \"Read-only explorer: locates code and reports file:line evidence\",\n\t\tinstructions:\n\t\t\t\"You are an exploration subagent. Find the relevant code, configuration or documentation and report concrete \" +\n\t\t\t\"evidence as file:line references with a one-line note per hit. Do not modify any file and do not run commands \" +\n\t\t\t\"with side effects. Answer with findings first, then the smallest set of next steps.\",\n\t}),\n\tObject.freeze({\n\t\tid: \"reviewer\",\n\t\tdescription: \"Code reviewer: reads a diff or change set and reports findings and risks\",\n\t\tinstructions:\n\t\t\t\"You are a code-review subagent. Read the change set you are given (diff, files or description) and report \" +\n\t\t\t\"findings ordered by severity: correctness first, then contract/regression risk, then clarity. For each finding \" +\n\t\t\t\"give the location, why it is wrong or risky, and the smallest fix. Do not modify files. State explicitly when \" +\n\t\t\t\"you found nothing in a category instead of inventing issues.\",\n\t}),\n]);\n\nconst ROLE_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,31}$/;\nconst ROLE_DESCRIPTION_CHARS = 200;\nconst ROLE_INSTRUCTIONS_CHARS = 8000;\n/** 角色数量与\"菜单\"总长上限:菜单是主代理工具描述的一部分,必须有界。 */\nconst ROLE_COUNT_LIMIT = 16;\nconst ROLE_MENU_CHARS = 2000;\nconst ROLE_KNOWN_KEYS = [\n\t\"description\",\n\t\"instructions\",\n\t\"model\",\n\t\"thinkingLevel\",\n\t\"tools\",\n\t\"mode\",\n\t\"systemPromptAppend\",\n\t\"parentView\",\n] as const;\n/** 用户角色目录(agentDir 下),与 `<agentDir>/capabilities/` 并列。 */\nconst ROLE_CATALOG_DIR = \"roles\";\n/** 解析结果:暴露集(按方案关联过滤后)+ 可诊断的异常,装载与诊断共用同一实现。 */\nexport interface SubagentRoleCatalogResolutionV1 {\n\t/** 当前方案可见的角色(内置定义 ∪ 用户目录,再按 associations 过滤)。 */\n\treadonly roles: ReadonlyMap<string, SubagentDelegateRoleV1>;\n\t/** 被方案关联、但内置与用户目录都没有定义的角色 id(诊断,不阻断)。 */\n\treadonly dangling: readonly string[];\n\t/** 未被关联、但用户文件非法的角色文件:只在被关联时才致命,这里记录用于诊断。 */\n\treadonly brokenUnassociated: readonly string[];\n}\n\nfunction validateRoleDefinition(id: string, value: unknown, origin: string): SubagentDelegateRoleV1 | undefined {\n\tif (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n\t\tthrow new Error(`${origin} must be an object with description and instructions`);\n\t}\n\tconst fields = value as Record<string, unknown>;\n\tfor (const key of Object.keys(fields)) {\n\t\tif (!(ROLE_KNOWN_KEYS as readonly string[]).includes(key)) {\n\t\t\tthrow new Error(`${origin}.${key} is not a recognized role field (known: ${ROLE_KNOWN_KEYS.join(\", \")})`);\n\t\t}\n\t}\n\tconst description = fields.description;\n\tif (typeof description !== \"string\" || description.trim() === \"\" || description.length > ROLE_DESCRIPTION_CHARS) {\n\t\tthrow new Error(\n\t\t\t`${origin}.description must be a non-empty string of at most ${ROLE_DESCRIPTION_CHARS} chars, got ` +\n\t\t\t\tJSON.stringify(description),\n\t\t);\n\t}\n\tconst instructions = fields.instructions;\n\tif (\n\t\ttypeof instructions !== \"string\" ||\n\t\tinstructions.trim() === \"\" ||\n\t\tinstructions.length > ROLE_INSTRUCTIONS_CHARS\n\t) {\n\t\tthrow new Error(`${origin}.instructions must be a non-empty string of at most ${ROLE_INSTRUCTIONS_CHARS} chars`);\n\t}\n\t// 档案字段交给公共解析器校验(类型/范围/未知键都在那里拦),错误文本带上文件来源。\n\tconst profileInput: Record<string, unknown> = { version: 1, roleId: id };\n\tfor (const key of [\"model\", \"thinkingLevel\", \"tools\", \"mode\", \"systemPromptAppend\"] as const) {\n\t\tif (fields[key] !== undefined) profileInput[key] = fields[key];\n\t}\n\tconst hasProfile = Object.keys(profileInput).length > 1;\n\tlet profile: AgentTaskChildProfileV1 | undefined;\n\tif (hasProfile) {\n\t\tconst parsed = parseAgentTaskChildProfileV1(profileInput);\n\t\tif (!parsed.ok) throw new Error(`${origin}: ${parsed.message}`);\n\t\tprofile = parsed.profile;\n\t}\n\tconst parentView = fields.parentView;\n\tif (parentView !== undefined && typeof parentView !== \"boolean\") {\n\t\tthrow new Error(`${origin}.parentView must be a boolean`);\n\t}\n\treturn {\n\t\tid,\n\t\tdescription,\n\t\tinstructions,\n\t\t...(profile === undefined ? {} : { profile }),\n\t\t...(parentView === true ? { parentView: true } : {}),\n\t};\n}\n\n/**\n * 解析当前方案的角色目录(**全部来自磁盘**,无编译内定义)。装载期一次性完成,\n * 任何会让\"配置了但没生效\"的情况都显式失败:\n * - 被关联 + 文件存在但非法 → 抛错(指名文件);\n * - 被关联但目录里没有该 id → dangling(诊断,角色不可见),调用该名走 `subagent_unknown_role`;\n * - 未被关联的坏文件只进诊断:别的方案不该被一个用不到的文件拖垮;\n * - 暴露集受 16 个 / 2000 字符上限约束(菜单是主代理工具描述的一部分)。\n */\nexport function resolveSubagentRoleCatalog(options: {\n\treadonly agentDir: string;\n\treadonly associations: readonly string[];\n}): SubagentRoleCatalogResolutionV1 {\n\tconst associated = [...new Set(options.associations)];\n\tconst roles = new Map<string, SubagentDelegateRoleV1>();\n\tconst dangling: string[] = [];\n\tconst brokenUnassociated: string[] = [];\n\tconst catalogDir = join(options.agentDir, ROLE_CATALOG_DIR);\n\tlet entries: string[] = [];\n\ttry {\n\t\tentries = readdirSync(catalogDir).filter((name) => name.endsWith(\".json\"));\n\t} catch {\n\t\t// 目录不存在(未种子化/未配置)是正常状态。\n\t\tentries = [];\n\t}\n\tfor (const entry of entries) {\n\t\tconst id = entry.slice(0, -\".json\".length);\n\t\tif (!ROLE_ID_PATTERN.test(id)) continue;\n\t\tconst origin = `${catalogDir}/${entry}`;\n\t\tconst isAssociated = associated.includes(id);\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = JSON.parse(readFileSync(join(catalogDir, entry), \"utf8\"));\n\t\t} catch (error) {\n\t\t\tif (isAssociated) {\n\t\t\t\tthrow new Error(`subagent role \"${id}\" is associated by this suite but ${origin} is unreadable`, {\n\t\t\t\t\tcause: error,\n\t\t\t\t});\n\t\t\t}\n\t\t\tbrokenUnassociated.push(id);\n\t\t\tcontinue;\n\t\t}\n\t\tlet role: SubagentDelegateRoleV1 | undefined;\n\t\ttry {\n\t\t\trole = validateRoleDefinition(id, parsed, origin);\n\t\t} catch (error) {\n\t\t\tif (isAssociated) throw error;\n\t\t\tbrokenUnassociated.push(id);\n\t\t\tcontinue;\n\t\t}\n\t\tif (isAssociated && role !== undefined) roles.set(id, role);\n\t}\n\tfor (const id of associated) {\n\t\tif (!ROLE_ID_PATTERN.test(id)) {\n\t\t\tthrow new Error(\n\t\t\t\t`suite associations.roles entry ${JSON.stringify(id)} is not a valid role id (expect lowercase [a-z0-9_-], ` +\n\t\t\t\t\t\"max 32 chars)\",\n\t\t\t);\n\t\t}\n\t\tif (!roles.has(id)) dangling.push(id);\n\t}\n\tif (roles.size > ROLE_COUNT_LIMIT) {\n\t\tthrow new Error(\n\t\t\t`suite associations.roles exposes ${roles.size} roles; at most ${ROLE_COUNT_LIMIT} are allowed — the role menu ` +\n\t\t\t\t\"rides in the parent's tool description on every request, and an unbounded menu degrades the main agent's own context\",\n\t\t);\n\t}\n\tconst menuChars = [...roles.values()].reduce(\n\t\t(total, role) => total + role.id.length + role.description.length + 3,\n\t\t0,\n\t);\n\tif (menuChars > ROLE_MENU_CHARS) {\n\t\tthrow new Error(\n\t\t\t`suite associations.roles menu is ${menuChars} chars; at most ${ROLE_MENU_CHARS} are allowed — shorten role ` +\n\t\t\t\t\"descriptions or associate fewer roles (the menu is part of the parent's tool description)\",\n\t\t);\n\t}\n\treturn { roles, dangling: Object.freeze(dangling), brokenUnassociated: Object.freeze(brokenUnassociated) };\n}\n\n/** 角色文件名(`<agentDir>/roles/<id>.json`),供种子与目录与文档共用一处。 */\nexport function subagentRoleCatalogPath(agentDir: string, id: string): string {\n\treturn join(agentDir, ROLE_CATALOG_DIR, `${id}.json`);\n}\n\n/**\n * 出厂种子落盘(仅补缺,绝不覆盖既有文件):默认方案关联 explore/reviewer,\n * 因此新环境开箱即用;这两个文件与用户自己写的角色完全同权——改文件即生效,\n * 删文件使关联成为 dangling 诊断。引擎运行期从不回退到编译内的角色定义。\n */\nexport function seedFactoryRoleCatalog(agentDir: string): void {\n\tconst dir = join(agentDir, ROLE_CATALOG_DIR);\n\ttry {\n\t\tmkdirSync(dir, { recursive: true });\n\t} catch {\n\t\treturn; // 不可写(只读 agentDir):角色可选,不阻断启动。\n\t}\n\tfor (const role of FACTORY_ROLE_SEEDS) {\n\t\tconst path = subagentRoleCatalogPath(agentDir, role.id);\n\t\tif (existsSync(path)) continue;\n\t\ttry {\n\t\t\twriteFileSync(\n\t\t\t\tpath,\n\t\t\t\t`${JSON.stringify({ description: role.description, instructions: role.instructions }, null, 2)}\\n`,\n\t\t\t\t{\n\t\t\t\t\tencoding: \"utf8\",\n\t\t\t\t\tflag: \"wx\",\n\t\t\t\t},\n\t\t\t);\n\t\t} catch {\n\t\t\t// 并发种子化或只读目录:已在磁盘上或不需要,保持现状。\n\t\t}\n\t}\n}\n\n/** 父投影上限:目标文本与工具面名单都取小值(这是\"背景\",不是父上下文回灌)。 */\nconst PARENT_VIEW_GOAL_CHARS = 2000;\nconst PARENT_VIEW_TOOL_LIMIT = 40;\n\n/** 从父会话消息里取最后一条用户文本(有界),作为\"父当前目标\"。 */\nfunction lastUserText(messages: readonly unknown[], limit: number): string | undefined {\n\tfor (let index = messages.length - 1; index >= 0; index -= 1) {\n\t\tconst message = messages[index];\n\t\tif (typeof message !== \"object\" || message === null) continue;\n\t\tconst record = message as { role?: string; content?: unknown };\n\t\tif (record.role !== \"user\") continue;\n\t\tconst raw =\n\t\t\ttypeof record.content === \"string\"\n\t\t\t\t? record.content\n\t\t\t\t: Array.isArray(record.content)\n\t\t\t\t\t? record.content.map((part) => (part as { type?: string; text?: string }).text ?? \"\").join(\"\")\n\t\t\t\t\t: \"\";\n\t\tconst trimmed = raw.trim();\n\t\tif (trimmed === \"\") continue;\n\t\treturn trimmed.length <= limit ? trimmed : `${trimmed.slice(0, limit)}…`;\n\t}\n\treturn undefined;\n}\n\n/**\n * 子读父(D-060 §4):父会话的**有界投影**——sessionId、当前目标(最后一条用户文本,截断)、\n * 工具面名单。默认关闭;开启后拼在角色指令与任务之间。父历史全文永不进入子会话。\n */\nfunction composeParentView(session: CapabilityAPI[\"session\"], parentSessionId: string): string | undefined {\n\tconst goal = session === undefined ? undefined : lastUserText(session.getMessages(), PARENT_VIEW_GOAL_CHARS);\n\tconst tools = session?.getActiveTools?.() ?? [];\n\tconst toolList = tools.slice(0, PARENT_VIEW_TOOL_LIMIT).join(\", \");\n\treturn [\n\t\t\"Parent context (bounded projection; the parent's full history is not available to you):\",\n\t\t`- parent session: ${parentSessionId}`,\n\t\t...(goal === undefined ? [] : [`- current goal: ${goal}`]),\n\t\t...(toolList === \"\" ? [] : [`- parent tools: ${toolList}`]),\n\t].join(\"\\n\");\n}\n\n/**\n * 角色框定:角色指令在上、任务在下,任务原文原样保留(子会话看不到父会话历史,\n * 因此这段文本是它唯一的\"我是谁 + 做什么\"来源)。组装后的字节数仍走 submit 侧\n * maxPromptBytes 预算,超限按预算拒绝而不是静默截断。\n */\nfunction composeRolePrompt(role: SubagentDelegateRoleV1, prompt: string): string {\n\treturn `${role.instructions}\\n\\nTask:\\n${prompt}`;\n}\n\n/**\n * 父→子指针注入(错误教训与召回质量护栏设计 §2.3):可选结构化 `context`(文件:行\n * 锚点、已试失败命令清单),确定性渲染进子会话首条 prompt 头部。只传路标不传内容:\n * 子代理仍自行读取真实文件,指针过期不产生错误事实。硬边界超限走 schema 校验失败\n * 整体拒绝——拒绝优于静默截断:父代理必须知道子代理没拿到完整锚点。\n */\nexport interface SubagentDelegateContextAnchorV1 {\n\treadonly path: string;\n\treadonly line?: number;\n\treadonly note?: string;\n}\n\nexport interface SubagentDelegateContextFailedAttemptV1 {\n\treadonly command: string;\n\treadonly note?: string;\n}\n\nexport interface SubagentDelegateContextV1 {\n\treadonly anchors?: readonly SubagentDelegateContextAnchorV1[];\n\treadonly failedAttempts?: readonly SubagentDelegateContextFailedAttemptV1[];\n}\n\n/** context 硬边界(设计 §2.3):两个列表 ≤10 项、note ≤200 字符、path/command ≤500 字符。 */\nconst CONTEXT_LIST_MAX_ITEMS = 10;\nconst CONTEXT_NOTE_MAX_CHARS = 200;\nconst CONTEXT_PATH_MAX_CHARS = 500;\nconst CONTEXT_COMMAND_MAX_CHARS = 500;\n\nfunction parseContextNote(value: unknown, origin: string): string | undefined {\n\tif (value === undefined) return undefined;\n\tif (typeof value !== \"string\" || value.length > CONTEXT_NOTE_MAX_CHARS) {\n\t\tthrow new SubagentDelegateError(\n\t\t\t\"subagent_invalid_input\",\n\t\t\t`${origin} must be a string of at most ${CONTEXT_NOTE_MAX_CHARS} chars`,\n\t\t);\n\t}\n\treturn value === \"\" ? undefined : value;\n}\n\nfunction parseContextList<T>(\n\tvalue: unknown,\n\tlabel: string,\n\tparseEntry: (entry: Record<string, unknown>, origin: string) => T,\n): readonly T[] {\n\tif (!Array.isArray(value)) {\n\t\tthrow new SubagentDelegateError(\"subagent_invalid_input\", `subagent_run context.${label} must be an array`);\n\t}\n\tif (value.length > CONTEXT_LIST_MAX_ITEMS) {\n\t\tthrow new SubagentDelegateError(\n\t\t\t\"subagent_invalid_input\",\n\t\t\t`subagent_run context.${label} accepts at most ${CONTEXT_LIST_MAX_ITEMS} entries; received ${value.length}`,\n\t\t);\n\t}\n\treturn value.map((entry, index) => {\n\t\tconst origin = `subagent_run context.${label}[${index}]`;\n\t\tif (!isPlainRecord(entry)) {\n\t\t\tthrow new SubagentDelegateError(\"subagent_invalid_input\", `${origin} must be an object`);\n\t\t}\n\t\treturn parseEntry(entry, origin);\n\t});\n}\n\n/**\n * 解析工具入参里的 `context`(形状/边界违规都以 `subagent_invalid_input` 整体拒绝);\n * 两个列表皆空时归一化为 undefined——与 context 缺省同一路径,整块不渲染。\n */\nfunction parseDelegateContext(value: unknown): SubagentDelegateContextV1 | undefined {\n\tif (value === undefined) return undefined;\n\tif (!isPlainRecord(value)) {\n\t\tthrow new SubagentDelegateError(\"subagent_invalid_input\", \"subagent_run context must be an object\");\n\t}\n\tconst anchors =\n\t\tvalue.anchors === undefined\n\t\t\t? undefined\n\t\t\t: parseContextList(value.anchors, \"anchors\", (entry, origin): SubagentDelegateContextAnchorV1 => {\n\t\t\t\t\tconst path = entry.path;\n\t\t\t\t\tif (typeof path !== \"string\" || path === \"\" || path.length > CONTEXT_PATH_MAX_CHARS) {\n\t\t\t\t\t\tthrow new SubagentDelegateError(\n\t\t\t\t\t\t\t\"subagent_invalid_input\",\n\t\t\t\t\t\t\t`${origin}.path must be a non-empty string of at most ${CONTEXT_PATH_MAX_CHARS} chars`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst rawLine = entry.line;\n\t\t\t\t\tconst line =\n\t\t\t\t\t\trawLine === undefined\n\t\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t\t: ((): number => {\n\t\t\t\t\t\t\t\t\tif (typeof rawLine !== \"number\" || !Number.isInteger(rawLine) || rawLine < 1) {\n\t\t\t\t\t\t\t\t\t\tthrow new SubagentDelegateError(\n\t\t\t\t\t\t\t\t\t\t\t\"subagent_invalid_input\",\n\t\t\t\t\t\t\t\t\t\t\t`${origin}.line must be a positive integer`,\n\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\treturn rawLine;\n\t\t\t\t\t\t\t\t})();\n\t\t\t\t\tconst note = parseContextNote(entry.note, `${origin}.note`);\n\t\t\t\t\treturn {\n\t\t\t\t\t\tpath,\n\t\t\t\t\t\t...(line === undefined ? {} : { line }),\n\t\t\t\t\t\t...(note === undefined ? {} : { note }),\n\t\t\t\t\t};\n\t\t\t\t});\n\tconst failedAttempts =\n\t\tvalue.failedAttempts === undefined\n\t\t\t? undefined\n\t\t\t: parseContextList(\n\t\t\t\t\tvalue.failedAttempts,\n\t\t\t\t\t\"failedAttempts\",\n\t\t\t\t\t(entry, origin): SubagentDelegateContextFailedAttemptV1 => {\n\t\t\t\t\t\tconst command = entry.command;\n\t\t\t\t\t\tif (typeof command !== \"string\" || command === \"\" || command.length > CONTEXT_COMMAND_MAX_CHARS) {\n\t\t\t\t\t\t\tthrow new SubagentDelegateError(\n\t\t\t\t\t\t\t\t\"subagent_invalid_input\",\n\t\t\t\t\t\t\t\t`${origin}.command must be a non-empty string of at most ${CONTEXT_COMMAND_MAX_CHARS} chars`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst note = parseContextNote(entry.note, `${origin}.note`);\n\t\t\t\t\t\treturn { command, ...(note === undefined ? {} : { note }) };\n\t\t\t\t\t},\n\t\t\t\t);\n\tif ((anchors?.length ?? 0) === 0 && (failedAttempts?.length ?? 0) === 0) return undefined;\n\treturn {\n\t\t...(anchors === undefined ? {} : { anchors }),\n\t\t...(failedAttempts === undefined ? {} : { failedAttempts }),\n\t};\n}\n\n/**\n * 把 context 确定性渲染为子会话首条 prompt 的头部块(模板逐字,设计 §2.3):line\n * 缺省只输出 path、note 缺省省略 \" — <note>\"、某列表为空不渲染对应小节、两个列表\n * 皆空(context 已在解析侧归一化)返回 undefined——子会话 prompt 与现状逐字节一致。\n */\nfunction renderDelegateContext(context: SubagentDelegateContextV1 | undefined): string | undefined {\n\tif (context === undefined) return undefined;\n\tconst lines = [\"[parent context anchors — may be stale; verify against current files]\"];\n\tconst anchors = context.anchors ?? [];\n\tif (anchors.length > 0) {\n\t\tlines.push(\"Key locations:\");\n\t\tfor (const anchor of anchors) {\n\t\t\tconst location = anchor.line === undefined ? anchor.path : `${anchor.path}:${anchor.line}`;\n\t\t\tlines.push(`- ${location}${anchor.note === undefined ? \"\" : ` — ${anchor.note}`}`);\n\t\t}\n\t}\n\tconst failedAttempts = context.failedAttempts ?? [];\n\tif (failedAttempts.length > 0) {\n\t\tlines.push(\"Tried and failed:\");\n\t\tfor (const attempt of failedAttempts) {\n\t\t\tlines.push(`- \\`${attempt.command}\\`${attempt.note === undefined ? \"\" : ` — ${attempt.note}`}`);\n\t\t}\n\t}\n\treturn lines.length === 1 ? undefined : lines.join(\"\\n\");\n}\n\n/**\n * 核验档案是否真的落到了子会话上:工具面必须是请求白名单的子集,模型必须等于请求值,\n * 模式请求 `readonly` 时必须已是只读。宿主不暴露可核验事实(getActiveTools/getModel)\n * 也算未落地——\"要求了但无法证明\"与\"没要求\"对上层是同一种风险。\n */\nfunction verifyChildProfileApplied(host: { session: unknown }, requested: unknown): string | undefined {\n\tconst parsed = parseAgentTaskChildProfileV1(requested);\n\tif (!parsed.ok) return `child task profile is invalid: ${parsed.message}`;\n\tconst profile = parsed.profile;\n\tconst session = host.session as {\n\t\tgetActiveTools?: () => string[];\n\t\tgetModel?: () => { id?: string } | undefined;\n\t};\n\tif (profile.tools !== undefined || profile.mode === \"readonly\") {\n\t\tconst active = session.getActiveTools?.();\n\t\tif (active === undefined) {\n\t\t\treturn \"host does not expose getActiveTools(); the requested tool narrowing cannot be verified\";\n\t\t}\n\t\tif (profile.tools !== undefined) {\n\t\t\tconst extra = active.filter((name) => !profile.tools?.includes(name));\n\t\t\tif (extra.length > 0) {\n\t\t\t\treturn `child session kept tools outside the requested allowlist: ${extra.join(\", \")}`;\n\t\t\t}\n\t\t}\n\t}\n\tif (profile.model !== undefined) {\n\t\tconst model = session.getModel?.();\n\t\tif (model === undefined) {\n\t\t\treturn \"host does not expose getModel(); the requested model cannot be verified\";\n\t\t}\n\t\tif (model.id !== profile.model) {\n\t\t\treturn `child session is on model ${JSON.stringify(model.id)} instead of the requested ${JSON.stringify(profile.model)}`;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nfunction resolveRole(\n\tvalue: unknown,\n\troles: ReadonlyMap<string, SubagentDelegateRoleV1>,\n): SubagentDelegateRoleV1 | undefined {\n\tif (value === undefined || value === null) return undefined;\n\tif (typeof value !== \"string\" || value.trim() === \"\") {\n\t\tthrow new SubagentDelegateError(\"subagent_invalid_input\", \"subagent_run role must be a non-empty string\");\n\t}\n\tconst role = roles.get(value);\n\tif (role === undefined) {\n\t\tconst available = [...roles.keys()].sort();\n\t\tthrow new SubagentDelegateError(\n\t\t\t\"subagent_unknown_role\",\n\t\t\t`unknown subagent role ${JSON.stringify(value)}; available roles: ` +\n\t\t\t\t(available.length === 0 ? \"(none configured)\" : available.join(\", \")),\n\t\t);\n\t}\n\treturn role;\n}\n\n/** 单条 summary 的字符上限(上下文预算:只回摘要,不回 messages 原文)。 */\nconst SUMMARY_CHARS = 200;\n\n/** `subagent_result` 默认/上限返回条数(有界回读,绝不默认回灌全文)。 */\nconst RESULT_READ_DEFAULT_MESSAGES = 20;\nconst RESULT_READ_MAX_MESSAGES = 50;\n/** `subagent_result` 的有界等待上限(后台结算收割),默认 0 = 只查一次。 */\nconst RESULT_READ_MAX_WAIT_MS = 120_000;\n\n/** `subagent_result` 的返回形状:两个来源(调度记录 / 持久化 transcript)共用一个有界信封。 */\nexport interface SubagentResultReadResultV1 {\n\treadonly found: boolean;\n\treadonly source: \"record\" | \"session\";\n\treadonly taskId?: string;\n\treadonly sessionId?: string;\n\treadonly status?: string;\n\treadonly reasonCode?: string;\n\treadonly reason?: string;\n\treadonly roleId?: string;\n\t/** 来源里的总条数(截断前)。 */\n\treadonly messageCount?: number;\n\treadonly entryCount?: number;\n\treadonly returnedMessages?: number;\n\t/** 整份来源的 UTF-8 字节数。 */\n\treadonly bytes?: number;\n\treadonly truncated?: boolean;\n\treadonly messages?: readonly unknown[];\n\t/** 该子会话 escalate 上抛的有界消息(取后即清);无积压时省略。 */\n\treadonly escalations?: readonly string[];\n\t/** taskId 已知但尚未结算(后台委派):稍后再取或带 waitMs 等待。 */\n\treadonly knownTask?: true;\n\t/** 未结算任务的当前调度器状态(queued/admitted/running)。 */\n\treadonly taskStatus?: string;\n}\n\n/** `subagent_list` 的条目:store 摘要 + 调度器状态富化(有则附)。 */\nexport interface SubagentSessionListEntryV1 {\n\treadonly sessionId: string;\n\treadonly agentRole?: string;\n\treadonly createdAt?: string;\n\treadonly modifiedAt: number;\n\treadonly bytes: number;\n\t/** 本调度器里仍有未结算任务时为 true。 */\n\treadonly active?: true;\n\t/** 终态记录可查时为调度器权威状态词汇。 */\n\treadonly status?: SubagentRuntimeStatus;\n\treadonly reasonCode?: string;\n\treadonly reason?: string;\n}\n\nexport interface SubagentSessionListResultV1 {\n\treadonly count: number;\n\treadonly sessions: readonly SubagentSessionListEntryV1[];\n}\n\n/** UTF-8 字节数(回读信封如实报告体积)。 */\nfunction utf8ByteLength(text: string): number {\n\treturn Buffer.byteLength(text, \"utf8\");\n}\n\nconst DEFAULT_MAX_TASKS_PER_CALL = 4;\nconst DEFAULT_TASK_TIMEOUT_MS = 300_000;\n\n/**\n * 配置通道读取(manifest 默认 + agent-dir 文件合并后的 `api.config`):\n * 非整数/低于下限即 fail fast 并指名键,不让错误配置静默生效。\n */\nfunction configInt(api: CapabilityAPI, key: string, min: number): number | undefined {\n\tconst value = api.config[key];\n\tif (value === undefined) return undefined;\n\tif (typeof value !== \"number\" || !Number.isInteger(value) || value < min) {\n\t\tthrow new Error(`subagent-delegate config ${key} must be an integer >= ${min}, got ${JSON.stringify(value)}`);\n\t}\n\treturn value;\n}\n\n/** options > config > 默认,最终值仍需通过同一套下限校验。 */\nfunction resolveInt(\n\texplicit: number | undefined,\n\tconfigured: number | undefined,\n\tfallback: number,\n\tmin: number,\n\tlabel: string,\n): number {\n\tconst value = explicit ?? configured ?? fallback;\n\tif (!Number.isInteger(value) || value < min) {\n\t\tthrow new Error(`subagent-delegate ${label} must be an integer >= ${min}, got ${JSON.stringify(value)}`);\n\t}\n\treturn value;\n}\n\n/**\n * 宽松调用兼容(对齐 goal/plan/todo-set):标准签名的第二参为 params,直接调用时第一参即 params。\n * 生产装配向 runtime invoke 面注入 toolExecutionContext 作为 execute 的第二参(公共契约),\n * 因此\"第二参非 undefined 即输入\"的判定会把 context 误当输入(prompts 读成 undefined)。\n * 改按形状判定:第一参是普通对象即取第一参,否则看第二参。\n */\nfunction objectInput(first: unknown, second: unknown): Record<string, unknown> {\n\tconst primary = isPlainRecord(first) ? first : second;\n\tif (!isPlainRecord(primary)) {\n\t\tthrow new SubagentDelegateError(\"subagent_invalid_input\", \"subagent_run input must be an object\");\n\t}\n\treturn primary;\n}\n\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction readPrompts(value: unknown, maxTasksPerCall: number): string[] {\n\tif (!Array.isArray(value) || value.length === 0) {\n\t\tthrow new SubagentDelegateError(\"subagent_invalid_input\", \"subagent_run requires a non-empty prompts array\");\n\t}\n\tconst prompts: string[] = [];\n\tfor (const entry of value) {\n\t\tif (typeof entry !== \"string\" || entry.trim() === \"\") {\n\t\t\tthrow new SubagentDelegateError(\"subagent_invalid_input\", \"subagent_run prompts must be non-empty strings\");\n\t\t}\n\t\tprompts.push(entry);\n\t}\n\tif (prompts.length > maxTasksPerCall) {\n\t\tthrow new SubagentDelegateError(\n\t\t\t\"subagent_batch_too_large\",\n\t\t\t`subagent_run accepts at most ${maxTasksPerCall} prompts per call; received ${prompts.length}`,\n\t\t);\n\t}\n\treturn prompts;\n}\n\nfunction errorText(error: unknown): string {\n\tif (error instanceof Error) return error.message;\n\tif (typeof error === \"string\") return error;\n\ttry {\n\t\tconst serialized = JSON.stringify(error);\n\t\treturn serialized === undefined ? String(error) : serialized;\n\t} catch {\n\t\treturn String(error);\n\t}\n}\n\n/** 结构化原因码:优先取错误自带的 code(调度器/适配器的结构化错误),否则表达为 submit 拒绝。 */\nfunction reasonCodeOf(error: unknown): string {\n\tif (error && typeof error === \"object\" && typeof (error as { code?: unknown }).code === \"string\") {\n\t\treturn (error as { code: string }).code;\n\t}\n\treturn \"subagent_submit_rejected\";\n}\n\n/** 文本块抽取:content 为字符串或 text 块数组;无文本返回 undefined。 */\nfunction messageText(content: unknown): string | undefined {\n\tif (typeof content === \"string\") return content === \"\" ? undefined : content;\n\tif (!Array.isArray(content)) return undefined;\n\tconst parts: string[] = [];\n\tfor (const item of content) {\n\t\tif (!item || typeof item !== \"object\") continue;\n\t\tconst block = item as Record<string, unknown>;\n\t\tif (block.type !== \"text\" || typeof block.text !== \"string\") continue;\n\t\tparts.push(block.text);\n\t}\n\tconst text = parts.join(\"\");\n\treturn text === \"\" ? undefined : text;\n}\n\n/** 最后一条带文本的 assistant 消息 → 有界 summary;找不到(或全无文本)省略。 */\nfunction assistantSummary(messages: readonly unknown[]): string | undefined {\n\tfor (let index = messages.length - 1; index >= 0; index -= 1) {\n\t\tconst message = messages[index];\n\t\tif (!message || typeof message !== \"object\") continue;\n\t\tconst record = message as Record<string, unknown>;\n\t\tif (record.role !== \"assistant\" && record.type !== \"assistant\") continue;\n\t\tconst text = messageText(record.content);\n\t\tif (text === undefined) continue;\n\t\treturn text.length <= SUMMARY_CHARS ? text : text.slice(0, SUMMARY_CHARS);\n\t}\n\treturn undefined;\n}\n\nfunction projectRecord(record: SchedulerTaskRecordV1): SubagentDelegateTaskProjectionV1 {\n\tconst summary = assistantSummary(record.messages);\n\treturn {\n\t\ttaskId: record.taskId,\n\t\tstatus: record.status,\n\t\t...(record.reasonCode === undefined ? {} : { reasonCode: record.reasonCode }),\n\t\t...(record.reason === undefined ? {} : { reason: record.reason }),\n\t\t...(record.truncated ? { truncated: true as const } : {}),\n\t\tmessageCount: record.messages.length,\n\t\t...(record.sessionId === undefined || record.sessionId === \"\" ? {} : { sessionId: record.sessionId }),\n\t\t...(summary === undefined ? {} : { summary }),\n\t};\n}\n\n/**\n * 血缘深度(承载退役切片 2,Option A):沿 S2 落盘头行的 `parentAgentSession` 上行,\n * 返回本会话的祖先层数。root(无头行/非落盘会话)返回 undefined——不设限。\n * 最多上行走 `maxWalk` 步(有界,防环按步数截断)。\n *\n * 迁移注记(D-075 S4 第三批):宿主实现的逐头回读(`readAgentSession` 循环)改为\n * plugin-sdk 的 `readAgentSessionLineage`——同一落盘契约的只读首行实现。语义对齐点:\n * `ancestors` 只含**可解析**的祖先,而宿主 depth 对\"父指针存在但父会话未落盘\"的\n * 最后一条边也计数(root 在内存里、文件不可达是常态),因此 dangling 边补 +1;\n * `truncated`(走满 maxWalk 步仍有可解析父)时 ancestors.length 与宿主的 maxWalk\n * 返回值一致。\n */\nfunction lineageDepth(sessionId: string, agentDir: string, maxWalk: number): number | undefined {\n\tconst lineage = readAgentSessionLineage(agentDir, sessionId, maxWalk);\n\tif (lineage.self === undefined) return undefined;\n\tconst last = lineage.ancestors.at(-1) ?? lineage.self;\n\tconst danglingParent = last.parentAgentSession !== undefined && last.parentAgentSession !== last.sessionId;\n\treturn lineage.ancestors.length + (danglingParent && !lineage.truncated ? 1 : 0);\n}\n\n/**\n * 第一方编排 capability:一个调度器实例 + `subagent_run` 工具 + 释放登记。\n * 只消费公共 API(session/agents/sessionAbortSignal/tools),不 import core 内部私有模块。\n */\nexport function createCapability(\n\tapi: CapabilityAPI,\n\toptions?: SubagentDelegateCapabilityOptions,\n): DisposableRegistration[] {\n\tconst session = api.session;\n\tconst agents = api.agents;\n\tif (!session) throw new Error(\"subagent-delegate requires the session capability\");\n\t// 通信面配置在装载期 fail fast(两个上下文都读同一份 agent-dir 配置文件)。\n\tconst communication = readCommunicationConfig(api.config.communication);\n\tconst wiring = options?.communicationWiring;\n\t// 子侧通信面(子→父 / 子↔子):只要宿主接线了 post 通道且配置开启就注册——\n\t// 叶子会话(无 agents)只拿这一面;获准自建会话(D-060 S5,agents 在场)两面都要:\n\t// 它既能向下委派(parent face),也仍能向自己的父上抛/收兄弟消息(child face)。\n\tconst childTools: DisposableRegistration[] = [];\n\tif (wiring?.postEscalate !== undefined && communication.escalate === true) {\n\t\tchildTools.push(\n\t\t\tapi.registerTool({\n\t\t\t\tname: \"subagent_escalate\",\n\t\t\t\tlabel: \"Escalate to parent\",\n\t\t\t\tdescription:\n\t\t\t\t\t\"Send one bounded message up to your parent agent. It does NOT interrupt the parent; it is delivered \" +\n\t\t\t\t\t\"with the parent's next task result. Use it for a blocker or a key finding the task result alone would lose.\",\n\t\t\t\tparameters: Type.Object({\n\t\t\t\t\ttext: Type.String({\n\t\t\t\t\t\tminLength: 1,\n\t\t\t\t\t\tdescription: `Message to the parent (at most ${SUBAGENT_MAILBOX_MAX_MESSAGE_BYTES} utf-8 bytes)`,\n\t\t\t\t\t}),\n\t\t\t\t}),\n\t\t\t\texecute: async (first: unknown, second?: unknown) => {\n\t\t\t\t\tconst input = objectInput(first, second);\n\t\t\t\t\tconst text = typeof input.text === \"string\" ? input.text : \"\";\n\t\t\t\t\tif (text.trim() === \"\") {\n\t\t\t\t\t\tthrow new SubagentDelegateError(\n\t\t\t\t\t\t\t\"subagent_invalid_input\",\n\t\t\t\t\t\t\t\"subagent_escalate requires non-empty text\",\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\ttry {\n\t\t\t\t\t\twiring.postEscalate?.(text);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tmapCommunicationError(error);\n\t\t\t\t\t}\n\t\t\t\t\treturn { delivered: true, note: \"the parent reads this with its next task result\" };\n\t\t\t\t},\n\t\t\t}),\n\t\t);\n\t}\n\tif (wiring?.sendSibling !== undefined && communication.sibling === true) {\n\t\tchildTools.push(\n\t\t\tapi.registerTool({\n\t\t\t\tname: \"subagent_send\",\n\t\t\t\tlabel: \"Message a sibling\",\n\t\t\t\tdescription:\n\t\t\t\t\t\"Send one bounded message to a sibling subagent (a child of the same parent), addressed by its sessionId. \" +\n\t\t\t\t\t\"Cross-family addressing is rejected.\",\n\t\t\t\tparameters: Type.Object({\n\t\t\t\t\tto: Type.String({ description: \"Recipient child session id\" }),\n\t\t\t\t\ttext: Type.String({\n\t\t\t\t\t\tminLength: 1,\n\t\t\t\t\t\tdescription: `Message (at most ${SUBAGENT_MAILBOX_MAX_MESSAGE_BYTES} utf-8 bytes)`,\n\t\t\t\t\t}),\n\t\t\t\t}),\n\t\t\t\texecute: async (first: unknown, second?: unknown) => {\n\t\t\t\t\tconst input = objectInput(first, second);\n\t\t\t\t\tconst to = typeof input.to === \"string\" ? input.to : \"\";\n\t\t\t\t\tconst text = typeof input.text === \"string\" ? input.text : \"\";\n\t\t\t\t\tif (to === \"\" || text.trim() === \"\") {\n\t\t\t\t\t\tthrow new SubagentDelegateError(\n\t\t\t\t\t\t\t\"subagent_invalid_input\",\n\t\t\t\t\t\t\t\"subagent_send requires to and non-empty text\",\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\ttry {\n\t\t\t\t\t\twiring.sendSibling?.(to, text);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tmapCommunicationError(error);\n\t\t\t\t\t}\n\t\t\t\t\treturn { delivered: true };\n\t\t\t\t},\n\t\t\t}),\n\t\t\tapi.registerTool({\n\t\t\t\tname: \"subagent_inbox\",\n\t\t\t\tlabel: \"Read sibling inbox\",\n\t\t\t\tdescription:\n\t\t\t\t\t\"Read and clear the earliest bounded messages your siblings sent you (subagent_send). Messages are \" +\n\t\t\t\t\t\"consumed by this call; poll it when coordination is expected.\",\n\t\t\t\tparameters: Type.Object({\n\t\t\t\t\tlimit: Type.Optional(\n\t\t\t\t\t\tType.Number({ description: \"Max messages to take (1..20, default 10)\", minimum: 1, maximum: 20 }),\n\t\t\t\t\t),\n\t\t\t\t}),\n\t\t\t\texecute: async (first: unknown, second?: unknown) => {\n\t\t\t\t\tconst input = objectInput(first, second);\n\t\t\t\t\tconst requested = input.limit === undefined ? 10 : Number(input.limit);\n\t\t\t\t\tif (!Number.isInteger(requested) || requested < 1 || requested > 20) {\n\t\t\t\t\t\tthrow new SubagentDelegateError(\n\t\t\t\t\t\t\t\"subagent_invalid_input\",\n\t\t\t\t\t\t\t\"subagent_inbox limit must be an integer in 1..20\",\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst messages = wiring.drainInbox?.(requested) ?? [];\n\t\t\t\t\treturn { messages };\n\t\t\t\t},\n\t\t\t}),\n\t\t);\n\t}\n\t// 调度器限额:options.limits 显式覆盖优先;否则由配置通道逐键取值,缺省回落到调度器默认。\n\tconst configuredLimits = {\n\t\tmaxConcurrentSubagents: configInt(api, \"maxConcurrentSubagents\", 1),\n\t\tmaxQueueDepth: configInt(api, \"maxQueueDepth\", 1),\n\t\tmaxPromptBytes: configInt(api, \"maxPromptBytes\", 1),\n\t\tmaxMetadataBytes: configInt(api, \"maxMetadataBytes\", 1),\n\t\tmaxResultBytes: configInt(api, \"maxResultBytes\", 1),\n\t\tmaxMessages: configInt(api, \"maxMessages\", 1),\n\t\tdrainTimeoutMs: configInt(api, \"drainTimeoutMs\", 1),\n\t\tmaxRetainedRecords: configInt(api, \"maxRetainedRecords\", 1),\n\t};\n\tconst limits =\n\t\toptions?.limits ??\n\t\t(Object.values(configuredLimits).every((value) => value === undefined)\n\t\t\t? undefined\n\t\t\t: (configuredLimits as SubagentRuntimeLimitsV1));\n\tconst sessionAbortSignal = api.sessionAbortSignal;\n\n\t/**\n\t * adapter 选择(优先级):宿主显式 adapter > 配置驱动的 pooled runner 池 > 共享模式(裸 primitive)。\n\t * pooled 关闭时零额外进程;启用时 entryPath 必须显式给出(不猜安装形态),缺失即装载失败。\n\t * 退役切片 1(生产默认 pooled,D-075 S4 第三批收敛):自动池化旗标不再由宿主 options\n\t * 注入,改由既有配置键 `runnerEntryAuto: true` 驱动——未显式配置 `runnerMaxProcesses`/\n\t * `runnerEntryPath` 且未注入 spawner 时,若 self-invocation 可解析则自动以池化装配\n\t * (默认 2 个 runner、每池 8 会话);不可解析则保持共享模式。\n\t */\n\tconst pool: SubagentRunnerPoolV1 | undefined = (() => {\n\t\tconst configuredMaxProcesses = configInt(api, \"runnerMaxProcesses\", 0);\n\t\tlet runnerMaxProcesses = resolveInt(undefined, configuredMaxProcesses, 0, 0, \"runnerMaxProcesses\");\n\t\tconst selfInvocation = resolveSelfInvocationV1();\n\t\tconst autoPool =\n\t\t\trunnerMaxProcesses === 0 &&\n\t\t\tapi.config.runnerEntryAuto === true &&\n\t\t\toptions?.runnerSpawner === undefined &&\n\t\t\ttypeof api.config.runnerEntryPath !== \"string\";\n\t\tif (autoPool && selfInvocation !== undefined) runnerMaxProcesses = 2;\n\t\tif (runnerMaxProcesses === 0) return undefined;\n\t\t// runner 配置存在即做一致性校验(即使宿主注入了 adapter,错误配置也不得静默):\n\t\t// 非法值/缺失入口/容量不覆盖并发都在装载期暴露。\n\t\tconst maxSessionsPerRunner = resolveInt(\n\t\t\tundefined,\n\t\t\tconfigInt(api, \"runnerMaxSessionsPerRunner\", 1),\n\t\t\t8,\n\t\t\t1,\n\t\t\t\"runnerMaxSessionsPerRunner\",\n\t\t);\n\t\tconst idleRunnerTimeoutMs = resolveInt(\n\t\t\tundefined,\n\t\t\tconfigInt(api, \"runnerIdleTimeoutMs\", 1),\n\t\t\t300_000,\n\t\t\t1,\n\t\t\t\"runnerIdleTimeoutMs\",\n\t\t);\n\t\tconst idleCheckIntervalMs = resolveInt(\n\t\t\tundefined,\n\t\t\tconfigInt(api, \"runnerSweepIntervalMs\", 1),\n\t\t\t60_000,\n\t\t\t1,\n\t\t\t\"runnerSweepIntervalMs\",\n\t\t);\n\t\t// 有效并发取\"已配并发\"或调度器默认值:容量校验必须在未显式配置并发时也生效,\n\t\t// 否则池化容量小于默认并发(8)会被静默容忍,直到运行期才以 capacity 错误暴露。\n\t\tconst effectiveMaxConcurrent = options?.limits?.maxConcurrentSubagents ?? limits?.maxConcurrentSubagents ?? 8;\n\t\t// spawn 工厂优先用注入;否则要求配置显式给出 runner 入口(装载期校验,不推迟到首次 spawn)。\n\t\tlet spawnRunner = options?.runnerSpawner;\n\t\tif (spawnRunner === undefined) {\n\t\t\tconst entryPath = api.config.runnerEntryPath;\n\t\t\tif (typeof entryPath === \"string\" && entryPath.trim() !== \"\") {\n\t\t\t\tconst nodeArgs = Array.isArray(api.config.runnerNodeArgs)\n\t\t\t\t\t? api.config.runnerNodeArgs.filter((value): value is string => typeof value === \"string\")\n\t\t\t\t\t: entryPath.endsWith(\".ts\")\n\t\t\t\t\t\t? [\"--experimental-strip-types\"]\n\t\t\t\t\t\t: [];\n\t\t\t\tconst spawner = createNodeSubagentRunnerSpawnerV1({ entryPath, nodeArgs });\n\t\t\t\tspawnRunner = () => spawner();\n\t\t\t} else if (autoPool) {\n\t\t\t\t// 退役切片 1:CLI 宿主的自动池化——self-invocation 已在上方预检可解析。\n\t\t\t\tconst self = selfInvocation as SelfInvocationV1;\n\t\t\t\tconst nodeArgs = Array.isArray(api.config.runnerNodeArgs)\n\t\t\t\t\t? api.config.runnerNodeArgs.filter((value): value is string => typeof value === \"string\")\n\t\t\t\t\t: [...self.nodeArgs];\n\t\t\t\tconst spawner = createNodeSubagentRunnerSpawnerV1({ entryPath: self.entryPath, nodeArgs });\n\t\t\t\tspawnRunner = () => spawner();\n\t\t\t} else if (api.config.runnerEntryAuto === true) {\n\t\t\t\t// 显式开启的零配置路径:把当前进程的启动方式当作 runner 入口(见 self-invocation)。\n\t\t\t\tconst self = resolveSelfInvocationV1();\n\t\t\t\tif (self === undefined) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\"subagent-delegate config runnerEntryAuto is enabled but the current process has no script entry \" +\n\t\t\t\t\t\t\t\"(compiled binaries must set runnerEntryPath explicitly)\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tconst nodeArgs = Array.isArray(api.config.runnerNodeArgs)\n\t\t\t\t\t? api.config.runnerNodeArgs.filter((value): value is string => typeof value === \"string\")\n\t\t\t\t\t: [...self.nodeArgs];\n\t\t\t\tconst spawner = createNodeSubagentRunnerSpawnerV1({ entryPath: self.entryPath, nodeArgs });\n\t\t\t\tspawnRunner = () => spawner();\n\t\t\t} else {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t\"subagent-delegate config runnerEntryPath is required when runnerMaxProcesses >= 1 \" +\n\t\t\t\t\t\t\"(or enable runnerEntryAuto to reuse this process's own entry)\",\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tif (options?.adapter !== undefined) return undefined;\n\t\t// 开发态 runner(TS 引导的 CLI)冷启动可能超过默认 5s 握手预算,宿主可配置放宽。\n\t\tconst handshakeTimeoutMs = configInt(api, \"runnerHandshakeTimeoutMs\", 1);\n\t\treturn createPooledAgentTaskAdapterV1({\n\t\t\tspawnRunner,\n\t\t\tmaxRunnerProcesses: runnerMaxProcesses,\n\t\t\tmaxSessionsPerRunner,\n\t\t\tidleRunnerTimeoutMs,\n\t\t\tidleCheckIntervalMs,\n\t\t\tmaxConcurrentSubagents: effectiveMaxConcurrent,\n\t\t\t...(handshakeTimeoutMs === undefined ? {} : { handshakeTimeoutMs }),\n\t\t\tonDiagnostic: (message) => {\n\t\t\t\tapi.logger?.debug?.(`subagent-delegate runner: ${message}`);\n\t\t\t},\n\t\t});\n\t})();\n\t// 桥转换只在 agents 暴露任务面(create)时成立(承载退役切片 2:reader-only 宿主的\n\t// agents 只承载回读,不承载委派)。父面 = 宿主 adapter > 配置/自动池 > 桥。\n\tconst baseAdapter =\n\t\toptions?.adapter ?? pool ?? (agents?.create === undefined ? undefined : createAgentTaskApiAdapterV1(agents));\n\n\t// 门控(承载退役):父面 = 池或宿主 adapter 任一在即可,不再依赖 agents 能力——\n\t// runner 叶子带自动池化(退役切片 1)因此同样获得委派父面(嵌套委派,深度硬限兜底)。\n\t// 无任何 adapter 时委派不适用:只保留子侧通信面(fail-closed)。\n\tif (baseAdapter === undefined) {\n\t\tif (childTools.length === 0) {\n\t\t\tapi.logger?.debug?.(\n\t\t\t\t\"subagent-delegate: no runner pool and no host adapter; delegation tool stays unregistered\",\n\t\t\t);\n\t\t\treturn [];\n\t\t}\n\t\treturn childTools;\n\t}\n\t// 配置错误在装载时 fail fast,不让 0/负数静默地拒绝每一次调用或永不超时。\n\tconst maxTasksPerCall = resolveInt(\n\t\toptions?.maxTasksPerCall,\n\t\tconfigInt(api, \"maxTasksPerCall\", 1),\n\t\tDEFAULT_MAX_TASKS_PER_CALL,\n\t\t1,\n\t\t\"maxTasksPerCall\",\n\t);\n\tconst taskTimeoutMs = resolveInt(\n\t\toptions?.taskTimeoutMs,\n\t\tconfigInt(api, \"taskTimeoutMs\", 1),\n\t\tDEFAULT_TASK_TIMEOUT_MS,\n\t\t1,\n\t\t\"taskTimeoutMs\",\n\t);\n\t// 嵌套深度硬限(承载退役切片 2,Option A):runner 叶子带自动池化后同样获得委派\n\t// 父面,递归边界由血缘深度兜底——本会话已是第 maxNestedDepth 层时再委派按\n\t// `subagent_depth_exceeded` 结构化拒绝。与 S5 shared 侧的深度上限同一默认(2 层)。\n\tconst maxNestedDepth = resolveInt(undefined, configInt(api, \"maxNestedDepth\", 1), 2, 1, \"maxNestedDepth\");\n\t// 每父级累计可创建的子代理总量(承载退役切片 2,Option A;与 S5 shared 侧同一默认\n\t// 100):对**非 resume** 的每次 submit 计数,超限按 `subagent_child_quota` 拒绝。\n\tconst maxTotalPerParent = resolveInt(undefined, configInt(api, \"maxTotalPerParent\", 0), 100, 0, \"maxTotalPerParent\");\n\tlet createdSubagentCount = 0;\n\t// 后台委派开关(D-060 S6):默认关;显式 `true` 才给 `subagent_run` 加 `background`\n\t// 参数(提交即返回票据,不阻塞父回合;结算摘要经宿主 sink 进父的下一次模型轮次)。\n\tif (api.config.background !== undefined && api.config.background !== true) {\n\t\tthrow new Error(\"subagent-delegate config background must be true or omitted\");\n\t}\n\tconst backgroundEnabled = api.config.background === true;\n\t// 角色目录 + 方案关联在装载期解析:非法文件/非法关联直接失败,不留\"配置了但没生效\"的假象。\n\tconst roleCatalog =\n\t\toptions?.roles === undefined\n\t\t\t? { roles: new Map<string, SubagentDelegateRoleV1>(), dangling: [], brokenUnassociated: [] }\n\t\t\t: resolveSubagentRoleCatalog(options.roles);\n\tconst roles = roleCatalog.roles;\n\tfor (const id of roleCatalog.dangling) {\n\t\tapi.logger?.warn?.(\n\t\t\t`suite associations.roles lists \"${id}\" but no role file exists: ${subagentRoleCatalogPath(\n\t\t\t\toptions?.roles?.agentDir ?? \"\",\n\t\t\t\tid,\n\t\t\t)} (role stays unavailable for this session)`,\n\t\t);\n\t}\n\tfor (const id of roleCatalog.brokenUnassociated) {\n\t\tapi.logger?.warn?.(\n\t\t\t`role file for \"${id}\" is invalid and is not associated by this suite; ignored (fix it or remove the file)`,\n\t\t);\n\t}\n\tconst roleSummary = [...roles.values()]\n\t\t.sort((left, right) => (left.id < right.id ? -1 : 1))\n\t\t.map((role) => `${role.id} — ${role.description}`)\n\t\t.join(\"; \");\n\n\t/**\n\t * D-044 §5.6:把支持进度面的 adapter 装饰一层,累计\"运行中 step\"与最近一条\n\t * 摘要,供工具 onUpdate 节流上报父 UI(只回计数与摘要,不回正文)。\n\t */\n\tconst progressState = { steps: 0, last: undefined as string | undefined };\n\tconst progressUnsubscribes = new Set<() => void>();\n\tconst adapter: AgentTaskAdapter = {\n\t\tcreate: async (request) => {\n\t\t\tconst requestedProfile = request.metadata?.[AGENT_TASK_CHILD_PROFILE_KEY];\n\t\t\tconst host = await baseAdapter.create(request);\n\t\t\t// 档案核验:宿主必须把收窄落到了子会话上,且要能自证(getActiveTools/getModel)。\n\t\t\t// 核验失败 = 上层以为的\"只读评审者\"没有生效,因此这里就地失败并回收该子会话,\n\t\t\t// 而不是让它带着比预期更大的权限去跑。\n\t\t\tif (requestedProfile !== undefined) {\n\t\t\t\tconst violation = verifyChildProfileApplied(host, requestedProfile);\n\t\t\t\tif (violation !== undefined) {\n\t\t\t\t\tawait host.dispose().catch(() => {});\n\t\t\t\t\tthrow new SubagentDelegateError(\"subagent_profile_not_applied\", violation);\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst taskId = host.session.getSessionId();\n\t\t\tconst unsubscribe = subscribeTaskProgressV1(baseAdapter, taskId, (message) => {\n\t\t\t\tprogressState.steps += 1;\n\t\t\t\tif (message !== undefined) progressState.last = message;\n\t\t\t});\n\t\t\tprogressUnsubscribes.add(unsubscribe);\n\t\t\treturn host;\n\t\t},\n\t\t...(baseAdapter.dispose === undefined\n\t\t\t? {}\n\t\t\t: { dispose: async (): Promise<void> => void (await baseAdapter.dispose?.()) }),\n\t};\n\n\tconst runtime: SubagentRuntimeV1 = createSubagentRuntime({\n\t\tadapter,\n\t\towner: capabilityManifest.id,\n\t\tparentSessionId: session.getSessionId(),\n\t\t...(sessionAbortSignal === undefined ? {} : { parentSignal: sessionAbortSignal }),\n\t\t...(limits === undefined ? {} : { limits }),\n\t});\n\n\tconst parameters = Type.Object({\n\t\tprompts: Type.Array(\n\t\t\tType.String({\n\t\t\t\tminLength: 1,\n\t\t\t\tdescription: \"Self-contained prompt for one subagent task; the child session sees no parent history\",\n\t\t\t}),\n\t\t\t{\n\t\t\t\tminItems: 1,\n\t\t\t\tdescription: `1..${maxTasksPerCall} independent prompts run as separate subagent tasks`,\n\t\t\t},\n\t\t),\n\t\tcontext: Type.Optional(\n\t\t\tType.Object(\n\t\t\t\t{\n\t\t\t\t\tanchors: Type.Optional(\n\t\t\t\t\t\tType.Array(\n\t\t\t\t\t\t\tType.Object({\n\t\t\t\t\t\t\t\tpath: Type.String({\n\t\t\t\t\t\t\t\t\tminLength: 1,\n\t\t\t\t\t\t\t\t\tmaxLength: CONTEXT_PATH_MAX_CHARS,\n\t\t\t\t\t\t\t\t\tdescription: \"File path the child should start from\",\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\tline: Type.Optional(\n\t\t\t\t\t\t\t\t\tType.Integer({ minimum: 1, description: \"Line number in path (positive integer)\" }),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\tnote: Type.Optional(\n\t\t\t\t\t\t\t\t\tType.String({\n\t\t\t\t\t\t\t\t\t\tmaxLength: CONTEXT_NOTE_MAX_CHARS,\n\t\t\t\t\t\t\t\t\t\tdescription: \"One-line why-it-matters note\",\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t{ maxItems: CONTEXT_LIST_MAX_ITEMS },\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t\tfailedAttempts: Type.Optional(\n\t\t\t\t\t\tType.Array(\n\t\t\t\t\t\t\tType.Object({\n\t\t\t\t\t\t\t\tcommand: Type.String({\n\t\t\t\t\t\t\t\t\tminLength: 1,\n\t\t\t\t\t\t\t\t\tmaxLength: CONTEXT_COMMAND_MAX_CHARS,\n\t\t\t\t\t\t\t\t\tdescription: \"Command that was already tried and failed\",\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\tnote: Type.Optional(\n\t\t\t\t\t\t\t\t\tType.String({ maxLength: CONTEXT_NOTE_MAX_CHARS, description: \"One-line failure note\" }),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t{ maxItems: CONTEXT_LIST_MAX_ITEMS },\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t\"Parent pointers rendered as a header block into each child prompt: file:line anchors and commands \" +\n\t\t\t\t\t\t\"already tried and failed. Pointers only — children still read the real files.\",\n\t\t\t\t},\n\t\t\t),\n\t\t),\n\t\t...(roles.size === 0\n\t\t\t? {}\n\t\t\t: {\n\t\t\t\t\trole: Type.Optional(\n\t\t\t\t\t\tType.String({\n\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t`Named subagent role applied to every prompt in this call; one of: ${[...roles.keys()].sort().join(\", \")}. ` +\n\t\t\t\t\t\t\t\t\"Omit for a generic task. Roles frame the task; they do not change the child's model or tools.\",\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t}),\n\t});\n\n\tconst tool = api.registerTool({\n\t\tname: \"subagent_run\",\n\t\tlabel: \"Run subagents\",\n\t\tdescription:\n\t\t\t`Delegate 1..${maxTasksPerCall} independent subagent tasks in one call and return their bounded terminal results. ` +\n\t\t\t\"Each prompt runs in its own isolated child session. Only status, reason codes, message counts and a shortened \" +\n\t\t\t\"final-assistant summary come back; ask a child for more detail in its prompt if you need it.\" +\n\t\t\t\" Optionally pass `context` — file:line anchors and commands already tried and failed are rendered as a \" +\n\t\t\t\"header block at the top of each child prompt (pointers only).\" +\n\t\t\t(roles.size === 0\n\t\t\t\t? \"\"\n\t\t\t\t: ` Configured subagent roles (pass \\`role\\`): ${roleSummary}. A role's instructions frame every task in the call; ` +\n\t\t\t\t\t\"child model and tools stay inherited from this session.\"),\n\t\tparameters: {\n\t\t\t...parameters,\n\t\t\t...(backgroundEnabled\n\t\t\t\t? {\n\t\t\t\t\t\tbackground: Type.Optional(\n\t\t\t\t\t\t\tType.Boolean({\n\t\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\t\"Submit without waiting: return task tickets immediately and keep working; settled results are \" +\n\t\t\t\t\t\t\t\t\t\"announced at your next model round and can be read with subagent_result (optionally waiting).\",\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t),\n\t\t\t\t\t}\n\t\t\t\t: {}),\n\t\t},\n\t\texecute: async (\n\t\t\tfirst: unknown,\n\t\t\tsecond?: unknown,\n\t\t\t_signal?: AbortSignal,\n\t\t\tonUpdate?: (partial: { content: { type: \"text\"; text: string }[]; details: unknown }) => void,\n\t\t): Promise<SubagentDelegateResultV1 | SubagentDelegateBackgroundResultV1> => {\n\t\t\tconst input = objectInput(first, second);\n\t\t\tconst prompts = readPrompts(input.prompts, maxTasksPerCall);\n\t\t\tconst role = resolveRole(input.role, roles);\n\t\t\t// context 注入(设计 §2.3):形状/边界违规在此整体拒绝;两个列表皆空归一化为\n\t\t\t// undefined,后续路径与 context 缺省完全一致。\n\t\t\tconst contextBlock = renderDelegateContext(parseDelegateContext(input.context));\n\t\t\tconst background = backgroundEnabled && input.background === true;\n\t\t\t// 嵌套深度硬限(退役切片 2,Option A):沿 S2 血缘头回溯本会话层级,已达\n\t\t\t// maxNestedDepth 层时整次调用结构化拒绝——递归的兜底边界(不依赖模型自觉)。\n\t\t\t// resume 不涉深度(S5 裁定:续写既有会话不计新层级,走 subagent_resume)。\n\t\t\t// agentDir 单源(D-075 S4 第三批):显式 options 优先,缺省回落 api.host.agentDir。\n\t\t\tconst agentDir = options?.agentDir ?? api.host.agentDir ?? \"\";\n\t\t\tif (agentDir !== \"\") {\n\t\t\t\tconst depth = lineageDepth(session.getSessionId(), agentDir, maxNestedDepth + 2);\n\t\t\t\tif (depth !== undefined && depth + 1 > maxNestedDepth) {\n\t\t\t\t\tthrow new SubagentDelegateError(\n\t\t\t\t\t\t\"subagent_depth_exceeded\",\n\t\t\t\t\t\t`this session already sits at nested depth ${depth}; delegation beyond maxNestedDepth ${maxNestedDepth} is refused`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// submit 是同步抛出契约(预算/队列/已释放),逐条捕获为条目级 rejected;\n\t\t\t// 全部提交完成后再逐条 drain,任务因此并行执行。\n\t\t\tconst pending: { index: number; handle: Promise<SubagentTaskHandleV1> }[] = [];\n\t\t\tconst tasks: (SubagentDelegateTaskProjectionV1 | undefined)[] = new Array(prompts.length).fill(undefined);\n\t\t\t// 回合级血缘(第 3 期第二批):提交动作发生在父回合工具阶段,此时 correlationId\n\t\t\t// 有值;getter 缺席或返回 undefined 时不写键(行为回退)。仅 subagent_run 新建\n\t\t\t// 路径携带——resume 打开既有会话、头定格,写键即\"要求了但不生效\"。\n\t\t\tconst turnCorrelationId = options?.getTurnCorrelationId?.();\n\t\t\tfor (let index = 0; index < prompts.length; index += 1) {\n\t\t\t\tconst prompt = prompts[index] as string;\n\t\t\t\t// 总量硬限(承载退役切片 2,Option A):本会话累计创建数达上限时按结构化\n\t\t\t\t// 码拒绝该条目(同批其余条目继续)。深度硬限在 execute 开头沿 S2 血缘头\n\t\t\t\t// 整调用拒绝(见上),不在此重复。\n\t\t\t\tif (createdSubagentCount >= maxTotalPerParent) {\n\t\t\t\t\ttasks[index] = {\n\t\t\t\t\t\ttaskId: \"\",\n\t\t\t\t\t\tstatus: \"rejected\",\n\t\t\t\t\t\treasonCode: \"subagent_child_quota\",\n\t\t\t\t\t\treason: `this session already created ${createdSubagentCount} child sessions (cap ${maxTotalPerParent})`,\n\t\t\t\t\t\tmessageCount: 0,\n\t\t\t\t\t};\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tcreatedSubagentCount += 1;\n\t\t\t\t// 首条 prompt 组装(角色框定 + 可选父投影);context 头部块(有则)在最上、\n\t\t\t\t// 与后续内容空一行,无块时与现状逐字节一致。字节数仍走 submit 侧 maxPromptBytes\n\t\t\t\t// 预算,超限按预算拒绝而不是静默截断。\n\t\t\t\tconst composed =\n\t\t\t\t\trole === undefined\n\t\t\t\t\t\t? prompt\n\t\t\t\t\t\t: role.parentView === true\n\t\t\t\t\t\t\t? `${composeRolePrompt(role, prompt)}\\n\\n${composeParentView(session, session.getSessionId()) ?? \"\"}`.trim()\n\t\t\t\t\t\t\t: composeRolePrompt(role, prompt);\n\t\t\t\ttry {\n\t\t\t\t\tpending.push({\n\t\t\t\t\t\tindex,\n\t\t\t\t\t\thandle: runtime.submit({\n\t\t\t\t\t\t\tprompt: contextBlock === undefined ? composed : `${contextBlock}\\n\\n${composed}`,\n\t\t\t\t\t\t\t// 血缘(D-060 S1):父会话 id 随 metadata 过线,宿主写进子会话文件头;\n\t\t\t\t\t\t\t// 角色档案(若角色定义带收窄)一并带上。\n\t\t\t\t\t\t\tmetadata: {\n\t\t\t\t\t\t\t\t[AGENT_TASK_PARENT_SESSION_KEY]: session.getSessionId(),\n\t\t\t\t\t\t\t\t...(turnCorrelationId === undefined\n\t\t\t\t\t\t\t\t\t? {}\n\t\t\t\t\t\t\t\t\t: { [AGENT_TASK_PARENT_TRACE_KEY]: turnCorrelationId }),\n\t\t\t\t\t\t\t\t...(role?.profile === undefined ? {} : { [AGENT_TASK_CHILD_PROFILE_KEY]: role.profile }),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}),\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\ttasks[index] = {\n\t\t\t\t\t\ttaskId: \"\",\n\t\t\t\t\t\tstatus: \"rejected\",\n\t\t\t\t\t\treasonCode: reasonCodeOf(error),\n\t\t\t\t\t\treason: errorText(error),\n\t\t\t\t\t\tmessageCount: 0,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t\t// 后台委派(D-060 S6,配置门默认关):提交即返回票据,不 drain——任务挂在\n\t\t\t// 调度器上继续跑(会话级生命周期,父栅栏/会话销毁统一回收)。每个后台任务\n\t\t\t// 挂一个结算观察器:结算一行有界 JSON 进宿主 sink,随父的下一次模型轮次注入。\n\t\t\tif (background === true) {\n\t\t\t\tconst tickets: SubagentDelegateBackgroundTaskV1[] = [];\n\t\t\t\tfor (const item of pending) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst handle = await item.handle;\n\t\t\t\t\t\ttickets.push({ taskId: handle.id });\n\t\t\t\t\t\tconst sink = options?.backgroundDigestSink;\n\t\t\t\t\t\tif (sink !== undefined) {\n\t\t\t\t\t\t\tvoid handle\n\t\t\t\t\t\t\t\t.result()\n\t\t\t\t\t\t\t\t.then((record) => {\n\t\t\t\t\t\t\t\t\tsink(\n\t\t\t\t\t\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\t\t\t\t\t\ttaskId: record.taskId,\n\t\t\t\t\t\t\t\t\t\t\tsessionId: record.sessionId,\n\t\t\t\t\t\t\t\t\t\t\tstatus: record.status,\n\t\t\t\t\t\t\t\t\t\t\t...(record.reasonCode === undefined ? {} : { reasonCode: record.reasonCode }),\n\t\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t.catch(() => {\n\t\t\t\t\t\t\t\t\t// 摘要观察失败不影响任务本身(终态已在调度器落库可查)。\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// submit 同步抛错已在上面捕获为条目级 rejected——后台票据只收成功提交。\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn { background: true, tasks: tickets };\n\t\t\t}\n\t\t\tlet completed = tasks.filter((task) => task !== undefined).length;\n\t\t\tfor (const item of pending) {\n\t\t\t\tlet taskId = \"\";\n\t\t\t\ttry {\n\t\t\t\t\tconst handle = await item.handle;\n\t\t\t\t\ttaskId = handle.id;\n\t\t\t\t\tconst record = await handle.drain({ timeoutMs: taskTimeoutMs });\n\t\t\t\t\t// escalate 上抛(D-060 S4):随任务投影一起交回父模型(取后即清)。\n\t\t\t\t\tconst escalations = wiring?.drainEscalations?.(record.sessionId) ?? [];\n\t\t\t\t\ttasks[item.index] = {\n\t\t\t\t\t\t...projectRecord(record),\n\t\t\t\t\t\t...(escalations.length === 0 ? {} : { escalations }),\n\t\t\t\t\t};\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// drain 抛错(如持久化失败):保留原始 message,不静默吞掉。\n\t\t\t\t\ttasks[item.index] = { taskId, status: \"failed\", reason: errorText(error), messageCount: 0 };\n\t\t\t\t}\n\t\t\t\tcompleted += 1;\n\t\t\t\t// D-044 §5.6:进度随任务完成节流上报(累计 step 数 + 最近一条回合摘要);\n\t\t\t\t// 只回摘要,不回模型正文。\n\t\t\t\tconst progressSuffix =\n\t\t\t\t\tprogressState.steps === 0\n\t\t\t\t\t\t? \"\"\n\t\t\t\t\t\t: ` · steps ${progressState.steps}${progressState.last === undefined ? \"\" : ` · ${progressState.last}`}`;\n\t\t\t\tonUpdate?.({\n\t\t\t\t\tcontent: [{ type: \"text\", text: `subagent ${completed}/${prompts.length}${progressSuffix}` }],\n\t\t\t\t\tdetails: {},\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn { tasks: tasks.filter((task): task is SubagentDelegateTaskProjectionV1 => task !== undefined) };\n\t\t},\n\t});\n\n\t/**\n\t * 只读回读工具(D-060 S3):按 `taskId` 或 `sessionId` 取回**有界**的子会话片段。\n\t * - `taskId` → 调度记录(内存/durable,owner+parent 作用域);\n\t * - `sessionId` → 持久化 transcript(宿主注入的会话回读器;未注入时结构化报错)。\n\t * 返回默认只给最后 N 条(可调),并如实报告条数/字节/截断——绝不默认回灌全文。\n\t */\n\tconst resultTool = api.registerTool({\n\t\tname: \"subagent_result\",\n\t\tlabel: \"Read subagent result\",\n\t\tdescription:\n\t\t\t\"Read back a bounded slice of a subagent's session: pass the taskId from subagent_run, or the child's sessionId \" +\n\t\t\t\"to read its persisted transcript. Returns at most `maxMessages` trailing entries (default 20) plus counts, \" +\n\t\t\t\"byte size and a truncated flag; it never returns the whole transcript by default.\",\n\t\tparameters: Type.Object({\n\t\t\ttaskId: Type.Optional(Type.String({ description: \"Task id from subagent_run (scheduler record read)\" })),\n\t\t\tsessionId: Type.Optional(\n\t\t\t\tType.String({\n\t\t\t\t\tdescription: \"Child session id (persisted transcript read; requires a host session reader)\",\n\t\t\t\t}),\n\t\t\t),\n\t\t\tmaxMessages: Type.Optional(\n\t\t\t\tType.Number({ description: \"Trailing entries to return (1..50, default 20)\", minimum: 1, maximum: 50 }),\n\t\t\t),\n\t\t\twaitMs: Type.Optional(\n\t\t\t\tType.Number({\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t\"Bounded wait for a background task to settle, in ms (0..120000, default 0 = check once; \" +\n\t\t\t\t\t\t\"unsettled tasks come back with knownTask/taskStatus instead of the transcript)\",\n\t\t\t\t\tminimum: 0,\n\t\t\t\t\tmaximum: 120000,\n\t\t\t\t}),\n\t\t\t),\n\t\t}),\n\t\texecute: async (first: unknown, second?: unknown): Promise<SubagentResultReadResultV1> => {\n\t\t\tconst input = objectInput(first, second);\n\t\t\tconst taskId = typeof input.taskId === \"string\" && input.taskId.trim() !== \"\" ? input.taskId : undefined;\n\t\t\tconst sessionId =\n\t\t\t\ttypeof input.sessionId === \"string\" && input.sessionId.trim() !== \"\" ? input.sessionId : undefined;\n\t\t\tconst requested = input.maxMessages === undefined ? RESULT_READ_DEFAULT_MESSAGES : Number(input.maxMessages);\n\t\t\tif (!Number.isInteger(requested) || requested < 1 || requested > RESULT_READ_MAX_MESSAGES) {\n\t\t\t\tthrow new SubagentDelegateError(\n\t\t\t\t\t\"subagent_invalid_input\",\n\t\t\t\t\t`subagent_result maxMessages must be an integer in 1..${RESULT_READ_MAX_MESSAGES}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst waitMs = input.waitMs === undefined ? 0 : Number(input.waitMs);\n\t\t\tif (!Number.isInteger(waitMs) || waitMs < 0 || waitMs > RESULT_READ_MAX_WAIT_MS) {\n\t\t\t\tthrow new SubagentDelegateError(\n\t\t\t\t\t\"subagent_invalid_input\",\n\t\t\t\t\t`subagent_result waitMs must be an integer in 0..${RESULT_READ_MAX_WAIT_MS}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (taskId === undefined && sessionId === undefined) {\n\t\t\t\tthrow new SubagentDelegateError(\n\t\t\t\t\t\"subagent_invalid_input\",\n\t\t\t\t\t\"subagent_result requires taskId (scheduler record) or sessionId (persisted transcript)\",\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (taskId !== undefined) {\n\t\t\t\tconst requester = {\n\t\t\t\t\towner: capabilityManifest.id,\n\t\t\t\t\tparentSessionId: session.getSessionId(),\n\t\t\t\t};\n\t\t\t\t// 后台委派(D-060 S6):waitMs > 0 时有界等待结算;否则查一次。\n\t\t\t\tconst record =\n\t\t\t\t\twaitMs > 0 ? await runtime.wait(taskId, waitMs, requester) : await runtime.result(taskId, requester);\n\t\t\t\tif (record === undefined) {\n\t\t\t\t\t// 未结算与不存在分开表达:已知任务回当前状态,模型可稍后再取。\n\t\t\t\t\tconst known = runtime.peek(taskId, requester);\n\t\t\t\t\treturn known === undefined\n\t\t\t\t\t\t? { found: false, source: \"record\", taskId }\n\t\t\t\t\t\t: { found: false, source: \"record\", taskId, knownTask: true, taskStatus: known.status };\n\t\t\t\t}\n\t\t\t\tconst messages = record.messages.slice(-requested);\n\t\t\t\t// escalate 上抛(D-060 S4):按子会话 id 取走并随信封返回(取后即清)。\n\t\t\t\tconst escalations = wiring?.drainEscalations?.(record.sessionId) ?? [];\n\t\t\t\treturn {\n\t\t\t\t\tfound: true,\n\t\t\t\t\tsource: \"record\",\n\t\t\t\t\ttaskId: record.taskId,\n\t\t\t\t\tsessionId: record.sessionId,\n\t\t\t\t\tstatus: record.status,\n\t\t\t\t\t...(record.reasonCode === undefined ? {} : { reasonCode: record.reasonCode }),\n\t\t\t\t\t...(record.reason === undefined ? {} : { reason: record.reason }),\n\t\t\t\t\tmessageCount: record.messages.length,\n\t\t\t\t\treturnedMessages: messages.length,\n\t\t\t\t\tbytes: utf8ByteLength(JSON.stringify(record.messages)),\n\t\t\t\t\ttruncated: record.truncated || messages.length < record.messages.length,\n\t\t\t\t\tmessages: messages.map((message) => detachedSessionSnapshot(message)),\n\t\t\t\t\t...(escalations.length === 0 ? {} : { escalations }),\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst reader = agents?.session;\n\t\t\tif (reader === undefined) {\n\t\t\t\tthrow new SubagentDelegateError(\n\t\t\t\t\t\"subagent_profile_not_applied\",\n\t\t\t\t\t\"this host exposes no session reader; pass taskId instead (or run on a host that injects one)\",\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst view = await reader(sessionId as string);\n\t\t\tif (view === undefined) return { found: false, source: \"session\", sessionId };\n\t\t\tconst entries = view.entries.slice(-requested);\n\t\t\tconst escalations = wiring?.drainEscalations?.(view.sessionId) ?? [];\n\t\t\treturn {\n\t\t\t\tfound: true,\n\t\t\t\tsource: \"session\",\n\t\t\t\tsessionId: view.sessionId,\n\t\t\t\t...(view.agentRole === undefined ? {} : { roleId: view.agentRole }),\n\t\t\t\tentryCount: view.entryCount,\n\t\t\t\treturnedMessages: entries.length,\n\t\t\t\tbytes: view.bytes,\n\t\t\t\ttruncated: view.truncated || entries.length < view.entryCount,\n\t\t\t\tmessages: entries.map((entry) => detachedSessionSnapshot(entry)),\n\t\t\t\t...(escalations.length === 0 ? {} : { escalations }),\n\t\t\t};\n\t\t},\n\t});\n\n\t// steer 不设模型面工具(D-060 S4):`subagent_run` 是阻塞式 drain-to-terminal,\n\t// 父模型在工具调用期间不存在\"运行中的子任务\"窗口——暴露即死 affordance(与\n\t// `deadlineMs` 同一纪律)。父→子 steer 走程序面:agents 桥的 `steer`(运行中\n\t// 的后台任务)与本调度器的 `steer`/`findBySession` 原语,供编排插件组合。\n\n\t/**\n\t * 父→子续跑(D-060 S4,默认关):对**已终态且已落盘**的子会话按 sessionId 继续\n\t * 提交一轮任务(同一份 JSONL 续写,多轮子任务)。血缘作用域由宿主会话回读器判定,\n\t * 活跃任务占用以 `subagent_session_busy` 拒绝。shared 与 pooled 都支持——pooled 下\n\t * resume 键随 metadata 过线,runner 打开既有会话文件续写。\n\t */\n\tconst resumeTool =\n\t\tcommunication.resume === true\n\t\t\t? api.registerTool({\n\t\t\t\t\tname: \"subagent_resume\",\n\t\t\t\t\tlabel: \"Resume a subagent\",\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t\"Continue a finished subagent session by its sessionId: submit one more prompt into the same persisted \" +\n\t\t\t\t\t\t\"transcript (multi-turn subtask) and get the bounded terminal projection, like subagent_run for one task.\",\n\t\t\t\t\tparameters: Type.Object({\n\t\t\t\t\t\tsessionId: Type.String({ description: \"Child session id of a finished subagent task\" }),\n\t\t\t\t\t\tprompt: Type.String({ minLength: 1, description: \"Follow-up prompt for the continued session\" }),\n\t\t\t\t\t}),\n\t\t\t\t\texecute: async (first: unknown, second?: unknown) => {\n\t\t\t\t\t\tconst input = objectInput(first, second);\n\t\t\t\t\t\tconst sessionId = typeof input.sessionId === \"string\" ? input.sessionId : \"\";\n\t\t\t\t\t\tconst prompt = typeof input.prompt === \"string\" ? input.prompt : \"\";\n\t\t\t\t\t\tif (sessionId === \"\" || prompt.trim() === \"\") {\n\t\t\t\t\t\t\tthrow new SubagentDelegateError(\n\t\t\t\t\t\t\t\t\"subagent_invalid_input\",\n\t\t\t\t\t\t\t\t\"subagent_resume requires sessionId and prompt\",\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst reader = agents?.session;\n\t\t\t\t\t\tif (reader === undefined) {\n\t\t\t\t\t\t\tthrow new SubagentDelegateError(\n\t\t\t\t\t\t\t\t\"subagent_profile_not_applied\",\n\t\t\t\t\t\t\t\t\"this host exposes no session reader; resume cannot verify the session scope\",\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst view = await reader(sessionId);\n\t\t\t\t\t\tif (view === undefined) {\n\t\t\t\t\t\t\tthrow new SubagentDelegateError(\n\t\t\t\t\t\t\t\t\"subagent_unknown_session\",\n\t\t\t\t\t\t\t\t`no child session ${sessionId} under this parent`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst requester = { owner: capabilityManifest.id, parentSessionId: session.getSessionId() };\n\t\t\t\t\t\tconst active = runtime.findBySession(sessionId, requester);\n\t\t\t\t\t\tif (active !== undefined && !active.settled) {\n\t\t\t\t\t\t\tthrow new SubagentDelegateError(\n\t\t\t\t\t\t\t\t\"subagent_session_busy\",\n\t\t\t\t\t\t\t\t`session ${sessionId} still has an unsettled task (${active.taskId}); steer or wait instead`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlet handle: SubagentTaskHandleV1;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\thandle = await runtime.submit({\n\t\t\t\t\t\t\t\tprompt,\n\t\t\t\t\t\t\t\tmetadata: {\n\t\t\t\t\t\t\t\t\t[AGENT_TASK_PARENT_SESSION_KEY]: session.getSessionId(),\n\t\t\t\t\t\t\t\t\t[AGENT_TASK_RESUME_SESSION_KEY]: sessionId,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tthrow new SubagentDelegateError(\"subagent_submit_rejected\", errorText(error));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst record = await handle.drain({ timeoutMs: taskTimeoutMs });\n\t\t\t\t\t\treturn { tasks: [projectRecord(record)] };\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t: undefined;\n\n\t/**\n\t * 子会话盘点(D-060 模型面):列出**本会话自己的**子代理会话(最近活动倒序、\n\t * 有界),并用调度器记录富化活跃/终态信息。宿主注入 `agents.list` 才注册;\n\t * 读的是落盘目录,父会话跨进程重启后仍能找回自己的子代理。返回的 sessionId\n\t * 可直接用于 `subagent_result` / `subagent_resume`。\n\t */\n\tconst listTool =\n\t\tagents?.list === undefined\n\t\t\t? undefined\n\t\t\t: api.registerTool({\n\t\t\t\t\tname: \"subagent_list\",\n\t\t\t\t\tlabel: \"List subagent sessions\",\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t\"List this session's own subagent sessions (newest activity first) with role, timestamps and transcript \" +\n\t\t\t\t\t\t\"size; tasks still in flight are marked active, settled ones carry their terminal status. Read-only. \" +\n\t\t\t\t\t\t\"Use a listed sessionId with subagent_result or subagent_resume.\",\n\t\t\t\t\tparameters: Type.Object({\n\t\t\t\t\t\tlimit: Type.Optional(\n\t\t\t\t\t\t\tType.Number({ description: \"Max entries to return (1..50, default 20)\", minimum: 1, maximum: 50 }),\n\t\t\t\t\t\t),\n\t\t\t\t\t}),\n\t\t\t\t\texecute: async (first: unknown, second?: unknown): Promise<SubagentSessionListResultV1> => {\n\t\t\t\t\t\tconst input = objectInput(first, second);\n\t\t\t\t\t\tconst requested = input.limit === undefined ? 20 : Number(input.limit);\n\t\t\t\t\t\tif (!Number.isInteger(requested) || requested < 1 || requested > 50) {\n\t\t\t\t\t\t\tthrow new SubagentDelegateError(\n\t\t\t\t\t\t\t\t\"subagent_invalid_input\",\n\t\t\t\t\t\t\t\t\"subagent_list limit must be an integer in 1..50\",\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst summaries = (await agents.list?.()) ?? [];\n\t\t\t\t\t\tconst requester = { owner: capabilityManifest.id, parentSessionId: session.getSessionId() };\n\t\t\t\t\t\tconst sessions = [];\n\t\t\t\t\t\tfor (const summary of summaries.slice(0, requested)) {\n\t\t\t\t\t\t\tconst live = runtime.findBySession(summary.sessionId, requester);\n\t\t\t\t\t\t\tif (live !== undefined && !live.settled) {\n\t\t\t\t\t\t\t\tsessions.push({ ...summary, active: true as const });\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (live !== undefined) {\n\t\t\t\t\t\t\t\tconst record = await runtime.result(live.taskId, requester);\n\t\t\t\t\t\t\t\tif (record !== undefined) {\n\t\t\t\t\t\t\t\t\tsessions.push({\n\t\t\t\t\t\t\t\t\t\t...summary,\n\t\t\t\t\t\t\t\t\t\tstatus: record.status,\n\t\t\t\t\t\t\t\t\t\t...(record.reasonCode === undefined ? {} : { reasonCode: record.reasonCode }),\n\t\t\t\t\t\t\t\t\t\t...(record.reason === undefined ? {} : { reason: record.reason }),\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// 无内存记录(父重启前的旧子会话):只回摘要,transcript 仍在盘上。\n\t\t\t\t\t\t\tsessions.push(summary);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn { count: sessions.length, sessions };\n\t\t\t\t\t},\n\t\t\t\t});\n\n\treturn [\n\t\ttool,\n\t\tresultTool,\n\t\t...(resumeTool === undefined ? [] : [resumeTool]),\n\t\t...(listTool === undefined ? [] : [listTool]),\n\t\t// 子侧通信面(D-060 S4/S5):获准自建会话两面并存(向下委派 + 向父上抛/兄弟消息)。\n\t\t...childTools,\n\t\tcreateDisposableRegistration(\n\t\t\t\"subagent-delegate.runtime\",\n\t\t\t// 调度器释放是有界的 async 过程(cancel fence + 有界 drain),但 embedded\n\t\t\t// builtin 走同步 loader(loadCapabilityPluginSync -> disposePluginSync 拒绝\n\t\t\t// 异步 cleanup),因此登记处同步发起、观察 promise,不返回 Promise。\n\t\t\t() => {\n\t\t\t\tfor (const unsubscribe of progressUnsubscribes) unsubscribe();\n\t\t\t\tprogressUnsubscribes.clear();\n\t\t\t\t// 先释放调度器(任务终态落定),再回收 runner 池(先释放任务再杀进程)。\n\t\t\t\tvoid runtime\n\t\t\t\t\t.dispose()\n\t\t\t\t\t.then(() => pool?.shutdown())\n\t\t\t\t\t.catch(() => {\n\t\t\t\t\t\t// 释放失败的收尾错误已由 runtime 内部按任务暴露;同步 cleanup 路径\n\t\t\t\t\t\t// 无法再抛,这里只保证不产生未处理的 rejection。\n\t\t\t\t\t});\n\t\t\t},\n\t\t\t`registration:${capabilityManifest.id}:runtime`,\n\t\t),\n\t];\n}\n"]}
|