@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
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subagent-task 远程 adapter(D-045 / pooled 切片第 1 增量)。
|
|
3
|
+
*
|
|
4
|
+
* `AgentTaskAdapter` 的跨进程实现:经 `SubagentTaskChannelV1` 与 runner 侧
|
|
5
|
+
* server 对话(create/run/cancel/dispose/result)。run 语义与本地一致——
|
|
6
|
+
* `host.run()` resolve = 该次 prompt 完成(aborted 是正常结果);signal 中止
|
|
7
|
+
* 时发 cancel 并等待 run 终态。`session.getMessages()` 为同步签名,消息在
|
|
8
|
+
* run 终态时经 result op 预取缓存。本文件不做进程 spawn(runner 生命周期属
|
|
9
|
+
* pooled 切片第 2 增量)。
|
|
10
|
+
*
|
|
11
|
+
* 迁自宿主 `src/plugins/subagent-task/remote-adapter.ts`(D-075 S4 第三批);公共契约
|
|
12
|
+
* 改自 plugin-sdk,wire 协议/进度/server 事实类型改自 @agent-forge/protocol(宿主侧
|
|
13
|
+
* 同名模块已下沉该包)。
|
|
14
|
+
*/
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
import { AGENT_TASK_CHILD_PROFILE_KEY, AGENT_TASK_PARENT_SESSION_KEY, AGENT_TASK_PARENT_TRACE_KEY, AGENT_TASK_RESUME_SESSION_KEY, } from "@agent-forge/plugin-sdk";
|
|
17
|
+
import { SUBAGENT_TASK_PROTOCOL_VERSION, SubagentTaskProtocolError, } from "@agent-forge/protocol/subagent";
|
|
18
|
+
/** 与本地 SDK adapter 相同的取消原因规范化:非字符串 reason 记为 "cancelled"。 */
|
|
19
|
+
function abortReasonText(signal) {
|
|
20
|
+
return typeof signal.reason === "string" ? signal.reason : "cancelled";
|
|
21
|
+
}
|
|
22
|
+
/** 身份违规(跨任务应答):一律硬失败,绝不降级成"拿不到结果"。 */
|
|
23
|
+
function isCrossTaskViolation(error) {
|
|
24
|
+
return typeof error === "object" && error !== null && error.code === "cross_task_response";
|
|
25
|
+
}
|
|
26
|
+
/** 与 sdk.ts abortError 同形的取消类错误:name="AbortError",由 guards 归到 cancelled。 */
|
|
27
|
+
function abortLikeError(message) {
|
|
28
|
+
const error = new Error(message);
|
|
29
|
+
error.name = "AbortError";
|
|
30
|
+
return error;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 远程 host.session 只实现调度路径实际消费的面:getSessionId/getMessages
|
|
34
|
+
* (run 终态时预取的有界快照)/isIdle/abort(映射为 cancel)/waitForIdle。
|
|
35
|
+
* prompt 为桩——远程路径经 host.run 走 run op,不支持对 remote session 直接
|
|
36
|
+
* prompt。桩边界以 docs/design/子代理pooled切片与Agent服务协议边界设计.md §4 为准。
|
|
37
|
+
*/
|
|
38
|
+
function createRemoteSessionProxy(taskId, client, agentSessionId) {
|
|
39
|
+
// 远程调度路径不使用的 SessionAPI 成员:显式桩(抛协议错误),不静默返回假数据。
|
|
40
|
+
const unsupportedAsync = (name) => {
|
|
41
|
+
return async () => {
|
|
42
|
+
throw new SubagentTaskProtocolError(`remote session does not support ${name}`);
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
const unsupportedSync = (name) => {
|
|
46
|
+
return () => {
|
|
47
|
+
throw new SubagentTaskProtocolError(`remote session does not support ${name}`);
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
getSessionId: () => taskId,
|
|
52
|
+
// 叶子会话自身身份:父侧记录与回读用它(而不是 wire 任务 id)。
|
|
53
|
+
...(agentSessionId === undefined ? {} : { getAgentSessionId: () => agentSessionId }),
|
|
54
|
+
getMessages: () => client.cachedMessages(taskId),
|
|
55
|
+
isIdle: () => !client.hasActiveRun(taskId),
|
|
56
|
+
prompt: unsupportedAsync("direct prompt; use AgentTaskHost.run"),
|
|
57
|
+
abort: () => client.cancelTask(taskId, "session abort"),
|
|
58
|
+
waitForIdle: () => client.waitForIdle(taskId),
|
|
59
|
+
appendEntry: unsupportedSync("appendEntry"),
|
|
60
|
+
getBranchEntries: unsupportedSync("getBranchEntries"),
|
|
61
|
+
// 事实面(runner 自报):档案核验需要它们。runner 未回事实时**不声明**这些成员——
|
|
62
|
+
// 上层因此看到"宿主不暴露事实"并按无法自证处理,而不是被空数组糊弄成通过。
|
|
63
|
+
...(client.appliedProfile(taskId)?.activeTools === undefined
|
|
64
|
+
? {}
|
|
65
|
+
: { getActiveTools: () => [...(client.appliedProfile(taskId)?.activeTools ?? [])] }),
|
|
66
|
+
...(client.appliedProfile(taskId)?.model === undefined
|
|
67
|
+
? {}
|
|
68
|
+
: { getModel: () => ({ ...client.appliedProfile(taskId)?.model }) }),
|
|
69
|
+
// 父→子引导(D-060 S5 pooled parity):经 wire steer op 到叶子会话自身 steer。
|
|
70
|
+
steer: (text) => client.steerTask(taskId, text),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
class RemoteTaskClient {
|
|
74
|
+
channel;
|
|
75
|
+
requestSequence = 0;
|
|
76
|
+
pending = new Map();
|
|
77
|
+
progressListeners = new Set();
|
|
78
|
+
taskProgressListeners = new Map();
|
|
79
|
+
activeRuns = new Map();
|
|
80
|
+
/** 每个任务当前 run 的 runId:进度事件按此校验,陈旧或串台 run 的进度不得投递。 */
|
|
81
|
+
activeRunIds = new Map();
|
|
82
|
+
idleWaiters = new Map();
|
|
83
|
+
messageCache = new Map();
|
|
84
|
+
/** create 应答里 runner 自报的收窄事实(工具面/模型/模式):代理据此暴露可核验面。 */
|
|
85
|
+
appliedProfiles = new Map();
|
|
86
|
+
reader;
|
|
87
|
+
closed = false;
|
|
88
|
+
constructor(channel) {
|
|
89
|
+
this.channel = channel;
|
|
90
|
+
}
|
|
91
|
+
start() {
|
|
92
|
+
if (this.reader !== undefined)
|
|
93
|
+
return;
|
|
94
|
+
this.reader = (async () => {
|
|
95
|
+
try {
|
|
96
|
+
for await (const env of this.channel) {
|
|
97
|
+
this.dispatch(env);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
this.failAllPending(error instanceof Error ? error : new SubagentTaskProtocolError("channel read failed"));
|
|
102
|
+
}
|
|
103
|
+
// 通道关闭 = 对端(runner 进程)已消失:在途任务按 runner_lost 结算,
|
|
104
|
+
// 由调度器投影为 failed + reasonCode subagent_runner_lost(D-044 §4.3)。
|
|
105
|
+
this.failAllPending(new SubagentTaskProtocolError("subagent runner lost: channel closed", "runner_lost"));
|
|
106
|
+
})();
|
|
107
|
+
void this.reader.catch(() => { });
|
|
108
|
+
}
|
|
109
|
+
dispatch(env) {
|
|
110
|
+
if (env.kind === "response") {
|
|
111
|
+
const pending = this.pending.get(env.id);
|
|
112
|
+
if (pending === undefined)
|
|
113
|
+
return; // 未知响应:丢弃(迟到回执)
|
|
114
|
+
this.pending.delete(env.id);
|
|
115
|
+
if (env.ok)
|
|
116
|
+
pending.resolve(env);
|
|
117
|
+
else
|
|
118
|
+
pending.reject(new SubagentTaskProtocolError(env.message, env.code));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (env.kind === "event") {
|
|
122
|
+
// 隔离校验:只有"该任务当前 run"的进度才投递。runner 侧陈旧批次、
|
|
123
|
+
// 或身份错标(B 任务的进度挂在 A 的 taskId 上)一律丢弃,宁可少一条
|
|
124
|
+
// 提示也不让父侧把别的子会话的进度算到当前任务上。
|
|
125
|
+
const expectedRunId = this.activeRunIds.get(env.taskId);
|
|
126
|
+
if (expectedRunId === undefined || env.runId !== expectedRunId)
|
|
127
|
+
return;
|
|
128
|
+
for (const listener of this.progressListeners)
|
|
129
|
+
listener(env);
|
|
130
|
+
const taskListeners = this.taskProgressListeners.get(env.taskId);
|
|
131
|
+
if (taskListeners !== undefined) {
|
|
132
|
+
for (const listener of taskListeners)
|
|
133
|
+
listener(env.message, env.seq);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
failAllPending(error) {
|
|
138
|
+
this.closed = true;
|
|
139
|
+
for (const pending of this.pending.values())
|
|
140
|
+
pending.reject(error);
|
|
141
|
+
this.pending.clear();
|
|
142
|
+
}
|
|
143
|
+
call(request) {
|
|
144
|
+
// 通道已关闭后不再入队:立即 reject,避免调用方永久悬挂。
|
|
145
|
+
if (this.closed) {
|
|
146
|
+
return Promise.reject(new SubagentTaskProtocolError("subagent runner lost: channel closed", "runner_lost"));
|
|
147
|
+
}
|
|
148
|
+
this.start();
|
|
149
|
+
const id = `req:${++this.requestSequence}:${randomUUID().slice(0, 8)}`;
|
|
150
|
+
return new Promise((resolve, reject) => {
|
|
151
|
+
this.pending.set(id, { resolve, reject });
|
|
152
|
+
void this.channel.write({ v: SUBAGENT_TASK_PROTOCOL_VERSION, kind: "request", id, ...request });
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
onProgress(listener) {
|
|
156
|
+
this.progressListeners.add(listener);
|
|
157
|
+
return () => this.progressListeners.delete(listener);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 任务作用域调用:请求与应答必须是同一个任务。runner 若就 A 的请求回 B 的
|
|
161
|
+
* taskId/runId(身份错标、状态错位),这里按 `cross_task_response` 硬失败,
|
|
162
|
+
* 绝不把别的子会话的消息缓存成 A 的结果。
|
|
163
|
+
*/
|
|
164
|
+
async callForTask(taskId, request, runId) {
|
|
165
|
+
const response = await this.call({ ...request, taskId, ...(runId === undefined ? {} : { runId }) });
|
|
166
|
+
if (response.taskId !== taskId) {
|
|
167
|
+
throw new SubagentTaskProtocolError(`runner answered task ${String(response.taskId)} for a task ${taskId} request`, "cross_task_response");
|
|
168
|
+
}
|
|
169
|
+
if (runId !== undefined && response.runId !== runId) {
|
|
170
|
+
throw new SubagentTaskProtocolError(`runner answered run ${String(response.runId)} for a run ${runId} request`, "cross_task_response");
|
|
171
|
+
}
|
|
172
|
+
return response;
|
|
173
|
+
}
|
|
174
|
+
/** 订阅某个任务(wire taskId)的进度事件;返回退订函数。 */
|
|
175
|
+
onTaskProgress(taskId, listener) {
|
|
176
|
+
const listeners = this.taskProgressListeners.get(taskId);
|
|
177
|
+
if (listeners === undefined)
|
|
178
|
+
this.taskProgressListeners.set(taskId, new Set([listener]));
|
|
179
|
+
else
|
|
180
|
+
listeners.add(listener);
|
|
181
|
+
return () => {
|
|
182
|
+
const current = this.taskProgressListeners.get(taskId);
|
|
183
|
+
if (current === undefined)
|
|
184
|
+
return;
|
|
185
|
+
current.delete(listener);
|
|
186
|
+
if (current.size === 0)
|
|
187
|
+
this.taskProgressListeners.delete(taskId);
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/** 登记 in-flight run 并接管 signal→cancel 级联;释放必须经 releaseRun。 */
|
|
191
|
+
trackRun(taskId, runId, signal) {
|
|
192
|
+
this.activeRuns.set(taskId, (this.activeRuns.get(taskId) ?? 0) + 1);
|
|
193
|
+
this.activeRunIds.set(taskId, runId);
|
|
194
|
+
// 一次 run 至多挂一个 abort 监听;server 侧 cancel 幂等,重复触发无害。
|
|
195
|
+
signal.addEventListener("abort", () => {
|
|
196
|
+
void this.cancelTask(taskId, abortReasonText(signal)).catch(() => { });
|
|
197
|
+
}, { once: true });
|
|
198
|
+
}
|
|
199
|
+
/** run 终态(resolve 或 reject)后调用:递减活跃计数并唤醒 waitForIdle。 */
|
|
200
|
+
releaseRun(taskId, runId) {
|
|
201
|
+
const current = this.activeRuns.get(taskId) ?? 0;
|
|
202
|
+
if (current <= 1) {
|
|
203
|
+
this.activeRuns.delete(taskId);
|
|
204
|
+
// 仅释放属于该 run 的 runId 槽:并发的同名释放不会顶掉新 run 的登记。
|
|
205
|
+
if (runId === undefined || this.activeRunIds.get(taskId) === runId) {
|
|
206
|
+
this.activeRunIds.delete(taskId);
|
|
207
|
+
}
|
|
208
|
+
this.wakeIdle(taskId);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
this.activeRuns.set(taskId, current - 1);
|
|
212
|
+
}
|
|
213
|
+
hasActiveRun(taskId) {
|
|
214
|
+
return (this.activeRuns.get(taskId) ?? 0) > 0;
|
|
215
|
+
}
|
|
216
|
+
waitForIdle(taskId) {
|
|
217
|
+
if (!this.hasActiveRun(taskId))
|
|
218
|
+
return Promise.resolve();
|
|
219
|
+
return new Promise((resolve) => {
|
|
220
|
+
const waiters = this.idleWaiters.get(taskId);
|
|
221
|
+
if (waiters === undefined)
|
|
222
|
+
this.idleWaiters.set(taskId, [resolve]);
|
|
223
|
+
else
|
|
224
|
+
waiters.push(resolve);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
wakeIdle(taskId) {
|
|
228
|
+
for (const wake of this.idleWaiters.get(taskId)?.splice(0) ?? [])
|
|
229
|
+
wake();
|
|
230
|
+
}
|
|
231
|
+
async refreshMessages(taskId) {
|
|
232
|
+
const response = await this.callForTask(taskId, { op: "result" });
|
|
233
|
+
this.messageCache.set(taskId, response.messages ?? []);
|
|
234
|
+
}
|
|
235
|
+
cachedMessages(taskId) {
|
|
236
|
+
return this.messageCache.get(taskId) ?? [];
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* 释放某任务的缓存:终态后消息快照与进度订阅都不再需要,长寿命 runner 连接
|
|
240
|
+
* 不能按任务数无界增长(每条快照上限是 settle 预算)。释放顺序由调度器保证:
|
|
241
|
+
* settle 先读 getMessages,再调用 host.dispose。
|
|
242
|
+
*/
|
|
243
|
+
dropTask(taskId) {
|
|
244
|
+
this.messageCache.delete(taskId);
|
|
245
|
+
this.taskProgressListeners.delete(taskId);
|
|
246
|
+
this.activeRunIds.delete(taskId);
|
|
247
|
+
this.appliedProfiles.delete(taskId);
|
|
248
|
+
}
|
|
249
|
+
/** 记录 runner 自报的档案事实(create 应答)。 */
|
|
250
|
+
rememberAppliedProfile(taskId, applied) {
|
|
251
|
+
if (applied === undefined)
|
|
252
|
+
return;
|
|
253
|
+
this.appliedProfiles.set(taskId, applied);
|
|
254
|
+
}
|
|
255
|
+
appliedProfile(taskId) {
|
|
256
|
+
return this.appliedProfiles.get(taskId);
|
|
257
|
+
}
|
|
258
|
+
async cancelTask(taskId, reason) {
|
|
259
|
+
await this.callForTask(taskId, { op: "cancel", ...(reason === undefined ? {} : { reason }) });
|
|
260
|
+
}
|
|
261
|
+
/** 父→子引导(D-060 S5 pooled parity):steer op 到 runner 叶子会话。 */
|
|
262
|
+
async steerTask(taskId, text) {
|
|
263
|
+
await this.callForTask(taskId, { op: "steer", text });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* 远程 `AgentTaskAdapter`:与本地 SDK adapter 语义对齐——create 分配 runner 侧
|
|
268
|
+
* 会话;run resolve = prompt 完成,`aborted` 终局抛 `AbortError` 类取消错误
|
|
269
|
+
* (与 sdk.ts 的 aborted PromptOutcome 处理一致,由 guards 归到 cancelled,
|
|
270
|
+
* 不得落成 succeeded);cancel 幂等;dispose 释放。channel 关闭或读失败时
|
|
271
|
+
* 全部在途请求 reject(保留错误码),关闭后的新请求立即 reject 不悬挂。
|
|
272
|
+
*/
|
|
273
|
+
export function createRemoteAgentTaskAdapter(channel) {
|
|
274
|
+
const client = new RemoteTaskClient(channel);
|
|
275
|
+
const liveTaskIds = new Set();
|
|
276
|
+
return {
|
|
277
|
+
/** D-044 §5.6:把 runner 侧的 progress 事件按 wire taskId 转给上层订阅者。 */
|
|
278
|
+
onTaskProgress(taskId, listener) {
|
|
279
|
+
return client.onTaskProgress(taskId, listener);
|
|
280
|
+
},
|
|
281
|
+
create: async (request) => {
|
|
282
|
+
// 子任务档案随 create 过线:runner 侧叶子会话按它收窄,并在应答里回实际事实。
|
|
283
|
+
const profile = request.metadata?.[AGENT_TASK_CHILD_PROFILE_KEY];
|
|
284
|
+
const parentSessionId = request.metadata?.[AGENT_TASK_PARENT_SESSION_KEY];
|
|
285
|
+
// resume 目标(D-060 S5 pooled parity):runner 打开既有会话文件续写,不新建。
|
|
286
|
+
const resumeSessionId = request.metadata?.[AGENT_TASK_RESUME_SESSION_KEY];
|
|
287
|
+
// 回合级血缘(第 3 期第二批):创建子会话的父回合 correlationId,仅 create 新建消费。
|
|
288
|
+
const parentTraceId = request.metadata?.[AGENT_TASK_PARENT_TRACE_KEY];
|
|
289
|
+
const created = await client.call({
|
|
290
|
+
op: "create",
|
|
291
|
+
prompt: request.prompt,
|
|
292
|
+
...(profile === undefined ? {} : { profile }),
|
|
293
|
+
...(typeof parentSessionId === "string" ? { parentSessionId } : {}),
|
|
294
|
+
...(typeof resumeSessionId === "string" ? { resumeSessionId } : {}),
|
|
295
|
+
...(typeof parentTraceId === "string" ? { parentTraceId } : {}),
|
|
296
|
+
});
|
|
297
|
+
const taskId = created.taskId;
|
|
298
|
+
if (taskId === undefined) {
|
|
299
|
+
throw new SubagentTaskProtocolError("create response missing taskId", "invalid_request");
|
|
300
|
+
}
|
|
301
|
+
// 任务 id 必须唯一:runner 若把已存活任务的 id 再发一次,就是身份复用,
|
|
302
|
+
// 之后两个任务的应答将无法区分——直接硬失败而不是继续跑。
|
|
303
|
+
if (liveTaskIds.has(taskId)) {
|
|
304
|
+
throw new SubagentTaskProtocolError(`runner reused the id of a live task: ${taskId}`, "cross_task_response");
|
|
305
|
+
}
|
|
306
|
+
liveTaskIds.add(taskId);
|
|
307
|
+
client.rememberAppliedProfile(taskId, created.appliedProfile);
|
|
308
|
+
// 叶子会话自身的 sessionId(D-060 S1):宿主可见的 getSessionId() 是 wire 任务 id,
|
|
309
|
+
// 真正的 agent 会话 id 走 getAgentSessionId 暴露。
|
|
310
|
+
const agentSessionId = created.sessionId;
|
|
311
|
+
const host = {
|
|
312
|
+
session: createRemoteSessionProxy(taskId, client, agentSessionId),
|
|
313
|
+
run: (prompt, signal) => {
|
|
314
|
+
const runId = `run:${randomUUID().slice(0, 8)}`;
|
|
315
|
+
if (signal.aborted) {
|
|
316
|
+
// 与本地 SDK adapter 一致:入口即中止只本地拒绝,不向 runner 发取消——
|
|
317
|
+
// 什么都没有启动,任务保持可用(发取消会把任务标记为终态)。
|
|
318
|
+
return Promise.reject(abortLikeError(`run aborted before start: ${abortReasonText(signal)}`));
|
|
319
|
+
}
|
|
320
|
+
client.trackRun(taskId, runId, signal);
|
|
321
|
+
return client
|
|
322
|
+
.callForTask(taskId, { op: "run", prompt }, runId)
|
|
323
|
+
.then(async (response) => {
|
|
324
|
+
// run 终态后预取有界消息,供调度器 settle 同步读取。
|
|
325
|
+
// 预取失败本身可容忍(拿不到消息不等于任务失败),但身份违规不可
|
|
326
|
+
// 容忍:那是 runner 把别的任务的消息算到了这个任务上。
|
|
327
|
+
await client.refreshMessages(taskId).catch((error) => {
|
|
328
|
+
if (isCrossTaskViolation(error))
|
|
329
|
+
throw error;
|
|
330
|
+
});
|
|
331
|
+
if (response.status === "cancelled") {
|
|
332
|
+
throw abortLikeError("agent task run was cancelled");
|
|
333
|
+
}
|
|
334
|
+
})
|
|
335
|
+
.catch((error) => {
|
|
336
|
+
// 取消先于 run 到达:runner 以 task_terminal 拒绝启动。父侧确实请求过取消,
|
|
337
|
+
// 因此仍按取消类错误结算(与"aborted 终局"同一分类),不落成 failed。
|
|
338
|
+
if (signal.aborted &&
|
|
339
|
+
typeof error === "object" &&
|
|
340
|
+
error !== null &&
|
|
341
|
+
error.code === "task_terminal") {
|
|
342
|
+
throw abortLikeError(`agent task run was cancelled before start: ${abortReasonText(signal)}`);
|
|
343
|
+
}
|
|
344
|
+
throw error;
|
|
345
|
+
})
|
|
346
|
+
.finally(() => client.releaseRun(taskId, runId));
|
|
347
|
+
},
|
|
348
|
+
cancel: (reason) => client.cancelTask(taskId, reason),
|
|
349
|
+
dispose: async () => {
|
|
350
|
+
liveTaskIds.delete(taskId);
|
|
351
|
+
await client.callForTask(taskId, { op: "dispose" });
|
|
352
|
+
client.dropTask(taskId);
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
return host;
|
|
356
|
+
},
|
|
357
|
+
dispose: async () => {
|
|
358
|
+
// 宿主会话销毁时释放全部未 dispose 的任务(逐个容错,不阻断其余回收)。
|
|
359
|
+
for (const taskId of [...liveTaskIds]) {
|
|
360
|
+
liveTaskIds.delete(taskId);
|
|
361
|
+
await client.callForTask(taskId, { op: "dispose" }).catch(() => { });
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
//# sourceMappingURL=remote-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-adapter.js","sourceRoot":"","sources":["../src/remote-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * subagent-task 远程 adapter(D-045 / pooled 切片第 1 增量)。\n *\n * `AgentTaskAdapter` 的跨进程实现:经 `SubagentTaskChannelV1` 与 runner 侧\n * server 对话(create/run/cancel/dispose/result)。run 语义与本地一致——\n * `host.run()` resolve = 该次 prompt 完成(aborted 是正常结果);signal 中止\n * 时发 cancel 并等待 run 终态。`session.getMessages()` 为同步签名,消息在\n * run 终态时经 result op 预取缓存。本文件不做进程 spawn(runner 生命周期属\n * pooled 切片第 2 增量)。\n *\n * 迁自宿主 `src/plugins/subagent-task/remote-adapter.ts`(D-075 S4 第三批);公共契约\n * 改自 plugin-sdk,wire 协议/进度/server 事实类型改自 @agent-forge/protocol(宿主侧\n * 同名模块已下沉该包)。\n */\nimport { randomUUID } from \"node:crypto\";\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 AgentTaskCreateRequest,\n\ttype AgentTaskHost,\n\ttype ModelFactsViewV1,\n\ttype SessionAPI,\n} from \"@agent-forge/plugin-sdk\";\nimport {\n\tSUBAGENT_TASK_PROTOCOL_VERSION,\n\ttype SubagentTaskAppliedProfileV1,\n\ttype SubagentTaskChannelV1,\n\ttype SubagentTaskEnvelopeV1,\n\ttype SubagentTaskOkResponseEnvelopeV1,\n\ttype SubagentTaskProgressAdapterV1,\n\tSubagentTaskProtocolError,\n} from \"@agent-forge/protocol/subagent\";\n\ninterface PendingRequest {\n\tresolve(env: SubagentTaskOkResponseEnvelopeV1): void;\n\treject(error: SubagentTaskProtocolError): void;\n}\n\n/** 与本地 SDK adapter 相同的取消原因规范化:非字符串 reason 记为 \"cancelled\"。 */\nfunction abortReasonText(signal: AbortSignal): string {\n\treturn typeof signal.reason === \"string\" ? signal.reason : \"cancelled\";\n}\n\n/** 身份违规(跨任务应答):一律硬失败,绝不降级成\"拿不到结果\"。 */\nfunction isCrossTaskViolation(error: unknown): boolean {\n\treturn typeof error === \"object\" && error !== null && (error as { code?: unknown }).code === \"cross_task_response\";\n}\n\n/** 与 sdk.ts abortError 同形的取消类错误:name=\"AbortError\",由 guards 归到 cancelled。 */\nfunction abortLikeError(message: string): Error {\n\tconst error = new Error(message);\n\terror.name = \"AbortError\";\n\treturn error;\n}\n\n/**\n * 远程 host.session 只实现调度路径实际消费的面:getSessionId/getMessages\n * (run 终态时预取的有界快照)/isIdle/abort(映射为 cancel)/waitForIdle。\n * prompt 为桩——远程路径经 host.run 走 run op,不支持对 remote session 直接\n * prompt。桩边界以 docs/design/子代理pooled切片与Agent服务协议边界设计.md §4 为准。\n */\n\nfunction createRemoteSessionProxy(taskId: string, client: RemoteTaskClient, agentSessionId?: string): SessionAPI {\n\t// 远程调度路径不使用的 SessionAPI 成员:显式桩(抛协议错误),不静默返回假数据。\n\tconst unsupportedAsync = (name: string): (() => Promise<never>) => {\n\t\treturn async () => {\n\t\t\tthrow new SubagentTaskProtocolError(`remote session does not support ${name}`);\n\t\t};\n\t};\n\tconst unsupportedSync = (name: string): (() => never) => {\n\t\treturn () => {\n\t\t\tthrow new SubagentTaskProtocolError(`remote session does not support ${name}`);\n\t\t};\n\t};\n\treturn {\n\t\tgetSessionId: () => taskId,\n\t\t// 叶子会话自身身份:父侧记录与回读用它(而不是 wire 任务 id)。\n\t\t...(agentSessionId === undefined ? {} : { getAgentSessionId: () => agentSessionId }),\n\t\tgetMessages: () => client.cachedMessages(taskId),\n\t\tisIdle: () => !client.hasActiveRun(taskId),\n\t\tprompt: unsupportedAsync(\"direct prompt; use AgentTaskHost.run\"),\n\t\tabort: () => client.cancelTask(taskId, \"session abort\"),\n\t\twaitForIdle: () => client.waitForIdle(taskId),\n\t\tappendEntry: unsupportedSync(\"appendEntry\"),\n\t\tgetBranchEntries: unsupportedSync(\"getBranchEntries\"),\n\t\t// 事实面(runner 自报):档案核验需要它们。runner 未回事实时**不声明**这些成员——\n\t\t// 上层因此看到\"宿主不暴露事实\"并按无法自证处理,而不是被空数组糊弄成通过。\n\t\t...(client.appliedProfile(taskId)?.activeTools === undefined\n\t\t\t? {}\n\t\t\t: { getActiveTools: (): string[] => [...(client.appliedProfile(taskId)?.activeTools ?? [])] }),\n\t\t...(client.appliedProfile(taskId)?.model === undefined\n\t\t\t? {}\n\t\t\t: { getModel: (): ModelFactsViewV1 => ({ ...(client.appliedProfile(taskId)?.model as ModelFactsViewV1) }) }),\n\t\t// 父→子引导(D-060 S5 pooled parity):经 wire steer op 到叶子会话自身 steer。\n\t\tsteer: (text: string) => client.steerTask(taskId, text),\n\t};\n}\n\nclass RemoteTaskClient {\n\tprivate readonly channel: SubagentTaskChannelV1;\n\tprivate requestSequence = 0;\n\tprivate readonly pending = new Map<string, PendingRequest>();\n\tprivate readonly progressListeners = new Set<(env: SubagentTaskEnvelopeV1) => void>();\n\tprivate readonly taskProgressListeners = new Map<string, Set<(message: string | undefined, seq: number) => void>>();\n\tprivate readonly activeRuns = new Map<string, number>();\n\t/** 每个任务当前 run 的 runId:进度事件按此校验,陈旧或串台 run 的进度不得投递。 */\n\tprivate readonly activeRunIds = new Map<string, string>();\n\tprivate readonly idleWaiters = new Map<string, Array<() => void>>();\n\tprivate readonly messageCache = new Map<string, readonly unknown[]>();\n\t/** create 应答里 runner 自报的收窄事实(工具面/模型/模式):代理据此暴露可核验面。 */\n\tprivate readonly appliedProfiles = new Map<string, SubagentTaskAppliedProfileV1>();\n\tprivate reader: Promise<void> | undefined;\n\tprivate closed = false;\n\n\tconstructor(channel: SubagentTaskChannelV1) {\n\t\tthis.channel = channel;\n\t}\n\n\tstart(): void {\n\t\tif (this.reader !== undefined) return;\n\t\tthis.reader = (async () => {\n\t\t\ttry {\n\t\t\t\tfor await (const env of this.channel) {\n\t\t\t\t\tthis.dispatch(env);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tthis.failAllPending(error instanceof Error ? error : new SubagentTaskProtocolError(\"channel read failed\"));\n\t\t\t}\n\t\t\t// 通道关闭 = 对端(runner 进程)已消失:在途任务按 runner_lost 结算,\n\t\t\t// 由调度器投影为 failed + reasonCode subagent_runner_lost(D-044 §4.3)。\n\t\t\tthis.failAllPending(new SubagentTaskProtocolError(\"subagent runner lost: channel closed\", \"runner_lost\"));\n\t\t})();\n\t\tvoid this.reader.catch(() => {});\n\t}\n\n\tprivate dispatch(env: SubagentTaskEnvelopeV1): void {\n\t\tif (env.kind === \"response\") {\n\t\t\tconst pending = this.pending.get(env.id);\n\t\t\tif (pending === undefined) return; // 未知响应:丢弃(迟到回执)\n\t\t\tthis.pending.delete(env.id);\n\t\t\tif (env.ok) pending.resolve(env);\n\t\t\telse pending.reject(new SubagentTaskProtocolError(env.message, env.code));\n\t\t\treturn;\n\t\t}\n\t\tif (env.kind === \"event\") {\n\t\t\t// 隔离校验:只有\"该任务当前 run\"的进度才投递。runner 侧陈旧批次、\n\t\t\t// 或身份错标(B 任务的进度挂在 A 的 taskId 上)一律丢弃,宁可少一条\n\t\t\t// 提示也不让父侧把别的子会话的进度算到当前任务上。\n\t\t\tconst expectedRunId = this.activeRunIds.get(env.taskId);\n\t\t\tif (expectedRunId === undefined || env.runId !== expectedRunId) return;\n\t\t\tfor (const listener of this.progressListeners) listener(env);\n\t\t\tconst taskListeners = this.taskProgressListeners.get(env.taskId);\n\t\t\tif (taskListeners !== undefined) {\n\t\t\t\tfor (const listener of taskListeners) listener(env.message, env.seq);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate failAllPending(error: SubagentTaskProtocolError): void {\n\t\tthis.closed = true;\n\t\tfor (const pending of this.pending.values()) pending.reject(error);\n\t\tthis.pending.clear();\n\t}\n\n\tcall(\n\t\trequest: Omit<Extract<SubagentTaskEnvelopeV1, { kind: \"request\" }>, \"v\" | \"kind\" | \"id\">,\n\t): Promise<SubagentTaskOkResponseEnvelopeV1> {\n\t\t// 通道已关闭后不再入队:立即 reject,避免调用方永久悬挂。\n\t\tif (this.closed) {\n\t\t\treturn Promise.reject(new SubagentTaskProtocolError(\"subagent runner lost: channel closed\", \"runner_lost\"));\n\t\t}\n\t\tthis.start();\n\t\tconst id = `req:${++this.requestSequence}:${randomUUID().slice(0, 8)}`;\n\t\treturn new Promise<SubagentTaskOkResponseEnvelopeV1>((resolve, reject) => {\n\t\t\tthis.pending.set(id, { resolve, reject });\n\t\t\tvoid this.channel.write({ v: SUBAGENT_TASK_PROTOCOL_VERSION, kind: \"request\", id, ...request });\n\t\t});\n\t}\n\n\tonProgress(listener: (env: SubagentTaskEnvelopeV1) => void): () => void {\n\t\tthis.progressListeners.add(listener);\n\t\treturn () => this.progressListeners.delete(listener);\n\t}\n\n\t/**\n\t * 任务作用域调用:请求与应答必须是同一个任务。runner 若就 A 的请求回 B 的\n\t * taskId/runId(身份错标、状态错位),这里按 `cross_task_response` 硬失败,\n\t * 绝不把别的子会话的消息缓存成 A 的结果。\n\t */\n\tasync callForTask(\n\t\ttaskId: string,\n\t\trequest: Omit<Extract<SubagentTaskEnvelopeV1, { kind: \"request\" }>, \"v\" | \"kind\" | \"id\" | \"taskId\">,\n\t\trunId?: string,\n\t): Promise<SubagentTaskOkResponseEnvelopeV1> {\n\t\tconst response = await this.call({ ...request, taskId, ...(runId === undefined ? {} : { runId }) });\n\t\tif (response.taskId !== taskId) {\n\t\t\tthrow new SubagentTaskProtocolError(\n\t\t\t\t`runner answered task ${String(response.taskId)} for a task ${taskId} request`,\n\t\t\t\t\"cross_task_response\",\n\t\t\t);\n\t\t}\n\t\tif (runId !== undefined && response.runId !== runId) {\n\t\t\tthrow new SubagentTaskProtocolError(\n\t\t\t\t`runner answered run ${String(response.runId)} for a run ${runId} request`,\n\t\t\t\t\"cross_task_response\",\n\t\t\t);\n\t\t}\n\t\treturn response;\n\t}\n\n\t/** 订阅某个任务(wire taskId)的进度事件;返回退订函数。 */\n\tonTaskProgress(taskId: string, listener: (message: string | undefined, seq: number) => void): () => void {\n\t\tconst listeners = this.taskProgressListeners.get(taskId);\n\t\tif (listeners === undefined) this.taskProgressListeners.set(taskId, new Set([listener]));\n\t\telse listeners.add(listener);\n\t\treturn () => {\n\t\t\tconst current = this.taskProgressListeners.get(taskId);\n\t\t\tif (current === undefined) return;\n\t\t\tcurrent.delete(listener);\n\t\t\tif (current.size === 0) this.taskProgressListeners.delete(taskId);\n\t\t};\n\t}\n\n\t/** 登记 in-flight run 并接管 signal→cancel 级联;释放必须经 releaseRun。 */\n\ttrackRun(taskId: string, runId: string, signal: AbortSignal): void {\n\t\tthis.activeRuns.set(taskId, (this.activeRuns.get(taskId) ?? 0) + 1);\n\t\tthis.activeRunIds.set(taskId, runId);\n\t\t// 一次 run 至多挂一个 abort 监听;server 侧 cancel 幂等,重复触发无害。\n\t\tsignal.addEventListener(\n\t\t\t\"abort\",\n\t\t\t() => {\n\t\t\t\tvoid this.cancelTask(taskId, abortReasonText(signal)).catch(() => {});\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\t/** run 终态(resolve 或 reject)后调用:递减活跃计数并唤醒 waitForIdle。 */\n\treleaseRun(taskId: string, runId?: string): void {\n\t\tconst current = this.activeRuns.get(taskId) ?? 0;\n\t\tif (current <= 1) {\n\t\t\tthis.activeRuns.delete(taskId);\n\t\t\t// 仅释放属于该 run 的 runId 槽:并发的同名释放不会顶掉新 run 的登记。\n\t\t\tif (runId === undefined || this.activeRunIds.get(taskId) === runId) {\n\t\t\t\tthis.activeRunIds.delete(taskId);\n\t\t\t}\n\t\t\tthis.wakeIdle(taskId);\n\t\t\treturn;\n\t\t}\n\t\tthis.activeRuns.set(taskId, current - 1);\n\t}\n\n\thasActiveRun(taskId: string): boolean {\n\t\treturn (this.activeRuns.get(taskId) ?? 0) > 0;\n\t}\n\n\twaitForIdle(taskId: string): Promise<void> {\n\t\tif (!this.hasActiveRun(taskId)) return Promise.resolve();\n\t\treturn new Promise<void>((resolve) => {\n\t\t\tconst waiters = this.idleWaiters.get(taskId);\n\t\t\tif (waiters === undefined) this.idleWaiters.set(taskId, [resolve]);\n\t\t\telse waiters.push(resolve);\n\t\t});\n\t}\n\n\tprivate wakeIdle(taskId: string): void {\n\t\tfor (const wake of this.idleWaiters.get(taskId)?.splice(0) ?? []) wake();\n\t}\n\n\tasync refreshMessages(taskId: string): Promise<void> {\n\t\tconst response = await this.callForTask(taskId, { op: \"result\" });\n\t\tthis.messageCache.set(taskId, response.messages ?? []);\n\t}\n\n\tcachedMessages(taskId: string): readonly unknown[] {\n\t\treturn this.messageCache.get(taskId) ?? [];\n\t}\n\n\t/**\n\t * 释放某任务的缓存:终态后消息快照与进度订阅都不再需要,长寿命 runner 连接\n\t * 不能按任务数无界增长(每条快照上限是 settle 预算)。释放顺序由调度器保证:\n\t * settle 先读 getMessages,再调用 host.dispose。\n\t */\n\tdropTask(taskId: string): void {\n\t\tthis.messageCache.delete(taskId);\n\t\tthis.taskProgressListeners.delete(taskId);\n\t\tthis.activeRunIds.delete(taskId);\n\t\tthis.appliedProfiles.delete(taskId);\n\t}\n\n\t/** 记录 runner 自报的档案事实(create 应答)。 */\n\trememberAppliedProfile(taskId: string, applied: SubagentTaskAppliedProfileV1 | undefined): void {\n\t\tif (applied === undefined) return;\n\t\tthis.appliedProfiles.set(taskId, applied);\n\t}\n\n\tappliedProfile(taskId: string): SubagentTaskAppliedProfileV1 | undefined {\n\t\treturn this.appliedProfiles.get(taskId);\n\t}\n\n\tasync cancelTask(taskId: string, reason?: string): Promise<void> {\n\t\tawait this.callForTask(taskId, { op: \"cancel\", ...(reason === undefined ? {} : { reason }) });\n\t}\n\n\t/** 父→子引导(D-060 S5 pooled parity):steer op 到 runner 叶子会话。 */\n\tasync steerTask(taskId: string, text: string): Promise<void> {\n\t\tawait this.callForTask(taskId, { op: \"steer\", text });\n\t}\n}\n\n/**\n * 远程 `AgentTaskAdapter`:与本地 SDK adapter 语义对齐——create 分配 runner 侧\n * 会话;run resolve = prompt 完成,`aborted` 终局抛 `AbortError` 类取消错误\n * (与 sdk.ts 的 aborted PromptOutcome 处理一致,由 guards 归到 cancelled,\n * 不得落成 succeeded);cancel 幂等;dispose 释放。channel 关闭或读失败时\n * 全部在途请求 reject(保留错误码),关闭后的新请求立即 reject 不悬挂。\n */\nexport function createRemoteAgentTaskAdapter(\n\tchannel: SubagentTaskChannelV1,\n): AgentTaskAdapter & SubagentTaskProgressAdapterV1 {\n\tconst client = new RemoteTaskClient(channel);\n\tconst liveTaskIds = new Set<string>();\n\treturn {\n\t\t/** D-044 §5.6:把 runner 侧的 progress 事件按 wire taskId 转给上层订阅者。 */\n\t\tonTaskProgress(taskId: string, listener: (message: string | undefined, seq: number) => void): () => void {\n\t\t\treturn client.onTaskProgress(taskId, listener);\n\t\t},\n\t\tcreate: async (request: AgentTaskCreateRequest): Promise<AgentTaskHost> => {\n\t\t\t// 子任务档案随 create 过线:runner 侧叶子会话按它收窄,并在应答里回实际事实。\n\t\t\tconst profile = request.metadata?.[AGENT_TASK_CHILD_PROFILE_KEY];\n\t\t\tconst parentSessionId = request.metadata?.[AGENT_TASK_PARENT_SESSION_KEY];\n\t\t\t// resume 目标(D-060 S5 pooled parity):runner 打开既有会话文件续写,不新建。\n\t\t\tconst resumeSessionId = request.metadata?.[AGENT_TASK_RESUME_SESSION_KEY];\n\t\t\t// 回合级血缘(第 3 期第二批):创建子会话的父回合 correlationId,仅 create 新建消费。\n\t\t\tconst parentTraceId = request.metadata?.[AGENT_TASK_PARENT_TRACE_KEY];\n\t\t\tconst created = await client.call({\n\t\t\t\top: \"create\",\n\t\t\t\tprompt: request.prompt,\n\t\t\t\t...(profile === undefined ? {} : { profile }),\n\t\t\t\t...(typeof parentSessionId === \"string\" ? { parentSessionId } : {}),\n\t\t\t\t...(typeof resumeSessionId === \"string\" ? { resumeSessionId } : {}),\n\t\t\t\t...(typeof parentTraceId === \"string\" ? { parentTraceId } : {}),\n\t\t\t});\n\t\t\tconst taskId = created.taskId;\n\t\t\tif (taskId === undefined) {\n\t\t\t\tthrow new SubagentTaskProtocolError(\"create response missing taskId\", \"invalid_request\");\n\t\t\t}\n\t\t\t// 任务 id 必须唯一:runner 若把已存活任务的 id 再发一次,就是身份复用,\n\t\t\t// 之后两个任务的应答将无法区分——直接硬失败而不是继续跑。\n\t\t\tif (liveTaskIds.has(taskId)) {\n\t\t\t\tthrow new SubagentTaskProtocolError(\n\t\t\t\t\t`runner reused the id of a live task: ${taskId}`,\n\t\t\t\t\t\"cross_task_response\",\n\t\t\t\t);\n\t\t\t}\n\t\t\tliveTaskIds.add(taskId);\n\t\t\tclient.rememberAppliedProfile(taskId, created.appliedProfile);\n\t\t\t// 叶子会话自身的 sessionId(D-060 S1):宿主可见的 getSessionId() 是 wire 任务 id,\n\t\t\t// 真正的 agent 会话 id 走 getAgentSessionId 暴露。\n\t\t\tconst agentSessionId = created.sessionId;\n\t\t\tconst host: AgentTaskHost = {\n\t\t\t\tsession: createRemoteSessionProxy(taskId, client, agentSessionId),\n\t\t\t\trun: (prompt: string, signal: AbortSignal): Promise<void> => {\n\t\t\t\t\tconst runId = `run:${randomUUID().slice(0, 8)}`;\n\t\t\t\t\tif (signal.aborted) {\n\t\t\t\t\t\t// 与本地 SDK adapter 一致:入口即中止只本地拒绝,不向 runner 发取消——\n\t\t\t\t\t\t// 什么都没有启动,任务保持可用(发取消会把任务标记为终态)。\n\t\t\t\t\t\treturn Promise.reject(abortLikeError(`run aborted before start: ${abortReasonText(signal)}`));\n\t\t\t\t\t}\n\t\t\t\t\tclient.trackRun(taskId, runId, signal);\n\t\t\t\t\treturn client\n\t\t\t\t\t\t.callForTask(taskId, { op: \"run\", prompt }, runId)\n\t\t\t\t\t\t.then(async (response) => {\n\t\t\t\t\t\t\t// run 终态后预取有界消息,供调度器 settle 同步读取。\n\t\t\t\t\t\t\t// 预取失败本身可容忍(拿不到消息不等于任务失败),但身份违规不可\n\t\t\t\t\t\t\t// 容忍:那是 runner 把别的任务的消息算到了这个任务上。\n\t\t\t\t\t\t\tawait client.refreshMessages(taskId).catch((error: unknown) => {\n\t\t\t\t\t\t\t\tif (isCrossTaskViolation(error)) throw error;\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tif (response.status === \"cancelled\") {\n\t\t\t\t\t\t\t\tthrow abortLikeError(\"agent task run was cancelled\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.catch((error: unknown) => {\n\t\t\t\t\t\t\t// 取消先于 run 到达:runner 以 task_terminal 拒绝启动。父侧确实请求过取消,\n\t\t\t\t\t\t\t// 因此仍按取消类错误结算(与\"aborted 终局\"同一分类),不落成 failed。\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\tsignal.aborted &&\n\t\t\t\t\t\t\t\ttypeof error === \"object\" &&\n\t\t\t\t\t\t\t\terror !== null &&\n\t\t\t\t\t\t\t\t(error as { code?: unknown }).code === \"task_terminal\"\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tthrow abortLikeError(`agent task run was cancelled before start: ${abortReasonText(signal)}`);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tthrow error;\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.finally(() => client.releaseRun(taskId, runId));\n\t\t\t\t},\n\t\t\t\tcancel: (reason?: string): Promise<void> => client.cancelTask(taskId, reason),\n\t\t\t\tdispose: async (): Promise<void> => {\n\t\t\t\t\tliveTaskIds.delete(taskId);\n\t\t\t\t\tawait client.callForTask(taskId, { op: \"dispose\" });\n\t\t\t\t\tclient.dropTask(taskId);\n\t\t\t\t},\n\t\t\t};\n\t\t\treturn host;\n\t\t},\n\t\tdispose: async (): Promise<void> => {\n\t\t\t// 宿主会话销毁时释放全部未 dispose 的任务(逐个容错,不阻断其余回收)。\n\t\t\tfor (const taskId of [...liveTaskIds]) {\n\t\t\t\tliveTaskIds.delete(taskId);\n\t\t\t\tawait client.callForTask(taskId, { op: \"dispose\" }).catch(() => {});\n\t\t\t}\n\t\t},\n\t};\n}\n"]}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { AgentTaskAdapter } from "@agent-forge/plugin-sdk";
|
|
2
|
+
import { type SubagentTaskChannelV1 } from "@agent-forge/protocol/subagent";
|
|
3
|
+
export interface SubagentRunnerSpawnHandleV1 {
|
|
4
|
+
/** 与 runner 进程对话的通道(父侧)。 */
|
|
5
|
+
readonly channel: SubagentTaskChannelV1;
|
|
6
|
+
/** 进程退出通知:error 非空表示异常退出。 */
|
|
7
|
+
onExit(handler: (error?: Error) => void): void;
|
|
8
|
+
/** 请求进程退出(空闲回收与池关闭使用)。 */
|
|
9
|
+
kill(): void;
|
|
10
|
+
}
|
|
11
|
+
export interface SubagentRunnerPoolOptionsV1 {
|
|
12
|
+
spawnRunner(): Promise<SubagentRunnerSpawnHandleV1> | SubagentRunnerSpawnHandleV1;
|
|
13
|
+
maxRunnerProcesses: number;
|
|
14
|
+
maxSessionsPerRunner: number;
|
|
15
|
+
idleRunnerTimeoutMs: number;
|
|
16
|
+
/**
|
|
17
|
+
* 宿主全局并发(调度器 maxConcurrentSubagents)。提供时参与 fail-fast 校验:
|
|
18
|
+
* 必须 <= maxRunnerProcesses * maxSessionsPerRunner,否则超出容量的准入会以容量错误失败。
|
|
19
|
+
*/
|
|
20
|
+
maxConcurrentSubagents?: number;
|
|
21
|
+
/** 就绪握手超时(默认 5000ms):runner 未在期限内应答 hello 视为启动失败。 */
|
|
22
|
+
handshakeTimeoutMs?: number;
|
|
23
|
+
/** 确定性时钟(默认 Date.now)。 */
|
|
24
|
+
clock?: () => number;
|
|
25
|
+
idleCheckIntervalMs?: number;
|
|
26
|
+
onDiagnostic?(message: string): void;
|
|
27
|
+
}
|
|
28
|
+
export interface SubagentRunnerPoolStatsV1 {
|
|
29
|
+
readonly runners: number;
|
|
30
|
+
readonly aliveRunners: number;
|
|
31
|
+
readonly liveTasks: number;
|
|
32
|
+
readonly spawnedTotal: number;
|
|
33
|
+
}
|
|
34
|
+
export interface SubagentRunnerPoolV1 extends AgentTaskAdapter {
|
|
35
|
+
/** D-044 §5.6:按 wire taskId(= host.session.getSessionId())订阅运行中进度。 */
|
|
36
|
+
onTaskProgress(taskId: string, listener: (message: string | undefined, seq: number) => void): () => void;
|
|
37
|
+
/** 空闲回收扫描(宿主心跳调用;也可由内部定时器驱动)。 */
|
|
38
|
+
sweep(): void;
|
|
39
|
+
stats(): SubagentRunnerPoolStatsV1;
|
|
40
|
+
shutdown(): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
export declare class SubagentRunnerPoolError extends Error {
|
|
43
|
+
readonly code: "subagent_runner_capacity" | "subagent_runner_lost" | "subagent_pool_closed" | "subagent_pool_config_invalid";
|
|
44
|
+
constructor(code: "subagent_runner_capacity" | "subagent_runner_lost" | "subagent_pool_closed" | "subagent_pool_config_invalid", message: string);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 宿主接线 fail-fast 校验(D-044 §8):配置违规在构造期抛出,不静默钳制。
|
|
48
|
+
* `maxRunnerProcesses = 0` 表示不启用 pooled(由宿主决定不构造本 adapter),
|
|
49
|
+
* 因此本校验要求 >= 1。
|
|
50
|
+
*/
|
|
51
|
+
export declare function validateSubagentRunnerPoolOptionsV1(options: {
|
|
52
|
+
maxRunnerProcesses: number;
|
|
53
|
+
maxSessionsPerRunner: number;
|
|
54
|
+
maxConcurrentSubagents?: number;
|
|
55
|
+
idleRunnerTimeoutMs: number;
|
|
56
|
+
handshakeTimeoutMs?: number;
|
|
57
|
+
}): void;
|
|
58
|
+
export declare function createPooledAgentTaskAdapterV1(options: SubagentRunnerPoolOptionsV1): SubagentRunnerPoolV1;
|
|
59
|
+
//# sourceMappingURL=runner-pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner-pool.d.ts","sourceRoot":"","sources":["../src/runner-pool.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,gBAAgB,EAAqD,MAAM,yBAAyB,CAAC;AACnH,OAAO,EAEN,KAAK,qBAAqB,EAE1B,MAAM,gCAAgC,CAAC;AAGxC,MAAM,WAAW,2BAA2B;IAC3C,sDAA4B;IAC5B,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAC;IACxC,6DAA6B;IAC7B,MAAM,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;IAC/C,gEAA0B;IAC1B,IAAI,IAAI,IAAI,CAAC;CACb;AAED,MAAM,WAAW,2BAA2B;IAC3C,WAAW,IAAI,OAAO,CAAC,2BAA2B,CAAC,GAAG,2BAA2B,CAAC;IAClF,kBAAkB,EAAE,MAAM,CAAC;IAC3B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,mBAAmB,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,uGAAqD;IACrD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,8CAA0B;IAC1B,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;IACrB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,YAAY,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,yBAAyB;IACzC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,oBAAqB,SAAQ,gBAAgB;IAC7D,+FAAsE;IACtE,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACzG,qFAAiC;IACjC,KAAK,IAAI,IAAI,CAAC;IACd,KAAK,IAAI,yBAAyB,CAAC;IACnC,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,qBAAa,uBAAwB,SAAQ,KAAK;IACjD,QAAQ,CAAC,IAAI,EACV,0BAA0B,GAC1B,sBAAsB,GACtB,sBAAsB,GACtB,8BAA8B,CAAC;IAElC,YACC,IAAI,EACD,0BAA0B,GAC1B,sBAAsB,GACtB,sBAAsB,GACtB,8BAA8B,EACjC,OAAO,EAAE,MAAM,EAKf;CACD;AAKD;;;;GAIG;AACH,wBAAgB,mCAAmC,CAAC,OAAO,EAAE;IAC5D,kBAAkB,EAAE,MAAM,CAAC;IAC3B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B,GAAG,IAAI,CAkCP;AA6DD,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,2BAA2B,GAAG,oBAAoB,CA6NzG","sourcesContent":["/**\n * 池化 runner adapter(D-045 / pooled 切片增量 2)。\n *\n * `AgentTaskAdapter` 的 runner 池实现:按 `maxSessionsPerRunner` 选\"空闲槽位最多\"\n * 的 runner(确定性);无空闲槽位且未达 `maxRunnerProcesses` 时按需 spawn;runner\n * 空闲超时退出(不迁移任务);进程死亡时在途任务以 `runner_lost` 结算(D-044 §4.3\n * 投影 failed + reasonCode subagent_runner_lost)。\n *\n * 分层约束:调度器(SubagentRuntime)继续持有队列与准入;本层只提供\"容量 + 进程\n * 生命周期\"。宿主接线必须保证 `maxConcurrentSubagents <= maxRunnerProcesses *\n * maxSessionsPerRunner`,否则超出容量的准入会以 `subagent_runner_capacity` 失败。\n * spawn 由 `spawnRunner` 注入:生产侧经 stdio 派生子进程,测试注入进程内 fake。\n *\n * 迁自宿主 `src/plugins/subagent-task/runner-pool.ts`(D-075 S4 第三批);公共契约改自\n * plugin-sdk,wire 协议类型改自 @agent-forge/protocol(宿主侧同名模块已下沉该包)。\n */\nimport { randomUUID } from \"node:crypto\";\nimport type { AgentTaskAdapter, AgentTaskCreateRequest, AgentTaskHost, SessionAPI } from \"@agent-forge/plugin-sdk\";\nimport {\n\tSUBAGENT_TASK_PROTOCOL_VERSION,\n\ttype SubagentTaskChannelV1,\n\tSubagentTaskProtocolError,\n} from \"@agent-forge/protocol/subagent\";\nimport { createRemoteAgentTaskAdapter } from \"./remote-adapter.ts\";\n\nexport interface SubagentRunnerSpawnHandleV1 {\n\t/** 与 runner 进程对话的通道(父侧)。 */\n\treadonly channel: SubagentTaskChannelV1;\n\t/** 进程退出通知:error 非空表示异常退出。 */\n\tonExit(handler: (error?: Error) => void): void;\n\t/** 请求进程退出(空闲回收与池关闭使用)。 */\n\tkill(): void;\n}\n\nexport interface SubagentRunnerPoolOptionsV1 {\n\tspawnRunner(): Promise<SubagentRunnerSpawnHandleV1> | SubagentRunnerSpawnHandleV1;\n\tmaxRunnerProcesses: number;\n\tmaxSessionsPerRunner: number;\n\tidleRunnerTimeoutMs: number;\n\t/**\n\t * 宿主全局并发(调度器 maxConcurrentSubagents)。提供时参与 fail-fast 校验:\n\t * 必须 <= maxRunnerProcesses * maxSessionsPerRunner,否则超出容量的准入会以容量错误失败。\n\t */\n\tmaxConcurrentSubagents?: number;\n\t/** 就绪握手超时(默认 5000ms):runner 未在期限内应答 hello 视为启动失败。 */\n\thandshakeTimeoutMs?: number;\n\t/** 确定性时钟(默认 Date.now)。 */\n\tclock?: () => number;\n\tidleCheckIntervalMs?: number;\n\tonDiagnostic?(message: string): void;\n}\n\nexport interface SubagentRunnerPoolStatsV1 {\n\treadonly runners: number;\n\treadonly aliveRunners: number;\n\treadonly liveTasks: number;\n\treadonly spawnedTotal: number;\n}\n\nexport interface SubagentRunnerPoolV1 extends AgentTaskAdapter {\n\t/** D-044 §5.6:按 wire taskId(= host.session.getSessionId())订阅运行中进度。 */\n\tonTaskProgress(taskId: string, listener: (message: string | undefined, seq: number) => void): () => void;\n\t/** 空闲回收扫描(宿主心跳调用;也可由内部定时器驱动)。 */\n\tsweep(): void;\n\tstats(): SubagentRunnerPoolStatsV1;\n\tshutdown(): Promise<void>;\n}\n\nexport class SubagentRunnerPoolError extends Error {\n\treadonly code:\n\t\t| \"subagent_runner_capacity\"\n\t\t| \"subagent_runner_lost\"\n\t\t| \"subagent_pool_closed\"\n\t\t| \"subagent_pool_config_invalid\";\n\n\tconstructor(\n\t\tcode:\n\t\t\t| \"subagent_runner_capacity\"\n\t\t\t| \"subagent_runner_lost\"\n\t\t\t| \"subagent_pool_closed\"\n\t\t\t| \"subagent_pool_config_invalid\",\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"SubagentRunnerPoolError\";\n\t\tthis.code = code;\n\t}\n}\n\nconst MAX_RUNNER_PROCESSES = 5;\nconst DEFAULT_HANDSHAKE_TIMEOUT_MS = 5000;\n\n/**\n * 宿主接线 fail-fast 校验(D-044 §8):配置违规在构造期抛出,不静默钳制。\n * `maxRunnerProcesses = 0` 表示不启用 pooled(由宿主决定不构造本 adapter),\n * 因此本校验要求 >= 1。\n */\nexport function validateSubagentRunnerPoolOptionsV1(options: {\n\tmaxRunnerProcesses: number;\n\tmaxSessionsPerRunner: number;\n\tmaxConcurrentSubagents?: number;\n\tidleRunnerTimeoutMs: number;\n\thandshakeTimeoutMs?: number;\n}): void {\n\tconst fail = (message: string): never => {\n\t\tthrow new SubagentRunnerPoolError(\"subagent_pool_config_invalid\", message);\n\t};\n\tif (\n\t\t!Number.isInteger(options.maxRunnerProcesses) ||\n\t\toptions.maxRunnerProcesses < 1 ||\n\t\toptions.maxRunnerProcesses > MAX_RUNNER_PROCESSES\n\t) {\n\t\tfail(\n\t\t\t`maxRunnerProcesses must be an integer between 1 and ${MAX_RUNNER_PROCESSES}, got ${String(options.maxRunnerProcesses)}`,\n\t\t);\n\t}\n\tif (!Number.isInteger(options.maxSessionsPerRunner) || options.maxSessionsPerRunner < 1) {\n\t\tfail(`maxSessionsPerRunner must be a positive integer, got ${String(options.maxSessionsPerRunner)}`);\n\t}\n\tif (typeof options.maxConcurrentSubagents === \"number\") {\n\t\tif (!Number.isInteger(options.maxConcurrentSubagents) || options.maxConcurrentSubagents < 1) {\n\t\t\tfail(`maxConcurrentSubagents must be a positive integer, got ${String(options.maxConcurrentSubagents)}`);\n\t\t}\n\t\tconst capacity = options.maxRunnerProcesses * options.maxSessionsPerRunner;\n\t\tif (options.maxConcurrentSubagents > capacity) {\n\t\t\tfail(`maxConcurrentSubagents (${options.maxConcurrentSubagents}) exceeds runner capacity (${capacity})`);\n\t\t}\n\t}\n\tif (!Number.isInteger(options.idleRunnerTimeoutMs) || options.idleRunnerTimeoutMs < 1) {\n\t\tfail(`idleRunnerTimeoutMs must be a positive integer, got ${String(options.idleRunnerTimeoutMs)}`);\n\t}\n\tif (\n\t\toptions.handshakeTimeoutMs !== undefined &&\n\t\t(!Number.isInteger(options.handshakeTimeoutMs) || options.handshakeTimeoutMs < 1)\n\t) {\n\t\tfail(`handshakeTimeoutMs must be a positive integer, got ${String(options.handshakeTimeoutMs)}`);\n\t}\n}\n\n/**\n * 就绪握手:在把通道交给 remote adapter 之前做一次 hello 往返,确认 runner 存活\n * 且协议版本一致;超时/版本不符视为启动失败(调用方负责 kill)。\n */\nasync function handshakeRunnerV1(channel: SubagentTaskChannelV1, timeoutMs: number): Promise<void> {\n\tconst id = `hello:${randomUUID().slice(0, 8)}`;\n\tconst iterator = channel[Symbol.asyncIterator]();\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tconst timeout = new Promise<never>((_resolve, reject) => {\n\t\ttimer = setTimeout(\n\t\t\t() => reject(new SubagentTaskProtocolError(`runner handshake timed out after ${timeoutMs}ms`, \"runner_lost\")),\n\t\t\ttimeoutMs,\n\t\t);\n\t});\n\ttry {\n\t\tawait channel.write({ v: SUBAGENT_TASK_PROTOCOL_VERSION, kind: \"request\", id, op: \"hello\" });\n\t\tfor (;;) {\n\t\t\tconst nextPromise = iterator.next();\n\t\t\tvoid nextPromise.catch(() => {});\n\t\t\tconst next = await Promise.race([nextPromise, timeout]);\n\t\t\tif (next.done) {\n\t\t\t\tthrow new SubagentTaskProtocolError(\"runner closed during handshake\", \"runner_lost\");\n\t\t\t}\n\t\t\tconst env = next.value;\n\t\t\tif (env.kind !== \"response\" || env.id !== id) continue; // 握手期不应有其它信封:忽略等待\n\t\t\tif (!env.ok) {\n\t\t\t\tthrow new SubagentTaskProtocolError(`runner rejected handshake: ${env.message}`, env.code);\n\t\t\t}\n\t\t\tif (env.protocolVersion !== SUBAGENT_TASK_PROTOCOL_VERSION) {\n\t\t\t\tthrow new SubagentTaskProtocolError(\n\t\t\t\t\t`subagent-task protocol mismatch: runner=${String(env.protocolVersion)} host=${SUBAGENT_TASK_PROTOCOL_VERSION}`,\n\t\t\t\t\t\"invalid_request\",\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t} finally {\n\t\tif (timer !== undefined) clearTimeout(timer);\n\t}\n}\n\ninterface PoolRunner {\n\treadonly adapter: AgentTaskAdapter;\n\treadonly handle: SubagentRunnerSpawnHandleV1;\n\treadonly liveTasks: Set<string>;\n\t/**\n\t * 分配中(adapter.create 已发起、回执未到)的预留槽位。并发 create 必须在等待\n\t * 回执期间就占位,否则同一 runner 会被超发(压测:40 个并发 create 全落在同一个\n\t * 刚 spawn 的 runner 上,扇出从未发生)。\n\t */\n\tpending: number;\n\talive: boolean;\n\tidleSince: number;\n}\n\nfunction slotsUsed(runner: PoolRunner): number {\n\treturn runner.liveTasks.size + runner.pending;\n}\n\nexport function createPooledAgentTaskAdapterV1(options: SubagentRunnerPoolOptionsV1): SubagentRunnerPoolV1 {\n\t// 配置违规在构造期 fail-fast(D-044 §8),不静默钳制。\n\tvalidateSubagentRunnerPoolOptionsV1(options);\n\tconst clock = options.clock ?? Date.now;\n\tconst handshakeTimeoutMs = options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;\n\tconst diagnostic = options.onDiagnostic ?? ((): void => {});\n\tconst runners: PoolRunner[] = [];\n\t/** wire taskId(= host.session.getSessionId())-> 承载它的 runner adapter,用于进度转发。 */\n\tconst taskRunnerAdapters = new Map<string, AgentTaskAdapter>();\n\tlet spawnedTotal = 0;\n\tlet closed = false;\n\tlet sweepTimer: ReturnType<typeof setInterval> | undefined;\n\tlet spawnInFlight: Promise<PoolRunner> | undefined;\n\n\t/** 摘除已死 runner(句柄与状态都不再需要),避免长寿命池按 spawn 次数增长。 */\n\tconst pruneDeadRunners = (): void => {\n\t\tfor (let index = runners.length - 1; index >= 0; index -= 1) {\n\t\t\tif (!runners[index]?.alive) runners.splice(index, 1);\n\t\t}\n\t};\n\n\tconst markDead = (runner: PoolRunner, error?: Error): void => {\n\t\tif (!runner.alive) return;\n\t\trunner.alive = false;\n\t\trunner.liveTasks.clear();\n\t\tdiagnostic(`subagent runner lost: ${error?.message ?? \"process exited\"}`);\n\t};\n\n\tconst trackRunner = (handle: SubagentRunnerSpawnHandleV1): PoolRunner => {\n\t\tconst runner: PoolRunner = {\n\t\t\tadapter: createRemoteAgentTaskAdapter(handle.channel),\n\t\t\thandle,\n\t\t\tliveTasks: new Set<string>(),\n\t\t\tpending: 0,\n\t\t\talive: true,\n\t\t\tidleSince: clock(),\n\t\t};\n\t\thandle.onExit((error?: Error) => markDead(runner, error));\n\t\trunners.push(runner);\n\t\tspawnedTotal += 1;\n\t\treturn runner;\n\t};\n\n\tconst spawnRunner = async (): Promise<PoolRunner> => {\n\t\t// 并发 create 共享同一次 spawn,避免超发进程。\n\t\tspawnInFlight ??= (async () => {\n\t\t\ttry {\n\t\t\t\tconst handle = await options.spawnRunner();\n\t\t\t\ttry {\n\t\t\t\t\t// 就绪握手在把通道交给 remote adapter 之前完成(adapter 启动后独占读取)。\n\t\t\t\t\tawait handshakeRunnerV1(handle.channel, handshakeTimeoutMs);\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// 握手失败:进程不可用,回收并上抛(调用方按启动失败处理)。\n\t\t\t\t\ttry {\n\t\t\t\t\t\thandle.kill();\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// 已退出:忽略\n\t\t\t\t\t}\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\treturn trackRunner(handle);\n\t\t\t} finally {\n\t\t\t\tspawnInFlight = undefined;\n\t\t\t}\n\t\t})();\n\t\treturn spawnInFlight;\n\t};\n\n\tconst pickRunner = (): PoolRunner | undefined => {\n\t\tlet best: PoolRunner | undefined;\n\t\tlet bestFree = 0;\n\t\tfor (const runner of runners) {\n\t\t\tif (!runner.alive) continue;\n\t\t\tconst free = options.maxSessionsPerRunner - slotsUsed(runner);\n\t\t\tif (free > bestFree) {\n\t\t\t\tbest = runner;\n\t\t\t\tbestFree = free;\n\t\t\t}\n\t\t}\n\t\treturn best;\n\t};\n\n\tconst createOnRunner = async (runner: PoolRunner, request: AgentTaskCreateRequest): Promise<AgentTaskHost> => {\n\t\trunner.pending += 1;\n\t\tlet host: AgentTaskHost;\n\t\ttry {\n\t\t\thost = await runner.adapter.create(request);\n\t\t} finally {\n\t\t\trunner.pending -= 1;\n\t\t}\n\t\tconst taskId = host.session.getSessionId();\n\t\trunner.liveTasks.add(taskId);\n\t\ttaskRunnerAdapters.set(taskId, runner.adapter);\n\t\trunner.idleSince = 0;\n\t\tconst remoteSession = host.session;\n\t\t// 包装 session:调度器只读 getSessionId/getMessages,其余透传;dispose 时归还槽位。\n\t\t// 事实面(getActiveTools/getModel)必须一并透传:档案核验靠它自证,包装层吞掉\n\t\t// 就等于把\"已核验\"变成\"无法核验\"。\n\t\tconst session: SessionAPI = {\n\t\t\tgetSessionId: () => remoteSession.getSessionId(),\n\t\t\tgetMessages: () => remoteSession.getMessages(),\n\t\t\tisIdle: () => remoteSession.isIdle(),\n\t\t\tprompt: (text) => remoteSession.prompt(text),\n\t\t\tabort: () => remoteSession.abort(),\n\t\t\twaitForIdle: () => remoteSession.waitForIdle(),\n\t\t\tappendEntry: (customType, data) => remoteSession.appendEntry(customType, data),\n\t\t\tgetBranchEntries: () => remoteSession.getBranchEntries(),\n\t\t\t...(remoteSession.getActiveTools === undefined\n\t\t\t\t? {}\n\t\t\t\t: { getActiveTools: () => remoteSession.getActiveTools?.() ?? [] }),\n\t\t\t...(remoteSession.getModel === undefined ? {} : { getModel: () => remoteSession.getModel?.() }),\n\t\t};\n\t\treturn {\n\t\t\tsession,\n\t\t\trun: (prompt, signal) => host.run(prompt, signal),\n\t\t\tcancel: (reason) => host.cancel(reason),\n\t\t\tdispose: async () => {\n\t\t\t\trunner.liveTasks.delete(taskId);\n\t\t\t\ttaskRunnerAdapters.delete(taskId);\n\t\t\t\tif (runner.liveTasks.size === 0) runner.idleSince = clock();\n\t\t\t\tawait host.dispose();\n\t\t\t},\n\t\t};\n\t};\n\n\tconst sweep = (): void => {\n\t\tif (closed) return;\n\t\tpruneDeadRunners();\n\t\tconst now = clock();\n\t\tfor (const runner of runners) {\n\t\t\t// pending > 0 = create 仍在途(真实 runner 的 spawn+握手+会话创建可达\n\t\t\t// 数秒):在途创建是活跃工作,不是空闲——sweep 不得打点也不得回收,\n\t\t\t// 否则激进 idleRunnerTimeoutMs 下 create 中途通道被杀(live 用例实证)。\n\t\t\tif (!runner.alive || runner.pending > 0 || runner.liveTasks.size > 0) continue;\n\t\t\tif (runner.idleSince === 0) runner.idleSince = now;\n\t\t\tif (now - runner.idleSince < options.idleRunnerTimeoutMs) continue;\n\t\t\t// 空闲超时回收:退出前必须无活跃任务(不迁移),dead 标记阻止后续分配。\n\t\t\trunner.alive = false;\n\t\t\tdiagnostic(\"subagent runner idle exit\");\n\t\t\ttry {\n\t\t\t\trunner.handle.kill();\n\t\t\t} catch {\n\t\t\t\t// 进程已退出:忽略\n\t\t\t}\n\t\t}\n\t};\n\n\tconst adapter: SubagentRunnerPoolV1 = {\n\t\t/** D-044 §5.6:转发到承载该任务的 runner 远程 adapter。 */\n\t\tonTaskProgress(taskId: string, listener: (message: string | undefined, seq: number) => void): () => void {\n\t\t\tconst runnerAdapter = taskRunnerAdapters.get(taskId) as\n\t\t\t\t| (AgentTaskAdapter & { onTaskProgress?: (id: string, cb: typeof listener) => () => void })\n\t\t\t\t| undefined;\n\t\t\treturn runnerAdapter?.onTaskProgress?.(taskId, listener) ?? (() => {});\n\t\t},\n\t\tasync create(request: AgentTaskCreateRequest): Promise<AgentTaskHost> {\n\t\t\tif (closed) throw new SubagentRunnerPoolError(\"subagent_pool_closed\", \"subagent runner pool is closed\");\n\t\t\tpruneDeadRunners();\n\t\t\t// 放置循环:并发 create 共享同一次 spawn,因此拿到共享结果后必须**重新**挑选\n\t\t\t// 有空槽的 runner(新 spawn 的那个同样有槽位上限),容量耗尽才报错。\n\t\t\tlet runner = pickRunner();\n\t\t\twhile (runner === undefined) {\n\t\t\t\tif (runners.filter((candidate) => candidate.alive).length >= options.maxRunnerProcesses) {\n\t\t\t\t\tthrow new SubagentRunnerPoolError(\n\t\t\t\t\t\t\"subagent_runner_capacity\",\n\t\t\t\t\t\t`no runner capacity: ${options.maxRunnerProcesses} runner(s) at ${options.maxSessionsPerRunner} session(s) each`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tawait spawnRunner();\n\t\t\t\trunner = pickRunner();\n\t\t\t}\n\t\t\ttry {\n\t\t\t\treturn await createOnRunner(runner, request);\n\t\t\t} catch (error) {\n\t\t\t\tif (error instanceof SubagentTaskProtocolError && error.code === \"runner_lost\") {\n\t\t\t\t\t// 分配期失败无副作用:换一个 runner 重试一次;无可用槽位且不能扩则上抛容量错误。\n\t\t\t\t\tmarkDead(runner, error);\n\t\t\t\t\tlet fallback = pickRunner();\n\t\t\t\t\tif (fallback === undefined) {\n\t\t\t\t\t\tif (runners.filter((candidate) => candidate.alive).length >= options.maxRunnerProcesses) {\n\t\t\t\t\t\t\tthrow new SubagentRunnerPoolError(\"subagent_runner_lost\", \"runner lost and no spare capacity\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfallback = await spawnRunner();\n\t\t\t\t\t}\n\t\t\t\t\tif (fallback !== runner) return createOnRunner(fallback, request);\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t},\n\t\tsweep,\n\t\tstats(): SubagentRunnerPoolStatsV1 {\n\t\t\treturn {\n\t\t\t\trunners: runners.length,\n\t\t\t\taliveRunners: runners.filter((runner) => runner.alive).length,\n\t\t\t\tliveTasks: runners.reduce((total, runner) => total + runner.liveTasks.size, 0),\n\t\t\t\tspawnedTotal,\n\t\t\t};\n\t\t},\n\t\tasync shutdown(): Promise<void> {\n\t\t\tif (closed) return;\n\t\t\tclosed = true;\n\t\t\tif (sweepTimer !== undefined) clearInterval(sweepTimer);\n\t\t\tconst alive = runners.filter((runner) => runner.alive);\n\t\t\tawait Promise.allSettled(alive.map((runner) => runner.adapter.dispose?.()));\n\t\t\tfor (const runner of alive) {\n\t\t\t\trunner.alive = false;\n\t\t\t\ttry {\n\t\t\t\t\trunner.handle.kill();\n\t\t\t\t} catch {\n\t\t\t\t\t// 已退出:忽略\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t};\n\n\tif (options.idleCheckIntervalMs !== undefined && options.idleCheckIntervalMs > 0) {\n\t\tsweepTimer = setInterval(sweep, options.idleCheckIntervalMs);\n\t\tsweepTimer.unref?.();\n\t}\n\n\treturn adapter;\n}\n"]}
|