@springbrand/agent-runtime 0.1.3-alpha.2 → 0.1.3-alpha.4
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/package.json +1 -1
- package/src/db/agent-tool.repo.ts +27 -0
- package/src/db/index.ts +33 -0
- package/src/db/interaction.repo.ts +185 -0
- package/src/db/schema.ts +15 -0
- package/src/db/submission.repo.ts +29 -0
- package/src/index.ts +8 -17
- package/src/kernel/approval-lifecycle.ts +41 -6
- package/src/kernel/bindings.ts +37 -0
- package/src/kernel/interaction-lifecycle.ts +395 -0
- package/src/kernel/public-contracts.ts +2 -0
- package/src/kernel/recoverable-chat-agent.ts +10 -2
- package/src/kernel/runtime-assembly-view.ts +37 -0
- package/src/kernel/runtime-assembly.ts +41 -0
- package/src/kernel/runtime-config.ts +4 -0
- package/src/kernel/runtime-load.ts +102 -0
- package/src/kernel/state.ts +8 -1
- package/src/kernel/submission-lifecycle.ts +30 -0
- package/src/lib/telemetry-dev.ts +7 -4
- package/src/pi/runtime-adapter/assembly.ts +13 -1
- package/src/pi/runtime-adapter/execution.ts +109 -9
- package/src/pi/runtime-adapter/index.ts +10 -3
- package/src/pi/runtime-adapter/models.ts +162 -16
- package/src/pi/runtime-adapter/recovery.ts +188 -1
- package/src/pi/tool/base.ts +62 -9
- package/src/pi/tool/compiler.ts +34 -0
- package/src/pi/tool/gateway.ts +54 -0
- package/src/pi/tool/index.ts +1 -0
- package/src/pi/tool/mcp.ts +93 -64
- package/src/pi/turn/index.ts +20 -0
- package/src/pi/turn/interaction.ts +181 -0
- package/src/pi/turn/tool-recovery.ts +244 -1
- package/src/runtime-agent.ts +246 -113
- package/src/{plugins.ts → runtime-assembler.ts} +65 -282
- package/src/runtime.ts +454 -162
package/src/runtime.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AgentToolLifecycleResult,
|
|
3
|
+
AgentToolRunInfo,
|
|
4
|
+
Connection,
|
|
5
|
+
} from "agents";
|
|
2
6
|
import {
|
|
3
7
|
ChatStreamStalledError,
|
|
4
8
|
MessageType,
|
|
@@ -13,6 +17,7 @@ import {
|
|
|
13
17
|
ApprovalLifecycle,
|
|
14
18
|
type ApprovalContinuationData,
|
|
15
19
|
} from "./kernel/approval-lifecycle";
|
|
20
|
+
import { InteractionLifecycle } from "./kernel/interaction-lifecycle";
|
|
16
21
|
import {
|
|
17
22
|
SubmissionLifecycle,
|
|
18
23
|
SubmissionQueueFullError,
|
|
@@ -29,21 +34,23 @@ import {
|
|
|
29
34
|
type SubmissionReceipt,
|
|
30
35
|
} from "./kernel/receipts";
|
|
31
36
|
import type { ExecutionLevel } from "./lib/execution-level";
|
|
37
|
+
import type { RuntimeGatewaySession } from "./kernel/bindings";
|
|
38
|
+
import type { RuntimeAssemblyView } from "./kernel/runtime-assembly-view";
|
|
39
|
+
import { projectRuntimeAssembly } from "./kernel/runtime-assembly";
|
|
32
40
|
import type {
|
|
33
41
|
RuntimeExtensionPermissions,
|
|
34
42
|
} from "./kernel/extensions";
|
|
35
43
|
import type {
|
|
36
44
|
RuntimeActivity,
|
|
37
|
-
RuntimeLoadPhase,
|
|
38
|
-
RuntimeLoadState,
|
|
39
45
|
RuntimeState,
|
|
40
46
|
RuntimeTurnState,
|
|
41
47
|
} from "./kernel/state";
|
|
48
|
+
import { RuntimeLoadTracker } from "./kernel/runtime-load";
|
|
42
49
|
import {
|
|
43
|
-
|
|
44
|
-
type
|
|
50
|
+
prepareRuntimeCandidate,
|
|
51
|
+
type RuntimeAssemblyInput,
|
|
45
52
|
type RuntimeSnapshot,
|
|
46
|
-
} from "./
|
|
53
|
+
} from "./runtime-assembler";
|
|
47
54
|
import { connectConfiguredMcpServers } from "./lib/mcp";
|
|
48
55
|
import { installConsoleSink } from "./lib/telemetry-dev";
|
|
49
56
|
import {
|
|
@@ -66,7 +73,11 @@ import {
|
|
|
66
73
|
type PreparedPiTurnAdapter,
|
|
67
74
|
} from "./pi/runtime-adapter";
|
|
68
75
|
import type { UIMessage, UIMessageChunk } from "ai";
|
|
69
|
-
import type {
|
|
76
|
+
import type {
|
|
77
|
+
AssistantMessage,
|
|
78
|
+
ToolResultMessage,
|
|
79
|
+
} from "@earendil-works/pi-ai";
|
|
80
|
+
import { serializeOutput } from "./lib/artifacts";
|
|
70
81
|
import type {
|
|
71
82
|
RuntimeModelUsageEvent,
|
|
72
83
|
RuntimeToolSettlementEvent,
|
|
@@ -96,7 +107,13 @@ import {
|
|
|
96
107
|
|
|
97
108
|
const SCHEDULED_STABLE_TIMEOUT_MS = 30_000;
|
|
98
109
|
const TURN_EVENT_RETRY_SECONDS = 10;
|
|
99
|
-
const CHAT_RECOVERY_MAX_ATTEMPTS = 5;
|
|
110
|
+
export const CHAT_RECOVERY_MAX_ATTEMPTS = 5;
|
|
111
|
+
// stall 单独收窄。理由不是「stall 更不值得救」,而是它的重试**期望值和别的错不一样**:
|
|
112
|
+
// 瞬时错(5xx / 断流)重跑一次往往就好了;stall 的重跑是拿同一份 transcript 让模型
|
|
113
|
+
// 重新想同样久,如果它本来就超预算,再跑几次也一样超。把看门狗放宽到 240s 之后,
|
|
114
|
+
// 真该救的那一类已经在第一次就跑完了,剩下还在 stall 的基本是连接真死——
|
|
115
|
+
// 那种情况下 5 次 × 240s ≈ 20 分钟的空转纯属折磨用户。3 次约 12 分钟封顶。
|
|
116
|
+
export const CHAT_STALL_MAX_ATTEMPTS = 3;
|
|
100
117
|
const CHAT_RECOVERY_TERMINAL_MESSAGE =
|
|
101
118
|
"多次恢复仍未成功,本次生成已停止,当前进度已保留。请发送新消息继续。";
|
|
102
119
|
|
|
@@ -175,6 +192,27 @@ function assistantUsageEvent(
|
|
|
175
192
|
// 作用:把用户消息内容变成可比较的稳定形状。
|
|
176
193
|
// 调用:重新生成回答时,提交入口用它核对客户端与已存用户消息。
|
|
177
194
|
// 原因:字符串和分段内容必须先归一化,否则语义相同的消息会被误判为不同。
|
|
195
|
+
// 把 park 时存下的 Tool 输入还原成对象,交给该 Tool 自己的 settle 映射。
|
|
196
|
+
// respondToolInteraction 调用;解析失败按 undefined 处理,因为映射函数是否用得到它由 Tool 决定。
|
|
197
|
+
function safeParseJson(text: string): unknown {
|
|
198
|
+
try {
|
|
199
|
+
return JSON.parse(text);
|
|
200
|
+
} catch {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Tool 没有提供 settle 时的缺省映射:响应原样成为 ToolResult 的 details。
|
|
206
|
+
// text 用 serializeOutput 而不是裸 JSON.stringify,与其他 Tool 结果的文本形态保持一致。
|
|
207
|
+
function defaultInteractionResult(
|
|
208
|
+
response: unknown,
|
|
209
|
+
): { content: ToolResultMessage["content"]; details: unknown } {
|
|
210
|
+
return {
|
|
211
|
+
content: [{ type: "text", text: serializeOutput(response).text }],
|
|
212
|
+
details: response,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
178
216
|
function userContentKey(message: PiCanonicalUserInput): string {
|
|
179
217
|
return json(
|
|
180
218
|
typeof message.content === "string"
|
|
@@ -183,22 +221,6 @@ function userContentKey(message: PiCanonicalUserInput): string {
|
|
|
183
221
|
);
|
|
184
222
|
}
|
|
185
223
|
|
|
186
|
-
function submissionNeedsInput(
|
|
187
|
-
messages: readonly UIMessage[],
|
|
188
|
-
assistantMessageId: string,
|
|
189
|
-
): boolean {
|
|
190
|
-
const assistant = messages.find(
|
|
191
|
-
(message) =>
|
|
192
|
-
message.id === assistantMessageId && message.role === "assistant",
|
|
193
|
-
);
|
|
194
|
-
return assistant?.parts.some(
|
|
195
|
-
(part) =>
|
|
196
|
-
part.type === "dynamic-tool" &&
|
|
197
|
-
part.toolName === "ask_user" &&
|
|
198
|
-
part.state === "output-available",
|
|
199
|
-
) ?? false;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
224
|
interface PendingTemporaryAgentApproval {
|
|
203
225
|
receipt: ApprovalReceipt;
|
|
204
226
|
resolve(decision: TemporaryAgentApprovalDecision): void;
|
|
@@ -234,6 +256,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
234
256
|
private runtimeSnapshot?: RuntimeSnapshot;
|
|
235
257
|
private runtimeRevision?: string;
|
|
236
258
|
private runtimePi?: PreparedPiRuntime;
|
|
259
|
+
private runtimeGatewaySession?: RuntimeGatewaySession;
|
|
237
260
|
private piAdapter?: PiRuntimeAdapter;
|
|
238
261
|
private readonly transcript: PiRuntimeTranscript;
|
|
239
262
|
private readonly submissions: SubmissionLifecycle<
|
|
@@ -241,7 +264,10 @@ export abstract class AgentRuntimeKernel<
|
|
|
241
264
|
ActiveTurn
|
|
242
265
|
>;
|
|
243
266
|
private readonly approvals: ApprovalLifecycle<StoredSubmission>;
|
|
267
|
+
private readonly interactions: InteractionLifecycle<StoredSubmission>;
|
|
268
|
+
private runtimeLoadTracker?: RuntimeLoadTracker;
|
|
244
269
|
private readonly streamBySubmission = new Map<string, string>();
|
|
270
|
+
private migratedSubmissionIds?: Set<string>;
|
|
245
271
|
private db!: RuntimeDatabase;
|
|
246
272
|
private readonly temporaryAgents = new TemporaryAgentCoordinator();
|
|
247
273
|
private readonly temporaryAgentApprovals =
|
|
@@ -256,6 +282,24 @@ export abstract class AgentRuntimeKernel<
|
|
|
256
282
|
return this.piAdapter ??= new PiRuntimeAdapter();
|
|
257
283
|
}
|
|
258
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Runtime 装载进度的状态机。
|
|
287
|
+
*
|
|
288
|
+
* @remarks
|
|
289
|
+
* 本类和 `defineRuntimeAgent` 生成的宿主共同驱动它:宿主管一轮尝试的起止,
|
|
290
|
+
* `initConfig` 管中途的阶段推进,因此它是 protected 而非 private。
|
|
291
|
+
*
|
|
292
|
+
* 写成惰性访问器而不是字段:本类有测试用 `Object.create(prototype)` 造替身,
|
|
293
|
+
* 那条路径既不跑构造函数也不跑字段初始化器。
|
|
294
|
+
*/
|
|
295
|
+
protected get runtimeLoad(): RuntimeLoadTracker {
|
|
296
|
+
return this.runtimeLoadTracker ??= new RuntimeLoadTracker({
|
|
297
|
+
read: () => this.state.runtimeLoad,
|
|
298
|
+
publish: (runtimeLoad) => this.setState({ ...this.state, runtimeLoad }),
|
|
299
|
+
isAvailable: () => Boolean(this.runtimeSnapshot),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
259
303
|
/**
|
|
260
304
|
* 为一个 Cloudflare Durable Object 实例建立持久化与执行协作器。
|
|
261
305
|
*
|
|
@@ -307,6 +351,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
307
351
|
commitTerminal: (submission, outcome, message) =>
|
|
308
352
|
this.commitTerminalOutcome(submission, outcome, message),
|
|
309
353
|
abortActive: (turn) => turn.agent.abort(),
|
|
354
|
+
interruptActive: (turn) => turn.agent.interrupt(),
|
|
310
355
|
});
|
|
311
356
|
this.transcript = this.pi.createTranscript({
|
|
312
357
|
sql: this.sql.bind(this),
|
|
@@ -344,6 +389,22 @@ export abstract class AgentRuntimeKernel<
|
|
|
344
389
|
},
|
|
345
390
|
onApprovalsChanged: () => this.broadcastApprovals(),
|
|
346
391
|
});
|
|
392
|
+
this.interactions = new InteractionLifecycle({
|
|
393
|
+
db: this.db,
|
|
394
|
+
pi: this.pi,
|
|
395
|
+
findSubmission: (submissionId) =>
|
|
396
|
+
this.readSubmission(submissionId),
|
|
397
|
+
applyRecoveryMutations: (submission, mutations) =>
|
|
398
|
+
this.applyPiRecoveryMutations(submission, mutations),
|
|
399
|
+
materializeRecoveredToolResults: (submission) =>
|
|
400
|
+
this.materializeRecoveredToolResults(submission),
|
|
401
|
+
// 与审批不同,这里不经 `schedule` 绕一圈:RPC 已经把 DO 叫醒了,
|
|
402
|
+
// 而 interaction 没有自己的续跑键,续跑就是普通的「已结算 Tool 恢复原 Turn」。
|
|
403
|
+
resumeSubmission: async (submissionId) => {
|
|
404
|
+
await this.submissions.recover(submissionId);
|
|
405
|
+
},
|
|
406
|
+
onInteractionsChanged: () => this.broadcastApprovals(),
|
|
407
|
+
});
|
|
347
408
|
}
|
|
348
409
|
|
|
349
410
|
// 作用:把通用聊天恢复协议接到本 Runtime 的 Submission 和 Pi 里程碑。
|
|
@@ -400,49 +461,93 @@ export abstract class AgentRuntimeKernel<
|
|
|
400
461
|
// 作用:准备并原子切换一份新的 Runtime 配置。
|
|
401
462
|
// 调用:生成 Agent 在首次启动或显式重载 Runtime key 时调用。
|
|
402
463
|
// 原因:先完整 prepare 再替换 Snapshot,且活跃 Turn 期间禁止换 revision,可避免半装配和恢复时能力漂移。
|
|
403
|
-
protected async initConfig(
|
|
464
|
+
protected async initConfig(input: RuntimeAssemblyInput): Promise<void> {
|
|
404
465
|
const previous = this.runtimeSnapshot;
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
next = snapshot;
|
|
409
|
-
});
|
|
410
|
-
if (!next) throw new Error("Runtime assembly did not produce a snapshot");
|
|
411
|
-
const candidateSnapshot = next;
|
|
466
|
+
this.runtimeLoad.advance("assembly");
|
|
467
|
+
const candidate = await prepareRuntimeCandidate(input);
|
|
468
|
+
const candidateSnapshot = candidate.snapshot;
|
|
412
469
|
|
|
413
|
-
this.
|
|
470
|
+
this.runtimeLoad.advance("mcp");
|
|
414
471
|
await connectConfiguredMcpServers(
|
|
415
472
|
this,
|
|
416
473
|
candidateSnapshot.profile.mcpServers,
|
|
417
474
|
);
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
475
|
+
const gatewayDegradations = [];
|
|
476
|
+
let gatewaySession: RuntimeGatewaySession | undefined;
|
|
477
|
+
if (candidateSnapshot.bindings.gateway) {
|
|
478
|
+
try {
|
|
479
|
+
gatewaySession = await candidateSnapshot.bindings.gateway.open(
|
|
480
|
+
`${this.ctx.id.toString()}:${crypto.randomUUID()}`,
|
|
481
|
+
);
|
|
482
|
+
} catch {
|
|
483
|
+
gatewayDegradations.push({
|
|
484
|
+
capability: "connector" as const,
|
|
485
|
+
reason: "unavailable" as const,
|
|
486
|
+
detail: "Connector Gateway MCP",
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
this.runtimeLoad.advance("pi");
|
|
492
|
+
let prepared: PreparedPiRuntime;
|
|
493
|
+
try {
|
|
494
|
+
prepared = await this.pi.prepare({
|
|
495
|
+
snapshot: candidateSnapshot,
|
|
496
|
+
mcpHost: this,
|
|
497
|
+
gatewaySession,
|
|
498
|
+
additionalDegradations: gatewayDegradations,
|
|
499
|
+
createExtensionHostBinding: (permissions, ownContextLabels) =>
|
|
500
|
+
this.createExtensionHostBinding(
|
|
501
|
+
permissions,
|
|
502
|
+
ownContextLabels,
|
|
503
|
+
),
|
|
504
|
+
});
|
|
505
|
+
} catch (error) {
|
|
506
|
+
await gatewaySession?.close().catch(() => undefined);
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
428
509
|
const revision = await this.hash(prepared.revisionDescriptor);
|
|
429
|
-
|
|
430
|
-
|
|
510
|
+
// 停在人机等待上的 Submission 是「未完成」,但没有任何模型请求在飞、
|
|
511
|
+
// 没有任何工具在执行 —— 换装配对它是安全的,而且这正是「决定时才装上
|
|
512
|
+
// 新能力,同一轮接着跑」所依赖的那一步。真正在执行的 Turn 仍然被挡住。
|
|
513
|
+
const contested = Boolean(previous) &&
|
|
431
514
|
revision !== this.runtimeRevision &&
|
|
432
|
-
this.submissions.isBusy()
|
|
433
|
-
)
|
|
515
|
+
this.submissions.isBusy();
|
|
516
|
+
const parked = contested && this.db.everyUnfinishedSubmissionParked();
|
|
517
|
+
if (contested && !parked) {
|
|
518
|
+
await gatewaySession?.close().catch(() => undefined);
|
|
434
519
|
throw new Error(
|
|
435
520
|
"Cannot reload Runtime while a revision-pinned Pi Turn is active",
|
|
436
521
|
);
|
|
437
522
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
523
|
+
const repin = parked
|
|
524
|
+
? await this.prepareParkedSubmissionRepin(prepared, revision)
|
|
525
|
+
: undefined;
|
|
526
|
+
let activated = false;
|
|
527
|
+
const previousGatewaySession = this.runtimeGatewaySession;
|
|
528
|
+
try {
|
|
529
|
+
for (const guard of candidate.commitGuards) await guard();
|
|
530
|
+
if (candidateSnapshot.bindings.platform.telemetryConsole) {
|
|
531
|
+
installConsoleSink();
|
|
532
|
+
}
|
|
533
|
+
this.pi.activate(candidateSnapshot);
|
|
534
|
+
activated = true;
|
|
535
|
+
repin?.commit();
|
|
536
|
+
this.runtimeSnapshot = candidateSnapshot;
|
|
537
|
+
this.runtimeRevision = revision;
|
|
538
|
+
this.runtimePi = prepared;
|
|
539
|
+
this.runtimeGatewaySession = gatewaySession;
|
|
540
|
+
} catch (error) {
|
|
541
|
+
repin?.abort();
|
|
542
|
+
if (activated && previous) this.pi.activate(previous);
|
|
543
|
+
await gatewaySession?.close().catch(() => undefined);
|
|
544
|
+
throw error;
|
|
445
545
|
}
|
|
546
|
+
await repin?.interrupt().catch((error) => {
|
|
547
|
+
console.error("[runtime-repin:interrupt-failed]", error);
|
|
548
|
+
});
|
|
549
|
+
await previousGatewaySession?.close().catch(() => undefined);
|
|
550
|
+
|
|
446
551
|
const degradations = prepared.degradations;
|
|
447
552
|
if (degradations.length > 0) {
|
|
448
553
|
console.warn(
|
|
@@ -508,6 +613,10 @@ export abstract class AgentRuntimeKernel<
|
|
|
508
613
|
}
|
|
509
614
|
},
|
|
510
615
|
),
|
|
616
|
+
// spec 在这里用不上:响应侧一律从重新装配出的 candidate 按 toolName 取回同一份,
|
|
617
|
+
// 好让「DO 一直醒着」和「park 期间睡过一觉」走完全相同的一条代码路径。
|
|
618
|
+
requestToolInteraction: (interaction, _spec, signal) =>
|
|
619
|
+
this.interactions.request(submission, interaction, signal),
|
|
511
620
|
appendToolInput: (input) =>
|
|
512
621
|
this.appendToolInput(submission, input),
|
|
513
622
|
settleTool: (call) =>
|
|
@@ -522,6 +631,10 @@ export abstract class AgentRuntimeKernel<
|
|
|
522
631
|
this.readSubmission(submission.submissionId)?.abortReason,
|
|
523
632
|
onRecord: output.onRecord ?? (() => undefined),
|
|
524
633
|
onCanonicalMessage: async (commit) => {
|
|
634
|
+
// 换装配把这条执行器中断掉了。它退场路上还会吐出「工具被中止」这样的
|
|
635
|
+
// 消息 —— 那不是这一轮的事实,写进权威 transcript 会让重建出来的续跑
|
|
636
|
+
// 看到一份自己从没产生过的工具结果。
|
|
637
|
+
if (this.migratedSubmissions.has(submission.submissionId)) return;
|
|
525
638
|
let consumedSteer = false;
|
|
526
639
|
this.db.transaction(() => {
|
|
527
640
|
if (commit.kind !== "append-user") {
|
|
@@ -589,7 +702,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
589
702
|
* 不会延长 Durable Object 生命;本文件仍在三处用它启动后台任务。
|
|
590
703
|
*/
|
|
591
704
|
async onStart(): Promise<void> {
|
|
592
|
-
this.
|
|
705
|
+
this.runtimeLoad.reset();
|
|
593
706
|
if (this.db.runtimeEvents.hasPending()) {
|
|
594
707
|
this.ctx.waitUntil(
|
|
595
708
|
this.ensureRuntimeReady().then(() => this.drainRuntimeEvents()),
|
|
@@ -743,78 +856,21 @@ export abstract class AgentRuntimeKernel<
|
|
|
743
856
|
private assembly(): RuntimeSnapshot {
|
|
744
857
|
if (!this.runtimeSnapshot) {
|
|
745
858
|
throw new Error(
|
|
746
|
-
"
|
|
859
|
+
"Runtime must be assembled before use",
|
|
747
860
|
);
|
|
748
861
|
}
|
|
749
862
|
return this.runtimeSnapshot;
|
|
750
863
|
}
|
|
751
864
|
|
|
752
|
-
//
|
|
753
|
-
//
|
|
754
|
-
//
|
|
755
|
-
protected
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
// 原因:config 阶段可能包含身份、D1 和 Resource 解析,必须在首个慢请求前可见。
|
|
762
|
-
protected beginRuntimeLoad(): void {
|
|
763
|
-
const now = Date.now();
|
|
764
|
-
this.publishRuntimeLoad({
|
|
765
|
-
status: "loading",
|
|
766
|
-
phase: "config",
|
|
767
|
-
available: Boolean(this.runtimeSnapshot),
|
|
768
|
-
startedAt: now,
|
|
769
|
-
updatedAt: now,
|
|
770
|
-
});
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
// 作用:推进当前 Runtime 装载尝试的阶段。
|
|
774
|
-
// 调用:initConfig 在进入 Plugin、MCP 和 Pi 边界时调用。
|
|
775
|
-
// 原因:保持一个稳定的粗粒度协议,不向前端泄漏具体 Plugin 实现和并发细节。
|
|
776
|
-
private advanceRuntimeLoad(phase: RuntimeLoadPhase): void {
|
|
777
|
-
const current = this.state.runtimeLoad;
|
|
778
|
-
const now = Date.now();
|
|
779
|
-
this.publishRuntimeLoad({
|
|
780
|
-
status: "loading",
|
|
781
|
-
phase,
|
|
782
|
-
available: Boolean(this.runtimeSnapshot),
|
|
783
|
-
startedAt:
|
|
784
|
-
current?.status === "loading" ? current.startedAt : now,
|
|
785
|
-
updatedAt: now,
|
|
786
|
-
});
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// 作用:把成功提交的 Runtime 装载尝试标记为可用。
|
|
790
|
-
// 调用:生成 Agent 在 initConfig 和 Runtime key 提交完成后调用。
|
|
791
|
-
// 原因:只有完整原子提交后才能向客户端承诺 ready。
|
|
792
|
-
protected completeRuntimeLoad(): void {
|
|
793
|
-
const current = this.state.runtimeLoad;
|
|
794
|
-
const now = Date.now();
|
|
795
|
-
this.publishRuntimeLoad({
|
|
796
|
-
status: "ready",
|
|
797
|
-
available: true,
|
|
798
|
-
startedAt:
|
|
799
|
-
current?.status === "loading" ? current.startedAt : now,
|
|
800
|
-
completedAt: now,
|
|
801
|
-
});
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
// 作用:记录 Runtime 装载失败,同时保留旧 Runtime 是否仍可用的信息。
|
|
805
|
-
// 调用:生成 Agent 收口 createConfig 或 initConfig 的异常时调用。
|
|
806
|
-
// 原因:前端需要状态但不应接收可能包含存储细节的底层错误文本。
|
|
807
|
-
protected failRuntimeLoad(): void {
|
|
808
|
-
const current = this.state.runtimeLoad;
|
|
809
|
-
const now = Date.now();
|
|
810
|
-
this.publishRuntimeLoad({
|
|
811
|
-
status: "error",
|
|
812
|
-
phase: current?.status === "loading" ? current.phase : "config",
|
|
813
|
-
available: Boolean(this.runtimeSnapshot),
|
|
814
|
-
startedAt:
|
|
815
|
-
current?.status === "loading" ? current.startedAt : now,
|
|
816
|
-
failedAt: now,
|
|
817
|
-
});
|
|
865
|
+
// 作用:把当前已安装的 Runtime Snapshot 投影成可序列化的调试视图。
|
|
866
|
+
// 调用:Session facet 的 `getRuntimeAssembly` RPC。
|
|
867
|
+
// 原因:UI 需要读取真实装配结果,而不是上层配置声明。
|
|
868
|
+
protected readRuntimeAssembly(): RuntimeAssemblyView {
|
|
869
|
+
return projectRuntimeAssembly(
|
|
870
|
+
this.assembly(),
|
|
871
|
+
this.runtimeRevision ?? null,
|
|
872
|
+
this.preparedPi(),
|
|
873
|
+
);
|
|
818
874
|
}
|
|
819
875
|
|
|
820
876
|
// 作用:为 Runtime Extension 创建只包含授权能力的 Worker 回环绑定。
|
|
@@ -860,6 +916,20 @@ export abstract class AgentRuntimeKernel<
|
|
|
860
916
|
// 作用:返回当前已准备好的 Pi Runtime。
|
|
861
917
|
// 调用:创建 Turn 适配器或准入固定装配时调用。
|
|
862
918
|
// 原因:明确区分“已有 Snapshot”和“Pi 已完成准备”,防止半初始化状态进入 Turn。
|
|
919
|
+
/**
|
|
920
|
+
* 正在被换装配中断、等待持久续跑接手的 Submission。
|
|
921
|
+
*
|
|
922
|
+
* 中断和取消在执行路径上长得一模一样(都是 abort),但结局相反:取消要写终态,
|
|
923
|
+
* 换装配中断必须让 Submission 停在 running,等决策把它交给续跑。这个集合是
|
|
924
|
+
* 两者唯一的区分依据,只在一次 `repinParkedSubmissions` 的窗口内有成员。
|
|
925
|
+
*
|
|
926
|
+
* 写成惰性访问器而不是字段:本类有测试用 `Object.create(prototype)` 造替身,
|
|
927
|
+
* 那条路径不跑字段初始化器。
|
|
928
|
+
*/
|
|
929
|
+
private get migratedSubmissions(): Set<string> {
|
|
930
|
+
return this.migratedSubmissionIds ??= new Set<string>();
|
|
931
|
+
}
|
|
932
|
+
|
|
863
933
|
private preparedPi(): PreparedPiRuntime {
|
|
864
934
|
if (!this.runtimePi) {
|
|
865
935
|
throw new Error(
|
|
@@ -881,13 +951,31 @@ export abstract class AgentRuntimeKernel<
|
|
|
881
951
|
descriptor: string;
|
|
882
952
|
}> {
|
|
883
953
|
await this.ensureRuntimeReady();
|
|
954
|
+
return this.currentAssemblyPin();
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// 作用:按当前已装配的 Runtime 生成一份不可变装配描述。
|
|
958
|
+
// 调用:准入 pin 走 `admissionPin`;`initConfig` 内部的重新 pin 直接调用本方法。
|
|
959
|
+
// 原因:`initConfig` 已经在装配串行入口里面,再走 `ensureRuntimeReady` 会等自己,
|
|
960
|
+
// 所以「确保装配就绪」和「按当前装配取 pin」必须是两步。
|
|
961
|
+
private async currentAssemblyPin(): Promise<{
|
|
962
|
+
revision: string;
|
|
963
|
+
descriptor: string;
|
|
964
|
+
}> {
|
|
884
965
|
await this.drainRuntimeEvents();
|
|
885
966
|
if (!this.runtimeRevision) {
|
|
886
967
|
throw new Error("Runtime revision is not initialized");
|
|
887
968
|
}
|
|
969
|
+
return this.pinAssembly(this.preparedPi(), this.runtimeRevision);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
private async pinAssembly(
|
|
973
|
+
prepared: PreparedPiRuntime,
|
|
974
|
+
baseRevision: string,
|
|
975
|
+
): Promise<{ revision: string; descriptor: string }> {
|
|
888
976
|
const pinned = await this.pi.pin({
|
|
889
|
-
prepared
|
|
890
|
-
baseRevision
|
|
977
|
+
prepared,
|
|
978
|
+
baseRevision,
|
|
891
979
|
readExtensionContext: ({ label }) =>
|
|
892
980
|
this._hostGetContext(label),
|
|
893
981
|
});
|
|
@@ -925,6 +1013,89 @@ export abstract class AgentRuntimeKernel<
|
|
|
925
1013
|
return pinned;
|
|
926
1014
|
}
|
|
927
1015
|
|
|
1016
|
+
/** 准备 parked Submission 的续跑切换,持久 pin 只在最终 commit 中修改。 */
|
|
1017
|
+
private async prepareParkedSubmissionRepin(
|
|
1018
|
+
prepared: PreparedPiRuntime,
|
|
1019
|
+
baseRevision: string,
|
|
1020
|
+
): Promise<{ commit(): void; interrupt(): Promise<void>; abort(): void }> {
|
|
1021
|
+
await this.drainRuntimeEvents();
|
|
1022
|
+
const { revision, descriptor } = await this.pinAssembly(
|
|
1023
|
+
prepared,
|
|
1024
|
+
baseRevision,
|
|
1025
|
+
);
|
|
1026
|
+
const plans: Array<{
|
|
1027
|
+
submission: StoredSubmission;
|
|
1028
|
+
decision: PiRecoveryDecision;
|
|
1029
|
+
}> = [];
|
|
1030
|
+
for (const submissionId of this.db.submissions.listUnfinishedIds()) {
|
|
1031
|
+
const submission = this.readSubmission(submissionId);
|
|
1032
|
+
if (
|
|
1033
|
+
!submission?.assemblyRevision ||
|
|
1034
|
+
!submission.assemblyDescriptor ||
|
|
1035
|
+
submission.assemblyRevision === revision
|
|
1036
|
+
) {
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
const decision = this.decidePiRecovery(submission, {
|
|
1040
|
+
kind: "repin-assembly",
|
|
1041
|
+
nextAssemblyRevision: revision,
|
|
1042
|
+
});
|
|
1043
|
+
plans.push({ submission, decision });
|
|
1044
|
+
}
|
|
1045
|
+
let settled = false;
|
|
1046
|
+
let committed = false;
|
|
1047
|
+
const cleanup = () => {
|
|
1048
|
+
for (const { submission } of plans) {
|
|
1049
|
+
this.migratedSubmissions.delete(submission.submissionId);
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
return {
|
|
1053
|
+
commit: () => {
|
|
1054
|
+
if (settled) throw new Error("Parked Submission repin already settled");
|
|
1055
|
+
for (const { submission } of plans) {
|
|
1056
|
+
this.migratedSubmissions.add(submission.submissionId);
|
|
1057
|
+
}
|
|
1058
|
+
try {
|
|
1059
|
+
this.db.transaction(() => {
|
|
1060
|
+
for (const { submission, decision } of plans) {
|
|
1061
|
+
this.applyPiRecoveryMutations(submission, decision.mutations);
|
|
1062
|
+
this.db.submissions.repinAssembly(
|
|
1063
|
+
submission.submissionId,
|
|
1064
|
+
revision,
|
|
1065
|
+
descriptor,
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
committed = true;
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
settled = true;
|
|
1072
|
+
cleanup();
|
|
1073
|
+
throw error;
|
|
1074
|
+
}
|
|
1075
|
+
},
|
|
1076
|
+
interrupt: async () => {
|
|
1077
|
+
if (!committed || settled) return;
|
|
1078
|
+
try {
|
|
1079
|
+
for (const { submission } of plans) {
|
|
1080
|
+
const submissionId = submission.submissionId;
|
|
1081
|
+
await this.submissions.interrupt(
|
|
1082
|
+
submissionId,
|
|
1083
|
+
() => this.approvals.discardWaiters(submissionId),
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
} finally {
|
|
1087
|
+
settled = true;
|
|
1088
|
+
cleanup();
|
|
1089
|
+
}
|
|
1090
|
+
},
|
|
1091
|
+
abort: () => {
|
|
1092
|
+
if (settled) return;
|
|
1093
|
+
settled = true;
|
|
1094
|
+
cleanup();
|
|
1095
|
+
},
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
|
|
928
1099
|
// 作用:检查 Submission 保存的装配描述与 revision 仍然匹配。
|
|
929
1100
|
// 调用:普通恢复和审批续跑在重建 Pi Turn 前调用。
|
|
930
1101
|
// 原因:若持久描述已变,继续执行会把同一 Turn 切成两套能力语义。
|
|
@@ -1165,6 +1336,28 @@ export abstract class AgentRuntimeKernel<
|
|
|
1165
1336
|
);
|
|
1166
1337
|
continue;
|
|
1167
1338
|
}
|
|
1339
|
+
if (mutation.kind === "record-interaction") {
|
|
1340
|
+
this.db.interactions.insert({
|
|
1341
|
+
interactionId: mutation.interaction.interactionId,
|
|
1342
|
+
submissionId: submission.submissionId,
|
|
1343
|
+
requestId: mutation.interaction.requestId,
|
|
1344
|
+
toolCallId: mutation.interaction.toolCallId,
|
|
1345
|
+
toolName: mutation.interaction.toolName,
|
|
1346
|
+
inputJson: mutation.interaction.inputJson,
|
|
1347
|
+
status: "pending",
|
|
1348
|
+
createdAt: mutation.interaction.createdAt,
|
|
1349
|
+
});
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
if (mutation.kind === "settle-interaction") {
|
|
1353
|
+
this.db.interactions.settle(
|
|
1354
|
+
mutation.interactionId,
|
|
1355
|
+
mutation.status,
|
|
1356
|
+
mutation.settledAt,
|
|
1357
|
+
mutation.responseJson,
|
|
1358
|
+
);
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1168
1361
|
const inserted = this.db.milestones.upsert(
|
|
1169
1362
|
submission.submissionId,
|
|
1170
1363
|
mutation.key,
|
|
@@ -1387,7 +1580,11 @@ export abstract class AgentRuntimeKernel<
|
|
|
1387
1580
|
latest,
|
|
1388
1581
|
effectiveMessage ?? "Turn ended before approval",
|
|
1389
1582
|
);
|
|
1390
|
-
|
|
1583
|
+
// 还挂着的 interaction 同样要收尾,否则 Turn 结束后 pending 行会变成孤儿,
|
|
1584
|
+
// 用户还能看见一张点了没反应的卡。
|
|
1585
|
+
const closedInteractions = this.interactions
|
|
1586
|
+
.cancelPendingForSubmissionSync(latest);
|
|
1587
|
+
if (closedApprovals || closedInteractions) {
|
|
1391
1588
|
this.materializeRecoveredToolResultsSync(latest);
|
|
1392
1589
|
}
|
|
1393
1590
|
const status =
|
|
@@ -1463,20 +1660,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
1463
1660
|
} catch (error) {
|
|
1464
1661
|
console.error("[chat-recovery] terminal settlement failed", error);
|
|
1465
1662
|
}
|
|
1466
|
-
|
|
1467
|
-
if (terminal.status === "completed") {
|
|
1468
|
-
try {
|
|
1469
|
-
if (submissionNeedsInput(
|
|
1470
|
-
await this.getMessages(),
|
|
1471
|
-
terminal.assistantMessageId,
|
|
1472
|
-
)) {
|
|
1473
|
-
inactiveActivity = "needs-input";
|
|
1474
|
-
}
|
|
1475
|
-
} catch {
|
|
1476
|
-
// Transcript projection is retried by the normal terminal path.
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
await this.broadcastApprovals(inactiveActivity);
|
|
1663
|
+
await this.broadcastApprovals();
|
|
1480
1664
|
try {
|
|
1481
1665
|
if (
|
|
1482
1666
|
effectiveOutcome === "failed" ||
|
|
@@ -1726,6 +1910,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
1726
1910
|
failed.status === "error"
|
|
1727
1911
|
? failed.error ?? "Pi turn failed"
|
|
1728
1912
|
: undefined,
|
|
1913
|
+
recovery,
|
|
1729
1914
|
);
|
|
1730
1915
|
return failed;
|
|
1731
1916
|
} finally {
|
|
@@ -1878,6 +2063,12 @@ export abstract class AgentRuntimeKernel<
|
|
|
1878
2063
|
}
|
|
1879
2064
|
},
|
|
1880
2065
|
);
|
|
2066
|
+
// 换装配把这条执行器中断掉了:Submission 没有结束,它在等持久续跑用新
|
|
2067
|
+
// 装配重建一条接着跑。这里写终态会把那一轮当场杀掉。
|
|
2068
|
+
if (this.migratedSubmissions.has(submissionId)) {
|
|
2069
|
+
if (streamId) this.failRecoverableStream(streamId);
|
|
2070
|
+
return this.readSubmission(submissionId)!;
|
|
2071
|
+
}
|
|
1881
2072
|
const intent = terminalIntent ?? {
|
|
1882
2073
|
outcome: "failed" as const,
|
|
1883
2074
|
message:
|
|
@@ -1892,6 +2083,11 @@ export abstract class AgentRuntimeKernel<
|
|
|
1892
2083
|
await this.projectTerminal(turn, terminal);
|
|
1893
2084
|
return terminal;
|
|
1894
2085
|
} catch (error) {
|
|
2086
|
+
// 同上:中断出来的 abort 不是失败,也不是取消。
|
|
2087
|
+
if (this.migratedSubmissions.has(submissionId)) {
|
|
2088
|
+
if (streamId) this.failRecoverableStream(streamId);
|
|
2089
|
+
return this.readSubmission(submissionId)!;
|
|
2090
|
+
}
|
|
1895
2091
|
const stalled = error instanceof ChatStreamStalledError;
|
|
1896
2092
|
const abortReason = this.readSubmission(submissionId)?.abortReason;
|
|
1897
2093
|
if (!abortReason && (stalled || error instanceof RetryableModelError)) {
|
|
@@ -1923,7 +2119,8 @@ export abstract class AgentRuntimeKernel<
|
|
|
1923
2119
|
?.abortReason;
|
|
1924
2120
|
if (
|
|
1925
2121
|
!stoppedDuringRecovery &&
|
|
1926
|
-
recoveryErrorCount <
|
|
2122
|
+
recoveryErrorCount <
|
|
2123
|
+
(stalled ? CHAT_STALL_MAX_ATTEMPTS : CHAT_RECOVERY_MAX_ATTEMPTS)
|
|
1927
2124
|
) {
|
|
1928
2125
|
const recoveryOutcome = await this.scheduleChatRecoveryRetry(
|
|
1929
2126
|
{
|
|
@@ -1936,7 +2133,9 @@ export abstract class AgentRuntimeKernel<
|
|
|
1936
2133
|
messages: await this.getMessages(),
|
|
1937
2134
|
}));
|
|
1938
2135
|
if (streamId) this.completeRecoverableStream(streamId);
|
|
1939
|
-
this.sendChatResponse(submission.requestId, "", true
|
|
2136
|
+
this.sendChatResponse(submission.requestId, "", true, {
|
|
2137
|
+
continuation: turn.continuation,
|
|
2138
|
+
});
|
|
1940
2139
|
},
|
|
1941
2140
|
);
|
|
1942
2141
|
if (
|
|
@@ -2004,17 +2203,24 @@ export abstract class AgentRuntimeKernel<
|
|
|
2004
2203
|
const persist = streamId
|
|
2005
2204
|
? this.appendRecoverableChunk(streamId, body)
|
|
2006
2205
|
: Promise.resolve();
|
|
2007
|
-
this.sendChatResponse(turn.requestId, body, false
|
|
2206
|
+
this.sendChatResponse(turn.requestId, body, false, {
|
|
2207
|
+
continuation: turn.continuation,
|
|
2208
|
+
});
|
|
2008
2209
|
await persist;
|
|
2009
2210
|
}
|
|
2010
2211
|
|
|
2011
|
-
private sendChatTerminal(
|
|
2212
|
+
private sendChatTerminal(
|
|
2213
|
+
requestId: string,
|
|
2214
|
+
error?: string,
|
|
2215
|
+
continuation = false,
|
|
2216
|
+
): void {
|
|
2012
2217
|
this.sendChatResponse(
|
|
2013
2218
|
requestId,
|
|
2014
2219
|
error
|
|
2015
2220
|
? json({ type: "error", errorText: error } satisfies UIMessageChunk)
|
|
2016
2221
|
: "",
|
|
2017
2222
|
true,
|
|
2223
|
+
{ continuation },
|
|
2018
2224
|
);
|
|
2019
2225
|
}
|
|
2020
2226
|
|
|
@@ -2030,14 +2236,10 @@ export abstract class AgentRuntimeKernel<
|
|
|
2030
2236
|
submission.status === "error"
|
|
2031
2237
|
? submission.error ?? "Pi turn failed"
|
|
2032
2238
|
: undefined,
|
|
2239
|
+
turn.continuation,
|
|
2033
2240
|
);
|
|
2034
2241
|
const messages = await this.getMessages();
|
|
2035
|
-
await this.broadcastApprovals(
|
|
2036
|
-
submission.status === "completed" &&
|
|
2037
|
-
submissionNeedsInput(messages, submission.assistantMessageId)
|
|
2038
|
-
? "needs-input"
|
|
2039
|
-
: "idle",
|
|
2040
|
-
);
|
|
2242
|
+
await this.broadcastApprovals();
|
|
2041
2243
|
this.broadcast(
|
|
2042
2244
|
json({
|
|
2043
2245
|
type: MessageType.CF_AGENT_CHAT_MESSAGES,
|
|
@@ -2469,6 +2671,19 @@ export abstract class AgentRuntimeKernel<
|
|
|
2469
2671
|
return submitted.receipt;
|
|
2470
2672
|
}
|
|
2471
2673
|
|
|
2674
|
+
// 作用:找出当前停在「等客户端结算某个 Tool」上的 Submission。
|
|
2675
|
+
// 调用:dispatchMessage 判断要不要把 delivery 提升成 steer 时调用。
|
|
2676
|
+
// 原因:park 期间 DO 可能已经睡过一觉,内存里的 active turn 不可信,只能问数据库。
|
|
2677
|
+
private findInteractionParkedSubmission(): StoredSubmission | null {
|
|
2678
|
+
const running = this.db.submissions.findRunning() as
|
|
2679
|
+
| StoredSubmission
|
|
2680
|
+
| null;
|
|
2681
|
+
return running &&
|
|
2682
|
+
this.interactions.hasPendingForSubmission(running.submissionId)
|
|
2683
|
+
? running
|
|
2684
|
+
: null;
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2472
2687
|
async dispatchMessage(
|
|
2473
2688
|
message: UIMessage,
|
|
2474
2689
|
delivery: MessageDelivery,
|
|
@@ -2489,9 +2704,14 @@ export abstract class AgentRuntimeKernel<
|
|
|
2489
2704
|
const userMessage = this.pi.normalizeUserInput(
|
|
2490
2705
|
message as UIMessage & { role: "user" },
|
|
2491
2706
|
);
|
|
2492
|
-
|
|
2707
|
+
// 停在 interaction park 上的 Turn 一律按 steer 处理,哪怕客户端发的是 enqueue:
|
|
2708
|
+
// enqueue 要等本 Turn 结束,而本 Turn 正在等一个永远不会来的答案 —— 死锁。
|
|
2709
|
+
const parked = this.findInteractionParkedSubmission();
|
|
2710
|
+
const effectiveDelivery = parked ? "steer" : delivery;
|
|
2711
|
+
if (effectiveDelivery === "steer") {
|
|
2493
2712
|
const active = this.submissions.currentActive();
|
|
2494
|
-
const target =
|
|
2713
|
+
const target = parked ??
|
|
2714
|
+
active ??
|
|
2495
2715
|
this.db.submissions.findRunning() ??
|
|
2496
2716
|
this.db.submissions.findNextPending();
|
|
2497
2717
|
if (target) {
|
|
@@ -2524,6 +2744,14 @@ export abstract class AgentRuntimeKernel<
|
|
|
2524
2744
|
}
|
|
2525
2745
|
await this.broadcastApprovals();
|
|
2526
2746
|
}
|
|
2747
|
+
// 先落 steer 再取消:取消会唤醒 park 住的 Tool 让 Turn 继续跑,
|
|
2748
|
+
// 顺序反过来 Turn 可能在这条消息落盘前就跑完了。
|
|
2749
|
+
if (parked?.submissionId === target.submissionId) {
|
|
2750
|
+
await this.interactions.cancelPendingForSubmission(
|
|
2751
|
+
target.submissionId,
|
|
2752
|
+
"user_replied_freeform",
|
|
2753
|
+
);
|
|
2754
|
+
}
|
|
2527
2755
|
return {
|
|
2528
2756
|
kind: "accepted",
|
|
2529
2757
|
submissionId: target.submissionId,
|
|
@@ -2797,6 +3025,43 @@ export abstract class AgentRuntimeKernel<
|
|
|
2797
3025
|
};
|
|
2798
3026
|
}
|
|
2799
3027
|
|
|
3028
|
+
/**
|
|
3029
|
+
* 把客户端投递的响应作为某次 Tool 调用的结果,并续跑原 Turn。
|
|
3030
|
+
*
|
|
3031
|
+
* @remarks
|
|
3032
|
+
* 前端经 WS RPC 调用,只带 `toolCallId` —— 它不知道 submissionId,也不该知道。
|
|
3033
|
+
*
|
|
3034
|
+
* 这是「结果由客户端提供」的 Tool 的唯一入口。与审批不同,这里没有任何业务身份
|
|
3035
|
+
* (审批的 `allow_level` 要改 User Agent 授权档位,所以必须经 Host),因此留在 Runtime。
|
|
3036
|
+
*
|
|
3037
|
+
* 查无、已结算、Tool 未声明 `interaction`、响应体不过校验,一律返回 `{ ok: false }` 而不抛 ——
|
|
3038
|
+
* 口径对齐 `cancelSubmissionById`:客户端重复点击不该看到异常。
|
|
3039
|
+
*/
|
|
3040
|
+
async respondToolInteraction(
|
|
3041
|
+
toolCallId: string,
|
|
3042
|
+
response: unknown,
|
|
3043
|
+
): Promise<{ ok: boolean }> {
|
|
3044
|
+
const pending = this.interactions.findPending(toolCallId);
|
|
3045
|
+
if (!pending) return { ok: false };
|
|
3046
|
+
const submission = this.readSubmission(pending.submissionId);
|
|
3047
|
+
if (!submission) return { ok: false };
|
|
3048
|
+
await this.ensureRuntimeReady();
|
|
3049
|
+
|
|
3050
|
+
const adapter = this.createSubmissionExecutionAdapter(submission);
|
|
3051
|
+
const spec = adapter.interactionSpec(pending.toolName);
|
|
3052
|
+
if (!spec) return { ok: false };
|
|
3053
|
+
if (!spec.validateResponse(response)) return { ok: false };
|
|
3054
|
+
|
|
3055
|
+
const input = safeParseJson(pending.inputJson);
|
|
3056
|
+
const settled = spec.settle
|
|
3057
|
+
? spec.settle(input, response)
|
|
3058
|
+
: defaultInteractionResult(response);
|
|
3059
|
+
return this.interactions.respond(pending.interactionId, response, {
|
|
3060
|
+
content: settled.content,
|
|
3061
|
+
details: settled.details,
|
|
3062
|
+
});
|
|
3063
|
+
}
|
|
3064
|
+
|
|
2800
3065
|
/**
|
|
2801
3066
|
* 对一条待处理执行应用一次允许或拒绝决定。
|
|
2802
3067
|
*
|
|
@@ -2910,12 +3175,29 @@ export abstract class AgentRuntimeKernel<
|
|
|
2910
3175
|
}
|
|
2911
3176
|
}
|
|
2912
3177
|
|
|
3178
|
+
// 作用:Agent Tool 子运行开始后重算一次活动投影。
|
|
3179
|
+
// 调用:Agents SDK 在登记子运行后调用。
|
|
3180
|
+
// 原因:分离的子运行不进 Submission 表,不在这里重算,Host 的列表会显示成已经空闲。
|
|
3181
|
+
override async onAgentToolStart(run: AgentToolRunInfo): Promise<void> {
|
|
3182
|
+
await super.onAgentToolStart(run);
|
|
3183
|
+
await this.broadcastApprovals();
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
// 作用:Agent Tool 子运行结束后重算一次活动投影。
|
|
3187
|
+
// 调用:Agents SDK 在子运行进入终态或被中断后调用。
|
|
3188
|
+
// 原因:中断且子进程仍在跑时活动不能落回空闲,判定只在 `hasRunningAgentTools` 一处。
|
|
3189
|
+
override async onAgentToolFinish(
|
|
3190
|
+
run: AgentToolRunInfo,
|
|
3191
|
+
result: AgentToolLifecycleResult,
|
|
3192
|
+
): Promise<void> {
|
|
3193
|
+
await super.onAgentToolFinish(run, result);
|
|
3194
|
+
await this.broadcastApprovals();
|
|
3195
|
+
}
|
|
3196
|
+
|
|
2913
3197
|
// 作用:把审批、队列和 Session 活动投影到 Agent 可广播状态。
|
|
2914
|
-
//
|
|
3198
|
+
// 调用:启动、准入、审批变化、interaction 变化、子运行变化和 Turn 完成时调用。
|
|
2915
3199
|
// 原因:这些都是可重建的 UI 投影,相同内容不重复 `setState`,投影失败也不能阻断执行。
|
|
2916
|
-
|
|
2917
|
-
inactiveActivity?: RuntimeActivity,
|
|
2918
|
-
): Promise<void> {
|
|
3200
|
+
protected async broadcastApprovals(): Promise<void> {
|
|
2919
3201
|
try {
|
|
2920
3202
|
const approvals = [
|
|
2921
3203
|
...this.approvals.list(),
|
|
@@ -2967,7 +3249,11 @@ export abstract class AgentRuntimeKernel<
|
|
|
2967
3249
|
? {}
|
|
2968
3250
|
: {
|
|
2969
3251
|
recoveryAttempt: current.recoveryErrorCount,
|
|
2970
|
-
|
|
3252
|
+
// 上限按当前恢复原因取,否则 stall 会显示 "2/5" 却在第 3 次就终止。
|
|
3253
|
+
recoveryMax:
|
|
3254
|
+
current.recoveryReason === "no_meaningful_model_progress"
|
|
3255
|
+
? CHAT_STALL_MAX_ATTEMPTS
|
|
3256
|
+
: CHAT_RECOVERY_MAX_ATTEMPTS,
|
|
2971
3257
|
...(current.recoveryReason
|
|
2972
3258
|
? { recoveryReason: current.recoveryReason }
|
|
2973
3259
|
: {}),
|
|
@@ -2986,19 +3272,25 @@ export abstract class AgentRuntimeKernel<
|
|
|
2986
3272
|
),
|
|
2987
3273
|
queued,
|
|
2988
3274
|
};
|
|
2989
|
-
|
|
3275
|
+
// activity 与这里其他字段一样,是当前 DB 的纯函数:不接受调用方的提示,
|
|
3276
|
+
// 也不粘住上一次的值。needs-input 只有一个意思 —— 此刻真挂着一件等人回应
|
|
3277
|
+
// 的事(审批,或结果由客户端结算的 Tool)。可重算因而能自愈:任何一次广播
|
|
3278
|
+
// 都会把陈旧状态冲掉,不需要谁记得来清。
|
|
3279
|
+
const activity: RuntimeActivity = approvals.length > 0 ||
|
|
3280
|
+
(current &&
|
|
3281
|
+
this.interactions.hasPendingForSubmission(current.submissionId))
|
|
2990
3282
|
? "needs-input"
|
|
2991
3283
|
: current || queued.length > 0
|
|
2992
3284
|
? "working"
|
|
2993
|
-
:
|
|
2994
|
-
|
|
2995
|
-
? "needs-input"
|
|
2996
|
-
: "idle");
|
|
3285
|
+
: "idle";
|
|
3286
|
+
const backgroundWork = this.db.agentTools.hasRunning();
|
|
2997
3287
|
const currentActivity = this.state.activity;
|
|
2998
|
-
const nextActivity = currentActivity?.activity === activity
|
|
3288
|
+
const nextActivity = currentActivity?.activity === activity &&
|
|
3289
|
+
currentActivity.backgroundWork === backgroundWork
|
|
2999
3290
|
? currentActivity
|
|
3000
3291
|
: {
|
|
3001
3292
|
activity,
|
|
3293
|
+
backgroundWork,
|
|
3002
3294
|
revision: (currentActivity?.revision ?? 0) + 1,
|
|
3003
3295
|
};
|
|
3004
3296
|
if (
|