@springbrand/agent-runtime 0.1.3-alpha.3 → 0.1.3-alpha.5
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 +3 -1
- package/src/adapter/cloudflare/index.ts +60 -0
- package/src/adapter/cloudflare/resources/runtime-resources.ts +86 -0
- package/src/adapter/cloudflare/sandbox/adapter.ts +1509 -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 +256 -0
- package/src/adapter/cloudflare/universal-agent/definition.ts +71 -0
- package/src/adapter/cloudflare/universal-agent/hooks.ts +35 -0
- package/src/adapter/cloudflare/universal-agent/preparation.ts +273 -0
- package/src/adapter/cloudflare/universal-agent/tools.ts +74 -0
- package/src/adapter/cloudflare/workspace/publisher.ts +31 -0
- package/src/adapter/cloudflare/workspace/scoped-workspace.ts +376 -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 +15 -0
- package/src/db/submission.repo.ts +29 -0
- package/src/index.ts +53 -21
- 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/layers/orchestration/temporary-agent/core.ts +12 -1
- package/src/layers/orchestration/temporary-agent/runner.ts +1 -2
- package/src/pi/message/contract.ts +7 -0
- package/src/pi/message/conversion.ts +9 -1
- package/src/pi/runtime-adapter/assembly.ts +17 -3
- package/src/pi/runtime-adapter/execution.ts +109 -9
- package/src/pi/runtime-adapter/index.ts +15 -5
- package/src/pi/runtime-adapter/recovery.ts +188 -1
- package/src/pi/tool/base.ts +79 -9
- package/src/pi/tool/compiler.ts +34 -0
- package/src/pi/tool/core.ts +13 -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/tool/schedule.ts +11 -0
- package/src/pi/tool/subagent.ts +14 -0
- package/src/pi/tool/workspace-sandbox.ts +15 -0
- 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 +569 -322
- package/src/{plugins.ts → runtime-assembler.ts} +312 -379
- package/src/runtime-definition.ts +173 -0
- package/src/runtime.ts +572 -164
- package/src/tool-registry.ts +143 -0
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,26 @@ 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,
|
|
52
|
+
type RuntimeCandidate,
|
|
45
53
|
type RuntimeSnapshot,
|
|
46
|
-
} from "./
|
|
54
|
+
} from "./runtime-assembler";
|
|
55
|
+
import type { RuntimeAgentHooks } from "./runtime-definition";
|
|
56
|
+
import type { RuntimeTurnEventsPort } from "./kernel/bindings";
|
|
47
57
|
import { connectConfiguredMcpServers } from "./lib/mcp";
|
|
48
58
|
import { installConsoleSink } from "./lib/telemetry-dev";
|
|
49
59
|
import {
|
|
@@ -55,6 +65,7 @@ import {
|
|
|
55
65
|
type PiCanonicalTranscriptSnapshot,
|
|
56
66
|
type PiChatRecoveryData,
|
|
57
67
|
type UIChatRequestBody,
|
|
68
|
+
type RequestedCapability,
|
|
58
69
|
type PiDurableMutation,
|
|
59
70
|
type PiRecoveryCommand,
|
|
60
71
|
type PiRecoveryDecision,
|
|
@@ -66,7 +77,11 @@ import {
|
|
|
66
77
|
type PreparedPiTurnAdapter,
|
|
67
78
|
} from "./pi/runtime-adapter";
|
|
68
79
|
import type { UIMessage, UIMessageChunk } from "ai";
|
|
69
|
-
import type {
|
|
80
|
+
import type {
|
|
81
|
+
AssistantMessage,
|
|
82
|
+
ToolResultMessage,
|
|
83
|
+
} from "@earendil-works/pi-ai";
|
|
84
|
+
import { serializeOutput } from "./lib/artifacts";
|
|
70
85
|
import type {
|
|
71
86
|
RuntimeModelUsageEvent,
|
|
72
87
|
RuntimeToolSettlementEvent,
|
|
@@ -181,6 +196,27 @@ function assistantUsageEvent(
|
|
|
181
196
|
// 作用:把用户消息内容变成可比较的稳定形状。
|
|
182
197
|
// 调用:重新生成回答时,提交入口用它核对客户端与已存用户消息。
|
|
183
198
|
// 原因:字符串和分段内容必须先归一化,否则语义相同的消息会被误判为不同。
|
|
199
|
+
// 把 park 时存下的 Tool 输入还原成对象,交给该 Tool 自己的 settle 映射。
|
|
200
|
+
// respondToolInteraction 调用;解析失败按 undefined 处理,因为映射函数是否用得到它由 Tool 决定。
|
|
201
|
+
function safeParseJson(text: string): unknown {
|
|
202
|
+
try {
|
|
203
|
+
return JSON.parse(text);
|
|
204
|
+
} catch {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Tool 没有提供 settle 时的缺省映射:响应原样成为 ToolResult 的 details。
|
|
210
|
+
// text 用 serializeOutput 而不是裸 JSON.stringify,与其他 Tool 结果的文本形态保持一致。
|
|
211
|
+
function defaultInteractionResult(
|
|
212
|
+
response: unknown,
|
|
213
|
+
): { content: ToolResultMessage["content"]; details: unknown } {
|
|
214
|
+
return {
|
|
215
|
+
content: [{ type: "text", text: serializeOutput(response).text }],
|
|
216
|
+
details: response,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
184
220
|
function userContentKey(message: PiCanonicalUserInput): string {
|
|
185
221
|
return json(
|
|
186
222
|
typeof message.content === "string"
|
|
@@ -189,20 +225,42 @@ function userContentKey(message: PiCanonicalUserInput): string {
|
|
|
189
225
|
);
|
|
190
226
|
}
|
|
191
227
|
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
228
|
+
function requestedCapabilitiesOf(metadata: unknown): RequestedCapability[] {
|
|
229
|
+
if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
const record = metadata as Record<string, unknown>;
|
|
233
|
+
if (!("requestedCapabilities" in record)) return [];
|
|
234
|
+
if (!Array.isArray(record.requestedCapabilities)) {
|
|
235
|
+
throw new Error("requestedCapabilities must be an array");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const result: RequestedCapability[] = [];
|
|
239
|
+
const seen = new Set<string>();
|
|
240
|
+
for (const value of record.requestedCapabilities) {
|
|
241
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
242
|
+
throw new Error("requestedCapabilities contains an invalid capability");
|
|
243
|
+
}
|
|
244
|
+
const capability = value as Record<string, unknown>;
|
|
245
|
+
if (
|
|
246
|
+
(capability.kind !== "skill" && capability.kind !== "plan") ||
|
|
247
|
+
typeof capability.name !== "string" ||
|
|
248
|
+
!capability.name.trim() ||
|
|
249
|
+
typeof capability.label !== "string" ||
|
|
250
|
+
!capability.label.trim()
|
|
251
|
+
) {
|
|
252
|
+
throw new Error("requestedCapabilities contains an invalid capability");
|
|
253
|
+
}
|
|
254
|
+
const key = `${capability.kind}:${capability.name}`;
|
|
255
|
+
if (seen.has(key)) continue;
|
|
256
|
+
seen.add(key);
|
|
257
|
+
result.push({
|
|
258
|
+
kind: capability.kind,
|
|
259
|
+
name: capability.name,
|
|
260
|
+
label: capability.label,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
206
264
|
}
|
|
207
265
|
|
|
208
266
|
interface PendingTemporaryAgentApproval {
|
|
@@ -240,6 +298,9 @@ export abstract class AgentRuntimeKernel<
|
|
|
240
298
|
private runtimeSnapshot?: RuntimeSnapshot;
|
|
241
299
|
private runtimeRevision?: string;
|
|
242
300
|
private runtimePi?: PreparedPiRuntime;
|
|
301
|
+
private runtimeGatewaySession?: RuntimeGatewaySession;
|
|
302
|
+
private runtimeTurnEvents?: RuntimeTurnEventsPort;
|
|
303
|
+
private runtimeHooks?: RuntimeAgentHooks;
|
|
243
304
|
private piAdapter?: PiRuntimeAdapter;
|
|
244
305
|
private readonly transcript: PiRuntimeTranscript;
|
|
245
306
|
private readonly submissions: SubmissionLifecycle<
|
|
@@ -247,7 +308,10 @@ export abstract class AgentRuntimeKernel<
|
|
|
247
308
|
ActiveTurn
|
|
248
309
|
>;
|
|
249
310
|
private readonly approvals: ApprovalLifecycle<StoredSubmission>;
|
|
311
|
+
private readonly interactions: InteractionLifecycle<StoredSubmission>;
|
|
312
|
+
private runtimeLoadTracker?: RuntimeLoadTracker;
|
|
250
313
|
private readonly streamBySubmission = new Map<string, string>();
|
|
314
|
+
private migratedSubmissionIds?: Set<string>;
|
|
251
315
|
private db!: RuntimeDatabase;
|
|
252
316
|
private readonly temporaryAgents = new TemporaryAgentCoordinator();
|
|
253
317
|
private readonly temporaryAgentApprovals =
|
|
@@ -262,6 +326,24 @@ export abstract class AgentRuntimeKernel<
|
|
|
262
326
|
return this.piAdapter ??= new PiRuntimeAdapter();
|
|
263
327
|
}
|
|
264
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Runtime 装载进度的状态机。
|
|
331
|
+
*
|
|
332
|
+
* @remarks
|
|
333
|
+
* 本类和 `defineRuntimeAgent` 生成的宿主共同驱动它:宿主管一轮尝试的起止,
|
|
334
|
+
* `initConfig` 管中途的阶段推进,因此它是 protected 而非 private。
|
|
335
|
+
*
|
|
336
|
+
* 写成惰性访问器而不是字段:本类有测试用 `Object.create(prototype)` 造替身,
|
|
337
|
+
* 那条路径既不跑构造函数也不跑字段初始化器。
|
|
338
|
+
*/
|
|
339
|
+
protected get runtimeLoad(): RuntimeLoadTracker {
|
|
340
|
+
return this.runtimeLoadTracker ??= new RuntimeLoadTracker({
|
|
341
|
+
read: () => this.state.runtimeLoad,
|
|
342
|
+
publish: (runtimeLoad) => this.setState({ ...this.state, runtimeLoad }),
|
|
343
|
+
isAvailable: () => Boolean(this.runtimeSnapshot),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
265
347
|
/**
|
|
266
348
|
* 为一个 Cloudflare Durable Object 实例建立持久化与执行协作器。
|
|
267
349
|
*
|
|
@@ -313,6 +395,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
313
395
|
commitTerminal: (submission, outcome, message) =>
|
|
314
396
|
this.commitTerminalOutcome(submission, outcome, message),
|
|
315
397
|
abortActive: (turn) => turn.agent.abort(),
|
|
398
|
+
interruptActive: (turn) => turn.agent.interrupt(),
|
|
316
399
|
});
|
|
317
400
|
this.transcript = this.pi.createTranscript({
|
|
318
401
|
sql: this.sql.bind(this),
|
|
@@ -350,6 +433,22 @@ export abstract class AgentRuntimeKernel<
|
|
|
350
433
|
},
|
|
351
434
|
onApprovalsChanged: () => this.broadcastApprovals(),
|
|
352
435
|
});
|
|
436
|
+
this.interactions = new InteractionLifecycle({
|
|
437
|
+
db: this.db,
|
|
438
|
+
pi: this.pi,
|
|
439
|
+
findSubmission: (submissionId) =>
|
|
440
|
+
this.readSubmission(submissionId),
|
|
441
|
+
applyRecoveryMutations: (submission, mutations) =>
|
|
442
|
+
this.applyPiRecoveryMutations(submission, mutations),
|
|
443
|
+
materializeRecoveredToolResults: (submission) =>
|
|
444
|
+
this.materializeRecoveredToolResults(submission),
|
|
445
|
+
// 与审批不同,这里不经 `schedule` 绕一圈:RPC 已经把 DO 叫醒了,
|
|
446
|
+
// 而 interaction 没有自己的续跑键,续跑就是普通的「已结算 Tool 恢复原 Turn」。
|
|
447
|
+
resumeSubmission: async (submissionId) => {
|
|
448
|
+
await this.submissions.recover(submissionId);
|
|
449
|
+
},
|
|
450
|
+
onInteractionsChanged: () => this.broadcastApprovals(),
|
|
451
|
+
});
|
|
353
452
|
}
|
|
354
453
|
|
|
355
454
|
// 作用:把通用聊天恢复协议接到本 Runtime 的 Submission 和 Pi 里程碑。
|
|
@@ -406,49 +505,103 @@ export abstract class AgentRuntimeKernel<
|
|
|
406
505
|
// 作用:准备并原子切换一份新的 Runtime 配置。
|
|
407
506
|
// 调用:生成 Agent 在首次启动或显式重载 Runtime key 时调用。
|
|
408
507
|
// 原因:先完整 prepare 再替换 Snapshot,且活跃 Turn 期间禁止换 revision,可避免半装配和恢复时能力漂移。
|
|
409
|
-
protected async initConfig(
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const candidateSnapshot = next;
|
|
508
|
+
protected async initConfig(input: RuntimeAssemblyInput): Promise<void> {
|
|
509
|
+
const candidate = await prepareRuntimeCandidate(input);
|
|
510
|
+
await this.initCandidate(candidate);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private turnEventsPort(): RuntimeTurnEventsPort | undefined {
|
|
514
|
+
return this.runtimeTurnEvents ?? this.runtimeSnapshot?.bindings.turnEvents;
|
|
515
|
+
}
|
|
418
516
|
|
|
419
|
-
|
|
517
|
+
protected async initCandidate(candidate: RuntimeCandidate): Promise<void> {
|
|
518
|
+
this.runtimeTurnEvents =
|
|
519
|
+
candidate.turnEvents ?? candidate.snapshot.bindings.turnEvents;
|
|
520
|
+
this.runtimeHooks = candidate.hooks;
|
|
521
|
+
const previous = this.runtimeSnapshot;
|
|
522
|
+
const candidateSnapshot = candidate.snapshot;
|
|
523
|
+
this.runtimeLoad.advance("assembly");
|
|
524
|
+
this.runtimeLoad.advance("mcp");
|
|
420
525
|
await connectConfiguredMcpServers(
|
|
421
526
|
this,
|
|
422
527
|
candidateSnapshot.profile.mcpServers,
|
|
423
528
|
);
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
529
|
+
const gatewayDegradations = [];
|
|
530
|
+
let gatewaySession: RuntimeGatewaySession | undefined;
|
|
531
|
+
if (candidateSnapshot.bindings.gateway) {
|
|
532
|
+
try {
|
|
533
|
+
gatewaySession = await candidateSnapshot.bindings.gateway.open(
|
|
534
|
+
`${this.ctx.id.toString()}:${crypto.randomUUID()}`,
|
|
535
|
+
);
|
|
536
|
+
} catch {
|
|
537
|
+
gatewayDegradations.push({
|
|
538
|
+
capability: "connector" as const,
|
|
539
|
+
reason: "unavailable" as const,
|
|
540
|
+
detail: "Connector Gateway MCP",
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
this.runtimeLoad.advance("pi");
|
|
546
|
+
let prepared: PreparedPiRuntime;
|
|
547
|
+
try {
|
|
548
|
+
prepared = await this.pi.prepare({
|
|
549
|
+
snapshot: candidateSnapshot,
|
|
550
|
+
mcpHost: this,
|
|
551
|
+
gatewaySession,
|
|
552
|
+
additionalDegradations: gatewayDegradations,
|
|
553
|
+
createExtensionHostBinding: (permissions, ownContextLabels) =>
|
|
554
|
+
this.createExtensionHostBinding(
|
|
555
|
+
permissions,
|
|
556
|
+
ownContextLabels,
|
|
557
|
+
),
|
|
558
|
+
});
|
|
559
|
+
} catch (error) {
|
|
560
|
+
await gatewaySession?.close().catch(() => undefined);
|
|
561
|
+
throw error;
|
|
562
|
+
}
|
|
434
563
|
const revision = await this.hash(prepared.revisionDescriptor);
|
|
435
|
-
|
|
436
|
-
|
|
564
|
+
// 停在人机等待上的 Submission 是「未完成」,但没有任何模型请求在飞、
|
|
565
|
+
// 没有任何工具在执行 —— 换装配对它是安全的,而且这正是「决定时才装上
|
|
566
|
+
// 新能力,同一轮接着跑」所依赖的那一步。真正在执行的 Turn 仍然被挡住。
|
|
567
|
+
const contested = Boolean(previous) &&
|
|
437
568
|
revision !== this.runtimeRevision &&
|
|
438
|
-
this.submissions.isBusy()
|
|
439
|
-
)
|
|
569
|
+
this.submissions.isBusy();
|
|
570
|
+
const parked = contested && this.db.everyUnfinishedSubmissionParked();
|
|
571
|
+
if (contested && !parked) {
|
|
572
|
+
await gatewaySession?.close().catch(() => undefined);
|
|
440
573
|
throw new Error(
|
|
441
574
|
"Cannot reload Runtime while a revision-pinned Pi Turn is active",
|
|
442
575
|
);
|
|
443
576
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
577
|
+
const repin = parked
|
|
578
|
+
? await this.prepareParkedSubmissionRepin(prepared, revision)
|
|
579
|
+
: undefined;
|
|
580
|
+
let activated = false;
|
|
581
|
+
const previousGatewaySession = this.runtimeGatewaySession;
|
|
582
|
+
try {
|
|
583
|
+
for (const guard of candidate.commitGuards) await guard();
|
|
584
|
+
if (candidateSnapshot.bindings.platform.telemetryConsole) {
|
|
585
|
+
installConsoleSink();
|
|
586
|
+
}
|
|
587
|
+
this.pi.activate(candidateSnapshot);
|
|
588
|
+
activated = true;
|
|
589
|
+
repin?.commit();
|
|
590
|
+
this.runtimeSnapshot = candidateSnapshot;
|
|
591
|
+
this.runtimeRevision = revision;
|
|
592
|
+
this.runtimePi = prepared;
|
|
593
|
+
this.runtimeGatewaySession = gatewaySession;
|
|
594
|
+
} catch (error) {
|
|
595
|
+
repin?.abort();
|
|
596
|
+
if (activated && previous) this.pi.activate(previous);
|
|
597
|
+
await gatewaySession?.close().catch(() => undefined);
|
|
598
|
+
throw error;
|
|
451
599
|
}
|
|
600
|
+
await repin?.interrupt().catch((error) => {
|
|
601
|
+
console.error("[runtime-repin:interrupt-failed]", error);
|
|
602
|
+
});
|
|
603
|
+
await previousGatewaySession?.close().catch(() => undefined);
|
|
604
|
+
|
|
452
605
|
const degradations = prepared.degradations;
|
|
453
606
|
if (degradations.length > 0) {
|
|
454
607
|
console.warn(
|
|
@@ -502,7 +655,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
502
655
|
async (created) => {
|
|
503
656
|
await onCreated?.();
|
|
504
657
|
try {
|
|
505
|
-
await this.
|
|
658
|
+
await this.turnEventsPort()?.onApproval?.({
|
|
506
659
|
submissionId: submission.submissionId,
|
|
507
660
|
approvalExecutionId: created.executionId,
|
|
508
661
|
});
|
|
@@ -514,6 +667,10 @@ export abstract class AgentRuntimeKernel<
|
|
|
514
667
|
}
|
|
515
668
|
},
|
|
516
669
|
),
|
|
670
|
+
// spec 在这里用不上:响应侧一律从重新装配出的 candidate 按 toolName 取回同一份,
|
|
671
|
+
// 好让「DO 一直醒着」和「park 期间睡过一觉」走完全相同的一条代码路径。
|
|
672
|
+
requestToolInteraction: (interaction, _spec, signal) =>
|
|
673
|
+
this.interactions.request(submission, interaction, signal),
|
|
517
674
|
appendToolInput: (input) =>
|
|
518
675
|
this.appendToolInput(submission, input),
|
|
519
676
|
settleTool: (call) =>
|
|
@@ -528,6 +685,10 @@ export abstract class AgentRuntimeKernel<
|
|
|
528
685
|
this.readSubmission(submission.submissionId)?.abortReason,
|
|
529
686
|
onRecord: output.onRecord ?? (() => undefined),
|
|
530
687
|
onCanonicalMessage: async (commit) => {
|
|
688
|
+
// 换装配把这条执行器中断掉了。它退场路上还会吐出「工具被中止」这样的
|
|
689
|
+
// 消息 —— 那不是这一轮的事实,写进权威 transcript 会让重建出来的续跑
|
|
690
|
+
// 看到一份自己从没产生过的工具结果。
|
|
691
|
+
if (this.migratedSubmissions.has(submission.submissionId)) return;
|
|
531
692
|
let consumedSteer = false;
|
|
532
693
|
this.db.transaction(() => {
|
|
533
694
|
if (commit.kind !== "append-user") {
|
|
@@ -595,7 +756,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
595
756
|
* 不会延长 Durable Object 生命;本文件仍在三处用它启动后台任务。
|
|
596
757
|
*/
|
|
597
758
|
async onStart(): Promise<void> {
|
|
598
|
-
this.
|
|
759
|
+
this.runtimeLoad.reset();
|
|
599
760
|
if (this.db.runtimeEvents.hasPending()) {
|
|
600
761
|
this.ctx.waitUntil(
|
|
601
762
|
this.ensureRuntimeReady().then(() => this.drainRuntimeEvents()),
|
|
@@ -620,7 +781,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
620
781
|
}
|
|
621
782
|
|
|
622
783
|
private async drainRuntimeEvents(idempotentRetry = true): Promise<void> {
|
|
623
|
-
const turnEvents = this.
|
|
784
|
+
const turnEvents = this.turnEventsPort();
|
|
624
785
|
if (!turnEvents) return;
|
|
625
786
|
|
|
626
787
|
let failed = false;
|
|
@@ -749,78 +910,56 @@ export abstract class AgentRuntimeKernel<
|
|
|
749
910
|
private assembly(): RuntimeSnapshot {
|
|
750
911
|
if (!this.runtimeSnapshot) {
|
|
751
912
|
throw new Error(
|
|
752
|
-
"
|
|
913
|
+
"Runtime must be assembled before use",
|
|
753
914
|
);
|
|
754
915
|
}
|
|
755
916
|
return this.runtimeSnapshot;
|
|
756
917
|
}
|
|
757
918
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
// 作用:开始一轮新的 Runtime 装载尝试。
|
|
766
|
-
// 调用:生成 Agent 在调用应用 createConfig 前调用。
|
|
767
|
-
// 原因:config 阶段可能包含身份、D1 和 Resource 解析,必须在首个慢请求前可见。
|
|
768
|
-
protected beginRuntimeLoad(): void {
|
|
769
|
-
const now = Date.now();
|
|
770
|
-
this.publishRuntimeLoad({
|
|
771
|
-
status: "loading",
|
|
772
|
-
phase: "config",
|
|
773
|
-
available: Boolean(this.runtimeSnapshot),
|
|
774
|
-
startedAt: now,
|
|
775
|
-
updatedAt: now,
|
|
776
|
-
});
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
// 作用:推进当前 Runtime 装载尝试的阶段。
|
|
780
|
-
// 调用:initConfig 在进入 Plugin、MCP 和 Pi 边界时调用。
|
|
781
|
-
// 原因:保持一个稳定的粗粒度协议,不向前端泄漏具体 Plugin 实现和并发细节。
|
|
782
|
-
private advanceRuntimeLoad(phase: RuntimeLoadPhase): void {
|
|
783
|
-
const current = this.state.runtimeLoad;
|
|
784
|
-
const now = Date.now();
|
|
785
|
-
this.publishRuntimeLoad({
|
|
786
|
-
status: "loading",
|
|
787
|
-
phase,
|
|
788
|
-
available: Boolean(this.runtimeSnapshot),
|
|
789
|
-
startedAt:
|
|
790
|
-
current?.status === "loading" ? current.startedAt : now,
|
|
791
|
-
updatedAt: now,
|
|
792
|
-
});
|
|
793
|
-
}
|
|
919
|
+
private async normalizeUIUserInput(
|
|
920
|
+
message: UIMessage & { role: "user" },
|
|
921
|
+
): Promise<PiCanonicalUserInput> {
|
|
922
|
+
const capabilities = requestedCapabilitiesOf(message.metadata);
|
|
923
|
+
if (capabilities.length === 0) return this.pi.normalizeUserInput(message);
|
|
794
924
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
const
|
|
800
|
-
const
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
925
|
+
await this.ensureRuntimeReady();
|
|
926
|
+
const installedSkills = new Set(
|
|
927
|
+
this.assembly().bindings.skills.sources.map(({ name }) => name),
|
|
928
|
+
);
|
|
929
|
+
const context: string[] = [];
|
|
930
|
+
for (const capability of capabilities) {
|
|
931
|
+
if (capability.kind === "skill") {
|
|
932
|
+
if (!installedSkills.has(capability.name)) {
|
|
933
|
+
throw new Error(
|
|
934
|
+
`Requested Skill is not installed: ${capability.name}`,
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
context.push(
|
|
938
|
+
`requested capability: skill/${capability.name}`,
|
|
939
|
+
`required action: call activate_skill for "${capability.name}" before handling the task`,
|
|
940
|
+
);
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
if (capability.name !== "plan") {
|
|
944
|
+
throw new Error(`Unknown plan capability: ${capability.name}`);
|
|
945
|
+
}
|
|
946
|
+
context.push(
|
|
947
|
+
"requested capability: plan/plan",
|
|
948
|
+
"required action: call update_plan with the complete plan before other work, then wait for user confirmation before execution",
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
return this.pi.normalizeUserInput(message, context.join("\n"));
|
|
808
952
|
}
|
|
809
953
|
|
|
810
|
-
//
|
|
811
|
-
//
|
|
812
|
-
//
|
|
813
|
-
protected
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
available: Boolean(this.runtimeSnapshot),
|
|
820
|
-
startedAt:
|
|
821
|
-
current?.status === "loading" ? current.startedAt : now,
|
|
822
|
-
failedAt: now,
|
|
823
|
-
});
|
|
954
|
+
// 作用:把当前已安装的 Runtime Snapshot 投影成可序列化的调试视图。
|
|
955
|
+
// 调用:Session facet 的 `getRuntimeAssembly` RPC。
|
|
956
|
+
// 原因:UI 需要读取真实装配结果,而不是上层配置声明。
|
|
957
|
+
protected readRuntimeAssembly(): RuntimeAssemblyView {
|
|
958
|
+
return projectRuntimeAssembly(
|
|
959
|
+
this.assembly(),
|
|
960
|
+
this.runtimeRevision ?? null,
|
|
961
|
+
this.preparedPi(),
|
|
962
|
+
);
|
|
824
963
|
}
|
|
825
964
|
|
|
826
965
|
// 作用:为 Runtime Extension 创建只包含授权能力的 Worker 回环绑定。
|
|
@@ -866,6 +1005,20 @@ export abstract class AgentRuntimeKernel<
|
|
|
866
1005
|
// 作用:返回当前已准备好的 Pi Runtime。
|
|
867
1006
|
// 调用:创建 Turn 适配器或准入固定装配时调用。
|
|
868
1007
|
// 原因:明确区分“已有 Snapshot”和“Pi 已完成准备”,防止半初始化状态进入 Turn。
|
|
1008
|
+
/**
|
|
1009
|
+
* 正在被换装配中断、等待持久续跑接手的 Submission。
|
|
1010
|
+
*
|
|
1011
|
+
* 中断和取消在执行路径上长得一模一样(都是 abort),但结局相反:取消要写终态,
|
|
1012
|
+
* 换装配中断必须让 Submission 停在 running,等决策把它交给续跑。这个集合是
|
|
1013
|
+
* 两者唯一的区分依据,只在一次 `repinParkedSubmissions` 的窗口内有成员。
|
|
1014
|
+
*
|
|
1015
|
+
* 写成惰性访问器而不是字段:本类有测试用 `Object.create(prototype)` 造替身,
|
|
1016
|
+
* 那条路径不跑字段初始化器。
|
|
1017
|
+
*/
|
|
1018
|
+
private get migratedSubmissions(): Set<string> {
|
|
1019
|
+
return this.migratedSubmissionIds ??= new Set<string>();
|
|
1020
|
+
}
|
|
1021
|
+
|
|
869
1022
|
private preparedPi(): PreparedPiRuntime {
|
|
870
1023
|
if (!this.runtimePi) {
|
|
871
1024
|
throw new Error(
|
|
@@ -887,13 +1040,31 @@ export abstract class AgentRuntimeKernel<
|
|
|
887
1040
|
descriptor: string;
|
|
888
1041
|
}> {
|
|
889
1042
|
await this.ensureRuntimeReady();
|
|
1043
|
+
return this.currentAssemblyPin();
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// 作用:按当前已装配的 Runtime 生成一份不可变装配描述。
|
|
1047
|
+
// 调用:准入 pin 走 `admissionPin`;`initConfig` 内部的重新 pin 直接调用本方法。
|
|
1048
|
+
// 原因:`initConfig` 已经在装配串行入口里面,再走 `ensureRuntimeReady` 会等自己,
|
|
1049
|
+
// 所以「确保装配就绪」和「按当前装配取 pin」必须是两步。
|
|
1050
|
+
private async currentAssemblyPin(): Promise<{
|
|
1051
|
+
revision: string;
|
|
1052
|
+
descriptor: string;
|
|
1053
|
+
}> {
|
|
890
1054
|
await this.drainRuntimeEvents();
|
|
891
1055
|
if (!this.runtimeRevision) {
|
|
892
1056
|
throw new Error("Runtime revision is not initialized");
|
|
893
1057
|
}
|
|
1058
|
+
return this.pinAssembly(this.preparedPi(), this.runtimeRevision);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
private async pinAssembly(
|
|
1062
|
+
prepared: PreparedPiRuntime,
|
|
1063
|
+
baseRevision: string,
|
|
1064
|
+
): Promise<{ revision: string; descriptor: string }> {
|
|
894
1065
|
const pinned = await this.pi.pin({
|
|
895
|
-
prepared
|
|
896
|
-
baseRevision
|
|
1066
|
+
prepared,
|
|
1067
|
+
baseRevision,
|
|
897
1068
|
readExtensionContext: ({ label }) =>
|
|
898
1069
|
this._hostGetContext(label),
|
|
899
1070
|
});
|
|
@@ -931,6 +1102,89 @@ export abstract class AgentRuntimeKernel<
|
|
|
931
1102
|
return pinned;
|
|
932
1103
|
}
|
|
933
1104
|
|
|
1105
|
+
/** 准备 parked Submission 的续跑切换,持久 pin 只在最终 commit 中修改。 */
|
|
1106
|
+
private async prepareParkedSubmissionRepin(
|
|
1107
|
+
prepared: PreparedPiRuntime,
|
|
1108
|
+
baseRevision: string,
|
|
1109
|
+
): Promise<{ commit(): void; interrupt(): Promise<void>; abort(): void }> {
|
|
1110
|
+
await this.drainRuntimeEvents();
|
|
1111
|
+
const { revision, descriptor } = await this.pinAssembly(
|
|
1112
|
+
prepared,
|
|
1113
|
+
baseRevision,
|
|
1114
|
+
);
|
|
1115
|
+
const plans: Array<{
|
|
1116
|
+
submission: StoredSubmission;
|
|
1117
|
+
decision: PiRecoveryDecision;
|
|
1118
|
+
}> = [];
|
|
1119
|
+
for (const submissionId of this.db.submissions.listUnfinishedIds()) {
|
|
1120
|
+
const submission = this.readSubmission(submissionId);
|
|
1121
|
+
if (
|
|
1122
|
+
!submission?.assemblyRevision ||
|
|
1123
|
+
!submission.assemblyDescriptor ||
|
|
1124
|
+
submission.assemblyRevision === revision
|
|
1125
|
+
) {
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
const decision = this.decidePiRecovery(submission, {
|
|
1129
|
+
kind: "repin-assembly",
|
|
1130
|
+
nextAssemblyRevision: revision,
|
|
1131
|
+
});
|
|
1132
|
+
plans.push({ submission, decision });
|
|
1133
|
+
}
|
|
1134
|
+
let settled = false;
|
|
1135
|
+
let committed = false;
|
|
1136
|
+
const cleanup = () => {
|
|
1137
|
+
for (const { submission } of plans) {
|
|
1138
|
+
this.migratedSubmissions.delete(submission.submissionId);
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
return {
|
|
1142
|
+
commit: () => {
|
|
1143
|
+
if (settled) throw new Error("Parked Submission repin already settled");
|
|
1144
|
+
for (const { submission } of plans) {
|
|
1145
|
+
this.migratedSubmissions.add(submission.submissionId);
|
|
1146
|
+
}
|
|
1147
|
+
try {
|
|
1148
|
+
this.db.transaction(() => {
|
|
1149
|
+
for (const { submission, decision } of plans) {
|
|
1150
|
+
this.applyPiRecoveryMutations(submission, decision.mutations);
|
|
1151
|
+
this.db.submissions.repinAssembly(
|
|
1152
|
+
submission.submissionId,
|
|
1153
|
+
revision,
|
|
1154
|
+
descriptor,
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
});
|
|
1158
|
+
committed = true;
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
settled = true;
|
|
1161
|
+
cleanup();
|
|
1162
|
+
throw error;
|
|
1163
|
+
}
|
|
1164
|
+
},
|
|
1165
|
+
interrupt: async () => {
|
|
1166
|
+
if (!committed || settled) return;
|
|
1167
|
+
try {
|
|
1168
|
+
for (const { submission } of plans) {
|
|
1169
|
+
const submissionId = submission.submissionId;
|
|
1170
|
+
await this.submissions.interrupt(
|
|
1171
|
+
submissionId,
|
|
1172
|
+
() => this.approvals.discardWaiters(submissionId),
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
} finally {
|
|
1176
|
+
settled = true;
|
|
1177
|
+
cleanup();
|
|
1178
|
+
}
|
|
1179
|
+
},
|
|
1180
|
+
abort: () => {
|
|
1181
|
+
if (settled) return;
|
|
1182
|
+
settled = true;
|
|
1183
|
+
cleanup();
|
|
1184
|
+
},
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
|
|
934
1188
|
// 作用:检查 Submission 保存的装配描述与 revision 仍然匹配。
|
|
935
1189
|
// 调用:普通恢复和审批续跑在重建 Pi Turn 前调用。
|
|
936
1190
|
// 原因:若持久描述已变,继续执行会把同一 Turn 切成两套能力语义。
|
|
@@ -1171,6 +1425,28 @@ export abstract class AgentRuntimeKernel<
|
|
|
1171
1425
|
);
|
|
1172
1426
|
continue;
|
|
1173
1427
|
}
|
|
1428
|
+
if (mutation.kind === "record-interaction") {
|
|
1429
|
+
this.db.interactions.insert({
|
|
1430
|
+
interactionId: mutation.interaction.interactionId,
|
|
1431
|
+
submissionId: submission.submissionId,
|
|
1432
|
+
requestId: mutation.interaction.requestId,
|
|
1433
|
+
toolCallId: mutation.interaction.toolCallId,
|
|
1434
|
+
toolName: mutation.interaction.toolName,
|
|
1435
|
+
inputJson: mutation.interaction.inputJson,
|
|
1436
|
+
status: "pending",
|
|
1437
|
+
createdAt: mutation.interaction.createdAt,
|
|
1438
|
+
});
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
if (mutation.kind === "settle-interaction") {
|
|
1442
|
+
this.db.interactions.settle(
|
|
1443
|
+
mutation.interactionId,
|
|
1444
|
+
mutation.status,
|
|
1445
|
+
mutation.settledAt,
|
|
1446
|
+
mutation.responseJson,
|
|
1447
|
+
);
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1174
1450
|
const inserted = this.db.milestones.upsert(
|
|
1175
1451
|
submission.submissionId,
|
|
1176
1452
|
mutation.key,
|
|
@@ -1393,7 +1669,11 @@ export abstract class AgentRuntimeKernel<
|
|
|
1393
1669
|
latest,
|
|
1394
1670
|
effectiveMessage ?? "Turn ended before approval",
|
|
1395
1671
|
);
|
|
1396
|
-
|
|
1672
|
+
// 还挂着的 interaction 同样要收尾,否则 Turn 结束后 pending 行会变成孤儿,
|
|
1673
|
+
// 用户还能看见一张点了没反应的卡。
|
|
1674
|
+
const closedInteractions = this.interactions
|
|
1675
|
+
.cancelPendingForSubmissionSync(latest);
|
|
1676
|
+
if (closedApprovals || closedInteractions) {
|
|
1397
1677
|
this.materializeRecoveredToolResultsSync(latest);
|
|
1398
1678
|
}
|
|
1399
1679
|
const status =
|
|
@@ -1469,20 +1749,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
1469
1749
|
} catch (error) {
|
|
1470
1750
|
console.error("[chat-recovery] terminal settlement failed", error);
|
|
1471
1751
|
}
|
|
1472
|
-
|
|
1473
|
-
if (terminal.status === "completed") {
|
|
1474
|
-
try {
|
|
1475
|
-
if (submissionNeedsInput(
|
|
1476
|
-
await this.getMessages(),
|
|
1477
|
-
terminal.assistantMessageId,
|
|
1478
|
-
)) {
|
|
1479
|
-
inactiveActivity = "needs-input";
|
|
1480
|
-
}
|
|
1481
|
-
} catch {
|
|
1482
|
-
// Transcript projection is retried by the normal terminal path.
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
await this.broadcastApprovals(inactiveActivity);
|
|
1752
|
+
await this.broadcastApprovals();
|
|
1486
1753
|
try {
|
|
1487
1754
|
if (
|
|
1488
1755
|
effectiveOutcome === "failed" ||
|
|
@@ -1732,6 +1999,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
1732
1999
|
failed.status === "error"
|
|
1733
2000
|
? failed.error ?? "Pi turn failed"
|
|
1734
2001
|
: undefined,
|
|
2002
|
+
recovery,
|
|
1735
2003
|
);
|
|
1736
2004
|
return failed;
|
|
1737
2005
|
} finally {
|
|
@@ -1884,6 +2152,12 @@ export abstract class AgentRuntimeKernel<
|
|
|
1884
2152
|
}
|
|
1885
2153
|
},
|
|
1886
2154
|
);
|
|
2155
|
+
// 换装配把这条执行器中断掉了:Submission 没有结束,它在等持久续跑用新
|
|
2156
|
+
// 装配重建一条接着跑。这里写终态会把那一轮当场杀掉。
|
|
2157
|
+
if (this.migratedSubmissions.has(submissionId)) {
|
|
2158
|
+
if (streamId) this.failRecoverableStream(streamId);
|
|
2159
|
+
return this.readSubmission(submissionId)!;
|
|
2160
|
+
}
|
|
1887
2161
|
const intent = terminalIntent ?? {
|
|
1888
2162
|
outcome: "failed" as const,
|
|
1889
2163
|
message:
|
|
@@ -1898,6 +2172,11 @@ export abstract class AgentRuntimeKernel<
|
|
|
1898
2172
|
await this.projectTerminal(turn, terminal);
|
|
1899
2173
|
return terminal;
|
|
1900
2174
|
} catch (error) {
|
|
2175
|
+
// 同上:中断出来的 abort 不是失败,也不是取消。
|
|
2176
|
+
if (this.migratedSubmissions.has(submissionId)) {
|
|
2177
|
+
if (streamId) this.failRecoverableStream(streamId);
|
|
2178
|
+
return this.readSubmission(submissionId)!;
|
|
2179
|
+
}
|
|
1901
2180
|
const stalled = error instanceof ChatStreamStalledError;
|
|
1902
2181
|
const abortReason = this.readSubmission(submissionId)?.abortReason;
|
|
1903
2182
|
if (!abortReason && (stalled || error instanceof RetryableModelError)) {
|
|
@@ -1943,7 +2222,9 @@ export abstract class AgentRuntimeKernel<
|
|
|
1943
2222
|
messages: await this.getMessages(),
|
|
1944
2223
|
}));
|
|
1945
2224
|
if (streamId) this.completeRecoverableStream(streamId);
|
|
1946
|
-
this.sendChatResponse(submission.requestId, "", true
|
|
2225
|
+
this.sendChatResponse(submission.requestId, "", true, {
|
|
2226
|
+
continuation: turn.continuation,
|
|
2227
|
+
});
|
|
1947
2228
|
},
|
|
1948
2229
|
);
|
|
1949
2230
|
if (
|
|
@@ -2011,17 +2292,24 @@ export abstract class AgentRuntimeKernel<
|
|
|
2011
2292
|
const persist = streamId
|
|
2012
2293
|
? this.appendRecoverableChunk(streamId, body)
|
|
2013
2294
|
: Promise.resolve();
|
|
2014
|
-
this.sendChatResponse(turn.requestId, body, false
|
|
2295
|
+
this.sendChatResponse(turn.requestId, body, false, {
|
|
2296
|
+
continuation: turn.continuation,
|
|
2297
|
+
});
|
|
2015
2298
|
await persist;
|
|
2016
2299
|
}
|
|
2017
2300
|
|
|
2018
|
-
private sendChatTerminal(
|
|
2301
|
+
private sendChatTerminal(
|
|
2302
|
+
requestId: string,
|
|
2303
|
+
error?: string,
|
|
2304
|
+
continuation = false,
|
|
2305
|
+
): void {
|
|
2019
2306
|
this.sendChatResponse(
|
|
2020
2307
|
requestId,
|
|
2021
2308
|
error
|
|
2022
2309
|
? json({ type: "error", errorText: error } satisfies UIMessageChunk)
|
|
2023
2310
|
: "",
|
|
2024
2311
|
true,
|
|
2312
|
+
{ continuation },
|
|
2025
2313
|
);
|
|
2026
2314
|
}
|
|
2027
2315
|
|
|
@@ -2037,21 +2325,17 @@ export abstract class AgentRuntimeKernel<
|
|
|
2037
2325
|
submission.status === "error"
|
|
2038
2326
|
? submission.error ?? "Pi turn failed"
|
|
2039
2327
|
: undefined,
|
|
2328
|
+
turn.continuation,
|
|
2040
2329
|
);
|
|
2041
2330
|
const messages = await this.getMessages();
|
|
2042
|
-
await this.broadcastApprovals(
|
|
2043
|
-
submission.status === "completed" &&
|
|
2044
|
-
submissionNeedsInput(messages, submission.assistantMessageId)
|
|
2045
|
-
? "needs-input"
|
|
2046
|
-
: "idle",
|
|
2047
|
-
);
|
|
2331
|
+
await this.broadcastApprovals();
|
|
2048
2332
|
this.broadcast(
|
|
2049
2333
|
json({
|
|
2050
2334
|
type: MessageType.CF_AGENT_CHAT_MESSAGES,
|
|
2051
2335
|
messages,
|
|
2052
2336
|
}),
|
|
2053
2337
|
);
|
|
2054
|
-
const events = this.
|
|
2338
|
+
const events = this.turnEventsPort();
|
|
2055
2339
|
if (events) {
|
|
2056
2340
|
try {
|
|
2057
2341
|
await events.onResponse(
|
|
@@ -2200,7 +2484,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
2200
2484
|
ReturnType<AgentRuntimeKernel["submitMessage"]>
|
|
2201
2485
|
>;
|
|
2202
2486
|
try {
|
|
2203
|
-
const normalized = this.
|
|
2487
|
+
const normalized = await this.normalizeUIUserInput(latest);
|
|
2204
2488
|
submitted = await this.submitMessage(normalized, {
|
|
2205
2489
|
requestId: event.id,
|
|
2206
2490
|
idempotencyKey: event.id,
|
|
@@ -2285,6 +2569,18 @@ export abstract class AgentRuntimeKernel<
|
|
|
2285
2569
|
return this.transcript.browserMessages();
|
|
2286
2570
|
}
|
|
2287
2571
|
|
|
2572
|
+
/** 读取当前 canonical transcript 中最后一条助手文本。 */
|
|
2573
|
+
protected async latestAssistantText(): Promise<string | undefined> {
|
|
2574
|
+
const message = [...await this.transcript.canonicalMessages()]
|
|
2575
|
+
.reverse()
|
|
2576
|
+
.find((entry): entry is AssistantMessage => entry.role === "assistant");
|
|
2577
|
+
const text = message?.content
|
|
2578
|
+
.flatMap((part) => part.type === "text" ? [part.text] : [])
|
|
2579
|
+
.join("")
|
|
2580
|
+
.trim();
|
|
2581
|
+
return text || undefined;
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2288
2584
|
// #endregion
|
|
2289
2585
|
|
|
2290
2586
|
// #region Runtime Extension Host 回环端口
|
|
@@ -2476,6 +2772,19 @@ export abstract class AgentRuntimeKernel<
|
|
|
2476
2772
|
return submitted.receipt;
|
|
2477
2773
|
}
|
|
2478
2774
|
|
|
2775
|
+
// 作用:找出当前停在「等客户端结算某个 Tool」上的 Submission。
|
|
2776
|
+
// 调用:dispatchMessage 判断要不要把 delivery 提升成 steer 时调用。
|
|
2777
|
+
// 原因:park 期间 DO 可能已经睡过一觉,内存里的 active turn 不可信,只能问数据库。
|
|
2778
|
+
private findInteractionParkedSubmission(): StoredSubmission | null {
|
|
2779
|
+
const running = this.db.submissions.findRunning() as
|
|
2780
|
+
| StoredSubmission
|
|
2781
|
+
| null;
|
|
2782
|
+
return running &&
|
|
2783
|
+
this.interactions.hasPendingForSubmission(running.submissionId)
|
|
2784
|
+
? running
|
|
2785
|
+
: null;
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2479
2788
|
async dispatchMessage(
|
|
2480
2789
|
message: UIMessage,
|
|
2481
2790
|
delivery: MessageDelivery,
|
|
@@ -2493,12 +2802,26 @@ export abstract class AgentRuntimeKernel<
|
|
|
2493
2802
|
};
|
|
2494
2803
|
}
|
|
2495
2804
|
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2805
|
+
let userMessage: PiCanonicalUserInput;
|
|
2806
|
+
try {
|
|
2807
|
+
userMessage = await this.normalizeUIUserInput(
|
|
2808
|
+
message as UIMessage & { role: "user" },
|
|
2809
|
+
);
|
|
2810
|
+
} catch (error) {
|
|
2811
|
+
return {
|
|
2812
|
+
kind: "rejected",
|
|
2813
|
+
code: "invalid_message",
|
|
2814
|
+
message: errorText(error),
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2817
|
+
// 停在 interaction park 上的 Turn 一律按 steer 处理,哪怕客户端发的是 enqueue:
|
|
2818
|
+
// enqueue 要等本 Turn 结束,而本 Turn 正在等一个永远不会来的答案 —— 死锁。
|
|
2819
|
+
const parked = this.findInteractionParkedSubmission();
|
|
2820
|
+
const effectiveDelivery = parked ? "steer" : delivery;
|
|
2821
|
+
if (effectiveDelivery === "steer") {
|
|
2500
2822
|
const active = this.submissions.currentActive();
|
|
2501
|
-
const target =
|
|
2823
|
+
const target = parked ??
|
|
2824
|
+
active ??
|
|
2502
2825
|
this.db.submissions.findRunning() ??
|
|
2503
2826
|
this.db.submissions.findNextPending();
|
|
2504
2827
|
if (target) {
|
|
@@ -2531,6 +2854,14 @@ export abstract class AgentRuntimeKernel<
|
|
|
2531
2854
|
}
|
|
2532
2855
|
await this.broadcastApprovals();
|
|
2533
2856
|
}
|
|
2857
|
+
// 先落 steer 再取消:取消会唤醒 park 住的 Tool 让 Turn 继续跑,
|
|
2858
|
+
// 顺序反过来 Turn 可能在这条消息落盘前就跑完了。
|
|
2859
|
+
if (parked?.submissionId === target.submissionId) {
|
|
2860
|
+
await this.interactions.cancelPendingForSubmission(
|
|
2861
|
+
target.submissionId,
|
|
2862
|
+
"user_replied_freeform",
|
|
2863
|
+
);
|
|
2864
|
+
}
|
|
2534
2865
|
return {
|
|
2535
2866
|
kind: "accepted",
|
|
2536
2867
|
submissionId: target.submissionId,
|
|
@@ -2804,6 +3135,43 @@ export abstract class AgentRuntimeKernel<
|
|
|
2804
3135
|
};
|
|
2805
3136
|
}
|
|
2806
3137
|
|
|
3138
|
+
/**
|
|
3139
|
+
* 把客户端投递的响应作为某次 Tool 调用的结果,并续跑原 Turn。
|
|
3140
|
+
*
|
|
3141
|
+
* @remarks
|
|
3142
|
+
* 前端经 WS RPC 调用,只带 `toolCallId` —— 它不知道 submissionId,也不该知道。
|
|
3143
|
+
*
|
|
3144
|
+
* 这是「结果由客户端提供」的 Tool 的唯一入口。与审批不同,这里没有任何业务身份
|
|
3145
|
+
* (审批的 `allow_level` 要改 User Agent 授权档位,所以必须经 Host),因此留在 Runtime。
|
|
3146
|
+
*
|
|
3147
|
+
* 查无、已结算、Tool 未声明 `interaction`、响应体不过校验,一律返回 `{ ok: false }` 而不抛 ——
|
|
3148
|
+
* 口径对齐 `cancelSubmissionById`:客户端重复点击不该看到异常。
|
|
3149
|
+
*/
|
|
3150
|
+
async respondToolInteraction(
|
|
3151
|
+
toolCallId: string,
|
|
3152
|
+
response: unknown,
|
|
3153
|
+
): Promise<{ ok: boolean }> {
|
|
3154
|
+
const pending = this.interactions.findPending(toolCallId);
|
|
3155
|
+
if (!pending) return { ok: false };
|
|
3156
|
+
const submission = this.readSubmission(pending.submissionId);
|
|
3157
|
+
if (!submission) return { ok: false };
|
|
3158
|
+
await this.ensureRuntimeReady();
|
|
3159
|
+
|
|
3160
|
+
const adapter = this.createSubmissionExecutionAdapter(submission);
|
|
3161
|
+
const spec = adapter.interactionSpec(pending.toolName);
|
|
3162
|
+
if (!spec) return { ok: false };
|
|
3163
|
+
if (!spec.validateResponse(response)) return { ok: false };
|
|
3164
|
+
|
|
3165
|
+
const input = safeParseJson(pending.inputJson);
|
|
3166
|
+
const settled = spec.settle
|
|
3167
|
+
? spec.settle(input, response)
|
|
3168
|
+
: defaultInteractionResult(response);
|
|
3169
|
+
return this.interactions.respond(pending.interactionId, response, {
|
|
3170
|
+
content: settled.content,
|
|
3171
|
+
details: settled.details,
|
|
3172
|
+
});
|
|
3173
|
+
}
|
|
3174
|
+
|
|
2807
3175
|
/**
|
|
2808
3176
|
* 对一条待处理执行应用一次允许或拒绝决定。
|
|
2809
3177
|
*
|
|
@@ -2917,12 +3285,47 @@ export abstract class AgentRuntimeKernel<
|
|
|
2917
3285
|
}
|
|
2918
3286
|
}
|
|
2919
3287
|
|
|
3288
|
+
// 作用:Agent Tool 子运行开始后重算一次活动投影。
|
|
3289
|
+
// 调用:Agents SDK 在登记子运行后调用。
|
|
3290
|
+
// 原因:分离的子运行不进 Submission 表,不在这里重算,Host 的列表会显示成已经空闲。
|
|
3291
|
+
override async onAgentToolStart(run: AgentToolRunInfo): Promise<void> {
|
|
3292
|
+
await super.onAgentToolStart(run);
|
|
3293
|
+
await this.broadcastApprovals();
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
// 作用:Agent Tool 子运行结束后重算一次活动投影。
|
|
3297
|
+
// 调用:Agents SDK 在子运行进入终态或被中断后调用。
|
|
3298
|
+
// 原因:中断且子进程仍在跑时活动不能落回空闲,判定只在 `hasRunningAgentTools` 一处。
|
|
3299
|
+
override async onAgentToolFinish(
|
|
3300
|
+
run: AgentToolRunInfo,
|
|
3301
|
+
result: AgentToolLifecycleResult,
|
|
3302
|
+
): Promise<void> {
|
|
3303
|
+
await super.onAgentToolFinish(run, result);
|
|
3304
|
+
await this.broadcastApprovals();
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
async _cfDetachedNotifyFinish(
|
|
3308
|
+
run: AgentToolRunInfo,
|
|
3309
|
+
result: AgentToolLifecycleResult,
|
|
3310
|
+
): Promise<void> {
|
|
3311
|
+
const outcome = result.status === "completed"
|
|
3312
|
+
? result.summary ?? "Completed without a result."
|
|
3313
|
+
: result.error ?? `Background run ${result.status}.`;
|
|
3314
|
+
await this.submitPrompt(
|
|
3315
|
+
[
|
|
3316
|
+
"A background sub-agent run has finished.",
|
|
3317
|
+
`runId: ${run.runId}`,
|
|
3318
|
+
`status: ${result.status}`,
|
|
3319
|
+
`result: ${outcome}`,
|
|
3320
|
+
].join("\n"),
|
|
3321
|
+
{ idempotencyKey: `detached-agent-tool:${run.runId}:${result.status}` },
|
|
3322
|
+
);
|
|
3323
|
+
}
|
|
3324
|
+
|
|
2920
3325
|
// 作用:把审批、队列和 Session 活动投影到 Agent 可广播状态。
|
|
2921
|
-
//
|
|
3326
|
+
// 调用:启动、准入、审批变化、interaction 变化、子运行变化和 Turn 完成时调用。
|
|
2922
3327
|
// 原因:这些都是可重建的 UI 投影,相同内容不重复 `setState`,投影失败也不能阻断执行。
|
|
2923
|
-
|
|
2924
|
-
inactiveActivity?: RuntimeActivity,
|
|
2925
|
-
): Promise<void> {
|
|
3328
|
+
protected async broadcastApprovals(): Promise<void> {
|
|
2926
3329
|
try {
|
|
2927
3330
|
const approvals = [
|
|
2928
3331
|
...this.approvals.list(),
|
|
@@ -2997,19 +3400,25 @@ export abstract class AgentRuntimeKernel<
|
|
|
2997
3400
|
),
|
|
2998
3401
|
queued,
|
|
2999
3402
|
};
|
|
3000
|
-
|
|
3403
|
+
// activity 与这里其他字段一样,是当前 DB 的纯函数:不接受调用方的提示,
|
|
3404
|
+
// 也不粘住上一次的值。needs-input 只有一个意思 —— 此刻真挂着一件等人回应
|
|
3405
|
+
// 的事(审批,或结果由客户端结算的 Tool)。可重算因而能自愈:任何一次广播
|
|
3406
|
+
// 都会把陈旧状态冲掉,不需要谁记得来清。
|
|
3407
|
+
const activity: RuntimeActivity = approvals.length > 0 ||
|
|
3408
|
+
(current &&
|
|
3409
|
+
this.interactions.hasPendingForSubmission(current.submissionId))
|
|
3001
3410
|
? "needs-input"
|
|
3002
3411
|
: current || queued.length > 0
|
|
3003
3412
|
? "working"
|
|
3004
|
-
:
|
|
3005
|
-
|
|
3006
|
-
? "needs-input"
|
|
3007
|
-
: "idle");
|
|
3413
|
+
: "idle";
|
|
3414
|
+
const backgroundWork = this.db.agentTools.hasRunning();
|
|
3008
3415
|
const currentActivity = this.state.activity;
|
|
3009
|
-
const nextActivity = currentActivity?.activity === activity
|
|
3416
|
+
const nextActivity = currentActivity?.activity === activity &&
|
|
3417
|
+
currentActivity.backgroundWork === backgroundWork
|
|
3010
3418
|
? currentActivity
|
|
3011
3419
|
: {
|
|
3012
3420
|
activity,
|
|
3421
|
+
backgroundWork,
|
|
3013
3422
|
revision: (currentActivity?.revision ?? 0) + 1,
|
|
3014
3423
|
};
|
|
3015
3424
|
if (
|
|
@@ -3025,8 +3434,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
3025
3434
|
turn,
|
|
3026
3435
|
});
|
|
3027
3436
|
}
|
|
3028
|
-
const projection = this.
|
|
3029
|
-
?.onActivityChanged?.(nextActivity);
|
|
3437
|
+
const projection = this.turnEventsPort()?.onActivityChanged?.(nextActivity);
|
|
3030
3438
|
if (projection) {
|
|
3031
3439
|
this.ctx.waitUntil(
|
|
3032
3440
|
projection.catch(() => undefined),
|