@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
|
@@ -1,9 +1,21 @@
|
|
|
1
|
+
import type { ExecutionLevel } from "../../../lib/execution-level";
|
|
2
|
+
|
|
3
|
+
export const TEMPORARY_AGENT_LAUNCH_KEY =
|
|
4
|
+
"universal-agent:temporary-agent-launch";
|
|
5
|
+
|
|
1
6
|
export interface TemporaryAgentRequest {
|
|
2
7
|
subagentName: string;
|
|
3
8
|
instructions: string;
|
|
4
9
|
task: string;
|
|
5
10
|
}
|
|
6
11
|
|
|
12
|
+
export interface TemporaryAgentLaunch<Config = unknown>
|
|
13
|
+
extends TemporaryAgentRequest {
|
|
14
|
+
runtimeKey: string;
|
|
15
|
+
config: Config;
|
|
16
|
+
executionLevel: ExecutionLevel;
|
|
17
|
+
}
|
|
18
|
+
|
|
7
19
|
export interface TemporaryAgentRunContext {
|
|
8
20
|
signal: AbortSignal;
|
|
9
21
|
requestId: string;
|
|
@@ -149,4 +161,3 @@ export class TemporaryAgentCoordinator {
|
|
|
149
161
|
});
|
|
150
162
|
}
|
|
151
163
|
}
|
|
152
|
-
import type { ExecutionLevel } from "../../../lib/execution-level";
|
|
@@ -10,7 +10,6 @@ const BLOCKED_TOOLS = new Set([
|
|
|
10
10
|
"resume_schedule",
|
|
11
11
|
"change_schedule_agent",
|
|
12
12
|
"cancel_schedule",
|
|
13
|
-
"execute",
|
|
14
13
|
"set_context",
|
|
15
14
|
"load_context",
|
|
16
15
|
"search_context",
|
|
@@ -25,7 +24,7 @@ const BLOCKED_TOOLS = new Set([
|
|
|
25
24
|
* @remarks
|
|
26
25
|
* 宿主在装配临时 Agent 的平台、Workspace、Sandbox 和其他 Tool 时调用;调用方可再加名字或前缀黑名单。
|
|
27
26
|
*
|
|
28
|
-
*
|
|
27
|
+
* 固定黑名单排除再次委派、调度和主 Session 上下文,以保持单层临时 Agent 和已确认的继承范围。
|
|
29
28
|
*
|
|
30
29
|
* Agent、Session、Workspace 和 Tool 的术语见 `src/index.ts`。
|
|
31
30
|
*/
|
|
@@ -2,6 +2,13 @@ import type { UIMessage } from "ai";
|
|
|
2
2
|
|
|
3
3
|
export type UIChatTrigger = "submit-message" | "regenerate-message";
|
|
4
4
|
|
|
5
|
+
/** A user-selected Runtime capability persisted with the visible UIMessage. */
|
|
6
|
+
export interface RequestedCapability {
|
|
7
|
+
readonly kind: "skill" | "plan";
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly label: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
export interface UIChatRequestBody {
|
|
6
13
|
readonly messages: readonly UIMessage[];
|
|
7
14
|
readonly trigger: UIChatTrigger;
|
|
@@ -26,6 +26,7 @@ function attachmentText(
|
|
|
26
26
|
|
|
27
27
|
function userContent(
|
|
28
28
|
parts: ReadonlyArray<UIMessage["parts"][number]>,
|
|
29
|
+
privateContext?: string,
|
|
29
30
|
): UserMessage["content"] {
|
|
30
31
|
const content: Exclude<UserMessage["content"], string> = [];
|
|
31
32
|
for (const part of parts) {
|
|
@@ -46,16 +47,23 @@ function userContent(
|
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
if (content.length === 0) throw new Error("User message content is required");
|
|
50
|
+
if (privateContext) {
|
|
51
|
+
content.unshift({
|
|
52
|
+
type: "text",
|
|
53
|
+
text: `[Agent private context]\n${privateContext}\n[/Agent private context]`,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
49
56
|
return content;
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
/** Convert an official user UIMessage into canonical Pi model input. */
|
|
53
60
|
export function uiUserMessageToPi(
|
|
54
61
|
message: UIMessage & { readonly role: "user" },
|
|
62
|
+
privateContext?: string,
|
|
55
63
|
): UserMessage {
|
|
56
64
|
return {
|
|
57
65
|
role: "user",
|
|
58
|
-
content: userContent(message.parts),
|
|
66
|
+
content: userContent(message.parts, privateContext),
|
|
59
67
|
timestamp: timestampOf(message),
|
|
60
68
|
};
|
|
61
69
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
RuntimeBindings,
|
|
3
|
+
RuntimeGatewaySession,
|
|
3
4
|
RuntimePlatformPort,
|
|
4
5
|
} from "../../kernel/bindings";
|
|
5
6
|
import type { RuntimeDegradation } from "../../kernel/degradation";
|
|
@@ -8,7 +9,7 @@ import type {
|
|
|
8
9
|
} from "../../kernel/extensions";
|
|
9
10
|
import type { RuntimeProfile } from "../../kernel/profile";
|
|
10
11
|
import type { ExecutionLevel } from "../../lib/execution-level";
|
|
11
|
-
import type { RuntimeSnapshot } from "../../
|
|
12
|
+
import type { RuntimeSnapshot } from "../../runtime-assembler";
|
|
12
13
|
import type { PiRuntimeAssembly } from "../assembly";
|
|
13
14
|
import {
|
|
14
15
|
assemblePiSystemContext,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
type PiLoadedExtension,
|
|
20
21
|
} from "../assembly";
|
|
21
22
|
import {
|
|
23
|
+
createPiGatewayToolCandidates,
|
|
22
24
|
createPiMcpToolCandidates,
|
|
23
25
|
listExtensionsPiToolCandidate,
|
|
24
26
|
type PiMcpHost,
|
|
@@ -58,6 +60,7 @@ interface PiAssemblySnapshot {
|
|
|
58
60
|
interface PreparePiAssemblyOptions {
|
|
59
61
|
readonly snapshot: PiAssemblySnapshot;
|
|
60
62
|
readonly mcpHost: PiMcpHost;
|
|
63
|
+
readonly gatewaySession?: RuntimeGatewaySession;
|
|
61
64
|
// 为一个扩展生成带权限参数的 Host Fetcher。
|
|
62
65
|
// loadPiExtension 在扩展需要 Host 能力时调用它。
|
|
63
66
|
// Worker 掌握真实绑定,装配层只保存这个窄函数,不能自行扩大权限。
|
|
@@ -90,6 +93,8 @@ interface PreparedPiAssembly {
|
|
|
90
93
|
export interface PreparePiRuntimeOptions {
|
|
91
94
|
readonly snapshot: RuntimeSnapshot;
|
|
92
95
|
readonly mcpHost: PiMcpHost;
|
|
96
|
+
readonly gatewaySession?: RuntimeGatewaySession;
|
|
97
|
+
readonly additionalDegradations?: readonly RuntimeDegradation[];
|
|
93
98
|
/**
|
|
94
99
|
* 为一个扩展创建受限的 Host 绑定。
|
|
95
100
|
*
|
|
@@ -115,6 +120,7 @@ export interface PreparePiRuntimeOptions {
|
|
|
115
120
|
export interface PreparedPiRuntime {
|
|
116
121
|
readonly revisionDescriptor: string;
|
|
117
122
|
readonly degradations: readonly RuntimeDegradation[];
|
|
123
|
+
readonly extensions: readonly { readonly name: string; readonly version: string }[];
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
/**
|
|
@@ -242,10 +248,12 @@ function describeTools(candidates: readonly PiToolCandidate[]) {
|
|
|
242
248
|
name: candidate.tool.name,
|
|
243
249
|
owner: candidate.owner,
|
|
244
250
|
label: candidate.tool.label,
|
|
245
|
-
description:
|
|
246
|
-
candidate.tool.description ?? candidate.tool.label,
|
|
251
|
+
description: candidate.tool.description ?? candidate.tool.label,
|
|
247
252
|
parameters: candidate.tool.parameters,
|
|
248
253
|
requiredExecutionLevel: candidate.requiredExecutionLevel,
|
|
254
|
+
...(candidate.alwaysRequiresApproval
|
|
255
|
+
? { alwaysRequiresApproval: true }
|
|
256
|
+
: {}),
|
|
249
257
|
...(candidate.source ? { source: candidate.source } : {}),
|
|
250
258
|
retry: piToolRetryPolicy(candidate),
|
|
251
259
|
}))
|
|
@@ -301,6 +309,7 @@ class PreparedRuntime implements PreparedPiRuntime {
|
|
|
301
309
|
constructor(
|
|
302
310
|
readonly revisionDescriptor: string,
|
|
303
311
|
readonly degradations: readonly RuntimeDegradation[],
|
|
312
|
+
readonly extensions: readonly { readonly name: string; readonly version: string }[],
|
|
304
313
|
private readonly owner: object,
|
|
305
314
|
private readonly state: PreparedPiRuntimeState,
|
|
306
315
|
) {}
|
|
@@ -414,6 +423,9 @@ async function preparePiAssembly(
|
|
|
414
423
|
candidates: snapshot.pi.toolSurface.finalize([
|
|
415
424
|
...extensions.candidates,
|
|
416
425
|
listExtensionsPiToolCandidate(extensions.loaded),
|
|
426
|
+
...(options.gatewaySession
|
|
427
|
+
? createPiGatewayToolCandidates(options.gatewaySession)
|
|
428
|
+
: []),
|
|
417
429
|
...createPiMcpToolCandidates(
|
|
418
430
|
options.mcpHost,
|
|
419
431
|
snapshot.profile.mcpServers,
|
|
@@ -444,8 +456,10 @@ export async function preparePiRuntime(
|
|
|
444
456
|
describeRuntime(options.snapshot, candidates, assembly.loaded),
|
|
445
457
|
Object.freeze([
|
|
446
458
|
...options.snapshot.degradations,
|
|
459
|
+
...(options.additionalDegradations ?? []),
|
|
447
460
|
...assembly.degradations,
|
|
448
461
|
]),
|
|
462
|
+
Object.freeze(assembly.loaded.map(({ name, version }) => ({ name, version }))),
|
|
449
463
|
owner,
|
|
450
464
|
Object.freeze({
|
|
451
465
|
snapshot: options.snapshot,
|
|
@@ -16,7 +16,11 @@ import type {
|
|
|
16
16
|
UserMessage,
|
|
17
17
|
} from "@earendil-works/pi-ai";
|
|
18
18
|
import { transformMessages } from "@earendil-works/pi-ai/api/transform-messages";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
parkPiToolApproval,
|
|
21
|
+
parkPiToolInteraction,
|
|
22
|
+
requiresPiToolApproval,
|
|
23
|
+
} from "../turn";
|
|
20
24
|
import { PiChunkEncoder } from "../message";
|
|
21
25
|
import type { UIMessageChunk } from "ai";
|
|
22
26
|
import { ChatStreamStalledError } from "agents/chat";
|
|
@@ -25,6 +29,7 @@ import {
|
|
|
25
29
|
createPiToolGovernance,
|
|
26
30
|
normalizeUpdatePlanArguments,
|
|
27
31
|
type PiToolCandidate,
|
|
32
|
+
type PiToolInteractionSpec,
|
|
28
33
|
type PiToolGovernance,
|
|
29
34
|
type PiToolTelemetry,
|
|
30
35
|
type SettledPiToolCall,
|
|
@@ -40,6 +45,7 @@ import {
|
|
|
40
45
|
resolvePiApiKey,
|
|
41
46
|
withProviderRetry,
|
|
42
47
|
} from "./models";
|
|
48
|
+
import { EXECUTION_LEVELS } from "../../lib/execution-level";
|
|
43
49
|
|
|
44
50
|
// #region Single-run Pi bridge
|
|
45
51
|
|
|
@@ -308,6 +314,18 @@ export interface PiTurnDurability {
|
|
|
308
314
|
signal?: AbortSignal,
|
|
309
315
|
onCreated?: () => void | Promise<void>,
|
|
310
316
|
): Promise<void>;
|
|
317
|
+
/**
|
|
318
|
+
* 保存一次客户端结算请求并等待响应。
|
|
319
|
+
*
|
|
320
|
+
* @remarks
|
|
321
|
+
* Tool candidate 声明了 `interaction` 时,PreparedPiTurnAdapter 在执行前调用它,`execute` 不会被调用。
|
|
322
|
+
* 返回后必须再读一次 settlement —— 响应、取消和跨休眠恢复三条路径都只把结果留在 settlement 里。
|
|
323
|
+
*/
|
|
324
|
+
requestToolInteraction(
|
|
325
|
+
interaction: import("../turn").PiToolInteraction,
|
|
326
|
+
settle: import("../tool").PiToolInteractionSpec,
|
|
327
|
+
signal?: AbortSignal,
|
|
328
|
+
): Promise<void>;
|
|
311
329
|
/**
|
|
312
330
|
* 记录工具输入,并返回它是否是第一次持久化尝试。
|
|
313
331
|
*
|
|
@@ -475,13 +493,21 @@ export class PreparedPiTurnAdapter {
|
|
|
475
493
|
...candidate.tool,
|
|
476
494
|
// 只在 Runtime 的持久化和策略门禁都放行后执行一个 Tool candidate。
|
|
477
495
|
// 实时运行由 PiCore 调用它,恢复或审批续跑则由 retryTool 进入同一路径。
|
|
478
|
-
//
|
|
496
|
+
// 顺序不能随便调整:门禁先于结果复用,审批可能生成结果,不确定的非幂等工作不能重放。
|
|
479
497
|
execute: async (toolCallId, input, signal, onUpdate) => {
|
|
498
|
+
const requiredExecutionLevel = candidate.requiredExecutionLevelForInput
|
|
499
|
+
? await candidate.requiredExecutionLevelForInput(input)
|
|
500
|
+
: candidate.requiredExecutionLevel;
|
|
501
|
+
if (!EXECUTION_LEVELS.includes(requiredExecutionLevel)) {
|
|
502
|
+
throw new Error(
|
|
503
|
+
`Pi Tool "${candidate.tool.name}" resolved an invalid execution level`,
|
|
504
|
+
);
|
|
505
|
+
}
|
|
480
506
|
await state.snapshot.bindings.platform.gateTool?.({
|
|
481
507
|
toolCallId,
|
|
482
508
|
toolName: candidate.tool.name,
|
|
483
509
|
input,
|
|
484
|
-
requiredExecutionLevel
|
|
510
|
+
requiredExecutionLevel,
|
|
485
511
|
signal: signal ?? new AbortController().signal,
|
|
486
512
|
});
|
|
487
513
|
const settled = options.durability.findToolSettlement(toolCallId);
|
|
@@ -494,10 +520,13 @@ export class PreparedPiTurnAdapter {
|
|
|
494
520
|
}
|
|
495
521
|
return settled.result;
|
|
496
522
|
}
|
|
497
|
-
if (
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
523
|
+
if (
|
|
524
|
+
candidate.alwaysRequiresApproval ||
|
|
525
|
+
requiresPiToolApproval({
|
|
526
|
+
executionLevel: descriptor.executionLevel,
|
|
527
|
+
requiredExecutionLevel,
|
|
528
|
+
})
|
|
529
|
+
) {
|
|
501
530
|
const approval = parkPiToolApproval({
|
|
502
531
|
executionId: `${options.submission.id}:${toolCallId}`,
|
|
503
532
|
requestId: options.submission.requestId,
|
|
@@ -509,7 +538,7 @@ export class PreparedPiTurnAdapter {
|
|
|
509
538
|
candidate.tool.label ??
|
|
510
539
|
candidate.tool.name,
|
|
511
540
|
executionLevel: descriptor.executionLevel,
|
|
512
|
-
requiredExecutionLevel
|
|
541
|
+
requiredExecutionLevel,
|
|
513
542
|
inputJson: JSON.stringify(input),
|
|
514
543
|
createdAt: Date.now(),
|
|
515
544
|
}).approval;
|
|
@@ -539,6 +568,43 @@ export class PreparedPiTurnAdapter {
|
|
|
539
568
|
const afterApproval = options.durability
|
|
540
569
|
.findToolSettlement(toolCallId);
|
|
541
570
|
if (afterApproval) return afterApproval.result;
|
|
571
|
+
// 结果由客户端提供的 Tool 在这里 park,永远不走到下面的 execute。
|
|
572
|
+
// 放在审批之后:一个 Tool 既要审批又要客户端结算时,先过许可再问用户,
|
|
573
|
+
// 否则会先向用户要答案、再告诉他这事根本不许做。
|
|
574
|
+
if (candidate.interaction) {
|
|
575
|
+
// 必须先把 Tool 输入落成里程碑再 park。恢复出来的结算靠这条记录取回
|
|
576
|
+
// `args`;缺了它,Tool 醒来返回结果时 settleTool 会拿 undefined 的 args
|
|
577
|
+
// 去和真 args 比对,判成 "Conflicting durable Tool settlement" —— 整个
|
|
578
|
+
// Turn 就死在这。(审批那条路不受影响:批准后 Tool 真的执行,args 在
|
|
579
|
+
// 下面的 appendToolInput 里补上了。)
|
|
580
|
+
options.durability.appendToolInput({
|
|
581
|
+
toolCallId,
|
|
582
|
+
toolName: candidate.tool.name,
|
|
583
|
+
input,
|
|
584
|
+
retry: piToolRetryPolicy(candidate),
|
|
585
|
+
});
|
|
586
|
+
const parked = parkPiToolInteraction({
|
|
587
|
+
interactionId: `${options.submission.id}:${toolCallId}`,
|
|
588
|
+
requestId: options.submission.requestId,
|
|
589
|
+
toolCallId,
|
|
590
|
+
toolName: candidate.tool.name,
|
|
591
|
+
inputJson: JSON.stringify(input),
|
|
592
|
+
createdAt: Date.now(),
|
|
593
|
+
}).interaction;
|
|
594
|
+
await options.durability.requestToolInteraction(
|
|
595
|
+
parked,
|
|
596
|
+
candidate.interaction,
|
|
597
|
+
signal,
|
|
598
|
+
);
|
|
599
|
+
const responded = options.durability
|
|
600
|
+
.findToolSettlement(toolCallId);
|
|
601
|
+
if (!responded) {
|
|
602
|
+
throw new Error(
|
|
603
|
+
`Tool interaction produced no durable settlement: ${candidate.tool.name}`,
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
return responded.result;
|
|
607
|
+
}
|
|
542
608
|
const retry = piToolRetryPolicy(candidate);
|
|
543
609
|
const firstAttempt = options.durability.appendToolInput({
|
|
544
610
|
toolCallId,
|
|
@@ -572,7 +638,11 @@ export class PreparedPiTurnAdapter {
|
|
|
572
638
|
),
|
|
573
639
|
modelSessionId: options.submission.requestId,
|
|
574
640
|
canonicalMessages: options.canonicalMessages,
|
|
575
|
-
|
|
641
|
+
// 被中断的 Turn 不写结算。它退场路上还会吐出「工具被中止」,而工具其实
|
|
642
|
+
// 没有产生任何结果 —— 结算是单调的,落了这条假结果,重建出来的续跑会
|
|
643
|
+
// 把一个从没跑过的工具当成跑过并失败。取消不走这里:取消要的就是终态。
|
|
644
|
+
settle: (call) =>
|
|
645
|
+
this.interrupted ? undefined : options.durability.settleTool(call),
|
|
576
646
|
onToolTelemetry: options.onToolTelemetry,
|
|
577
647
|
transformContext: options.transformContext,
|
|
578
648
|
});
|
|
@@ -582,6 +652,7 @@ export class PreparedPiTurnAdapter {
|
|
|
582
652
|
|
|
583
653
|
private readonly systemPrompt: string;
|
|
584
654
|
private readonly options: CreatePreparedPiTurnOptions;
|
|
655
|
+
private interrupted = false;
|
|
585
656
|
|
|
586
657
|
/**
|
|
587
658
|
* 请求取消这个 Turn。
|
|
@@ -595,6 +666,20 @@ export class PreparedPiTurnAdapter {
|
|
|
595
666
|
this.turn.abort();
|
|
596
667
|
}
|
|
597
668
|
|
|
669
|
+
/**
|
|
670
|
+
* 中断这个 Turn,把它交给持久续跑接手。
|
|
671
|
+
*
|
|
672
|
+
* @remarks
|
|
673
|
+
* 宿主在 park 期间换掉装配后调用。与 `abort` 的差别只有一处、但很要命:
|
|
674
|
+
* 中断途中冒出来的工具结算一律丢弃。这条执行器的系统提示和工具表锁在它被
|
|
675
|
+
* 创建的那一刻,接下来跑的是另一条按新装配重建的执行器,两边共用同一批
|
|
676
|
+
* `toolCallId` —— 让退场路上的假结果先落盘,新那条就再也纠正不过来了。
|
|
677
|
+
*/
|
|
678
|
+
interrupt(): void {
|
|
679
|
+
this.interrupted = true;
|
|
680
|
+
this.abort();
|
|
681
|
+
}
|
|
682
|
+
|
|
598
683
|
/**
|
|
599
684
|
* 把一条用户消息加入当前 Turn。
|
|
600
685
|
*
|
|
@@ -635,6 +720,21 @@ export class PreparedPiTurnAdapter {
|
|
|
635
720
|
: false;
|
|
636
721
|
}
|
|
637
722
|
|
|
723
|
+
/**
|
|
724
|
+
* 取回某个 Tool 的客户端结算声明,没有声明时返回 null。
|
|
725
|
+
*
|
|
726
|
+
* @remarks
|
|
727
|
+
* `Runtime.respondToolInteraction` 在把客户端响应映射成 ToolResult 前调用它。
|
|
728
|
+
*
|
|
729
|
+
* 声明里含函数,无法持久化;从 Pinned Runtime 的 candidate 现取,
|
|
730
|
+
* 可让「DO 一直醒着」和「park 期间睡过一觉」走同一条解析路径,而不是一条真解析、一条退化成缺省。
|
|
731
|
+
*/
|
|
732
|
+
interactionSpec(toolName: string): PiToolInteractionSpec | null {
|
|
733
|
+
return this.candidates.find(
|
|
734
|
+
(item) => item.tool.name === toolName,
|
|
735
|
+
)?.interaction ?? null;
|
|
736
|
+
}
|
|
737
|
+
|
|
638
738
|
/**
|
|
639
739
|
* 运行这个 Turn,直到 PiCore 和所有等待中的事件回调结束。
|
|
640
740
|
*
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from "../message";
|
|
9
9
|
import type { UIMessage } from "ai";
|
|
10
10
|
import { createModels, type MutableModels } from "@earendil-works/pi-ai";
|
|
11
|
-
import { configurePiModels, resolvePiApiKey } from "./models";
|
|
11
|
+
import { configurePiModels, createPiModels, resolvePiApiKey } from "./models";
|
|
12
12
|
export {
|
|
13
13
|
MODEL_STREAM_STALL_TIMEOUT_MS,
|
|
14
14
|
modelRequestUrl,
|
|
@@ -107,8 +107,11 @@ export class PiRuntimeAdapter {
|
|
|
107
107
|
* 实现理由:转换只生成 Pi UserMessage 的 content 和 timestamp,原始浏览器视图由 transcript sidecar 另行保存。
|
|
108
108
|
* 不要把 UI id、metadata 或原始 part 结构并入 canonical message。
|
|
109
109
|
*/
|
|
110
|
-
normalizeUserInput(
|
|
111
|
-
|
|
110
|
+
normalizeUserInput(
|
|
111
|
+
input: UIMessage & { role: "user" },
|
|
112
|
+
privateContext?: string,
|
|
113
|
+
) {
|
|
114
|
+
return uiUserMessageToPi(input, privateContext);
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
resolveApiKey(
|
|
@@ -129,6 +132,9 @@ export class PiRuntimeAdapter {
|
|
|
129
132
|
prepare(
|
|
130
133
|
options: PreparePiRuntimeOptions,
|
|
131
134
|
): Promise<PreparedPiRuntime> {
|
|
135
|
+
if (this.managesModels) {
|
|
136
|
+
createPiModels(options.snapshot.bindings.provider);
|
|
137
|
+
}
|
|
132
138
|
return preparePiRuntime(options, this.owner);
|
|
133
139
|
}
|
|
134
140
|
|
|
@@ -138,8 +144,7 @@ export class PiRuntimeAdapter {
|
|
|
138
144
|
* @remarks
|
|
139
145
|
* 调用方:Runtime 的 `initConfig()` 在 Prepared Runtime 和 revision 检查成功后、发布新快照前调用。
|
|
140
146
|
*
|
|
141
|
-
*
|
|
142
|
-
* 这解释了为什么一个未被选中的无效模型也会让配置启动失败;不要把激活误当成首次推理请求。
|
|
147
|
+
* 实现理由:`prepare()` 已校验所有 endpoint 模型,这里只把同一份部署目录切换到长期 Models 实例。
|
|
143
148
|
* 注入模型目录时跳过此步骤,以保留调用方对目录的配置权。
|
|
144
149
|
*/
|
|
145
150
|
activate(snapshot: PreparePiRuntimeOptions["snapshot"]): void {
|
|
@@ -207,9 +212,14 @@ export class PiRuntimeAdapter {
|
|
|
207
212
|
}
|
|
208
213
|
|
|
209
214
|
export type {
|
|
215
|
+
RequestedCapability,
|
|
210
216
|
UIChatRequestBody,
|
|
211
217
|
} from "../message";
|
|
212
218
|
export type { PiToolApproval } from "../turn";
|
|
219
|
+
export type {
|
|
220
|
+
PiToolInteraction,
|
|
221
|
+
PiToolInteractionCancelReason,
|
|
222
|
+
} from "../turn";
|
|
213
223
|
export type {
|
|
214
224
|
CreatePreparedPiTurnOptions,
|
|
215
225
|
PiCanonicalUserInput,
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
applyRecoveredPiApprovalDecision,
|
|
3
|
+
applyRecoveredPiToolInteractionSettlement,
|
|
3
4
|
commitPiRecoveryContinuation,
|
|
4
5
|
encodePiToolRecoveryMilestone,
|
|
5
6
|
planPiToolRecovery,
|
|
6
7
|
replayPiToolRecovery,
|
|
8
|
+
stagePiAssemblyRepin,
|
|
7
9
|
stagePiRecoveryContinuation,
|
|
8
10
|
type PiToolApproval,
|
|
11
|
+
type PiToolInteraction,
|
|
12
|
+
type PiToolInteractionCancelReason,
|
|
9
13
|
type PiToolRecoveryMilestone,
|
|
10
14
|
type PiToolRecoveryPlan,
|
|
11
15
|
type PiToolRecoveryState,
|
|
@@ -55,6 +59,32 @@ export type PiRecoveryCommand =
|
|
|
55
59
|
readonly kind: "record-approval";
|
|
56
60
|
readonly approval: PiToolApproval;
|
|
57
61
|
}
|
|
62
|
+
| {
|
|
63
|
+
readonly kind: "record-interaction";
|
|
64
|
+
readonly interaction: PiToolInteraction;
|
|
65
|
+
}
|
|
66
|
+
/** 停在人机等待期间换了装配:记下这次被承认的身份迁移。 */
|
|
67
|
+
| {
|
|
68
|
+
readonly kind: "repin-assembly";
|
|
69
|
+
readonly nextAssemblyRevision: string;
|
|
70
|
+
}
|
|
71
|
+
| {
|
|
72
|
+
readonly kind: "interaction-settlement";
|
|
73
|
+
readonly interactionId: string;
|
|
74
|
+
readonly settlement:
|
|
75
|
+
| {
|
|
76
|
+
readonly kind: "respond";
|
|
77
|
+
readonly response: unknown;
|
|
78
|
+
readonly result: {
|
|
79
|
+
readonly content: ToolResultMessage["content"];
|
|
80
|
+
readonly details: unknown;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
| {
|
|
84
|
+
readonly kind: "cancel";
|
|
85
|
+
readonly reason: PiToolInteractionCancelReason;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
58
88
|
| {
|
|
59
89
|
readonly kind: "record-tool-result";
|
|
60
90
|
readonly toolCallId: string;
|
|
@@ -109,6 +139,17 @@ export type PiDurableMutation =
|
|
|
109
139
|
readonly status: "approved" | "rejected";
|
|
110
140
|
readonly decidedAt: number;
|
|
111
141
|
readonly reason?: string;
|
|
142
|
+
}
|
|
143
|
+
| {
|
|
144
|
+
readonly kind: "record-interaction";
|
|
145
|
+
readonly interaction: PiToolInteraction;
|
|
146
|
+
}
|
|
147
|
+
| {
|
|
148
|
+
readonly kind: "settle-interaction";
|
|
149
|
+
readonly interactionId: string;
|
|
150
|
+
readonly status: "responded" | "cancelled";
|
|
151
|
+
readonly settledAt: number;
|
|
152
|
+
readonly responseJson?: string;
|
|
112
153
|
};
|
|
113
154
|
|
|
114
155
|
export type PiRecoveryEffect =
|
|
@@ -131,7 +172,11 @@ export type PiRecoveryEffect =
|
|
|
131
172
|
}
|
|
132
173
|
| {
|
|
133
174
|
readonly kind: "wait";
|
|
134
|
-
readonly reason?:
|
|
175
|
+
readonly reason?:
|
|
176
|
+
| "approval"
|
|
177
|
+
| "interaction"
|
|
178
|
+
| "uncertain-tool"
|
|
179
|
+
| "complete";
|
|
135
180
|
}
|
|
136
181
|
| { readonly kind: "resume-turn" };
|
|
137
182
|
|
|
@@ -198,6 +243,31 @@ function approvalExecutionId(continuationKey: string): string | null {
|
|
|
198
243
|
return match?.[1] ?? null;
|
|
199
244
|
}
|
|
200
245
|
|
|
246
|
+
// 把 Tool 自己的 settle 映射产出的内容包成一条权威 ToolResult。
|
|
247
|
+
// interaction-settlement 命令处理 respond 分支时调用它。
|
|
248
|
+
// 记录必须已经存在 —— 拿不到 toolCallId/toolName 就无法把结果绑回原调用,这时失败关闭而不是编一个 id。
|
|
249
|
+
function interactionToolResult(
|
|
250
|
+
interaction: PiToolInteraction | undefined,
|
|
251
|
+
result: {
|
|
252
|
+
readonly content: ToolResultMessage["content"];
|
|
253
|
+
readonly details: unknown;
|
|
254
|
+
},
|
|
255
|
+
timestamp: number,
|
|
256
|
+
): ToolResultMessage {
|
|
257
|
+
if (!interaction) {
|
|
258
|
+
throw new Error("Pi Tool interaction is missing for its response");
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
role: "toolResult",
|
|
262
|
+
toolCallId: interaction.toolCallId,
|
|
263
|
+
toolName: interaction.toolName,
|
|
264
|
+
content: result.content,
|
|
265
|
+
details: result.details,
|
|
266
|
+
isError: false,
|
|
267
|
+
timestamp,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
201
271
|
// 把纯恢复计划翻译成持久化操作和 Runtime 下一步动作。
|
|
202
272
|
// decidePiRecovery 在命令应用并重放最新状态后调用它。
|
|
203
273
|
// pending 续跑必须先形成里程碑并重放,再允许 dispatch,避免外部动作早于可恢复状态落盘。
|
|
@@ -223,6 +293,11 @@ function fromPlan(
|
|
|
223
293
|
mutations: [],
|
|
224
294
|
effect: { kind: "wait", reason: "approval" },
|
|
225
295
|
};
|
|
296
|
+
case "parked-interaction":
|
|
297
|
+
return {
|
|
298
|
+
mutations: [],
|
|
299
|
+
effect: { kind: "wait", reason: "interaction" },
|
|
300
|
+
};
|
|
226
301
|
case "park-uncertain-tool":
|
|
227
302
|
return {
|
|
228
303
|
mutations: [],
|
|
@@ -425,6 +500,112 @@ export function decidePiRecovery(
|
|
|
425
500
|
applied = true;
|
|
426
501
|
}
|
|
427
502
|
}
|
|
503
|
+
if (input.command.kind === "repin-assembly") {
|
|
504
|
+
const milestone = stagePiAssemblyRepin(
|
|
505
|
+
state,
|
|
506
|
+
input.command.nextAssemblyRevision,
|
|
507
|
+
input.now,
|
|
508
|
+
);
|
|
509
|
+
if (milestone) {
|
|
510
|
+
const mutation = milestoneMutation(
|
|
511
|
+
`assembly-repin:${input.command.nextAssemblyRevision}`,
|
|
512
|
+
milestone,
|
|
513
|
+
);
|
|
514
|
+
mutations.push(mutation);
|
|
515
|
+
bodies = [...bodies, mutation.body];
|
|
516
|
+
state = replayPiToolRecovery(bodies);
|
|
517
|
+
applied = true;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (input.command.kind === "record-interaction") {
|
|
521
|
+
const interaction = input.command.interaction;
|
|
522
|
+
const existing = state.interactions[interaction.interactionId];
|
|
523
|
+
if (existing) {
|
|
524
|
+
// 同一个 interactionId 必须描述同一次 Tool 调用,否则重放会把响应投给错的调用。
|
|
525
|
+
// 口径与 record-approval 的冲突检查一致。
|
|
526
|
+
if (
|
|
527
|
+
existing.requestId !== interaction.requestId ||
|
|
528
|
+
existing.toolCallId !== interaction.toolCallId ||
|
|
529
|
+
existing.toolName !== interaction.toolName ||
|
|
530
|
+
existing.inputJson !== interaction.inputJson
|
|
531
|
+
) {
|
|
532
|
+
throw new Error(
|
|
533
|
+
`Conflicting Pi Tool interaction: ${interaction.interactionId}`,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
const mutation = milestoneMutation(
|
|
538
|
+
`interaction:${interaction.interactionId}:${interaction.status}`,
|
|
539
|
+
{
|
|
540
|
+
...recoveryIdentity(input),
|
|
541
|
+
type: "interaction",
|
|
542
|
+
interaction,
|
|
543
|
+
},
|
|
544
|
+
);
|
|
545
|
+
mutations.push(
|
|
546
|
+
{ kind: "record-interaction", interaction },
|
|
547
|
+
mutation,
|
|
548
|
+
);
|
|
549
|
+
bodies = [...bodies, mutation.body];
|
|
550
|
+
state = replayPiToolRecovery(bodies);
|
|
551
|
+
applied = true;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (input.command.kind === "interaction-settlement") {
|
|
555
|
+
const command = input.command;
|
|
556
|
+
const settlement = applyRecoveredPiToolInteractionSettlement(
|
|
557
|
+
state,
|
|
558
|
+
command.settlement.kind === "respond"
|
|
559
|
+
? {
|
|
560
|
+
interactionId: command.interactionId,
|
|
561
|
+
kind: "respond",
|
|
562
|
+
response: command.settlement.response,
|
|
563
|
+
toolResult: interactionToolResult(
|
|
564
|
+
state.interactions[command.interactionId],
|
|
565
|
+
command.settlement.result,
|
|
566
|
+
input.now,
|
|
567
|
+
),
|
|
568
|
+
settledAt: input.now,
|
|
569
|
+
}
|
|
570
|
+
: {
|
|
571
|
+
interactionId: command.interactionId,
|
|
572
|
+
kind: "cancel",
|
|
573
|
+
reason: command.settlement.reason,
|
|
574
|
+
settledAt: input.now,
|
|
575
|
+
},
|
|
576
|
+
);
|
|
577
|
+
applied = settlement.outcome.kind !== "noop";
|
|
578
|
+
if (applied) {
|
|
579
|
+
const record = settlement.milestones.find(
|
|
580
|
+
(milestone) => milestone.type === "interaction",
|
|
581
|
+
);
|
|
582
|
+
if (!record || record.type !== "interaction") {
|
|
583
|
+
throw new Error("Pi interaction settlement milestone is missing");
|
|
584
|
+
}
|
|
585
|
+
mutations.push({
|
|
586
|
+
kind: "settle-interaction",
|
|
587
|
+
interactionId: record.interaction.interactionId,
|
|
588
|
+
status: record.interaction.status as "responded" | "cancelled",
|
|
589
|
+
settledAt: record.interaction.respondedAt ?? input.now,
|
|
590
|
+
...(record.interaction.responseJson === undefined
|
|
591
|
+
? {}
|
|
592
|
+
: { responseJson: record.interaction.responseJson }),
|
|
593
|
+
});
|
|
594
|
+
settlement.milestones.forEach((milestone, index) => {
|
|
595
|
+
mutations.push(milestoneMutation(
|
|
596
|
+
milestone.type === "interaction"
|
|
597
|
+
? `interaction:${command.interactionId}:${record.interaction.status}`
|
|
598
|
+
: `interaction-result:${command.interactionId}:${index}`,
|
|
599
|
+
milestone,
|
|
600
|
+
));
|
|
601
|
+
});
|
|
602
|
+
bodies = [
|
|
603
|
+
...bodies,
|
|
604
|
+
...settlement.milestones.map(encodePiToolRecoveryMilestone),
|
|
605
|
+
];
|
|
606
|
+
state = replayPiToolRecovery(bodies);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
428
609
|
if (input.command.kind === "record-tool-result") {
|
|
429
610
|
const command = input.command;
|
|
430
611
|
const toolResult: ToolResultMessage = {
|
|
@@ -649,6 +830,12 @@ export class PiRuntimeRecoveryAdapter
|
|
|
649
830
|
reason: "Pi Turn is waiting for Tool approval",
|
|
650
831
|
} as const;
|
|
651
832
|
}
|
|
833
|
+
if (decision.effect.reason === "interaction") {
|
|
834
|
+
return {
|
|
835
|
+
kind: "park",
|
|
836
|
+
reason: "Pi Turn is waiting for a client Tool interaction response",
|
|
837
|
+
} as const;
|
|
838
|
+
}
|
|
652
839
|
if (decision.effect.reason === "uncertain-tool") {
|
|
653
840
|
return {
|
|
654
841
|
kind: "park",
|