@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,849 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 子代理运行时调度器(D-044,《子代理运行时设计》§4-§6)——插件级 `SubagentRuntime`:
|
|
3
|
+
* 队列 / 准入 / 预算 / 终态 / 权限的唯一编排入口。
|
|
4
|
+
*
|
|
5
|
+
* - 只组合既有 primitive,不重建生命周期:`AgentTaskAdapter`
|
|
6
|
+
* (../core/extensions/public-api.ts,裸执行 primitive,仅 admitted 之后调用)
|
|
7
|
+
* 与 `createAgentTaskRegistry`(./multi-agent-tasks.ts,状态注册表 primitive,
|
|
8
|
+
* 作为调度终态的投影写入,不作为第二权威;timed_out 经 checkDeadlines 原生直映,
|
|
9
|
+
* 例外路径见 projectTerminalToRegistry,§4.3)。
|
|
10
|
+
* - 生命周期:submit -> queued -> admitted -> allocated(此刻才 `adapter.create`,
|
|
11
|
+
* 之前零 child session、零模型资源)-> running -> 终态。
|
|
12
|
+
* - 预算执行点是调度器本身:submit 拒绝(prompt/metadata UTF-8 字节上限、队列深度),
|
|
13
|
+
* settle 截断(maxMessages/maxResultBytes + `truncated` 标志)。registry 自建时显式传
|
|
14
|
+
* Infinity 预算:并发与结果预算不得依赖 registry 缺省,也不能让 registry 成为第二准入权威。
|
|
15
|
+
* - 终态不可覆盖:settle 先查终态守卫,迟到的 `host.run` 结算一律抑制;取消是幂等 fence,
|
|
16
|
+
* running 取消走有界 drain,超时终态 `drain_timeout`(reasonCode `subagent_drain_timeout`)。
|
|
17
|
+
* - 权限:`result(taskId, requester)` 仅在 owner 与 parentSessionId 均匹配时返回记录,
|
|
18
|
+
* 不区分"不存在"与"无权";durable 回读走结构化校验,不做裸断言。persist 失败不吞终态:
|
|
19
|
+
* 内存记录已捕获,persist 错误经 cancel()/drain() 的 reject 挂 cause 暴露。
|
|
20
|
+
* - 父栅栏(§4.2):宿主注入 `parentSignal` getter,每次 admitted 时读取当前信号
|
|
21
|
+
* (epoch 正确);信号已 aborted 的任务 admitted 即取消;abort 监听触发 cancelAll 语义。
|
|
22
|
+
* - 确定性:deadline 由显式 `tick()` 推进(无隐藏定时器);drain 超时是对 `host.run`
|
|
23
|
+
* promise 的定时 race(契约允许 "clock 推进或 race" 二选一,race 便于注入测试)。
|
|
24
|
+
* 共享 globalAdmission 的跨实例推进同样由显式 `tick()`(或任意实例的 submit/settle)
|
|
25
|
+
* 驱动,不引入轮询。
|
|
26
|
+
* - `runner_lost` 为 pooled 切片保留值,本实现不产生;`outputArtifactRef` /
|
|
27
|
+
* `messagesArtifactRef` 为 artifact 分页保留字段,本切片不产出。
|
|
28
|
+
*
|
|
29
|
+
* 迁自宿主 `src/plugins/subagent-runtime.ts`(D-075 S4 第三批);除 import 来源外
|
|
30
|
+
* 逐字保持:公共契约(AgentTaskAdapter/JsonValue/durable 类型)改自 plugin-sdk。
|
|
31
|
+
*/
|
|
32
|
+
import { randomUUID } from "node:crypto";
|
|
33
|
+
import { AgentTaskError, createAgentTaskRegistry } from "./multi-agent-tasks.js";
|
|
34
|
+
export class SubagentRuntimeError extends Error {
|
|
35
|
+
code;
|
|
36
|
+
constructor(code, message) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.name = "SubagentRuntimeError";
|
|
39
|
+
this.code = code;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** 状态投影(§4.3):调度器权威状态 -> `AgentTaskStatus` DTO 词汇;不产生映射表之外的值。 */
|
|
43
|
+
export function toAgentTaskStatusProjection(status) {
|
|
44
|
+
switch (status) {
|
|
45
|
+
case "queued":
|
|
46
|
+
case "admitted":
|
|
47
|
+
return "created";
|
|
48
|
+
case "running":
|
|
49
|
+
return "running";
|
|
50
|
+
case "succeeded":
|
|
51
|
+
return "succeeded";
|
|
52
|
+
case "failed":
|
|
53
|
+
return "failed";
|
|
54
|
+
case "cancelled":
|
|
55
|
+
return "cancelled";
|
|
56
|
+
case "timed_out":
|
|
57
|
+
case "drain_timeout":
|
|
58
|
+
case "runner_lost":
|
|
59
|
+
return "failed";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function createDeferred() {
|
|
63
|
+
let resolve;
|
|
64
|
+
const promise = new Promise((res) => {
|
|
65
|
+
resolve = res;
|
|
66
|
+
});
|
|
67
|
+
return { promise, resolve };
|
|
68
|
+
}
|
|
69
|
+
/** 可取消的定时 deferred(wait 的超时臂);cancel 同时清定时器并立即 resolve。 */
|
|
70
|
+
function sleepDeferred(ms) {
|
|
71
|
+
let timer;
|
|
72
|
+
let resolve;
|
|
73
|
+
const promise = new Promise((res) => {
|
|
74
|
+
resolve = res;
|
|
75
|
+
timer = setTimeout(res, ms);
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
promise,
|
|
79
|
+
cancel: () => {
|
|
80
|
+
if (timer !== undefined)
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
resolve();
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function createSlotGate(capacity) {
|
|
87
|
+
let held = 0;
|
|
88
|
+
return {
|
|
89
|
+
tryAcquire() {
|
|
90
|
+
if (held >= capacity)
|
|
91
|
+
return "limit";
|
|
92
|
+
held += 1;
|
|
93
|
+
return "claimed";
|
|
94
|
+
},
|
|
95
|
+
release() {
|
|
96
|
+
held -= 1;
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const textEncoder = new TextEncoder();
|
|
101
|
+
function utf8ByteLength(value) {
|
|
102
|
+
return textEncoder.encode(value).length;
|
|
103
|
+
}
|
|
104
|
+
/** JSON 序列化用于字节预算计算;不可序列化的值按空串计,不因预算计算抛错。 */
|
|
105
|
+
function safeStringify(value) {
|
|
106
|
+
try {
|
|
107
|
+
const serialized = JSON.stringify(value);
|
|
108
|
+
return serialized === undefined ? "" : serialized;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return "";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function errorText(error) {
|
|
115
|
+
if (error instanceof Error)
|
|
116
|
+
return error.message;
|
|
117
|
+
if (typeof error === "string")
|
|
118
|
+
return error;
|
|
119
|
+
return safeStringify(error);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* settle 有界化:先按 maxMessages 截断条数,再逐条累计 UTF-8 字节、超出 maxResultBytes
|
|
123
|
+
* 停止;任一截断发生即置 truncated(信封只持有有界快照,原文分页属后续 artifact 切片)。
|
|
124
|
+
*/
|
|
125
|
+
function boundMessages(messages, maxMessages, maxResultBytes) {
|
|
126
|
+
let truncated = false;
|
|
127
|
+
let selected = messages;
|
|
128
|
+
if (selected.length > maxMessages) {
|
|
129
|
+
selected = selected.slice(0, maxMessages);
|
|
130
|
+
truncated = true;
|
|
131
|
+
}
|
|
132
|
+
const bounded = [];
|
|
133
|
+
let used = 0;
|
|
134
|
+
for (const message of selected) {
|
|
135
|
+
const size = utf8ByteLength(safeStringify(message));
|
|
136
|
+
if (used + size > maxResultBytes) {
|
|
137
|
+
truncated = true;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
used += size;
|
|
141
|
+
bounded.push(message);
|
|
142
|
+
}
|
|
143
|
+
return { messages: bounded, truncated };
|
|
144
|
+
}
|
|
145
|
+
const RUNTIME_STATUSES = [
|
|
146
|
+
"queued",
|
|
147
|
+
"admitted",
|
|
148
|
+
"running",
|
|
149
|
+
"succeeded",
|
|
150
|
+
"failed",
|
|
151
|
+
"cancelled",
|
|
152
|
+
"timed_out",
|
|
153
|
+
"drain_timeout",
|
|
154
|
+
"runner_lost",
|
|
155
|
+
];
|
|
156
|
+
function isRuntimeStatus(value) {
|
|
157
|
+
return RUNTIME_STATUSES.includes(value);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 终态子集:`result()` 是**终态**回读契约,记录只在 settle 时落库,因此持久层里出现
|
|
161
|
+
* 非终态状态(queued/admitted/running)只能是脏数据或别的写入者——按不合格处理,
|
|
162
|
+
* 绝不把它当成"任务已完成"返回给调用方。
|
|
163
|
+
*/
|
|
164
|
+
const TERMINAL_RUNTIME_STATUSES = [
|
|
165
|
+
"succeeded",
|
|
166
|
+
"failed",
|
|
167
|
+
"cancelled",
|
|
168
|
+
"timed_out",
|
|
169
|
+
"drain_timeout",
|
|
170
|
+
"runner_lost",
|
|
171
|
+
];
|
|
172
|
+
function isTerminalRuntimeStatus(value) {
|
|
173
|
+
return TERMINAL_RUNTIME_STATUSES.includes(value);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* durable 回读的结构化校验(不做裸断言):字段类型逐一核对后显式重建记录,
|
|
177
|
+
* 不合格按 undefined 处理,与"不存在/无权"同型返回。
|
|
178
|
+
*/
|
|
179
|
+
function parseSchedulerRecord(value) {
|
|
180
|
+
if (typeof value !== "object" || value === null)
|
|
181
|
+
return undefined;
|
|
182
|
+
const candidate = value;
|
|
183
|
+
if (candidate.schedulerVersion !== 1)
|
|
184
|
+
return undefined;
|
|
185
|
+
const taskId = candidate.taskId;
|
|
186
|
+
if (typeof taskId !== "string" || taskId.length === 0)
|
|
187
|
+
return undefined;
|
|
188
|
+
const owner = candidate.owner;
|
|
189
|
+
if (typeof owner !== "string" || owner.length === 0)
|
|
190
|
+
return undefined;
|
|
191
|
+
const parentSessionId = candidate.parentSessionId;
|
|
192
|
+
if (typeof parentSessionId !== "string" || parentSessionId.length === 0)
|
|
193
|
+
return undefined;
|
|
194
|
+
const status = candidate.status;
|
|
195
|
+
if (typeof status !== "string" || !isRuntimeStatus(status) || !isTerminalRuntimeStatus(status))
|
|
196
|
+
return undefined;
|
|
197
|
+
const truncated = candidate.truncated;
|
|
198
|
+
if (typeof truncated !== "boolean")
|
|
199
|
+
return undefined;
|
|
200
|
+
const sessionId = candidate.sessionId;
|
|
201
|
+
if (typeof sessionId !== "string" || sessionId.length === 0)
|
|
202
|
+
return undefined;
|
|
203
|
+
const messages = candidate.messages;
|
|
204
|
+
if (!Array.isArray(messages))
|
|
205
|
+
return undefined;
|
|
206
|
+
const reasonCode = candidate.reasonCode;
|
|
207
|
+
if (reasonCode !== undefined && typeof reasonCode !== "string")
|
|
208
|
+
return undefined;
|
|
209
|
+
const reason = candidate.reason;
|
|
210
|
+
if (reason !== undefined && typeof reason !== "string")
|
|
211
|
+
return undefined;
|
|
212
|
+
const outputArtifactRef = candidate.outputArtifactRef;
|
|
213
|
+
if (outputArtifactRef !== undefined && typeof outputArtifactRef !== "string")
|
|
214
|
+
return undefined;
|
|
215
|
+
const messagesArtifactRef = candidate.messagesArtifactRef;
|
|
216
|
+
if (messagesArtifactRef !== undefined && typeof messagesArtifactRef !== "string")
|
|
217
|
+
return undefined;
|
|
218
|
+
return {
|
|
219
|
+
schedulerVersion: 1,
|
|
220
|
+
taskId,
|
|
221
|
+
owner,
|
|
222
|
+
parentSessionId,
|
|
223
|
+
status,
|
|
224
|
+
...(reasonCode === undefined ? {} : { reasonCode }),
|
|
225
|
+
...(reason === undefined ? {} : { reason }),
|
|
226
|
+
truncated,
|
|
227
|
+
sessionId,
|
|
228
|
+
messages: Object.freeze([...messages]),
|
|
229
|
+
...(outputArtifactRef === undefined ? {} : { outputArtifactRef }),
|
|
230
|
+
...(messagesArtifactRef === undefined ? {} : { messagesArtifactRef }),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function matchesRequester(record, requester) {
|
|
234
|
+
return record.owner === requester.owner && record.parentSessionId === requester.parentSessionId;
|
|
235
|
+
}
|
|
236
|
+
export function createSubagentRuntime(options) {
|
|
237
|
+
const clock = options.clock ?? (() => Date.now());
|
|
238
|
+
const limits = options.limits ?? {};
|
|
239
|
+
const maxConcurrentSubagents = limits.maxConcurrentSubagents ?? 8;
|
|
240
|
+
const maxQueueDepth = limits.maxQueueDepth ?? 200;
|
|
241
|
+
const maxPromptBytes = limits.maxPromptBytes ?? 65536;
|
|
242
|
+
const maxMetadataBytes = limits.maxMetadataBytes ?? 16384;
|
|
243
|
+
const maxResultBytes = limits.maxResultBytes ?? 524288;
|
|
244
|
+
const maxMessages = limits.maxMessages ?? 2000;
|
|
245
|
+
const drainTimeoutMs = limits.drainTimeoutMs ?? 30000;
|
|
246
|
+
const maxRetainedRecords = limits.maxRetainedRecords ?? 256;
|
|
247
|
+
const results = options.results;
|
|
248
|
+
// registry 是状态投影 primitive,不是第二准入/预算权威:并发与结果预算由调度器
|
|
249
|
+
// 自己的 gate 与 settle 截断执行,因此自建时显式传 Infinity(不依赖其缺省值)。
|
|
250
|
+
const registry = options.registry ??
|
|
251
|
+
createAgentTaskRegistry({
|
|
252
|
+
now: clock,
|
|
253
|
+
maxConcurrent: Number.POSITIVE_INFINITY,
|
|
254
|
+
maxResultBytes: Number.POSITIVE_INFINITY,
|
|
255
|
+
});
|
|
256
|
+
const localAdmission = createSlotGate(maxConcurrentSubagents);
|
|
257
|
+
const admission = () => options.globalAdmission ?? localAdmission;
|
|
258
|
+
const tasks = new Map();
|
|
259
|
+
const records = new Map();
|
|
260
|
+
const queue = [];
|
|
261
|
+
let seq = 0;
|
|
262
|
+
let disposed = false;
|
|
263
|
+
let pumping = false;
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// 终态 settle:终态守卫 -> 有界消息 -> 信封 -> 释放 -> 投影 -> 持久化
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
function settleEntry(entry, status, reasonCode, reason) {
|
|
268
|
+
// 终态守卫:settle 一律先查此标志,迟到结算(迟到的 host.run 结算、重复 fence)抑制。
|
|
269
|
+
if (entry.settled)
|
|
270
|
+
return;
|
|
271
|
+
entry.settled = true;
|
|
272
|
+
const bounded = entry.host
|
|
273
|
+
? boundMessages(entry.host.session.getMessages(), maxMessages, maxResultBytes)
|
|
274
|
+
: { messages: [], truncated: false };
|
|
275
|
+
// 子会话身份(D-060 S1):优先取"agent 会话自身的 id"(池化下宿主可见的 id 是 wire 任务 id),
|
|
276
|
+
// 退回宿主会话 id;两者都在终态前读取(settle 之后 host 被清空)。
|
|
277
|
+
const sessionId = entry.host?.session.getAgentSessionId?.() ?? entry.host?.session.getSessionId() ?? entry.taskId;
|
|
278
|
+
const record = Object.freeze({
|
|
279
|
+
schedulerVersion: 1,
|
|
280
|
+
taskId: entry.taskId,
|
|
281
|
+
owner: options.owner,
|
|
282
|
+
parentSessionId: options.parentSessionId,
|
|
283
|
+
status,
|
|
284
|
+
...(reasonCode === undefined ? {} : { reasonCode }),
|
|
285
|
+
...(reason === undefined ? {} : { reason }),
|
|
286
|
+
truncated: bounded.truncated,
|
|
287
|
+
sessionId,
|
|
288
|
+
messages: Object.freeze([...bounded.messages]),
|
|
289
|
+
});
|
|
290
|
+
entry.status = status;
|
|
291
|
+
records.set(entry.taskId, record);
|
|
292
|
+
// 有界保留:Map 按插入序迭代,淘汰最旧的终态记录。
|
|
293
|
+
while (records.size > maxRetainedRecords) {
|
|
294
|
+
const oldest = records.keys().next();
|
|
295
|
+
if (oldest.done === true)
|
|
296
|
+
break;
|
|
297
|
+
records.delete(oldest.value);
|
|
298
|
+
}
|
|
299
|
+
if (entry.gate !== undefined) {
|
|
300
|
+
const gate = entry.gate;
|
|
301
|
+
entry.gate = undefined;
|
|
302
|
+
gate.release();
|
|
303
|
+
}
|
|
304
|
+
if (entry.parentSignal !== undefined && entry.parentListener !== undefined) {
|
|
305
|
+
entry.parentSignal.removeEventListener("abort", entry.parentListener);
|
|
306
|
+
entry.parentSignal = undefined;
|
|
307
|
+
entry.parentListener = undefined;
|
|
308
|
+
}
|
|
309
|
+
if (entry.host !== undefined) {
|
|
310
|
+
// 终态后释放子会话(§5.2:cleanup 失败终态仍必须落库)——消息快照已在上方读取。
|
|
311
|
+
const host = entry.host;
|
|
312
|
+
entry.host = undefined;
|
|
313
|
+
void host.dispose().catch(() => {
|
|
314
|
+
// dispose 失败不改变已落的调度终态,任务记录已捕获。
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
entry.settleDeferred.resolve(record);
|
|
318
|
+
projectTerminalToRegistry(record);
|
|
319
|
+
persistRecord(entry, record);
|
|
320
|
+
// 槽位已释放:FIFO 推进队列(pumping 守卫保证与进行中的准入循环互斥)。
|
|
321
|
+
pump();
|
|
322
|
+
}
|
|
323
|
+
function projectTerminalToRegistry(record) {
|
|
324
|
+
// registry 抛 AgentTaskError(如 duplicate_terminal / unknown_task)不得让调度器
|
|
325
|
+
// 崩溃,也不得覆盖调度终态:调度记录是权威,投影失败按忽略处理。
|
|
326
|
+
try {
|
|
327
|
+
if (record.status === "succeeded") {
|
|
328
|
+
registry.complete(record.taskId, record.messages);
|
|
329
|
+
}
|
|
330
|
+
else if (record.status === "cancelled") {
|
|
331
|
+
registry.cancel(record.taskId, record.reason ?? "cancelled");
|
|
332
|
+
}
|
|
333
|
+
else if (record.status === "timed_out") {
|
|
334
|
+
// §4.3:timed_out 直映 registry 原生 timed_out——正常路径下注入的 registry 已在
|
|
335
|
+
// tick() 的 checkDeadlines 原生置 timed_out,此处不得再改写(会 duplicate_terminal);
|
|
336
|
+
// 仅当仍未终态(过期降级创建等例外路径,原生 checkDeadlines 覆盖不到)才以
|
|
337
|
+
// fail + reason "deadline_passed" 落投影。
|
|
338
|
+
const projected = registry.get(record.taskId);
|
|
339
|
+
if (projected !== undefined && (projected.status === "pending" || projected.status === "running")) {
|
|
340
|
+
registry.fail(record.taskId, record.reason ?? record.reasonCode ?? "deadline_passed");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
registry.fail(record.taskId, record.reason ?? record.reasonCode ?? record.status);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
// 投影失败不改变调度终态。
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function persistRecord(entry, record) {
|
|
352
|
+
if (results === undefined) {
|
|
353
|
+
entry.persistDone.resolve();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
void (async () => {
|
|
357
|
+
try {
|
|
358
|
+
await results.store.persistArtifact({
|
|
359
|
+
writerId: results.writerId,
|
|
360
|
+
artifactId: `subagent-task:${entry.taskId}`,
|
|
361
|
+
sessionId: options.parentSessionId,
|
|
362
|
+
data: record,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
// persist 失败不吞任务终态:内存记录已捕获;错误挂 cause 经 cancel()/drain()
|
|
367
|
+
// 的 reject 通道暴露(对齐 capability-runtime-guards 的 cause 模式)。
|
|
368
|
+
entry.persistFailed = true;
|
|
369
|
+
entry.persistError = error;
|
|
370
|
+
}
|
|
371
|
+
finally {
|
|
372
|
+
entry.persistDone.resolve();
|
|
373
|
+
}
|
|
374
|
+
})();
|
|
375
|
+
}
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
// 运行与取消
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
function startRun(entry, host) {
|
|
380
|
+
const runPromise = host.run(entry.request.prompt, entry.controller.signal);
|
|
381
|
+
void runPromise.then(() => onRunSettled(entry, undefined), (error) => onRunSettled(entry, error));
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* adapter 抛出的结构化 `code` → 调度器 reasonCode。纪律:只接受保留前缀 `subagent_`
|
|
385
|
+
* 与既有特例 `runner_lost`(映射为 `subagent_runner_lost`,D-044 §4.3)——记录里的
|
|
386
|
+
* reasonCode 沿用调度器词汇表,不让第三方错误码污染 §4.3 的映射表。
|
|
387
|
+
*/
|
|
388
|
+
function adapterReasonCode(error) {
|
|
389
|
+
if (typeof error !== "object" || error === null)
|
|
390
|
+
return undefined;
|
|
391
|
+
const code = error.code;
|
|
392
|
+
if (typeof code !== "string" || code.length === 0)
|
|
393
|
+
return undefined;
|
|
394
|
+
if (code === "runner_lost")
|
|
395
|
+
return "subagent_runner_lost";
|
|
396
|
+
return code.startsWith("subagent_") ? code : undefined;
|
|
397
|
+
}
|
|
398
|
+
function onRunSettled(entry, error) {
|
|
399
|
+
entry.runSettled.resolve();
|
|
400
|
+
if (entry.settled)
|
|
401
|
+
return; // 迟到结算:终态已落,抑制
|
|
402
|
+
if (entry.cancelRequested)
|
|
403
|
+
return; // fence 已落:由取消/drain 路径按 pendingTerminal 结算
|
|
404
|
+
if (error !== undefined) {
|
|
405
|
+
// failed 取原始错误 message(含字符串 throw 的原始文本);
|
|
406
|
+
// adapter 可用结构化 code 声明原因(见 adapterReasonCode 的白名单纪律)。
|
|
407
|
+
settleEntry(entry, "failed", adapterReasonCode(error), errorText(error));
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
settleEntry(entry, "succeeded", undefined, undefined);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* 有界 drain:对 host.run promise 的定时 race(真实定时器,不依赖 scheduler clock;
|
|
414
|
+
* 测试可用小 drainTimeoutMs 或 fake timers)。runSettled 已决时立即返回。
|
|
415
|
+
*/
|
|
416
|
+
function raceDrain(entry, timeoutMs) {
|
|
417
|
+
let settleOutcome;
|
|
418
|
+
const outcomePromise = new Promise((resolve) => {
|
|
419
|
+
settleOutcome = resolve;
|
|
420
|
+
});
|
|
421
|
+
const timer = setTimeout(() => settleOutcome("timeout"), timeoutMs);
|
|
422
|
+
void entry.runSettled.promise.then(() => settleOutcome("settled"));
|
|
423
|
+
void outcomePromise.then(() => clearTimeout(timer));
|
|
424
|
+
return outcomePromise;
|
|
425
|
+
}
|
|
426
|
+
/** 每个任务至多一个 drain driver:首个启动者的预算生效,后续调用只共享其结果。 */
|
|
427
|
+
function startDrainDriver(entry, timeoutMs) {
|
|
428
|
+
if (entry.drainDriverStarted)
|
|
429
|
+
return;
|
|
430
|
+
entry.drainDriverStarted = true;
|
|
431
|
+
void (async () => {
|
|
432
|
+
const outcome = await raceDrain(entry, timeoutMs);
|
|
433
|
+
if (entry.settled)
|
|
434
|
+
return;
|
|
435
|
+
if (outcome === "timeout") {
|
|
436
|
+
// 执行体未在预算内结束:终态 drain_timeout(reasonCode subagent_drain_timeout),
|
|
437
|
+
// 仍按 settle 路径保留现场消息快照,并尽力 abort + dispose 清理。
|
|
438
|
+
entry.controller.abort("subagent drain timeout");
|
|
439
|
+
settleEntry(entry, "drain_timeout", "subagent_drain_timeout", entry.cancelReason);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (entry.pendingTerminal !== undefined) {
|
|
443
|
+
settleEntry(entry, entry.pendingTerminal, entry.pendingReasonCode, entry.cancelReason);
|
|
444
|
+
}
|
|
445
|
+
// 无 fence 的 drain():in-time 结算已由 onRunSettled 自然完成。
|
|
446
|
+
})();
|
|
447
|
+
}
|
|
448
|
+
/** 取消 fence(幂等):queued 直接出队终态;admitted 由分配路径结算;running 有界 drain。 */
|
|
449
|
+
function cancelEntry(entry, reason) {
|
|
450
|
+
if (entry.settled || entry.cancelRequested)
|
|
451
|
+
return;
|
|
452
|
+
entry.cancelRequested = true;
|
|
453
|
+
entry.pendingTerminal = "cancelled";
|
|
454
|
+
entry.pendingReasonCode = undefined;
|
|
455
|
+
entry.cancelReason = reason;
|
|
456
|
+
entry.controller.abort(reason);
|
|
457
|
+
applyFence(entry);
|
|
458
|
+
}
|
|
459
|
+
/** deadline fence:终态方向为 timed_out(reasonCode deadline_passed)。 */
|
|
460
|
+
function deadlineFence(entry) {
|
|
461
|
+
if (entry.settled || entry.cancelRequested)
|
|
462
|
+
return;
|
|
463
|
+
entry.cancelRequested = true;
|
|
464
|
+
entry.pendingTerminal = "timed_out";
|
|
465
|
+
entry.pendingReasonCode = "deadline_passed";
|
|
466
|
+
entry.cancelReason = undefined;
|
|
467
|
+
entry.controller.abort("deadline passed");
|
|
468
|
+
applyFence(entry);
|
|
469
|
+
}
|
|
470
|
+
function applyFence(entry) {
|
|
471
|
+
switch (entry.status) {
|
|
472
|
+
case "queued": {
|
|
473
|
+
const index = queue.indexOf(entry);
|
|
474
|
+
if (index >= 0)
|
|
475
|
+
queue.splice(index, 1);
|
|
476
|
+
settleEntry(entry, entry.pendingTerminal ?? "cancelled", entry.pendingReasonCode, entry.cancelReason);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
case "admitted":
|
|
480
|
+
// 分配进行中:admitEntry 在 create 结算后按 fence 终态(终态守卫保证只结算一次)。
|
|
481
|
+
return;
|
|
482
|
+
case "running":
|
|
483
|
+
startDrainDriver(entry, drainTimeoutMs);
|
|
484
|
+
return;
|
|
485
|
+
default:
|
|
486
|
+
// 已终态:fence 幂等 no-op(entry.settled 守卫已挡住重复进入)。
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
// ---------------------------------------------------------------------------
|
|
491
|
+
// 准入(严格 FIFO)
|
|
492
|
+
// ---------------------------------------------------------------------------
|
|
493
|
+
function pump() {
|
|
494
|
+
if (pumping)
|
|
495
|
+
return;
|
|
496
|
+
pumping = true;
|
|
497
|
+
void admitNext();
|
|
498
|
+
}
|
|
499
|
+
async function admitNext() {
|
|
500
|
+
try {
|
|
501
|
+
while (queue.length > 0) {
|
|
502
|
+
const gate = admission();
|
|
503
|
+
if (gate.tryAcquire() === "limit")
|
|
504
|
+
break;
|
|
505
|
+
const entry = queue.shift();
|
|
506
|
+
if (entry === undefined) {
|
|
507
|
+
gate.release();
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
if (entry.settled) {
|
|
511
|
+
gate.release();
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
await admitEntry(entry, gate);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
finally {
|
|
518
|
+
pumping = false;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
async function admitEntry(entry, gate) {
|
|
522
|
+
entry.status = "admitted";
|
|
523
|
+
entry.gate = gate;
|
|
524
|
+
bindParentSignal(entry);
|
|
525
|
+
if (entry.cancelRequested) {
|
|
526
|
+
settleEntry(entry, entry.pendingTerminal ?? "cancelled", entry.pendingReasonCode, entry.cancelReason);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
try {
|
|
530
|
+
registry.start(entry.taskId);
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
// registry 抛 AgentTaskError 不得让调度器崩溃:包装为调度终态 failed。
|
|
534
|
+
settleEntry(entry, "failed", undefined, errorText(error));
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
// 延迟分配(§4.1):只有 admitted 才 adapter.create;queued 任务零 child session。
|
|
538
|
+
let host;
|
|
539
|
+
try {
|
|
540
|
+
host = await options.adapter.create({
|
|
541
|
+
prompt: entry.request.prompt,
|
|
542
|
+
...(entry.request.metadata === undefined ? {} : { metadata: entry.request.metadata }),
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
catch (error) {
|
|
546
|
+
settleEntry(entry, "failed", adapterReasonCode(error), errorText(error));
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (entry.cancelRequested) {
|
|
550
|
+
// 分配期间 fence 落下:host 立即进入 settle(dispose)路径,不启动 run。
|
|
551
|
+
entry.host = host;
|
|
552
|
+
settleEntry(entry, entry.pendingTerminal ?? "cancelled", entry.pendingReasonCode, entry.cancelReason);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
entry.host = host;
|
|
556
|
+
// 子会话 id 即刻捕获(D-060 S4):steer/resume 需要在运行期按 sessionId 定位任务。
|
|
557
|
+
entry.recordSessionId = host.session.getAgentSessionId?.() ?? host.session.getSessionId();
|
|
558
|
+
entry.status = "running";
|
|
559
|
+
startRun(entry, host);
|
|
560
|
+
}
|
|
561
|
+
/** 父栅栏(§4.2):每次 admitted 读取当前信号(epoch 正确);已 aborted 立即按 fence 处理。 */
|
|
562
|
+
function bindParentSignal(entry) {
|
|
563
|
+
const signal = options.parentSignal?.();
|
|
564
|
+
if (signal === undefined)
|
|
565
|
+
return;
|
|
566
|
+
if (signal.aborted) {
|
|
567
|
+
entry.cancelRequested = true;
|
|
568
|
+
entry.pendingTerminal = "cancelled";
|
|
569
|
+
entry.pendingReasonCode = undefined;
|
|
570
|
+
entry.cancelReason = "parent signal aborted";
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const listener = () => {
|
|
574
|
+
// 父栅栏级联(§5.2):cancel fence 全量子任务(queued 移除 + running 有界 drain)。
|
|
575
|
+
// runtime 实例绑定单一 owner/parentSessionId,cancelAll 即该父的全部任务;
|
|
576
|
+
// abort 之后以新 epoch 信号提交的任务不受本次级联影响。
|
|
577
|
+
void cancelAllEntries("parent signal aborted").catch(() => {
|
|
578
|
+
// 无调用方的级联路径:persist 失败等收尾错误已在各任务 cancel()/drain() 的
|
|
579
|
+
// reject 通道暴露,这里不能也不需要再抛。
|
|
580
|
+
});
|
|
581
|
+
};
|
|
582
|
+
entry.parentSignal = signal;
|
|
583
|
+
entry.parentListener = listener;
|
|
584
|
+
signal.addEventListener("abort", listener, { once: true });
|
|
585
|
+
}
|
|
586
|
+
// ---------------------------------------------------------------------------
|
|
587
|
+
// 公共 API
|
|
588
|
+
// ---------------------------------------------------------------------------
|
|
589
|
+
function handleOf(entry) {
|
|
590
|
+
return {
|
|
591
|
+
id: entry.taskId,
|
|
592
|
+
status: () => entry.status,
|
|
593
|
+
signal: entry.controller.signal,
|
|
594
|
+
result: () => awaitTerminalRecord(entry),
|
|
595
|
+
cancel: (reason) => {
|
|
596
|
+
cancelEntry(entry, reason);
|
|
597
|
+
return awaitTerminalVoid(entry);
|
|
598
|
+
},
|
|
599
|
+
drain: (drainOptions) => {
|
|
600
|
+
// queued/admitted 尚无执行体可 drain:等待其自然终态;running 才挂有界预算。
|
|
601
|
+
if (!entry.settled && entry.status === "running") {
|
|
602
|
+
startDrainDriver(entry, drainOptions?.timeoutMs ?? drainTimeoutMs);
|
|
603
|
+
}
|
|
604
|
+
return awaitTerminalRecord(entry);
|
|
605
|
+
},
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
async function awaitTerminalRecord(entry) {
|
|
609
|
+
const record = await entry.settleDeferred.promise;
|
|
610
|
+
await entry.persistDone.promise;
|
|
611
|
+
if (entry.persistFailed) {
|
|
612
|
+
throw new Error("Subagent task result could not be persisted", { cause: entry.persistError });
|
|
613
|
+
}
|
|
614
|
+
return record;
|
|
615
|
+
}
|
|
616
|
+
async function awaitTerminalVoid(entry) {
|
|
617
|
+
await entry.settleDeferred.promise;
|
|
618
|
+
await entry.persistDone.promise;
|
|
619
|
+
if (entry.persistFailed) {
|
|
620
|
+
throw new Error("Subagent task result could not be persisted", { cause: entry.persistError });
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
async function cancelAllEntries(reason) {
|
|
624
|
+
const pending = [...tasks.values()].filter((entry) => !entry.settled);
|
|
625
|
+
for (const entry of pending)
|
|
626
|
+
cancelEntry(entry, reason);
|
|
627
|
+
await Promise.all(pending.map((entry) => awaitTerminalVoid(entry)));
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
submit(request) {
|
|
631
|
+
if (disposed) {
|
|
632
|
+
throw new SubagentRuntimeError("subagent_invalid_request", "Subagent runtime has been disposed; submit is revoked");
|
|
633
|
+
}
|
|
634
|
+
if (typeof request !== "object" || request === null || typeof request.prompt !== "string") {
|
|
635
|
+
throw new SubagentRuntimeError("subagent_invalid_request", "Subagent submit request requires a string prompt");
|
|
636
|
+
}
|
|
637
|
+
if (utf8ByteLength(request.prompt) > maxPromptBytes) {
|
|
638
|
+
throw new SubagentRuntimeError("subagent_prompt_too_large", `Prompt of ${utf8ByteLength(request.prompt)} UTF-8 bytes exceeds the ${maxPromptBytes}-byte limit`);
|
|
639
|
+
}
|
|
640
|
+
if (request.metadata !== undefined) {
|
|
641
|
+
let serialized;
|
|
642
|
+
try {
|
|
643
|
+
serialized = JSON.stringify(request.metadata);
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
throw new SubagentRuntimeError("subagent_invalid_request", "Subagent submit metadata is not JSON-serializable");
|
|
647
|
+
}
|
|
648
|
+
if (utf8ByteLength(serialized) > maxMetadataBytes) {
|
|
649
|
+
throw new SubagentRuntimeError("subagent_metadata_too_large", `Metadata of ${utf8ByteLength(serialized)} UTF-8 bytes exceeds the ${maxMetadataBytes}-byte limit`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (queue.length >= maxQueueDepth) {
|
|
653
|
+
throw new SubagentRuntimeError("subagent_queue_full", `Subagent queue depth ${queue.length} reached the ${maxQueueDepth}-entry limit`);
|
|
654
|
+
}
|
|
655
|
+
seq += 1;
|
|
656
|
+
const taskId = `subagent:${options.owner}:${seq}:${randomUUID().slice(0, 8)}`;
|
|
657
|
+
const deadlineAt = request.deadlineAt;
|
|
658
|
+
// §4.3:deadline 优先写入 registry 投影(原生 checkDeadlines 可直映 timed_out)。
|
|
659
|
+
// registry 对已过期 deadline fail fast(deadline_passed 类 AgentTaskError),此时
|
|
660
|
+
// 降级为不带 deadline 的 createTask:过期任务由调度器 tick 结算,registry 投影
|
|
661
|
+
// 走 fail + reason 的例外路径(projectTerminalToRegistry)。
|
|
662
|
+
try {
|
|
663
|
+
registry.createTask({
|
|
664
|
+
taskId,
|
|
665
|
+
parentSessionId: options.parentSessionId,
|
|
666
|
+
...(deadlineAt === undefined ? {} : { deadlineAt }),
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
if (!(error instanceof AgentTaskError && error.code === "deadline_passed"))
|
|
671
|
+
throw error;
|
|
672
|
+
registry.createTask({ taskId, parentSessionId: options.parentSessionId });
|
|
673
|
+
}
|
|
674
|
+
const entry = {
|
|
675
|
+
taskId,
|
|
676
|
+
request,
|
|
677
|
+
deadlineAt,
|
|
678
|
+
controller: new AbortController(),
|
|
679
|
+
status: "queued",
|
|
680
|
+
gate: undefined,
|
|
681
|
+
settled: false,
|
|
682
|
+
cancelRequested: false,
|
|
683
|
+
pendingTerminal: undefined,
|
|
684
|
+
pendingReasonCode: undefined,
|
|
685
|
+
cancelReason: undefined,
|
|
686
|
+
drainDriverStarted: false,
|
|
687
|
+
host: undefined,
|
|
688
|
+
recordSessionId: undefined,
|
|
689
|
+
runSettled: createDeferred(),
|
|
690
|
+
parentSignal: undefined,
|
|
691
|
+
parentListener: undefined,
|
|
692
|
+
settleDeferred: createDeferred(),
|
|
693
|
+
persistDone: createDeferred(),
|
|
694
|
+
persistFailed: false,
|
|
695
|
+
persistError: undefined,
|
|
696
|
+
};
|
|
697
|
+
tasks.set(taskId, entry);
|
|
698
|
+
queue.push(entry);
|
|
699
|
+
pump();
|
|
700
|
+
return Promise.resolve(handleOf(entry));
|
|
701
|
+
},
|
|
702
|
+
result(taskId, requester) {
|
|
703
|
+
const cached = records.get(taskId);
|
|
704
|
+
if (cached !== undefined) {
|
|
705
|
+
return Promise.resolve(matchesRequester(cached, requester) ? cached : undefined);
|
|
706
|
+
}
|
|
707
|
+
if (results === undefined)
|
|
708
|
+
return Promise.resolve(undefined);
|
|
709
|
+
let loaded;
|
|
710
|
+
try {
|
|
711
|
+
loaded = results.store.loadArtifact({ artifactId: `subagent-task:${taskId}` });
|
|
712
|
+
}
|
|
713
|
+
catch (error) {
|
|
714
|
+
// 存储故障必须作为 **rejected promise** 冒泡:result 的签名是 Promise,
|
|
715
|
+
// 同步抛出会让 `runtime.result(...).catch(...)` 直接崩在调用点。
|
|
716
|
+
return Promise.reject(error);
|
|
717
|
+
}
|
|
718
|
+
if (loaded === undefined)
|
|
719
|
+
return Promise.resolve(undefined);
|
|
720
|
+
const record = parseSchedulerRecord(loaded.data);
|
|
721
|
+
if (record === undefined || !matchesRequester(record, requester))
|
|
722
|
+
return Promise.resolve(undefined);
|
|
723
|
+
return Promise.resolve(record);
|
|
724
|
+
},
|
|
725
|
+
findBySession(sessionId, requester) {
|
|
726
|
+
if (requester.owner !== options.owner || requester.parentSessionId !== options.parentSessionId) {
|
|
727
|
+
return undefined;
|
|
728
|
+
}
|
|
729
|
+
for (const entry of tasks.values()) {
|
|
730
|
+
if (entry.recordSessionId !== sessionId)
|
|
731
|
+
continue;
|
|
732
|
+
return { taskId: entry.taskId, settled: entry.settled };
|
|
733
|
+
}
|
|
734
|
+
return undefined;
|
|
735
|
+
},
|
|
736
|
+
async steer(taskId, text, requester) {
|
|
737
|
+
if (requester.owner !== options.owner || requester.parentSessionId !== options.parentSessionId) {
|
|
738
|
+
throw new SubagentRuntimeError("subagent_unknown_task", `no task ${taskId} in this scheduler scope`);
|
|
739
|
+
}
|
|
740
|
+
const entry = tasks.get(taskId);
|
|
741
|
+
if (entry === undefined) {
|
|
742
|
+
throw new SubagentRuntimeError("subagent_unknown_task", `no task ${taskId} in this scheduler scope`);
|
|
743
|
+
}
|
|
744
|
+
const host = entry.settled ? undefined : entry.host;
|
|
745
|
+
if (host === undefined) {
|
|
746
|
+
throw new SubagentRuntimeError("subagent_not_running", `task ${taskId} is not running (queued or settled)`);
|
|
747
|
+
}
|
|
748
|
+
const steerFn = host.session.steer;
|
|
749
|
+
if (typeof steerFn !== "function") {
|
|
750
|
+
throw new SubagentRuntimeError("subagent_steer_unsupported", `task ${taskId}'s host session exposes no steer`);
|
|
751
|
+
}
|
|
752
|
+
await steerFn.call(host.session, text);
|
|
753
|
+
},
|
|
754
|
+
async wait(taskId, timeoutMs, requester) {
|
|
755
|
+
const entry = tasks.get(taskId);
|
|
756
|
+
if (entry === undefined ||
|
|
757
|
+
requester.owner !== options.owner ||
|
|
758
|
+
requester.parentSessionId !== options.parentSessionId) {
|
|
759
|
+
return undefined;
|
|
760
|
+
}
|
|
761
|
+
if (entry.settled)
|
|
762
|
+
return records.get(taskId);
|
|
763
|
+
const boundedTimeout = Math.max(0, Math.min(timeoutMs, 120_000));
|
|
764
|
+
const timer = sleepDeferred(boundedTimeout);
|
|
765
|
+
const settled = Promise.race([entry.settleDeferred.promise, timer.promise]);
|
|
766
|
+
await settled.catch(() => { });
|
|
767
|
+
timer.cancel();
|
|
768
|
+
// settle 后记录必已落(settleEntry 先 records.set 再 resolve settleDeferred)。
|
|
769
|
+
return entry.settled ? records.get(taskId) : undefined;
|
|
770
|
+
},
|
|
771
|
+
peek(taskId, requester) {
|
|
772
|
+
const entry = tasks.get(taskId);
|
|
773
|
+
if (entry === undefined ||
|
|
774
|
+
requester.owner !== options.owner ||
|
|
775
|
+
requester.parentSessionId !== options.parentSessionId) {
|
|
776
|
+
return undefined;
|
|
777
|
+
}
|
|
778
|
+
return { settled: entry.settled, status: entry.status };
|
|
779
|
+
},
|
|
780
|
+
stats() {
|
|
781
|
+
let queued = 0;
|
|
782
|
+
let admitted = 0;
|
|
783
|
+
let running = 0;
|
|
784
|
+
let terminal = 0;
|
|
785
|
+
for (const entry of tasks.values()) {
|
|
786
|
+
if (entry.status === "queued")
|
|
787
|
+
queued += 1;
|
|
788
|
+
else if (entry.status === "admitted")
|
|
789
|
+
admitted += 1;
|
|
790
|
+
else if (entry.status === "running")
|
|
791
|
+
running += 1;
|
|
792
|
+
else
|
|
793
|
+
terminal += 1;
|
|
794
|
+
}
|
|
795
|
+
return { queued, admitted, running, terminal };
|
|
796
|
+
},
|
|
797
|
+
async cancelAll(reason) {
|
|
798
|
+
await cancelAllEntries(reason);
|
|
799
|
+
},
|
|
800
|
+
tick() {
|
|
801
|
+
const now = clock();
|
|
802
|
+
// §4.3:先推进注入 registry 的原生 deadline(running 任务原生直映 timed_out),
|
|
803
|
+
// 再与调度器自身计算的过期任务取并集;原生先行保证后续调度终态投影不会把
|
|
804
|
+
// 原生 timed_out 改写为 duplicate_terminal 失败。
|
|
805
|
+
let registryExpired = [];
|
|
806
|
+
try {
|
|
807
|
+
if (typeof registry.checkDeadlines === "function")
|
|
808
|
+
registryExpired = registry.checkDeadlines();
|
|
809
|
+
}
|
|
810
|
+
catch {
|
|
811
|
+
// 注入 registry 的原生推进失败不得让调度器崩溃:调度器自身计算仍会兜底。
|
|
812
|
+
}
|
|
813
|
+
const expired = new Set(registryExpired);
|
|
814
|
+
for (const entry of tasks.values()) {
|
|
815
|
+
if (entry.deadlineAt !== undefined && !entry.settled && now >= entry.deadlineAt) {
|
|
816
|
+
expired.add(entry.taskId);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
const converted = [];
|
|
820
|
+
for (const id of expired) {
|
|
821
|
+
const entry = tasks.get(id);
|
|
822
|
+
if (entry === undefined || entry.settled || entry.cancelRequested)
|
|
823
|
+
continue;
|
|
824
|
+
if (entry.status === "queued") {
|
|
825
|
+
// queued 过期:直接出队终态 timed_out。
|
|
826
|
+
const index = queue.indexOf(entry);
|
|
827
|
+
if (index >= 0)
|
|
828
|
+
queue.splice(index, 1);
|
|
829
|
+
settleEntry(entry, "timed_out", "deadline_passed", undefined);
|
|
830
|
+
}
|
|
831
|
+
else {
|
|
832
|
+
// running/admitted 过期:走 cancel fence + 有界 drain,终态 timed_out
|
|
833
|
+
//(drain 自身再超时则按 drain_timeout 收敛)。
|
|
834
|
+
deadlineFence(entry);
|
|
835
|
+
}
|
|
836
|
+
converted.push(id);
|
|
837
|
+
}
|
|
838
|
+
// 共享 globalAdmission 时,其他实例的槽位释放无法回调本实例;与其他显式推进
|
|
839
|
+
// 一致,由 tick()/submit/settle 的 pump 重试准入,不引入隐藏轮询。
|
|
840
|
+
pump();
|
|
841
|
+
return Object.freeze(converted);
|
|
842
|
+
},
|
|
843
|
+
async dispose() {
|
|
844
|
+
disposed = true;
|
|
845
|
+
await cancelAllEntries("subagent runtime disposed");
|
|
846
|
+
},
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
//# sourceMappingURL=subagent-runtime.js.map
|