@springbrand/agent-runtime 0.1.3-alpha.0 → 0.1.3-alpha.10
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 +12 -3
- package/src/adapter/cloudflare/index.ts +56 -0
- package/src/adapter/cloudflare/resources/runtime-resources.ts +89 -0
- package/src/adapter/cloudflare/sandbox/adapter.ts +1513 -0
- package/src/adapter/cloudflare/sandbox/id.ts +23 -0
- package/src/adapter/cloudflare/sandbox/policy.ts +15 -0
- package/src/adapter/cloudflare/subagent/definition.ts +574 -0
- package/src/adapter/cloudflare/subagent/runner.ts +175 -0
- package/src/adapter/cloudflare/subagent/tools.ts +254 -0
- package/src/adapter/cloudflare/universal-agent/hooks.ts +35 -0
- package/src/adapter/cloudflare/universal-agent/preparation.ts +277 -0
- package/src/adapter/cloudflare/universal-agent/tools.ts +80 -0
- package/src/adapter/cloudflare/workspace/git-fs.ts +178 -0
- package/src/adapter/cloudflare/workspace/publisher.ts +31 -0
- package/src/adapter/cloudflare/workspace/scoped-workspace.ts +376 -0
- package/src/adapter/cloudflare/workspace/version-control.ts +374 -0
- package/src/agent-tool-runtime.ts +152 -0
- 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 +25 -1
- package/src/db/submission.repo.ts +63 -1
- package/src/index.ts +61 -27
- package/src/kernel/approval-lifecycle.ts +41 -6
- package/src/kernel/bindings.ts +99 -12
- package/src/kernel/extensions.ts +1 -1
- package/src/kernel/interaction-lifecycle.ts +395 -0
- package/src/kernel/profile.ts +3 -4
- package/src/kernel/public-contracts.ts +2 -0
- package/src/kernel/recoverable-chat-agent.ts +104 -6
- 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 +191 -0
- package/src/kernel/state.ts +12 -1
- package/src/kernel/submission-lifecycle.ts +33 -2
- package/src/layers/orchestration/temporary-agent/core.ts +12 -1
- package/src/layers/orchestration/temporary-agent/runner.ts +1 -67
- package/src/lib/mcp.ts +7 -3
- package/src/lib/prompt.ts +1 -1
- package/src/lib/telemetry-dev.ts +7 -4
- package/src/pi/assembly/context.ts +4 -6
- package/src/pi/assembly/extensions.ts +11 -22
- package/src/pi/assembly/snapshot.ts +17 -9
- package/src/pi/message/contract.ts +7 -0
- package/src/pi/message/conversion.ts +9 -1
- package/src/pi/runtime-adapter/assembly.ts +42 -97
- package/src/pi/runtime-adapter/execution.ts +216 -21
- package/src/pi/runtime-adapter/index.ts +24 -8
- package/src/pi/runtime-adapter/models.ts +382 -35
- package/src/pi/runtime-adapter/recovery.ts +188 -1
- package/src/pi/runtime-adapter/transcript.ts +61 -3
- package/src/pi/tool/ai-adapter.ts +58 -1
- package/src/pi/tool/base.ts +190 -12
- package/src/pi/tool/compiler.ts +39 -33
- package/src/pi/tool/core-host.ts +28 -30
- package/src/pi/tool/core.ts +37 -124
- package/src/pi/tool/gateway.ts +54 -0
- package/src/pi/tool/index.ts +2 -0
- package/src/pi/tool/mcp.ts +98 -70
- package/src/pi/tool/schedule.ts +86 -20
- package/src/pi/tool/skill.ts +126 -420
- package/src/pi/tool/subagent.ts +14 -2
- package/src/pi/tool/web-fetch.ts +281 -0
- package/src/pi/tool/web-search/api.ts +34 -18
- package/src/pi/tool/web-search/web-search.ts +0 -1
- package/src/pi/tool/workspace-revision.ts +64 -0
- package/src/pi/tool/workspace-sandbox.ts +105 -263
- 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-context.ts +112 -0
- package/src/runtime-agent.ts +568 -321
- package/src/runtime-assembler.ts +797 -0
- package/src/runtime-definition.ts +175 -0
- package/src/runtime.ts +840 -208
- package/src/tool-registry.ts +143 -0
- package/src/workspace-versioning.ts +46 -0
- package/src/plugins.ts +0 -1033
package/src/runtime.ts
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AgentToolLifecycleResult,
|
|
3
|
+
AgentToolRunInfo,
|
|
4
|
+
Connection,
|
|
5
|
+
} from "agents";
|
|
2
6
|
import {
|
|
7
|
+
ChatStreamStalledError,
|
|
3
8
|
MessageType,
|
|
4
9
|
clearChatTerminal,
|
|
5
10
|
parseProtocolMessage,
|
|
6
11
|
recordChatTerminal,
|
|
7
12
|
sendIfOpen,
|
|
13
|
+
type ChatRecoveryConfig,
|
|
8
14
|
} from "agents/chat";
|
|
9
15
|
import { RecoverableChatAgent } from "./kernel/recoverable-chat-agent";
|
|
10
16
|
import {
|
|
11
17
|
ApprovalLifecycle,
|
|
12
18
|
type ApprovalContinuationData,
|
|
13
19
|
} from "./kernel/approval-lifecycle";
|
|
20
|
+
import { InteractionLifecycle } from "./kernel/interaction-lifecycle";
|
|
14
21
|
import {
|
|
15
22
|
SubmissionLifecycle,
|
|
16
23
|
SubmissionQueueFullError,
|
|
@@ -27,29 +34,41 @@ import {
|
|
|
27
34
|
type SubmissionReceipt,
|
|
28
35
|
} from "./kernel/receipts";
|
|
29
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";
|
|
30
40
|
import type {
|
|
31
41
|
RuntimeExtensionPermissions,
|
|
32
42
|
} from "./kernel/extensions";
|
|
33
43
|
import type {
|
|
34
44
|
RuntimeActivity,
|
|
35
|
-
RuntimeLoadPhase,
|
|
36
|
-
RuntimeLoadState,
|
|
37
45
|
RuntimeState,
|
|
38
46
|
RuntimeTurnState,
|
|
39
47
|
} from "./kernel/state";
|
|
40
48
|
import {
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
RuntimeLoadTracker,
|
|
50
|
+
withRuntimeLoadTimeout,
|
|
51
|
+
} from "./kernel/runtime-load";
|
|
52
|
+
import {
|
|
53
|
+
prepareRuntimeCandidate,
|
|
54
|
+
type RuntimeAssemblyInput,
|
|
55
|
+
type RuntimeCandidate,
|
|
43
56
|
type RuntimeSnapshot,
|
|
44
|
-
} from "./
|
|
57
|
+
} from "./runtime-assembler";
|
|
58
|
+
import type { RuntimeAgentHooks } from "./runtime-definition";
|
|
59
|
+
import type { RuntimeTurnEventsPort } from "./kernel/bindings";
|
|
45
60
|
import { connectConfiguredMcpServers } from "./lib/mcp";
|
|
46
61
|
import { installConsoleSink } from "./lib/telemetry-dev";
|
|
47
62
|
import {
|
|
48
63
|
PiRuntimeAdapter,
|
|
64
|
+
MODEL_STREAM_STALL_TIMEOUT_MS,
|
|
65
|
+
readModelStreamStallDetails,
|
|
66
|
+
RetryableModelError,
|
|
49
67
|
type PiCanonicalUserInput,
|
|
50
68
|
type PiCanonicalTranscriptSnapshot,
|
|
51
69
|
type PiChatRecoveryData,
|
|
52
70
|
type UIChatRequestBody,
|
|
71
|
+
type RequestedCapability,
|
|
53
72
|
type PiDurableMutation,
|
|
54
73
|
type PiRecoveryCommand,
|
|
55
74
|
type PiRecoveryDecision,
|
|
@@ -61,7 +80,11 @@ import {
|
|
|
61
80
|
type PreparedPiTurnAdapter,
|
|
62
81
|
} from "./pi/runtime-adapter";
|
|
63
82
|
import type { UIMessage, UIMessageChunk } from "ai";
|
|
64
|
-
import type {
|
|
83
|
+
import type {
|
|
84
|
+
AssistantMessage,
|
|
85
|
+
ToolResultMessage,
|
|
86
|
+
} from "@earendil-works/pi-ai";
|
|
87
|
+
import { serializeOutput } from "./lib/artifacts";
|
|
65
88
|
import type {
|
|
66
89
|
RuntimeModelUsageEvent,
|
|
67
90
|
RuntimeToolSettlementEvent,
|
|
@@ -91,6 +114,15 @@ import {
|
|
|
91
114
|
|
|
92
115
|
const SCHEDULED_STABLE_TIMEOUT_MS = 30_000;
|
|
93
116
|
const TURN_EVENT_RETRY_SECONDS = 10;
|
|
117
|
+
export const CHAT_RECOVERY_MAX_ATTEMPTS = 5;
|
|
118
|
+
// stall 单独收窄。理由不是「stall 更不值得救」,而是它的重试**期望值和别的错不一样**:
|
|
119
|
+
// 瞬时错(5xx / 断流)重跑一次往往就好了;stall 的重跑是拿同一份 transcript 让模型
|
|
120
|
+
// 重新想同样久,如果它本来就超预算,再跑几次也一样超。把看门狗放宽到 240s 之后,
|
|
121
|
+
// 真该救的那一类已经在第一次就跑完了,剩下还在 stall 的基本是连接真死——
|
|
122
|
+
// 那种情况下 5 次 × 240s ≈ 20 分钟的空转纯属折磨用户。3 次约 12 分钟封顶。
|
|
123
|
+
export const CHAT_STALL_MAX_ATTEMPTS = 3;
|
|
124
|
+
const CHAT_RECOVERY_TERMINAL_MESSAGE =
|
|
125
|
+
"多次恢复仍未成功,本次生成已停止,当前进度已保留。请发送新消息继续。";
|
|
94
126
|
|
|
95
127
|
type RuntimeEventOutboxPayload =
|
|
96
128
|
| { readonly type: "model-usage"; readonly event: RuntimeModelUsageEvent }
|
|
@@ -110,6 +142,8 @@ interface StoredSubmission extends SubmissionReceipt {
|
|
|
110
142
|
queuedUiMessageJson?: string | null;
|
|
111
143
|
userMessageId?: string | null;
|
|
112
144
|
regenerateMessageId?: string | null;
|
|
145
|
+
recoveryErrorCount: number;
|
|
146
|
+
recoveryReason?: "no_meaningful_model_progress" | "transient_model_error";
|
|
113
147
|
}
|
|
114
148
|
|
|
115
149
|
interface SubmitMessageOptions {
|
|
@@ -144,6 +178,16 @@ function json(value: unknown): string {
|
|
|
144
178
|
return JSON.stringify(value);
|
|
145
179
|
}
|
|
146
180
|
|
|
181
|
+
async function closeRuntimeGatewaySession(
|
|
182
|
+
session: RuntimeGatewaySession | undefined,
|
|
183
|
+
step: string,
|
|
184
|
+
): Promise<void> {
|
|
185
|
+
if (!session) return;
|
|
186
|
+
await withRuntimeLoadTimeout(step, () => session.close()).catch(
|
|
187
|
+
() => undefined,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
147
191
|
function assistantUsageEvent(
|
|
148
192
|
eventId: string,
|
|
149
193
|
submissionId: string,
|
|
@@ -165,6 +209,27 @@ function assistantUsageEvent(
|
|
|
165
209
|
// 作用:把用户消息内容变成可比较的稳定形状。
|
|
166
210
|
// 调用:重新生成回答时,提交入口用它核对客户端与已存用户消息。
|
|
167
211
|
// 原因:字符串和分段内容必须先归一化,否则语义相同的消息会被误判为不同。
|
|
212
|
+
// 把 park 时存下的 Tool 输入还原成对象,交给该 Tool 自己的 settle 映射。
|
|
213
|
+
// respondToolInteraction 调用;解析失败按 undefined 处理,因为映射函数是否用得到它由 Tool 决定。
|
|
214
|
+
function safeParseJson(text: string): unknown {
|
|
215
|
+
try {
|
|
216
|
+
return JSON.parse(text);
|
|
217
|
+
} catch {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Tool 没有提供 settle 时的缺省映射:响应原样成为 ToolResult 的 details。
|
|
223
|
+
// text 用 serializeOutput 而不是裸 JSON.stringify,与其他 Tool 结果的文本形态保持一致。
|
|
224
|
+
function defaultInteractionResult(
|
|
225
|
+
response: unknown,
|
|
226
|
+
): { content: ToolResultMessage["content"]; details: unknown } {
|
|
227
|
+
return {
|
|
228
|
+
content: [{ type: "text", text: serializeOutput(response).text }],
|
|
229
|
+
details: response,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
168
233
|
function userContentKey(message: PiCanonicalUserInput): string {
|
|
169
234
|
return json(
|
|
170
235
|
typeof message.content === "string"
|
|
@@ -173,20 +238,42 @@ function userContentKey(message: PiCanonicalUserInput): string {
|
|
|
173
238
|
);
|
|
174
239
|
}
|
|
175
240
|
|
|
176
|
-
function
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
241
|
+
function requestedCapabilitiesOf(metadata: unknown): RequestedCapability[] {
|
|
242
|
+
if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
const record = metadata as Record<string, unknown>;
|
|
246
|
+
if (!("requestedCapabilities" in record)) return [];
|
|
247
|
+
if (!Array.isArray(record.requestedCapabilities)) {
|
|
248
|
+
throw new Error("requestedCapabilities must be an array");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const result: RequestedCapability[] = [];
|
|
252
|
+
const seen = new Set<string>();
|
|
253
|
+
for (const value of record.requestedCapabilities) {
|
|
254
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
255
|
+
throw new Error("requestedCapabilities contains an invalid capability");
|
|
256
|
+
}
|
|
257
|
+
const capability = value as Record<string, unknown>;
|
|
258
|
+
if (
|
|
259
|
+
(capability.kind !== "skill" && capability.kind !== "plan") ||
|
|
260
|
+
typeof capability.name !== "string" ||
|
|
261
|
+
!capability.name.trim() ||
|
|
262
|
+
typeof capability.label !== "string" ||
|
|
263
|
+
!capability.label.trim()
|
|
264
|
+
) {
|
|
265
|
+
throw new Error("requestedCapabilities contains an invalid capability");
|
|
266
|
+
}
|
|
267
|
+
const key = `${capability.kind}:${capability.name}`;
|
|
268
|
+
if (seen.has(key)) continue;
|
|
269
|
+
seen.add(key);
|
|
270
|
+
result.push({
|
|
271
|
+
kind: capability.kind,
|
|
272
|
+
name: capability.name,
|
|
273
|
+
label: capability.label,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return result;
|
|
190
277
|
}
|
|
191
278
|
|
|
192
279
|
interface PendingTemporaryAgentApproval {
|
|
@@ -210,6 +297,11 @@ interface PendingTemporaryAgentApproval {
|
|
|
210
297
|
export abstract class AgentRuntimeKernel<
|
|
211
298
|
Env extends Cloudflare.Env = Cloudflare.Env,
|
|
212
299
|
> extends RecoverableChatAgent<Env, RuntimeState, PiChatRecoveryData> {
|
|
300
|
+
override chatRecovery: ChatRecoveryConfig = {
|
|
301
|
+
maxAttempts: CHAT_RECOVERY_MAX_ATTEMPTS,
|
|
302
|
+
terminalMessage: CHAT_RECOVERY_TERMINAL_MESSAGE,
|
|
303
|
+
};
|
|
304
|
+
|
|
213
305
|
initialState: RuntimeState = {
|
|
214
306
|
runtimeLoad: { status: "idle", available: false },
|
|
215
307
|
};
|
|
@@ -219,6 +311,9 @@ export abstract class AgentRuntimeKernel<
|
|
|
219
311
|
private runtimeSnapshot?: RuntimeSnapshot;
|
|
220
312
|
private runtimeRevision?: string;
|
|
221
313
|
private runtimePi?: PreparedPiRuntime;
|
|
314
|
+
private runtimeGatewaySession?: RuntimeGatewaySession;
|
|
315
|
+
private runtimeTurnEvents?: RuntimeTurnEventsPort;
|
|
316
|
+
private runtimeHooks?: RuntimeAgentHooks;
|
|
222
317
|
private piAdapter?: PiRuntimeAdapter;
|
|
223
318
|
private readonly transcript: PiRuntimeTranscript;
|
|
224
319
|
private readonly submissions: SubmissionLifecycle<
|
|
@@ -226,7 +321,10 @@ export abstract class AgentRuntimeKernel<
|
|
|
226
321
|
ActiveTurn
|
|
227
322
|
>;
|
|
228
323
|
private readonly approvals: ApprovalLifecycle<StoredSubmission>;
|
|
324
|
+
private readonly interactions: InteractionLifecycle<StoredSubmission>;
|
|
325
|
+
private runtimeLoadTracker?: RuntimeLoadTracker;
|
|
229
326
|
private readonly streamBySubmission = new Map<string, string>();
|
|
327
|
+
private migratedSubmissionIds?: Set<string>;
|
|
230
328
|
private db!: RuntimeDatabase;
|
|
231
329
|
private readonly temporaryAgents = new TemporaryAgentCoordinator();
|
|
232
330
|
private readonly temporaryAgentApprovals =
|
|
@@ -241,6 +339,24 @@ export abstract class AgentRuntimeKernel<
|
|
|
241
339
|
return this.piAdapter ??= new PiRuntimeAdapter();
|
|
242
340
|
}
|
|
243
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Runtime 装载进度的状态机。
|
|
344
|
+
*
|
|
345
|
+
* @remarks
|
|
346
|
+
* 本类和 `defineRuntimeAgent` 生成的宿主共同驱动它:宿主管一轮尝试的起止,
|
|
347
|
+
* `initConfig` 管中途的阶段推进,因此它是 protected 而非 private。
|
|
348
|
+
*
|
|
349
|
+
* 写成惰性访问器而不是字段:本类有测试用 `Object.create(prototype)` 造替身,
|
|
350
|
+
* 那条路径既不跑构造函数也不跑字段初始化器。
|
|
351
|
+
*/
|
|
352
|
+
protected get runtimeLoad(): RuntimeLoadTracker {
|
|
353
|
+
return this.runtimeLoadTracker ??= new RuntimeLoadTracker({
|
|
354
|
+
read: () => this.state.runtimeLoad,
|
|
355
|
+
publish: (runtimeLoad) => this.setState({ ...this.state, runtimeLoad }),
|
|
356
|
+
isAvailable: () => Boolean(this.runtimeSnapshot),
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
244
360
|
/**
|
|
245
361
|
* 为一个 Cloudflare Durable Object 实例建立持久化与执行协作器。
|
|
246
362
|
*
|
|
@@ -292,6 +408,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
292
408
|
commitTerminal: (submission, outcome, message) =>
|
|
293
409
|
this.commitTerminalOutcome(submission, outcome, message),
|
|
294
410
|
abortActive: (turn) => turn.agent.abort(),
|
|
411
|
+
interruptActive: (turn) => turn.agent.interrupt(),
|
|
295
412
|
});
|
|
296
413
|
this.transcript = this.pi.createTranscript({
|
|
297
414
|
sql: this.sql.bind(this),
|
|
@@ -329,6 +446,22 @@ export abstract class AgentRuntimeKernel<
|
|
|
329
446
|
},
|
|
330
447
|
onApprovalsChanged: () => this.broadcastApprovals(),
|
|
331
448
|
});
|
|
449
|
+
this.interactions = new InteractionLifecycle({
|
|
450
|
+
db: this.db,
|
|
451
|
+
pi: this.pi,
|
|
452
|
+
findSubmission: (submissionId) =>
|
|
453
|
+
this.readSubmission(submissionId),
|
|
454
|
+
applyRecoveryMutations: (submission, mutations) =>
|
|
455
|
+
this.applyPiRecoveryMutations(submission, mutations),
|
|
456
|
+
materializeRecoveredToolResults: (submission) =>
|
|
457
|
+
this.materializeRecoveredToolResults(submission),
|
|
458
|
+
// 与审批不同,这里不经 `schedule` 绕一圈:RPC 已经把 DO 叫醒了,
|
|
459
|
+
// 而 interaction 没有自己的续跑键,续跑就是普通的「已结算 Tool 恢复原 Turn」。
|
|
460
|
+
resumeSubmission: async (submissionId) => {
|
|
461
|
+
await this.submissions.recover(submissionId);
|
|
462
|
+
},
|
|
463
|
+
onInteractionsChanged: () => this.broadcastApprovals(),
|
|
464
|
+
});
|
|
332
465
|
}
|
|
333
466
|
|
|
334
467
|
// 作用:把通用聊天恢复协议接到本 Runtime 的 Submission 和 Pi 里程碑。
|
|
@@ -352,15 +485,25 @@ export abstract class AgentRuntimeKernel<
|
|
|
352
485
|
};
|
|
353
486
|
},
|
|
354
487
|
retryTurn: async (submissionId) => {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
488
|
+
try {
|
|
489
|
+
const receipt = await this.submissions.recover(submissionId);
|
|
490
|
+
return receipt.status === "completed"
|
|
491
|
+
? { status: "completed" as const }
|
|
492
|
+
: {
|
|
493
|
+
status: "failed" as const,
|
|
494
|
+
error:
|
|
495
|
+
receipt.error ??
|
|
496
|
+
`Pi recovery ended with status ${receipt.status}`,
|
|
497
|
+
};
|
|
498
|
+
} catch (error) {
|
|
499
|
+
if (
|
|
500
|
+
error instanceof ChatStreamStalledError ||
|
|
501
|
+
error instanceof RetryableModelError
|
|
502
|
+
) {
|
|
503
|
+
return { status: "scheduled" as const };
|
|
504
|
+
}
|
|
505
|
+
throw error;
|
|
506
|
+
}
|
|
364
507
|
},
|
|
365
508
|
failTurn: async (submissionId, message) => {
|
|
366
509
|
const submission = this.readSubmission(submissionId);
|
|
@@ -375,49 +518,121 @@ export abstract class AgentRuntimeKernel<
|
|
|
375
518
|
// 作用:准备并原子切换一份新的 Runtime 配置。
|
|
376
519
|
// 调用:生成 Agent 在首次启动或显式重载 Runtime key 时调用。
|
|
377
520
|
// 原因:先完整 prepare 再替换 Snapshot,且活跃 Turn 期间禁止换 revision,可避免半装配和恢复时能力漂移。
|
|
378
|
-
protected async initConfig(
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const candidateSnapshot = next;
|
|
521
|
+
protected async initConfig(input: RuntimeAssemblyInput): Promise<void> {
|
|
522
|
+
const candidate = await prepareRuntimeCandidate(input);
|
|
523
|
+
await this.initCandidate(candidate);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private turnEventsPort(): RuntimeTurnEventsPort | undefined {
|
|
527
|
+
return this.runtimeTurnEvents ?? this.runtimeSnapshot?.bindings.turnEvents;
|
|
528
|
+
}
|
|
387
529
|
|
|
388
|
-
|
|
530
|
+
protected async initCandidate(candidate: RuntimeCandidate): Promise<void> {
|
|
531
|
+
this.runtimeTurnEvents =
|
|
532
|
+
candidate.turnEvents ?? candidate.snapshot.bindings.turnEvents;
|
|
533
|
+
this.runtimeHooks = candidate.hooks;
|
|
534
|
+
const previous = this.runtimeSnapshot;
|
|
535
|
+
const candidateSnapshot = candidate.snapshot;
|
|
536
|
+
this.runtimeLoad.advance("assembly");
|
|
537
|
+
this.runtimeLoad.advance("mcp");
|
|
389
538
|
await connectConfiguredMcpServers(
|
|
390
539
|
this,
|
|
391
540
|
candidateSnapshot.profile.mcpServers,
|
|
392
541
|
);
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
542
|
+
const gatewayDegradations = [];
|
|
543
|
+
let gatewaySession: RuntimeGatewaySession | undefined;
|
|
544
|
+
if (candidateSnapshot.bindings.gateway) {
|
|
545
|
+
try {
|
|
546
|
+
gatewaySession = await withRuntimeLoadTimeout(
|
|
547
|
+
"gateway.open",
|
|
548
|
+
() => candidateSnapshot.bindings.gateway!.open(
|
|
549
|
+
`${this.ctx.id.toString()}:${crypto.randomUUID()}`,
|
|
550
|
+
),
|
|
551
|
+
{
|
|
552
|
+
onLateResult: (session) =>
|
|
553
|
+
closeRuntimeGatewaySession(session, "gateway.close.late"),
|
|
554
|
+
},
|
|
555
|
+
);
|
|
556
|
+
} catch {
|
|
557
|
+
gatewayDegradations.push({
|
|
558
|
+
capability: "connector" as const,
|
|
559
|
+
reason: "unavailable" as const,
|
|
560
|
+
detail: "Connector Gateway MCP",
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
this.runtimeLoad.advance("pi");
|
|
566
|
+
let prepared: PreparedPiRuntime;
|
|
567
|
+
try {
|
|
568
|
+
prepared = await this.pi.prepare({
|
|
569
|
+
snapshot: candidateSnapshot,
|
|
570
|
+
mcpHost: this,
|
|
571
|
+
gatewaySession,
|
|
572
|
+
additionalDegradations: gatewayDegradations,
|
|
573
|
+
createExtensionHostBinding: (permissions, ownContextLabels) =>
|
|
574
|
+
this.createExtensionHostBinding(
|
|
575
|
+
permissions,
|
|
576
|
+
ownContextLabels,
|
|
577
|
+
),
|
|
578
|
+
});
|
|
579
|
+
} catch (error) {
|
|
580
|
+
await closeRuntimeGatewaySession(gatewaySession, "gateway.close.failed");
|
|
581
|
+
throw error;
|
|
582
|
+
}
|
|
403
583
|
const revision = await this.hash(prepared.revisionDescriptor);
|
|
404
|
-
|
|
405
|
-
|
|
584
|
+
// 停在人机等待上的 Submission 是「未完成」,但没有任何模型请求在飞、
|
|
585
|
+
// 没有任何工具在执行 —— 换装配对它是安全的,而且这正是「决定时才装上
|
|
586
|
+
// 新能力,同一轮接着跑」所依赖的那一步。真正在执行的 Turn 仍然被挡住。
|
|
587
|
+
const contested = Boolean(previous) &&
|
|
406
588
|
revision !== this.runtimeRevision &&
|
|
407
|
-
this.submissions.isBusy()
|
|
408
|
-
)
|
|
589
|
+
this.submissions.isBusy();
|
|
590
|
+
const parked = contested && this.db.everyUnfinishedSubmissionParked();
|
|
591
|
+
if (contested && !parked) {
|
|
592
|
+
await closeRuntimeGatewaySession(gatewaySession, "gateway.close.contested");
|
|
409
593
|
throw new Error(
|
|
410
594
|
"Cannot reload Runtime while a revision-pinned Pi Turn is active",
|
|
411
595
|
);
|
|
412
596
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
597
|
+
const repin = parked
|
|
598
|
+
? await withRuntimeLoadTimeout(
|
|
599
|
+
"parked-submission.repin",
|
|
600
|
+
() => this.prepareParkedSubmissionRepin(prepared, revision),
|
|
601
|
+
)
|
|
602
|
+
: undefined;
|
|
603
|
+
let activated = false;
|
|
604
|
+
const previousGatewaySession = this.runtimeGatewaySession;
|
|
605
|
+
try {
|
|
606
|
+
for (const [index, guard] of candidate.commitGuards.entries()) {
|
|
607
|
+
await withRuntimeLoadTimeout(`commit-guard:${index}`, guard);
|
|
608
|
+
}
|
|
609
|
+
if (candidateSnapshot.bindings.platform.telemetryConsole) {
|
|
610
|
+
installConsoleSink();
|
|
611
|
+
}
|
|
612
|
+
this.pi.activate(candidateSnapshot);
|
|
613
|
+
activated = true;
|
|
614
|
+
repin?.commit();
|
|
615
|
+
this.runtimeSnapshot = candidateSnapshot;
|
|
616
|
+
this.runtimeRevision = revision;
|
|
617
|
+
this.runtimePi = prepared;
|
|
618
|
+
this.runtimeGatewaySession = gatewaySession;
|
|
619
|
+
} catch (error) {
|
|
620
|
+
repin?.abort();
|
|
621
|
+
if (activated && previous) this.pi.activate(previous);
|
|
622
|
+
await closeRuntimeGatewaySession(gatewaySession, "gateway.close.aborted");
|
|
623
|
+
throw error;
|
|
420
624
|
}
|
|
625
|
+
if (repin) {
|
|
626
|
+
await withRuntimeLoadTimeout(
|
|
627
|
+
"parked-submission.interrupt",
|
|
628
|
+
() => repin.interrupt(),
|
|
629
|
+
).catch(() => undefined);
|
|
630
|
+
}
|
|
631
|
+
await closeRuntimeGatewaySession(
|
|
632
|
+
previousGatewaySession,
|
|
633
|
+
"gateway.close.previous",
|
|
634
|
+
);
|
|
635
|
+
|
|
421
636
|
const degradations = prepared.degradations;
|
|
422
637
|
if (degradations.length > 0) {
|
|
423
638
|
console.warn(
|
|
@@ -450,7 +665,6 @@ export abstract class AgentRuntimeKernel<
|
|
|
450
665
|
const startedAt = output.startedAt ?? Date.now();
|
|
451
666
|
return this.pi.createTurn({
|
|
452
667
|
prepared: this.preparedPi(),
|
|
453
|
-
baseRevision: this.baseRevision(),
|
|
454
668
|
pinnedDescriptor: submission.assemblyDescriptor,
|
|
455
669
|
submission: {
|
|
456
670
|
id: submission.submissionId,
|
|
@@ -472,7 +686,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
472
686
|
async (created) => {
|
|
473
687
|
await onCreated?.();
|
|
474
688
|
try {
|
|
475
|
-
await this.
|
|
689
|
+
await this.turnEventsPort()?.onApproval?.({
|
|
476
690
|
submissionId: submission.submissionId,
|
|
477
691
|
approvalExecutionId: created.executionId,
|
|
478
692
|
});
|
|
@@ -484,6 +698,10 @@ export abstract class AgentRuntimeKernel<
|
|
|
484
698
|
}
|
|
485
699
|
},
|
|
486
700
|
),
|
|
701
|
+
// spec 在这里用不上:响应侧一律从重新装配出的 candidate 按 toolName 取回同一份,
|
|
702
|
+
// 好让「DO 一直醒着」和「park 期间睡过一觉」走完全相同的一条代码路径。
|
|
703
|
+
requestToolInteraction: (interaction, _spec, signal) =>
|
|
704
|
+
this.interactions.request(submission, interaction, signal),
|
|
487
705
|
appendToolInput: (input) =>
|
|
488
706
|
this.appendToolInput(submission, input),
|
|
489
707
|
settleTool: (call) =>
|
|
@@ -498,6 +716,10 @@ export abstract class AgentRuntimeKernel<
|
|
|
498
716
|
this.readSubmission(submission.submissionId)?.abortReason,
|
|
499
717
|
onRecord: output.onRecord ?? (() => undefined),
|
|
500
718
|
onCanonicalMessage: async (commit) => {
|
|
719
|
+
// 换装配把这条执行器中断掉了。它退场路上还会吐出「工具被中止」这样的
|
|
720
|
+
// 消息 —— 那不是这一轮的事实,写进权威 transcript 会让重建出来的续跑
|
|
721
|
+
// 看到一份自己从没产生过的工具结果。
|
|
722
|
+
if (this.migratedSubmissions.has(submission.submissionId)) return;
|
|
501
723
|
let consumedSteer = false;
|
|
502
724
|
this.db.transaction(() => {
|
|
503
725
|
if (commit.kind !== "append-user") {
|
|
@@ -565,7 +787,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
565
787
|
* 不会延长 Durable Object 生命;本文件仍在三处用它启动后台任务。
|
|
566
788
|
*/
|
|
567
789
|
async onStart(): Promise<void> {
|
|
568
|
-
this.
|
|
790
|
+
this.runtimeLoad.reset();
|
|
569
791
|
if (this.db.runtimeEvents.hasPending()) {
|
|
570
792
|
this.ctx.waitUntil(
|
|
571
793
|
this.ensureRuntimeReady().then(() => this.drainRuntimeEvents()),
|
|
@@ -590,7 +812,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
590
812
|
}
|
|
591
813
|
|
|
592
814
|
private async drainRuntimeEvents(idempotentRetry = true): Promise<void> {
|
|
593
|
-
const turnEvents = this.
|
|
815
|
+
const turnEvents = this.turnEventsPort();
|
|
594
816
|
if (!turnEvents) return;
|
|
595
817
|
|
|
596
818
|
let failed = false;
|
|
@@ -719,78 +941,56 @@ export abstract class AgentRuntimeKernel<
|
|
|
719
941
|
private assembly(): RuntimeSnapshot {
|
|
720
942
|
if (!this.runtimeSnapshot) {
|
|
721
943
|
throw new Error(
|
|
722
|
-
"
|
|
944
|
+
"Runtime must be assembled before use",
|
|
723
945
|
);
|
|
724
946
|
}
|
|
725
947
|
return this.runtimeSnapshot;
|
|
726
948
|
}
|
|
727
949
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// 作用:开始一轮新的 Runtime 装载尝试。
|
|
736
|
-
// 调用:生成 Agent 在调用应用 createConfig 前调用。
|
|
737
|
-
// 原因:config 阶段可能包含身份、D1 和 Resource 解析,必须在首个慢请求前可见。
|
|
738
|
-
protected beginRuntimeLoad(): void {
|
|
739
|
-
const now = Date.now();
|
|
740
|
-
this.publishRuntimeLoad({
|
|
741
|
-
status: "loading",
|
|
742
|
-
phase: "config",
|
|
743
|
-
available: Boolean(this.runtimeSnapshot),
|
|
744
|
-
startedAt: now,
|
|
745
|
-
updatedAt: now,
|
|
746
|
-
});
|
|
747
|
-
}
|
|
950
|
+
private async normalizeUIUserInput(
|
|
951
|
+
message: UIMessage & { role: "user" },
|
|
952
|
+
): Promise<PiCanonicalUserInput> {
|
|
953
|
+
const capabilities = requestedCapabilitiesOf(message.metadata);
|
|
954
|
+
if (capabilities.length === 0) return this.pi.normalizeUserInput(message);
|
|
748
955
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
const
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
completedAt: now,
|
|
777
|
-
});
|
|
956
|
+
await this.ensureRuntimeReady();
|
|
957
|
+
const installedSkills = new Set(
|
|
958
|
+
this.assembly().bindings.skills.sources.map(({ name }) => name),
|
|
959
|
+
);
|
|
960
|
+
const context: string[] = [];
|
|
961
|
+
for (const capability of capabilities) {
|
|
962
|
+
if (capability.kind === "skill") {
|
|
963
|
+
if (!installedSkills.has(capability.name)) {
|
|
964
|
+
throw new Error(
|
|
965
|
+
`Requested Skill is not installed: ${capability.name}`,
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
context.push(
|
|
969
|
+
`requested capability: skill/${capability.name}`,
|
|
970
|
+
`required action: call activate_skill for "${capability.name}" before handling the task`,
|
|
971
|
+
);
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
if (capability.name !== "plan") {
|
|
975
|
+
throw new Error(`Unknown plan capability: ${capability.name}`);
|
|
976
|
+
}
|
|
977
|
+
context.push(
|
|
978
|
+
"requested capability: plan/plan",
|
|
979
|
+
"required action: call update_plan with the complete plan before other work, then wait for user confirmation before execution",
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
return this.pi.normalizeUserInput(message, context.join("\n"));
|
|
778
983
|
}
|
|
779
984
|
|
|
780
|
-
//
|
|
781
|
-
//
|
|
782
|
-
//
|
|
783
|
-
protected
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
available: Boolean(this.runtimeSnapshot),
|
|
790
|
-
startedAt:
|
|
791
|
-
current?.status === "loading" ? current.startedAt : now,
|
|
792
|
-
failedAt: now,
|
|
793
|
-
});
|
|
985
|
+
// 作用:把当前已安装的 Runtime Snapshot 投影成可序列化的调试视图。
|
|
986
|
+
// 调用:Session facet 的 `getRuntimeAssembly` RPC。
|
|
987
|
+
// 原因:UI 需要读取真实装配结果,而不是上层配置声明。
|
|
988
|
+
protected readRuntimeAssembly(): RuntimeAssemblyView {
|
|
989
|
+
return projectRuntimeAssembly(
|
|
990
|
+
this.assembly(),
|
|
991
|
+
this.runtimeRevision ?? null,
|
|
992
|
+
this.preparedPi(),
|
|
993
|
+
);
|
|
794
994
|
}
|
|
795
995
|
|
|
796
996
|
// 作用:为 Runtime Extension 创建只包含授权能力的 Worker 回环绑定。
|
|
@@ -833,19 +1033,23 @@ export abstract class AgentRuntimeKernel<
|
|
|
833
1033
|
.join("");
|
|
834
1034
|
}
|
|
835
1035
|
|
|
836
|
-
// 作用:返回当前已激活配置的 revision。
|
|
837
|
-
// 调用:创建 Pi Turn 适配器时调用。
|
|
838
|
-
// 原因:把初始化不变式收口在一处,避免执行层带着空 revision 继续。
|
|
839
|
-
private baseRevision(): string {
|
|
840
|
-
if (!this.runtimeRevision) {
|
|
841
|
-
throw new Error("Runtime revision is not initialized");
|
|
842
|
-
}
|
|
843
|
-
return this.runtimeRevision;
|
|
844
|
-
}
|
|
845
|
-
|
|
846
1036
|
// 作用:返回当前已准备好的 Pi Runtime。
|
|
847
1037
|
// 调用:创建 Turn 适配器或准入固定装配时调用。
|
|
848
1038
|
// 原因:明确区分“已有 Snapshot”和“Pi 已完成准备”,防止半初始化状态进入 Turn。
|
|
1039
|
+
/**
|
|
1040
|
+
* 正在被换装配中断、等待持久续跑接手的 Submission。
|
|
1041
|
+
*
|
|
1042
|
+
* 中断和取消在执行路径上长得一模一样(都是 abort),但结局相反:取消要写终态,
|
|
1043
|
+
* 换装配中断必须让 Submission 停在 running,等决策把它交给续跑。这个集合是
|
|
1044
|
+
* 两者唯一的区分依据,只在一次 `repinParkedSubmissions` 的窗口内有成员。
|
|
1045
|
+
*
|
|
1046
|
+
* 写成惰性访问器而不是字段:本类有测试用 `Object.create(prototype)` 造替身,
|
|
1047
|
+
* 那条路径不跑字段初始化器。
|
|
1048
|
+
*/
|
|
1049
|
+
private get migratedSubmissions(): Set<string> {
|
|
1050
|
+
return this.migratedSubmissionIds ??= new Set<string>();
|
|
1051
|
+
}
|
|
1052
|
+
|
|
849
1053
|
private preparedPi(): PreparedPiRuntime {
|
|
850
1054
|
if (!this.runtimePi) {
|
|
851
1055
|
throw new Error(
|
|
@@ -867,13 +1071,31 @@ export abstract class AgentRuntimeKernel<
|
|
|
867
1071
|
descriptor: string;
|
|
868
1072
|
}> {
|
|
869
1073
|
await this.ensureRuntimeReady();
|
|
1074
|
+
return this.currentAssemblyPin();
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// 作用:按当前已装配的 Runtime 生成一份不可变装配描述。
|
|
1078
|
+
// 调用:准入 pin 走 `admissionPin`;`initConfig` 内部的重新 pin 直接调用本方法。
|
|
1079
|
+
// 原因:`initConfig` 已经在装配串行入口里面,再走 `ensureRuntimeReady` 会等自己,
|
|
1080
|
+
// 所以「确保装配就绪」和「按当前装配取 pin」必须是两步。
|
|
1081
|
+
private async currentAssemblyPin(): Promise<{
|
|
1082
|
+
revision: string;
|
|
1083
|
+
descriptor: string;
|
|
1084
|
+
}> {
|
|
870
1085
|
await this.drainRuntimeEvents();
|
|
871
1086
|
if (!this.runtimeRevision) {
|
|
872
1087
|
throw new Error("Runtime revision is not initialized");
|
|
873
1088
|
}
|
|
1089
|
+
return this.pinAssembly(this.preparedPi(), this.runtimeRevision);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
private async pinAssembly(
|
|
1093
|
+
prepared: PreparedPiRuntime,
|
|
1094
|
+
baseRevision: string,
|
|
1095
|
+
): Promise<{ revision: string; descriptor: string }> {
|
|
874
1096
|
const pinned = await this.pi.pin({
|
|
875
|
-
prepared
|
|
876
|
-
baseRevision
|
|
1097
|
+
prepared,
|
|
1098
|
+
baseRevision,
|
|
877
1099
|
readExtensionContext: ({ label }) =>
|
|
878
1100
|
this._hostGetContext(label),
|
|
879
1101
|
});
|
|
@@ -911,6 +1133,89 @@ export abstract class AgentRuntimeKernel<
|
|
|
911
1133
|
return pinned;
|
|
912
1134
|
}
|
|
913
1135
|
|
|
1136
|
+
/** 准备 parked Submission 的续跑切换,持久 pin 只在最终 commit 中修改。 */
|
|
1137
|
+
private async prepareParkedSubmissionRepin(
|
|
1138
|
+
prepared: PreparedPiRuntime,
|
|
1139
|
+
baseRevision: string,
|
|
1140
|
+
): Promise<{ commit(): void; interrupt(): Promise<void>; abort(): void }> {
|
|
1141
|
+
await this.drainRuntimeEvents();
|
|
1142
|
+
const { revision, descriptor } = await this.pinAssembly(
|
|
1143
|
+
prepared,
|
|
1144
|
+
baseRevision,
|
|
1145
|
+
);
|
|
1146
|
+
const plans: Array<{
|
|
1147
|
+
submission: StoredSubmission;
|
|
1148
|
+
decision: PiRecoveryDecision;
|
|
1149
|
+
}> = [];
|
|
1150
|
+
for (const submissionId of this.db.submissions.listUnfinishedIds()) {
|
|
1151
|
+
const submission = this.readSubmission(submissionId);
|
|
1152
|
+
if (
|
|
1153
|
+
!submission?.assemblyRevision ||
|
|
1154
|
+
!submission.assemblyDescriptor ||
|
|
1155
|
+
submission.assemblyRevision === revision
|
|
1156
|
+
) {
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
1159
|
+
const decision = this.decidePiRecovery(submission, {
|
|
1160
|
+
kind: "repin-assembly",
|
|
1161
|
+
nextAssemblyRevision: revision,
|
|
1162
|
+
});
|
|
1163
|
+
plans.push({ submission, decision });
|
|
1164
|
+
}
|
|
1165
|
+
let settled = false;
|
|
1166
|
+
let committed = false;
|
|
1167
|
+
const cleanup = () => {
|
|
1168
|
+
for (const { submission } of plans) {
|
|
1169
|
+
this.migratedSubmissions.delete(submission.submissionId);
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
return {
|
|
1173
|
+
commit: () => {
|
|
1174
|
+
if (settled) throw new Error("Parked Submission repin already settled");
|
|
1175
|
+
for (const { submission } of plans) {
|
|
1176
|
+
this.migratedSubmissions.add(submission.submissionId);
|
|
1177
|
+
}
|
|
1178
|
+
try {
|
|
1179
|
+
this.db.transaction(() => {
|
|
1180
|
+
for (const { submission, decision } of plans) {
|
|
1181
|
+
this.applyPiRecoveryMutations(submission, decision.mutations);
|
|
1182
|
+
this.db.submissions.repinAssembly(
|
|
1183
|
+
submission.submissionId,
|
|
1184
|
+
revision,
|
|
1185
|
+
descriptor,
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
committed = true;
|
|
1190
|
+
} catch (error) {
|
|
1191
|
+
settled = true;
|
|
1192
|
+
cleanup();
|
|
1193
|
+
throw error;
|
|
1194
|
+
}
|
|
1195
|
+
},
|
|
1196
|
+
interrupt: async () => {
|
|
1197
|
+
if (!committed || settled) return;
|
|
1198
|
+
try {
|
|
1199
|
+
for (const { submission } of plans) {
|
|
1200
|
+
const submissionId = submission.submissionId;
|
|
1201
|
+
await this.submissions.interrupt(
|
|
1202
|
+
submissionId,
|
|
1203
|
+
() => this.approvals.discardWaiters(submissionId),
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
} finally {
|
|
1207
|
+
settled = true;
|
|
1208
|
+
cleanup();
|
|
1209
|
+
}
|
|
1210
|
+
},
|
|
1211
|
+
abort: () => {
|
|
1212
|
+
if (settled) return;
|
|
1213
|
+
settled = true;
|
|
1214
|
+
cleanup();
|
|
1215
|
+
},
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
|
|
914
1219
|
// 作用:检查 Submission 保存的装配描述与 revision 仍然匹配。
|
|
915
1220
|
// 调用:普通恢复和审批续跑在重建 Pi Turn 前调用。
|
|
916
1221
|
// 原因:若持久描述已变,继续执行会把同一 Turn 切成两套能力语义。
|
|
@@ -922,23 +1227,24 @@ export abstract class AgentRuntimeKernel<
|
|
|
922
1227
|
await this.hash(submission.assemblyDescriptor)
|
|
923
1228
|
) {
|
|
924
1229
|
throw new Error(
|
|
925
|
-
"Pinned Runtime
|
|
1230
|
+
"Pinned Runtime assembly descriptor is invalid",
|
|
926
1231
|
);
|
|
927
1232
|
}
|
|
928
1233
|
}
|
|
929
1234
|
|
|
930
|
-
//
|
|
1235
|
+
// 作用:按当前模型输入预算压缩 Pi 上下文。
|
|
931
1236
|
// 调用:Pi Turn 适配器在发模型请求前通过 `transformContext` 回调。
|
|
932
|
-
//
|
|
1237
|
+
// 原因:模型窗口必须预留最大输出,压缩也必须使用本 Snapshot 的模型和密钥。
|
|
933
1238
|
private async transformPiContext(
|
|
934
1239
|
submissionId: string,
|
|
935
1240
|
messages: Parameters<PiRuntimeTranscript["compactContext"]>[0],
|
|
936
1241
|
signal?: AbortSignal,
|
|
937
1242
|
): ReturnType<PiRuntimeTranscript["compactContext"]> {
|
|
938
1243
|
const snapshot = this.assembly();
|
|
1244
|
+
const inputBudget = snapshot.pi.model.contextWindow -
|
|
1245
|
+
snapshot.pi.model.maxTokens;
|
|
939
1246
|
const compacted = await this.transcript.compactContext(messages, {
|
|
940
|
-
compactAfterTokens:
|
|
941
|
-
snapshot.profile.memory.compactAfterTokens,
|
|
1247
|
+
compactAfterTokens: inputBudget * 0.85,
|
|
942
1248
|
model: snapshot.pi.model,
|
|
943
1249
|
apiKey: this.pi.resolveApiKey(
|
|
944
1250
|
snapshot.bindings.provider,
|
|
@@ -1150,6 +1456,28 @@ export abstract class AgentRuntimeKernel<
|
|
|
1150
1456
|
);
|
|
1151
1457
|
continue;
|
|
1152
1458
|
}
|
|
1459
|
+
if (mutation.kind === "record-interaction") {
|
|
1460
|
+
this.db.interactions.insert({
|
|
1461
|
+
interactionId: mutation.interaction.interactionId,
|
|
1462
|
+
submissionId: submission.submissionId,
|
|
1463
|
+
requestId: mutation.interaction.requestId,
|
|
1464
|
+
toolCallId: mutation.interaction.toolCallId,
|
|
1465
|
+
toolName: mutation.interaction.toolName,
|
|
1466
|
+
inputJson: mutation.interaction.inputJson,
|
|
1467
|
+
status: "pending",
|
|
1468
|
+
createdAt: mutation.interaction.createdAt,
|
|
1469
|
+
});
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
if (mutation.kind === "settle-interaction") {
|
|
1473
|
+
this.db.interactions.settle(
|
|
1474
|
+
mutation.interactionId,
|
|
1475
|
+
mutation.status,
|
|
1476
|
+
mutation.settledAt,
|
|
1477
|
+
mutation.responseJson,
|
|
1478
|
+
);
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1153
1481
|
const inserted = this.db.milestones.upsert(
|
|
1154
1482
|
submission.submissionId,
|
|
1155
1483
|
mutation.key,
|
|
@@ -1177,12 +1505,41 @@ export abstract class AgentRuntimeKernel<
|
|
|
1177
1505
|
kind: "record-tool-input",
|
|
1178
1506
|
record: input,
|
|
1179
1507
|
});
|
|
1180
|
-
|
|
1508
|
+
const appended = this.db.transaction(() =>
|
|
1181
1509
|
this.applyPiRecoveryMutations(
|
|
1182
1510
|
submission,
|
|
1183
1511
|
decision.mutations,
|
|
1184
1512
|
),
|
|
1185
1513
|
);
|
|
1514
|
+
if (appended) this.announceToolStart(submission.submissionId, input);
|
|
1515
|
+
return appended;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// 作用:把「这个 Tool 开始跑了」连同参数投给 Host。
|
|
1519
|
+
// 调用:durable intent 第一次落库之后,Tool 真正执行之前。
|
|
1520
|
+
// 原因:人在等的是「正在写一个网页」,不是三十秒后的「write 完成了」。
|
|
1521
|
+
//
|
|
1522
|
+
// 只在第一次落库时发:恢复重放会再走一遍这条路径,那时 Tool 可能早就结束了,
|
|
1523
|
+
// 再宣布一次「开始」比不宣布更糟。也不进 durable outbox —— 这是活性信号,
|
|
1524
|
+
// 补投一条迟到的开始事件没有意义,投失败就算了,终态有 onToolSettled 兜底。
|
|
1525
|
+
private announceToolStart(
|
|
1526
|
+
submissionId: string,
|
|
1527
|
+
input: PiToolInputRecord,
|
|
1528
|
+
): void {
|
|
1529
|
+
const turnEvents = this.turnEventsPort();
|
|
1530
|
+
if (!turnEvents?.onToolStart) return;
|
|
1531
|
+
const delivery = turnEvents.onToolStart({
|
|
1532
|
+
submissionId,
|
|
1533
|
+
toolCallId: input.toolCallId,
|
|
1534
|
+
toolName: input.toolName,
|
|
1535
|
+
args: input.input,
|
|
1536
|
+
}).catch((error: unknown) => {
|
|
1537
|
+
console.warn(
|
|
1538
|
+
"[runtime-tool-start:degraded]",
|
|
1539
|
+
json({ toolCallId: input.toolCallId, error: errorText(error) }),
|
|
1540
|
+
);
|
|
1541
|
+
});
|
|
1542
|
+
this.ctx.waitUntil(delivery);
|
|
1186
1543
|
}
|
|
1187
1544
|
|
|
1188
1545
|
// 作用:持久化一次 Tool 结果,并推进相应的恢复里程碑。
|
|
@@ -1372,7 +1729,11 @@ export abstract class AgentRuntimeKernel<
|
|
|
1372
1729
|
latest,
|
|
1373
1730
|
effectiveMessage ?? "Turn ended before approval",
|
|
1374
1731
|
);
|
|
1375
|
-
|
|
1732
|
+
// 还挂着的 interaction 同样要收尾,否则 Turn 结束后 pending 行会变成孤儿,
|
|
1733
|
+
// 用户还能看见一张点了没反应的卡。
|
|
1734
|
+
const closedInteractions = this.interactions
|
|
1735
|
+
.cancelPendingForSubmissionSync(latest);
|
|
1736
|
+
if (closedApprovals || closedInteractions) {
|
|
1376
1737
|
this.materializeRecoveredToolResultsSync(latest);
|
|
1377
1738
|
}
|
|
1378
1739
|
const status =
|
|
@@ -1435,20 +1796,20 @@ export abstract class AgentRuntimeKernel<
|
|
|
1435
1796
|
return terminal;
|
|
1436
1797
|
});
|
|
1437
1798
|
await this.drainRuntimeEvents();
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1799
|
+
try {
|
|
1800
|
+
await this.settleChatRecovery(
|
|
1801
|
+
latest.requestId,
|
|
1802
|
+
effectiveOutcome === "succeeded"
|
|
1803
|
+
? "completed"
|
|
1804
|
+
: effectiveOutcome === "aborted"
|
|
1805
|
+
? "skipped"
|
|
1806
|
+
: "failed",
|
|
1807
|
+
effectiveMessage,
|
|
1808
|
+
);
|
|
1809
|
+
} catch (error) {
|
|
1810
|
+
console.error("[chat-recovery] terminal settlement failed", error);
|
|
1450
1811
|
}
|
|
1451
|
-
await this.broadcastApprovals(
|
|
1812
|
+
await this.broadcastApprovals();
|
|
1452
1813
|
try {
|
|
1453
1814
|
if (
|
|
1454
1815
|
effectiveOutcome === "failed" ||
|
|
@@ -1478,6 +1839,16 @@ export abstract class AgentRuntimeKernel<
|
|
|
1478
1839
|
): Promise<boolean> {
|
|
1479
1840
|
let decision = await this.materializeRecoveredToolResults(submission);
|
|
1480
1841
|
for (let step = 0; step < 32; step += 1) {
|
|
1842
|
+
const latest = this.readSubmission(submission.submissionId);
|
|
1843
|
+
if (!latest || isTerminalSubmissionStatus(latest.status)) return false;
|
|
1844
|
+
if (latest.abortReason) {
|
|
1845
|
+
await this.submissions.finish(
|
|
1846
|
+
latest,
|
|
1847
|
+
"aborted",
|
|
1848
|
+
latest.abortReason,
|
|
1849
|
+
);
|
|
1850
|
+
return false;
|
|
1851
|
+
}
|
|
1481
1852
|
this.db.transaction(() => {
|
|
1482
1853
|
this.applyPiRecoveryMutations(
|
|
1483
1854
|
submission,
|
|
@@ -1655,13 +2026,24 @@ export abstract class AgentRuntimeKernel<
|
|
|
1655
2026
|
submission.abortReason,
|
|
1656
2027
|
);
|
|
1657
2028
|
}
|
|
1658
|
-
if (recovery)
|
|
2029
|
+
if (recovery) {
|
|
2030
|
+
this.db.submissions.clearRecoveryReason(submissionId);
|
|
2031
|
+
await this.broadcastApprovals();
|
|
2032
|
+
await this.ensureRuntimeReady();
|
|
2033
|
+
}
|
|
1659
2034
|
try {
|
|
1660
2035
|
return await this.executeNonTerminalSubmission(
|
|
1661
2036
|
submission,
|
|
1662
2037
|
recovery,
|
|
1663
2038
|
);
|
|
1664
2039
|
} catch (error) {
|
|
2040
|
+
if (
|
|
2041
|
+
recovery &&
|
|
2042
|
+
(error instanceof ChatStreamStalledError ||
|
|
2043
|
+
error instanceof RetryableModelError)
|
|
2044
|
+
) {
|
|
2045
|
+
throw error;
|
|
2046
|
+
}
|
|
1665
2047
|
const failed = await this.submissions.finish(
|
|
1666
2048
|
submission,
|
|
1667
2049
|
"failed",
|
|
@@ -1677,6 +2059,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
1677
2059
|
failed.status === "error"
|
|
1678
2060
|
? failed.error ?? "Pi turn failed"
|
|
1679
2061
|
: undefined,
|
|
2062
|
+
recovery,
|
|
1680
2063
|
);
|
|
1681
2064
|
return failed;
|
|
1682
2065
|
} finally {
|
|
@@ -1711,11 +2094,33 @@ export abstract class AgentRuntimeKernel<
|
|
|
1711
2094
|
}
|
|
1712
2095
|
const recoveryAdapter = this.createSubmissionExecutionAdapter(submission);
|
|
1713
2096
|
if (recovery) {
|
|
1714
|
-
const
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
2097
|
+
const deactivatePreparation = this.submissions.activate(submission, {
|
|
2098
|
+
submissionId,
|
|
2099
|
+
requestId: submission.requestId,
|
|
2100
|
+
messageId: submission.assistantMessageId,
|
|
2101
|
+
startedAt: submission.createdAt,
|
|
2102
|
+
continuation: true,
|
|
2103
|
+
agent: recoveryAdapter,
|
|
2104
|
+
});
|
|
2105
|
+
let ready: boolean;
|
|
2106
|
+
try {
|
|
2107
|
+
ready = await this.prepareRecoveredTurn(
|
|
2108
|
+
submission,
|
|
2109
|
+
recoveryAdapter,
|
|
2110
|
+
);
|
|
2111
|
+
} finally {
|
|
2112
|
+
deactivatePreparation();
|
|
2113
|
+
}
|
|
2114
|
+
if (!ready) {
|
|
2115
|
+
const latest = this.readSubmission(submissionId)!;
|
|
2116
|
+
return latest.abortReason
|
|
2117
|
+
? this.submissions.finish(
|
|
2118
|
+
latest,
|
|
2119
|
+
"aborted",
|
|
2120
|
+
latest.abortReason,
|
|
2121
|
+
)
|
|
2122
|
+
: latest;
|
|
2123
|
+
}
|
|
1719
2124
|
} else {
|
|
1720
2125
|
await this.materializeRecoveredToolResults(submission);
|
|
1721
2126
|
}
|
|
@@ -1754,7 +2159,9 @@ export abstract class AgentRuntimeKernel<
|
|
|
1754
2159
|
continuation: recovery,
|
|
1755
2160
|
assistantOrdinal,
|
|
1756
2161
|
onRecord: (record) =>
|
|
1757
|
-
this.
|
|
2162
|
+
this.migratedSubmissions.has(submissionId)
|
|
2163
|
+
? undefined
|
|
2164
|
+
: this.persistAndBroadcastRecord(turn, record),
|
|
1758
2165
|
onTerminal: (intent) => {
|
|
1759
2166
|
terminalIntent = intent;
|
|
1760
2167
|
this.appendTerminalIntent(
|
|
@@ -1807,6 +2214,12 @@ export abstract class AgentRuntimeKernel<
|
|
|
1807
2214
|
}
|
|
1808
2215
|
},
|
|
1809
2216
|
);
|
|
2217
|
+
// 换装配把这条执行器中断掉了:Submission 没有结束,它在等持久续跑用新
|
|
2218
|
+
// 装配重建一条接着跑。这里写终态会把那一轮当场杀掉。
|
|
2219
|
+
if (this.migratedSubmissions.has(submissionId)) {
|
|
2220
|
+
if (streamId) this.failRecoverableStream(streamId);
|
|
2221
|
+
return this.readSubmission(submissionId)!;
|
|
2222
|
+
}
|
|
1810
2223
|
const intent = terminalIntent ?? {
|
|
1811
2224
|
outcome: "failed" as const,
|
|
1812
2225
|
message:
|
|
@@ -1821,13 +2234,85 @@ export abstract class AgentRuntimeKernel<
|
|
|
1821
2234
|
await this.projectTerminal(turn, terminal);
|
|
1822
2235
|
return terminal;
|
|
1823
2236
|
} catch (error) {
|
|
2237
|
+
// 同上:中断出来的 abort 不是失败,也不是取消。
|
|
2238
|
+
if (this.migratedSubmissions.has(submissionId)) {
|
|
2239
|
+
if (streamId) this.failRecoverableStream(streamId);
|
|
2240
|
+
return this.readSubmission(submissionId)!;
|
|
2241
|
+
}
|
|
2242
|
+
const stalled = error instanceof ChatStreamStalledError;
|
|
2243
|
+
const abortReason = this.readSubmission(submissionId)?.abortReason;
|
|
2244
|
+
if (!abortReason && (stalled || error instanceof RetryableModelError)) {
|
|
2245
|
+
const recoveryErrorCount = this.db.submissions
|
|
2246
|
+
.incrementRecoveryErrorCount(
|
|
2247
|
+
submissionId,
|
|
2248
|
+
stalled
|
|
2249
|
+
? "no_meaningful_model_progress"
|
|
2250
|
+
: "transient_model_error",
|
|
2251
|
+
);
|
|
2252
|
+
if (stalled) {
|
|
2253
|
+
const stallDetails = readModelStreamStallDetails(errorText(error)) ?? {
|
|
2254
|
+
lastMeaningfulActivityAt:
|
|
2255
|
+
Date.now() - MODEL_STREAM_STALL_TIMEOUT_MS,
|
|
2256
|
+
lastMeaningfulActivityType: "model_stream_started",
|
|
2257
|
+
idleMs: MODEL_STREAM_STALL_TIMEOUT_MS,
|
|
2258
|
+
};
|
|
2259
|
+
this._emit("chat:stream:stalled", {
|
|
2260
|
+
requestId: submission.requestId,
|
|
2261
|
+
submissionId,
|
|
2262
|
+
attempt: recoveryErrorCount,
|
|
2263
|
+
timeoutMs: MODEL_STREAM_STALL_TIMEOUT_MS,
|
|
2264
|
+
...stallDetails,
|
|
2265
|
+
reason: "no_meaningful_model_progress",
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
await this.broadcastApprovals();
|
|
2269
|
+
const stoppedDuringRecovery = this.readSubmission(submissionId)
|
|
2270
|
+
?.abortReason;
|
|
2271
|
+
if (
|
|
2272
|
+
!stoppedDuringRecovery &&
|
|
2273
|
+
recoveryErrorCount <
|
|
2274
|
+
(stalled ? CHAT_STALL_MAX_ATTEMPTS : CHAT_RECOVERY_MAX_ATTEMPTS)
|
|
2275
|
+
) {
|
|
2276
|
+
const recoveryOutcome = await this.scheduleChatRecoveryRetry(
|
|
2277
|
+
{
|
|
2278
|
+
submissionId,
|
|
2279
|
+
requestId: submission.requestId,
|
|
2280
|
+
},
|
|
2281
|
+
async () => {
|
|
2282
|
+
this.broadcast(json({
|
|
2283
|
+
type: MessageType.CF_AGENT_CHAT_MESSAGES,
|
|
2284
|
+
messages: await this.getMessages(),
|
|
2285
|
+
}));
|
|
2286
|
+
if (streamId) this.completeRecoverableStream(streamId);
|
|
2287
|
+
this.sendChatResponse(submission.requestId, "", true, {
|
|
2288
|
+
continuation: turn.continuation,
|
|
2289
|
+
});
|
|
2290
|
+
},
|
|
2291
|
+
);
|
|
2292
|
+
if (
|
|
2293
|
+
recoveryOutcome !== "disabled" &&
|
|
2294
|
+
!this.readSubmission(submissionId)?.abortReason
|
|
2295
|
+
) {
|
|
2296
|
+
if (recovery && recoveryOutcome === "scheduled") throw error;
|
|
2297
|
+
return this.readSubmission(submissionId)!;
|
|
2298
|
+
}
|
|
2299
|
+
} else if (!stoppedDuringRecovery) {
|
|
2300
|
+
terminalIntent = {
|
|
2301
|
+
outcome: "failed",
|
|
2302
|
+
message: CHAT_RECOVERY_TERMINAL_MESSAGE,
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
1824
2306
|
const latest = this.readSubmission(submissionId);
|
|
1825
|
-
const intent = terminalIntent ??
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
2307
|
+
const intent = terminalIntent ?? (latest?.abortReason
|
|
2308
|
+
? {
|
|
2309
|
+
outcome: "aborted" as const,
|
|
2310
|
+
message: latest.abortReason,
|
|
2311
|
+
}
|
|
2312
|
+
: {
|
|
2313
|
+
outcome: "failed" as const,
|
|
2314
|
+
message: errorText(error),
|
|
2315
|
+
});
|
|
1831
2316
|
const failed = await this.submissions.finish(
|
|
1832
2317
|
submission,
|
|
1833
2318
|
intent.outcome,
|
|
@@ -1856,28 +2341,37 @@ export abstract class AgentRuntimeKernel<
|
|
|
1856
2341
|
}
|
|
1857
2342
|
}
|
|
1858
2343
|
|
|
1859
|
-
//
|
|
2344
|
+
// 作用:持久化一条 Pi 流记录并广播给在线客户端。
|
|
1860
2345
|
// 调用:活跃 Turn 适配器每产生一条 stream record 时调用。
|
|
1861
|
-
//
|
|
2346
|
+
// 原因:先发 WebSocket 再等持久化完成,让客户端零延迟收到 token,同时
|
|
2347
|
+
// await 保证 Pi 的事件循环不越过尚未落盘的 chunk,续传完整性不受影响。
|
|
1862
2348
|
private async persistAndBroadcastRecord(
|
|
1863
2349
|
turn: ActiveTurn,
|
|
1864
2350
|
chunk: UIMessageChunk,
|
|
1865
2351
|
): Promise<void> {
|
|
1866
2352
|
const streamId = this.streamBySubmission.get(turn.submissionId);
|
|
1867
2353
|
const body = json(chunk);
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
this.sendChatResponse(turn.requestId, body, false
|
|
2354
|
+
const persist = streamId
|
|
2355
|
+
? this.appendRecoverableChunk(streamId, body)
|
|
2356
|
+
: Promise.resolve();
|
|
2357
|
+
this.sendChatResponse(turn.requestId, body, false, {
|
|
2358
|
+
continuation: turn.continuation,
|
|
2359
|
+
});
|
|
2360
|
+
await persist;
|
|
1872
2361
|
}
|
|
1873
2362
|
|
|
1874
|
-
private sendChatTerminal(
|
|
2363
|
+
private sendChatTerminal(
|
|
2364
|
+
requestId: string,
|
|
2365
|
+
error?: string,
|
|
2366
|
+
continuation = false,
|
|
2367
|
+
): void {
|
|
1875
2368
|
this.sendChatResponse(
|
|
1876
2369
|
requestId,
|
|
1877
2370
|
error
|
|
1878
2371
|
? json({ type: "error", errorText: error } satisfies UIMessageChunk)
|
|
1879
2372
|
: "",
|
|
1880
2373
|
true,
|
|
2374
|
+
{ continuation },
|
|
1881
2375
|
);
|
|
1882
2376
|
}
|
|
1883
2377
|
|
|
@@ -1893,21 +2387,17 @@ export abstract class AgentRuntimeKernel<
|
|
|
1893
2387
|
submission.status === "error"
|
|
1894
2388
|
? submission.error ?? "Pi turn failed"
|
|
1895
2389
|
: undefined,
|
|
2390
|
+
turn.continuation,
|
|
1896
2391
|
);
|
|
1897
2392
|
const messages = await this.getMessages();
|
|
1898
|
-
await this.broadcastApprovals(
|
|
1899
|
-
submission.status === "completed" &&
|
|
1900
|
-
submissionNeedsInput(messages, submission.assistantMessageId)
|
|
1901
|
-
? "needs-input"
|
|
1902
|
-
: "idle",
|
|
1903
|
-
);
|
|
2393
|
+
await this.broadcastApprovals();
|
|
1904
2394
|
this.broadcast(
|
|
1905
2395
|
json({
|
|
1906
2396
|
type: MessageType.CF_AGENT_CHAT_MESSAGES,
|
|
1907
2397
|
messages,
|
|
1908
2398
|
}),
|
|
1909
2399
|
);
|
|
1910
|
-
const events = this.
|
|
2400
|
+
const events = this.turnEventsPort();
|
|
1911
2401
|
if (events) {
|
|
1912
2402
|
try {
|
|
1913
2403
|
await events.onResponse(
|
|
@@ -2056,7 +2546,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
2056
2546
|
ReturnType<AgentRuntimeKernel["submitMessage"]>
|
|
2057
2547
|
>;
|
|
2058
2548
|
try {
|
|
2059
|
-
const normalized = this.
|
|
2549
|
+
const normalized = await this.normalizeUIUserInput(latest);
|
|
2060
2550
|
submitted = await this.submitMessage(normalized, {
|
|
2061
2551
|
requestId: event.id,
|
|
2062
2552
|
idempotencyKey: event.id,
|
|
@@ -2080,7 +2570,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
2080
2570
|
if (submission) {
|
|
2081
2571
|
await this.cancelSubmissionById(
|
|
2082
2572
|
submission.submissionId,
|
|
2083
|
-
|
|
2573
|
+
USER_STOP_REASON,
|
|
2084
2574
|
);
|
|
2085
2575
|
}
|
|
2086
2576
|
return;
|
|
@@ -2141,6 +2631,18 @@ export abstract class AgentRuntimeKernel<
|
|
|
2141
2631
|
return this.transcript.browserMessages();
|
|
2142
2632
|
}
|
|
2143
2633
|
|
|
2634
|
+
/** 读取当前 canonical transcript 中最后一条助手文本。 */
|
|
2635
|
+
protected async latestAssistantText(): Promise<string | undefined> {
|
|
2636
|
+
const message = [...await this.transcript.canonicalMessages()]
|
|
2637
|
+
.reverse()
|
|
2638
|
+
.find((entry): entry is AssistantMessage => entry.role === "assistant");
|
|
2639
|
+
const text = message?.content
|
|
2640
|
+
.flatMap((part) => part.type === "text" ? [part.text] : [])
|
|
2641
|
+
.join("")
|
|
2642
|
+
.trim();
|
|
2643
|
+
return text || undefined;
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2144
2646
|
// #endregion
|
|
2145
2647
|
|
|
2146
2648
|
// #region Runtime Extension Host 回环端口
|
|
@@ -2332,6 +2834,19 @@ export abstract class AgentRuntimeKernel<
|
|
|
2332
2834
|
return submitted.receipt;
|
|
2333
2835
|
}
|
|
2334
2836
|
|
|
2837
|
+
// 作用:找出当前停在「等客户端结算某个 Tool」上的 Submission。
|
|
2838
|
+
// 调用:dispatchMessage 判断要不要把 delivery 提升成 steer 时调用。
|
|
2839
|
+
// 原因:park 期间 DO 可能已经睡过一觉,内存里的 active turn 不可信,只能问数据库。
|
|
2840
|
+
private findInteractionParkedSubmission(): StoredSubmission | null {
|
|
2841
|
+
const running = this.db.submissions.findRunning() as
|
|
2842
|
+
| StoredSubmission
|
|
2843
|
+
| null;
|
|
2844
|
+
return running &&
|
|
2845
|
+
this.interactions.hasPendingForSubmission(running.submissionId)
|
|
2846
|
+
? running
|
|
2847
|
+
: null;
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2335
2850
|
async dispatchMessage(
|
|
2336
2851
|
message: UIMessage,
|
|
2337
2852
|
delivery: MessageDelivery,
|
|
@@ -2349,12 +2864,26 @@ export abstract class AgentRuntimeKernel<
|
|
|
2349
2864
|
};
|
|
2350
2865
|
}
|
|
2351
2866
|
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2867
|
+
let userMessage: PiCanonicalUserInput;
|
|
2868
|
+
try {
|
|
2869
|
+
userMessage = await this.normalizeUIUserInput(
|
|
2870
|
+
message as UIMessage & { role: "user" },
|
|
2871
|
+
);
|
|
2872
|
+
} catch (error) {
|
|
2873
|
+
return {
|
|
2874
|
+
kind: "rejected",
|
|
2875
|
+
code: "invalid_message",
|
|
2876
|
+
message: errorText(error),
|
|
2877
|
+
};
|
|
2878
|
+
}
|
|
2879
|
+
// 停在 interaction park 上的 Turn 一律按 steer 处理,哪怕客户端发的是 enqueue:
|
|
2880
|
+
// enqueue 要等本 Turn 结束,而本 Turn 正在等一个永远不会来的答案 —— 死锁。
|
|
2881
|
+
const parked = this.findInteractionParkedSubmission();
|
|
2882
|
+
const effectiveDelivery = parked ? "steer" : delivery;
|
|
2883
|
+
if (effectiveDelivery === "steer") {
|
|
2356
2884
|
const active = this.submissions.currentActive();
|
|
2357
|
-
const target =
|
|
2885
|
+
const target = parked ??
|
|
2886
|
+
active ??
|
|
2358
2887
|
this.db.submissions.findRunning() ??
|
|
2359
2888
|
this.db.submissions.findNextPending();
|
|
2360
2889
|
if (target) {
|
|
@@ -2387,6 +2916,14 @@ export abstract class AgentRuntimeKernel<
|
|
|
2387
2916
|
}
|
|
2388
2917
|
await this.broadcastApprovals();
|
|
2389
2918
|
}
|
|
2919
|
+
// 先落 steer 再取消:取消会唤醒 park 住的 Tool 让 Turn 继续跑,
|
|
2920
|
+
// 顺序反过来 Turn 可能在这条消息落盘前就跑完了。
|
|
2921
|
+
if (parked?.submissionId === target.submissionId) {
|
|
2922
|
+
await this.interactions.cancelPendingForSubmission(
|
|
2923
|
+
target.submissionId,
|
|
2924
|
+
"user_replied_freeform",
|
|
2925
|
+
);
|
|
2926
|
+
}
|
|
2390
2927
|
return {
|
|
2391
2928
|
kind: "accepted",
|
|
2392
2929
|
submissionId: target.submissionId,
|
|
@@ -2660,6 +3197,43 @@ export abstract class AgentRuntimeKernel<
|
|
|
2660
3197
|
};
|
|
2661
3198
|
}
|
|
2662
3199
|
|
|
3200
|
+
/**
|
|
3201
|
+
* 把客户端投递的响应作为某次 Tool 调用的结果,并续跑原 Turn。
|
|
3202
|
+
*
|
|
3203
|
+
* @remarks
|
|
3204
|
+
* 前端经 WS RPC 调用,只带 `toolCallId` —— 它不知道 submissionId,也不该知道。
|
|
3205
|
+
*
|
|
3206
|
+
* 这是「结果由客户端提供」的 Tool 的唯一入口。与审批不同,这里没有任何业务身份
|
|
3207
|
+
* (审批的 `allow_level` 要改 User Agent 授权档位,所以必须经 Host),因此留在 Runtime。
|
|
3208
|
+
*
|
|
3209
|
+
* 查无、已结算、Tool 未声明 `interaction`、响应体不过校验,一律返回 `{ ok: false }` 而不抛 ——
|
|
3210
|
+
* 口径对齐 `cancelSubmissionById`:客户端重复点击不该看到异常。
|
|
3211
|
+
*/
|
|
3212
|
+
async respondToolInteraction(
|
|
3213
|
+
toolCallId: string,
|
|
3214
|
+
response: unknown,
|
|
3215
|
+
): Promise<{ ok: boolean }> {
|
|
3216
|
+
const pending = this.interactions.findPending(toolCallId);
|
|
3217
|
+
if (!pending) return { ok: false };
|
|
3218
|
+
const submission = this.readSubmission(pending.submissionId);
|
|
3219
|
+
if (!submission) return { ok: false };
|
|
3220
|
+
await this.ensureRuntimeReady();
|
|
3221
|
+
|
|
3222
|
+
const adapter = this.createSubmissionExecutionAdapter(submission);
|
|
3223
|
+
const spec = adapter.interactionSpec(pending.toolName);
|
|
3224
|
+
if (!spec) return { ok: false };
|
|
3225
|
+
if (!spec.validateResponse(response)) return { ok: false };
|
|
3226
|
+
|
|
3227
|
+
const input = safeParseJson(pending.inputJson);
|
|
3228
|
+
const settled = spec.settle
|
|
3229
|
+
? spec.settle(input, response)
|
|
3230
|
+
: defaultInteractionResult(response);
|
|
3231
|
+
return this.interactions.respond(pending.interactionId, response, {
|
|
3232
|
+
content: settled.content,
|
|
3233
|
+
details: settled.details,
|
|
3234
|
+
});
|
|
3235
|
+
}
|
|
3236
|
+
|
|
2663
3237
|
/**
|
|
2664
3238
|
* 对一条待处理执行应用一次允许或拒绝决定。
|
|
2665
3239
|
*
|
|
@@ -2773,12 +3347,47 @@ export abstract class AgentRuntimeKernel<
|
|
|
2773
3347
|
}
|
|
2774
3348
|
}
|
|
2775
3349
|
|
|
3350
|
+
// 作用:Agent Tool 子运行开始后重算一次活动投影。
|
|
3351
|
+
// 调用:Agents SDK 在登记子运行后调用。
|
|
3352
|
+
// 原因:分离的子运行不进 Submission 表,不在这里重算,Host 的列表会显示成已经空闲。
|
|
3353
|
+
override async onAgentToolStart(run: AgentToolRunInfo): Promise<void> {
|
|
3354
|
+
await super.onAgentToolStart(run);
|
|
3355
|
+
await this.broadcastApprovals();
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
// 作用:Agent Tool 子运行结束后重算一次活动投影。
|
|
3359
|
+
// 调用:Agents SDK 在子运行进入终态或被中断后调用。
|
|
3360
|
+
// 原因:中断且子进程仍在跑时活动不能落回空闲,判定只在 `hasRunningAgentTools` 一处。
|
|
3361
|
+
override async onAgentToolFinish(
|
|
3362
|
+
run: AgentToolRunInfo,
|
|
3363
|
+
result: AgentToolLifecycleResult,
|
|
3364
|
+
): Promise<void> {
|
|
3365
|
+
await super.onAgentToolFinish(run, result);
|
|
3366
|
+
await this.broadcastApprovals();
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
async _cfDetachedNotifyFinish(
|
|
3370
|
+
run: AgentToolRunInfo,
|
|
3371
|
+
result: AgentToolLifecycleResult,
|
|
3372
|
+
): Promise<void> {
|
|
3373
|
+
const outcome = result.status === "completed"
|
|
3374
|
+
? result.summary ?? "Completed without a result."
|
|
3375
|
+
: result.error ?? `Background run ${result.status}.`;
|
|
3376
|
+
await this.submitPrompt(
|
|
3377
|
+
[
|
|
3378
|
+
"A background sub-agent run has finished.",
|
|
3379
|
+
`runId: ${run.runId}`,
|
|
3380
|
+
`status: ${result.status}`,
|
|
3381
|
+
`result: ${outcome}`,
|
|
3382
|
+
].join("\n"),
|
|
3383
|
+
{ idempotencyKey: `detached-agent-tool:${run.runId}:${result.status}` },
|
|
3384
|
+
);
|
|
3385
|
+
}
|
|
3386
|
+
|
|
2776
3387
|
// 作用:把审批、队列和 Session 活动投影到 Agent 可广播状态。
|
|
2777
|
-
//
|
|
3388
|
+
// 调用:启动、准入、审批变化、interaction 变化、子运行变化和 Turn 完成时调用。
|
|
2778
3389
|
// 原因:这些都是可重建的 UI 投影,相同内容不重复 `setState`,投影失败也不能阻断执行。
|
|
2779
|
-
|
|
2780
|
-
inactiveActivity?: RuntimeActivity,
|
|
2781
|
-
): Promise<void> {
|
|
3390
|
+
protected async broadcastApprovals(): Promise<void> {
|
|
2782
3391
|
try {
|
|
2783
3392
|
const approvals = [
|
|
2784
3393
|
...this.approvals.list(),
|
|
@@ -2821,7 +3430,25 @@ export abstract class AgentRuntimeKernel<
|
|
|
2821
3430
|
);
|
|
2822
3431
|
const turn: RuntimeTurnState = {
|
|
2823
3432
|
...(current
|
|
2824
|
-
? {
|
|
3433
|
+
? {
|
|
3434
|
+
activeSubmissionId: current.submissionId,
|
|
3435
|
+
...(current.requestId
|
|
3436
|
+
? { activeRequestId: current.requestId }
|
|
3437
|
+
: {}),
|
|
3438
|
+
...(current.recoveryErrorCount === undefined
|
|
3439
|
+
? {}
|
|
3440
|
+
: {
|
|
3441
|
+
recoveryAttempt: current.recoveryErrorCount,
|
|
3442
|
+
// 上限按当前恢复原因取,否则 stall 会显示 "2/5" 却在第 3 次就终止。
|
|
3443
|
+
recoveryMax:
|
|
3444
|
+
current.recoveryReason === "no_meaningful_model_progress"
|
|
3445
|
+
? CHAT_STALL_MAX_ATTEMPTS
|
|
3446
|
+
: CHAT_RECOVERY_MAX_ATTEMPTS,
|
|
3447
|
+
...(current.recoveryReason
|
|
3448
|
+
? { recoveryReason: current.recoveryReason }
|
|
3449
|
+
: {}),
|
|
3450
|
+
}),
|
|
3451
|
+
}
|
|
2825
3452
|
: {}),
|
|
2826
3453
|
steerable: Boolean(
|
|
2827
3454
|
current &&
|
|
@@ -2835,19 +3462,25 @@ export abstract class AgentRuntimeKernel<
|
|
|
2835
3462
|
),
|
|
2836
3463
|
queued,
|
|
2837
3464
|
};
|
|
2838
|
-
|
|
3465
|
+
// activity 与这里其他字段一样,是当前 DB 的纯函数:不接受调用方的提示,
|
|
3466
|
+
// 也不粘住上一次的值。needs-input 只有一个意思 —— 此刻真挂着一件等人回应
|
|
3467
|
+
// 的事(审批,或结果由客户端结算的 Tool)。可重算因而能自愈:任何一次广播
|
|
3468
|
+
// 都会把陈旧状态冲掉,不需要谁记得来清。
|
|
3469
|
+
const activity: RuntimeActivity = approvals.length > 0 ||
|
|
3470
|
+
(current &&
|
|
3471
|
+
this.interactions.hasPendingForSubmission(current.submissionId))
|
|
2839
3472
|
? "needs-input"
|
|
2840
3473
|
: current || queued.length > 0
|
|
2841
3474
|
? "working"
|
|
2842
|
-
:
|
|
2843
|
-
|
|
2844
|
-
? "needs-input"
|
|
2845
|
-
: "idle");
|
|
3475
|
+
: "idle";
|
|
3476
|
+
const backgroundWork = this.db.agentTools.hasRunning();
|
|
2846
3477
|
const currentActivity = this.state.activity;
|
|
2847
|
-
const nextActivity = currentActivity?.activity === activity
|
|
3478
|
+
const nextActivity = currentActivity?.activity === activity &&
|
|
3479
|
+
currentActivity.backgroundWork === backgroundWork
|
|
2848
3480
|
? currentActivity
|
|
2849
3481
|
: {
|
|
2850
3482
|
activity,
|
|
3483
|
+
backgroundWork,
|
|
2851
3484
|
revision: (currentActivity?.revision ?? 0) + 1,
|
|
2852
3485
|
};
|
|
2853
3486
|
if (
|
|
@@ -2863,8 +3496,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
2863
3496
|
turn,
|
|
2864
3497
|
});
|
|
2865
3498
|
}
|
|
2866
|
-
const projection = this.
|
|
2867
|
-
?.onActivityChanged?.(nextActivity);
|
|
3499
|
+
const projection = this.turnEventsPort()?.onActivityChanged?.(nextActivity);
|
|
2868
3500
|
if (projection) {
|
|
2869
3501
|
this.ctx.waitUntil(
|
|
2870
3502
|
projection.catch(() => undefined),
|