@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
|
@@ -9,18 +9,27 @@ import {
|
|
|
9
9
|
import type {
|
|
10
10
|
Api,
|
|
11
11
|
AssistantMessage,
|
|
12
|
+
Message,
|
|
12
13
|
Model,
|
|
13
14
|
Models,
|
|
14
15
|
ToolResultMessage,
|
|
15
16
|
UserMessage,
|
|
16
17
|
} from "@earendil-works/pi-ai";
|
|
17
|
-
import {
|
|
18
|
+
import { transformMessages } from "@earendil-works/pi-ai/api/transform-messages";
|
|
19
|
+
import {
|
|
20
|
+
parkPiToolApproval,
|
|
21
|
+
parkPiToolInteraction,
|
|
22
|
+
requiresPiToolApproval,
|
|
23
|
+
} from "../turn";
|
|
18
24
|
import { PiChunkEncoder } from "../message";
|
|
19
25
|
import type { UIMessageChunk } from "ai";
|
|
26
|
+
import { ChatStreamStalledError } from "agents/chat";
|
|
20
27
|
import {
|
|
21
28
|
compilePiTools,
|
|
22
29
|
createPiToolGovernance,
|
|
30
|
+
normalizeUpdatePlanArguments,
|
|
23
31
|
type PiToolCandidate,
|
|
32
|
+
type PiToolInteractionSpec,
|
|
24
33
|
type PiToolGovernance,
|
|
25
34
|
type PiToolTelemetry,
|
|
26
35
|
type SettledPiToolCall,
|
|
@@ -30,10 +39,29 @@ import {
|
|
|
30
39
|
readPinnedPiRuntime,
|
|
31
40
|
type PreparedPiRuntime,
|
|
32
41
|
} from "./assembly";
|
|
33
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
isRecoverableAssistantError,
|
|
44
|
+
isModelStreamStallMessage,
|
|
45
|
+
resolvePiApiKey,
|
|
46
|
+
withProviderRetry,
|
|
47
|
+
} from "./models";
|
|
48
|
+
import { EXECUTION_LEVELS } from "../../lib/execution-level";
|
|
34
49
|
|
|
35
50
|
// #region Single-run Pi bridge
|
|
36
51
|
|
|
52
|
+
const MAX_MODEL_TURNS = 30;
|
|
53
|
+
|
|
54
|
+
export class RetryableModelError extends Error {}
|
|
55
|
+
|
|
56
|
+
function normalizePlanToolCalls(message: AgentMessage): void {
|
|
57
|
+
if (message.role !== "assistant") return;
|
|
58
|
+
for (const part of message.content) {
|
|
59
|
+
if (part.type === "toolCall" && part.name === "update_plan") {
|
|
60
|
+
part.arguments = normalizeUpdatePlanArguments(part.arguments);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
37
65
|
/**
|
|
38
66
|
* 把 Pi 的终止原因翻译成本仓的回合结局。
|
|
39
67
|
*
|
|
@@ -70,6 +98,16 @@ function classifyPiStopReason(stopReason: string | undefined): {
|
|
|
70
98
|
}
|
|
71
99
|
}
|
|
72
100
|
|
|
101
|
+
function visibleAssistantContent(message: AssistantMessage) {
|
|
102
|
+
return message.content.filter((part) =>
|
|
103
|
+
part.type === "text"
|
|
104
|
+
? part.text.trim()
|
|
105
|
+
: part.type === "thinking"
|
|
106
|
+
? part.thinking.trim()
|
|
107
|
+
: false
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
73
111
|
interface PiTurnAdapterOptions {
|
|
74
112
|
readonly pi: {
|
|
75
113
|
readonly model: Model<Api>;
|
|
@@ -77,11 +115,11 @@ interface PiTurnAdapterOptions {
|
|
|
77
115
|
};
|
|
78
116
|
readonly models: Models;
|
|
79
117
|
readonly apiKey: string;
|
|
118
|
+
readonly modelSessionId: string;
|
|
80
119
|
// 读取这次执行真正要交给 Pi 的 canonical transcript。
|
|
81
120
|
// PiTurnAdapter.run 在创建 PiCore 前调用它,宿主应返回当时最新的 transcript。
|
|
82
121
|
// 这里保留为延迟读取,是为了避免适配器创建后新增的恢复消息被旧快照漏掉。
|
|
83
122
|
readonly canonicalMessages: () => Promise<readonly AgentMessage[]>;
|
|
84
|
-
readonly denyTools?: readonly string[];
|
|
85
123
|
// 把工具的最终结果交回宿主持久化。
|
|
86
124
|
// 共享工具编译器在向 Pi 返回成功或规范化失败前调用它。
|
|
87
125
|
// 这个先落库再发布结果的顺序是恢复边界,不能改成事后异步补写。
|
|
@@ -113,10 +151,9 @@ class PiTurnAdapter {
|
|
|
113
151
|
|
|
114
152
|
// 把 Tool candidate 变成 Pi 可以执行的受治理 Tool。
|
|
115
153
|
// 正常运行会编译 Pinned Runtime 的整份列表,恢复时只编译指定 Tool candidate。
|
|
116
|
-
//
|
|
154
|
+
// 输出限额和持久化结算统一走共享编译器,避免恢复另开旁路。
|
|
117
155
|
private compile(candidates: readonly PiToolCandidate[]) {
|
|
118
156
|
return compilePiTools(candidates, {
|
|
119
|
-
deny: this.opts.denyTools,
|
|
120
157
|
governance: this.governance,
|
|
121
158
|
settle: this.opts.settle,
|
|
122
159
|
});
|
|
@@ -129,10 +166,12 @@ class PiTurnAdapter {
|
|
|
129
166
|
candidate: PiToolCandidate,
|
|
130
167
|
toolCallId: string,
|
|
131
168
|
input: unknown,
|
|
169
|
+
signal?: AbortSignal,
|
|
132
170
|
): Promise<boolean> {
|
|
133
171
|
const [tool] = this.compile([candidate]);
|
|
134
172
|
if (!tool) return false;
|
|
135
|
-
|
|
173
|
+
signal?.throwIfAborted();
|
|
174
|
+
await tool.execute(toolCallId, input, signal);
|
|
136
175
|
return true;
|
|
137
176
|
}
|
|
138
177
|
|
|
@@ -162,19 +201,33 @@ class PiTurnAdapter {
|
|
|
162
201
|
readonly signal?: AbortSignal;
|
|
163
202
|
},
|
|
164
203
|
listener: (event: AgentEvent) => void | Promise<void>,
|
|
165
|
-
): Promise<
|
|
204
|
+
): Promise<boolean> {
|
|
166
205
|
const { systemPrompt, signal } = opts;
|
|
167
206
|
signal?.throwIfAborted();
|
|
168
207
|
const canonicalMessages = await this.opts.canonicalMessages();
|
|
169
208
|
const tools = this.compile(opts.tools);
|
|
209
|
+
let modelTurns = 0;
|
|
210
|
+
let reachedModelTurnLimit = false;
|
|
170
211
|
const pi = new PiCore({
|
|
171
212
|
convertToLlm,
|
|
172
213
|
streamFn: withProviderRetry(
|
|
173
214
|
this.opts.models.streamSimple.bind(this.opts.models),
|
|
215
|
+
0,
|
|
216
|
+
this.opts.modelSessionId,
|
|
174
217
|
),
|
|
175
218
|
getApiKey: () => this.opts.apiKey,
|
|
176
|
-
|
|
219
|
+
// transformMessages 在宿主上下文变换之后运行,确保跨 provider 的 tool call ID 格式兼容。
|
|
220
|
+
// OpenAI Responses API 生成含 `|` 的 450+ 字符 ID,Anthropic 只接受 ^[a-zA-Z0-9_-]+$(64 字符上限);
|
|
221
|
+
// 切换 provider 或消息跨 provider 回放时若不规范化,provider 会静默拒绝。
|
|
222
|
+
transformContext: async (messages, signal) => {
|
|
223
|
+
const ctx = await this.opts.transformContext(messages, signal);
|
|
224
|
+
return transformMessages(ctx as Message[], this.opts.pi.model) as AgentMessage[];
|
|
225
|
+
},
|
|
177
226
|
afterToolCall: this.governance.afterToolCall,
|
|
227
|
+
shouldStopAfterTurn: () => {
|
|
228
|
+
reachedModelTurnLimit = ++modelTurns >= MAX_MODEL_TURNS;
|
|
229
|
+
return signal?.aborted === true || reachedModelTurnLimit;
|
|
230
|
+
},
|
|
178
231
|
initialState: {
|
|
179
232
|
model: this.opts.pi.model,
|
|
180
233
|
systemPrompt,
|
|
@@ -203,6 +256,7 @@ class PiTurnAdapter {
|
|
|
203
256
|
pi.abort();
|
|
204
257
|
}
|
|
205
258
|
await running;
|
|
259
|
+
return reachedModelTurnLimit;
|
|
206
260
|
} finally {
|
|
207
261
|
signal?.removeEventListener("abort", abort);
|
|
208
262
|
unsubscribe();
|
|
@@ -260,6 +314,18 @@ export interface PiTurnDurability {
|
|
|
260
314
|
signal?: AbortSignal,
|
|
261
315
|
onCreated?: () => void | Promise<void>,
|
|
262
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>;
|
|
263
329
|
/**
|
|
264
330
|
* 记录工具输入,并返回它是否是第一次持久化尝试。
|
|
265
331
|
*
|
|
@@ -288,7 +354,6 @@ export interface PiTurnDurability {
|
|
|
288
354
|
*/
|
|
289
355
|
export interface CreatePreparedPiTurnOptions {
|
|
290
356
|
readonly prepared: PreparedPiRuntime;
|
|
291
|
-
readonly baseRevision: string;
|
|
292
357
|
readonly pinnedDescriptor: string;
|
|
293
358
|
readonly submission: {
|
|
294
359
|
readonly id: string;
|
|
@@ -391,6 +456,7 @@ export interface PiPreparedTurnRunOptions {
|
|
|
391
456
|
*/
|
|
392
457
|
export class PreparedPiTurnAdapter {
|
|
393
458
|
private readonly turn: PiTurnAdapter;
|
|
459
|
+
private readonly abortController = new AbortController();
|
|
394
460
|
private readonly candidates: readonly PiToolCandidate[];
|
|
395
461
|
private assistantOrdinal: number;
|
|
396
462
|
private readonly encoder: PiChunkEncoder;
|
|
@@ -415,7 +481,6 @@ export class PreparedPiTurnAdapter {
|
|
|
415
481
|
options.prepared,
|
|
416
482
|
dependencies.owner,
|
|
417
483
|
options.pinnedDescriptor,
|
|
418
|
-
options.baseRevision,
|
|
419
484
|
);
|
|
420
485
|
this.assistantOrdinal = options.submission.assistantOrdinal;
|
|
421
486
|
this.encoder = new PiChunkEncoder({
|
|
@@ -428,13 +493,21 @@ export class PreparedPiTurnAdapter {
|
|
|
428
493
|
...candidate.tool,
|
|
429
494
|
// 只在 Runtime 的持久化和策略门禁都放行后执行一个 Tool candidate。
|
|
430
495
|
// 实时运行由 PiCore 调用它,恢复或审批续跑则由 retryTool 进入同一路径。
|
|
431
|
-
//
|
|
496
|
+
// 顺序不能随便调整:门禁先于结果复用,审批可能生成结果,不确定的非幂等工作不能重放。
|
|
432
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
|
+
}
|
|
433
506
|
await state.snapshot.bindings.platform.gateTool?.({
|
|
434
507
|
toolCallId,
|
|
435
508
|
toolName: candidate.tool.name,
|
|
436
509
|
input,
|
|
437
|
-
requiredExecutionLevel
|
|
510
|
+
requiredExecutionLevel,
|
|
438
511
|
signal: signal ?? new AbortController().signal,
|
|
439
512
|
});
|
|
440
513
|
const settled = options.durability.findToolSettlement(toolCallId);
|
|
@@ -447,10 +520,13 @@ export class PreparedPiTurnAdapter {
|
|
|
447
520
|
}
|
|
448
521
|
return settled.result;
|
|
449
522
|
}
|
|
450
|
-
if (
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
523
|
+
if (
|
|
524
|
+
candidate.alwaysRequiresApproval ||
|
|
525
|
+
requiresPiToolApproval({
|
|
526
|
+
executionLevel: descriptor.executionLevel,
|
|
527
|
+
requiredExecutionLevel,
|
|
528
|
+
})
|
|
529
|
+
) {
|
|
454
530
|
const approval = parkPiToolApproval({
|
|
455
531
|
executionId: `${options.submission.id}:${toolCallId}`,
|
|
456
532
|
requestId: options.submission.requestId,
|
|
@@ -462,7 +538,7 @@ export class PreparedPiTurnAdapter {
|
|
|
462
538
|
candidate.tool.label ??
|
|
463
539
|
candidate.tool.name,
|
|
464
540
|
executionLevel: descriptor.executionLevel,
|
|
465
|
-
requiredExecutionLevel
|
|
541
|
+
requiredExecutionLevel,
|
|
466
542
|
inputJson: JSON.stringify(input),
|
|
467
543
|
createdAt: Date.now(),
|
|
468
544
|
}).approval;
|
|
@@ -492,6 +568,43 @@ export class PreparedPiTurnAdapter {
|
|
|
492
568
|
const afterApproval = options.durability
|
|
493
569
|
.findToolSettlement(toolCallId);
|
|
494
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
|
+
}
|
|
495
608
|
const retry = piToolRetryPolicy(candidate);
|
|
496
609
|
const firstAttempt = options.durability.appendToolInput({
|
|
497
610
|
toolCallId,
|
|
@@ -523,9 +636,13 @@ export class PreparedPiTurnAdapter {
|
|
|
523
636
|
state.snapshot.bindings.provider,
|
|
524
637
|
state.snapshot.pi.model.id,
|
|
525
638
|
),
|
|
639
|
+
modelSessionId: options.submission.requestId,
|
|
526
640
|
canonicalMessages: options.canonicalMessages,
|
|
527
|
-
|
|
528
|
-
|
|
641
|
+
// 被中断的 Turn 不写结算。它退场路上还会吐出「工具被中止」,而工具其实
|
|
642
|
+
// 没有产生任何结果 —— 结算是单调的,落了这条假结果,重建出来的续跑会
|
|
643
|
+
// 把一个从没跑过的工具当成跑过并失败。取消不走这里:取消要的就是终态。
|
|
644
|
+
settle: (call) =>
|
|
645
|
+
this.interrupted ? undefined : options.durability.settleTool(call),
|
|
529
646
|
onToolTelemetry: options.onToolTelemetry,
|
|
530
647
|
transformContext: options.transformContext,
|
|
531
648
|
});
|
|
@@ -535,6 +652,7 @@ export class PreparedPiTurnAdapter {
|
|
|
535
652
|
|
|
536
653
|
private readonly systemPrompt: string;
|
|
537
654
|
private readonly options: CreatePreparedPiTurnOptions;
|
|
655
|
+
private interrupted = false;
|
|
538
656
|
|
|
539
657
|
/**
|
|
540
658
|
* 请求取消这个 Turn。
|
|
@@ -544,9 +662,24 @@ export class PreparedPiTurnAdapter {
|
|
|
544
662
|
* 转交给 PiTurnAdapter 可保留启动前取消标记;run() 之前还没有可直接调用的 PiCore。
|
|
545
663
|
*/
|
|
546
664
|
abort(): void {
|
|
665
|
+
this.abortController.abort();
|
|
547
666
|
this.turn.abort();
|
|
548
667
|
}
|
|
549
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
|
+
|
|
550
683
|
/**
|
|
551
684
|
* 把一条用户消息加入当前 Turn。
|
|
552
685
|
*
|
|
@@ -582,10 +715,26 @@ export class PreparedPiTurnAdapter {
|
|
|
582
715
|
candidate,
|
|
583
716
|
request.toolCallId,
|
|
584
717
|
request.input,
|
|
718
|
+
this.abortController.signal,
|
|
585
719
|
)
|
|
586
720
|
: false;
|
|
587
721
|
}
|
|
588
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
|
+
|
|
589
738
|
/**
|
|
590
739
|
* 运行这个 Turn,直到 PiCore 和所有等待中的事件回调结束。
|
|
591
740
|
*
|
|
@@ -597,7 +746,7 @@ export class PreparedPiTurnAdapter {
|
|
|
597
746
|
options: PiPreparedTurnRunOptions,
|
|
598
747
|
): Promise<void> {
|
|
599
748
|
this.terminalIntent = undefined;
|
|
600
|
-
await this.turn.run(
|
|
749
|
+
const reachedModelTurnLimit = await this.turn.run(
|
|
601
750
|
{
|
|
602
751
|
systemPrompt: this.systemPrompt,
|
|
603
752
|
tools: this.candidates,
|
|
@@ -605,6 +754,12 @@ export class PreparedPiTurnAdapter {
|
|
|
605
754
|
},
|
|
606
755
|
(event) => this.handleEvent(event),
|
|
607
756
|
);
|
|
757
|
+
if (reachedModelTurnLimit && !this.terminalIntent) {
|
|
758
|
+
this.terminalIntent = {
|
|
759
|
+
outcome: "failed",
|
|
760
|
+
message: `Pi stopped after ${MAX_MODEL_TURNS} model turns`,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
608
763
|
if (this.terminalIntent) {
|
|
609
764
|
await this.options.onTerminal(this.terminalIntent);
|
|
610
765
|
}
|
|
@@ -614,9 +769,49 @@ export class PreparedPiTurnAdapter {
|
|
|
614
769
|
// PiTurnAdapter 会对每个 AgentEvent 调用它,PiCore 会等待该 Promise 后再越过订阅事件屏障。
|
|
615
770
|
// canonical message 先于流投影提交,恢复才不会依赖仅供浏览器消费的记录;调整顺序必须复核 recovery effect。
|
|
616
771
|
private async handleEvent(event: AgentEvent): Promise<void> {
|
|
772
|
+
if (
|
|
773
|
+
event.type === "message_start" ||
|
|
774
|
+
event.type === "message_update" ||
|
|
775
|
+
event.type === "message_end"
|
|
776
|
+
) {
|
|
777
|
+
normalizePlanToolCalls(event.message);
|
|
778
|
+
}
|
|
779
|
+
if (
|
|
780
|
+
event.type === "message_update" &&
|
|
781
|
+
event.assistantMessageEvent.type === "error" &&
|
|
782
|
+
(isModelStreamStallMessage(
|
|
783
|
+
event.assistantMessageEvent.error.errorMessage,
|
|
784
|
+
) ||
|
|
785
|
+
isRecoverableAssistantError(event.assistantMessageEvent.error))
|
|
786
|
+
) {
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
617
789
|
let projectedEvent = event;
|
|
618
790
|
if (event.type === "message_end") {
|
|
619
791
|
const message = event.message;
|
|
792
|
+
if (message.role === "assistant") {
|
|
793
|
+
const stalled = message.stopReason === "error" &&
|
|
794
|
+
isModelStreamStallMessage(message.errorMessage);
|
|
795
|
+
const retryable = isRecoverableAssistantError(message);
|
|
796
|
+
if (stalled || retryable) {
|
|
797
|
+
const content = visibleAssistantContent(message);
|
|
798
|
+
if (content.length > 0) {
|
|
799
|
+
this.assistantOrdinal += 1;
|
|
800
|
+
await this.options.onCanonicalMessage({
|
|
801
|
+
kind: "commit-turn",
|
|
802
|
+
ordinal: this.assistantOrdinal,
|
|
803
|
+
message: {
|
|
804
|
+
...message,
|
|
805
|
+
content,
|
|
806
|
+
},
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
if (stalled) {
|
|
810
|
+
throw new ChatStreamStalledError(message.errorMessage!);
|
|
811
|
+
}
|
|
812
|
+
throw new RetryableModelError(message.errorMessage);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
620
815
|
if (message.role === "user") {
|
|
621
816
|
await this.options.onCanonicalMessage({
|
|
622
817
|
kind: "append-user",
|
|
@@ -634,7 +829,7 @@ export class PreparedPiTurnAdapter {
|
|
|
634
829
|
abortReason
|
|
635
830
|
? {
|
|
636
831
|
...message,
|
|
637
|
-
content:
|
|
832
|
+
content: visibleAssistantContent(message),
|
|
638
833
|
stopReason: "aborted" as const,
|
|
639
834
|
errorMessage: abortReason,
|
|
640
835
|
}
|
|
@@ -2,13 +2,19 @@ import {
|
|
|
2
2
|
PreparedPiTurnAdapter,
|
|
3
3
|
type CreatePreparedPiTurnOptions,
|
|
4
4
|
} from "./execution";
|
|
5
|
+
export { RetryableModelError } from "./execution";
|
|
5
6
|
import {
|
|
6
7
|
uiUserMessageToPi,
|
|
7
8
|
} from "../message";
|
|
8
9
|
import type { UIMessage } from "ai";
|
|
9
10
|
import { createModels, type MutableModels } from "@earendil-works/pi-ai";
|
|
10
|
-
import { configurePiModels, resolvePiApiKey } from "./models";
|
|
11
|
-
export {
|
|
11
|
+
import { configurePiModels, createPiModels, resolvePiApiKey } from "./models";
|
|
12
|
+
export {
|
|
13
|
+
MODEL_STREAM_STALL_TIMEOUT_MS,
|
|
14
|
+
modelRequestUrl,
|
|
15
|
+
readModelStreamStallDetails,
|
|
16
|
+
withProviderRetry,
|
|
17
|
+
} from "./models";
|
|
12
18
|
import {
|
|
13
19
|
pinPiRuntime,
|
|
14
20
|
preparePiRuntime,
|
|
@@ -101,8 +107,11 @@ export class PiRuntimeAdapter {
|
|
|
101
107
|
* 实现理由:转换只生成 Pi UserMessage 的 content 和 timestamp,原始浏览器视图由 transcript sidecar 另行保存。
|
|
102
108
|
* 不要把 UI id、metadata 或原始 part 结构并入 canonical message。
|
|
103
109
|
*/
|
|
104
|
-
normalizeUserInput(
|
|
105
|
-
|
|
110
|
+
normalizeUserInput(
|
|
111
|
+
input: UIMessage & { role: "user" },
|
|
112
|
+
privateContext?: string,
|
|
113
|
+
) {
|
|
114
|
+
return uiUserMessageToPi(input, privateContext);
|
|
106
115
|
}
|
|
107
116
|
|
|
108
117
|
resolveApiKey(
|
|
@@ -123,6 +132,9 @@ export class PiRuntimeAdapter {
|
|
|
123
132
|
prepare(
|
|
124
133
|
options: PreparePiRuntimeOptions,
|
|
125
134
|
): Promise<PreparedPiRuntime> {
|
|
135
|
+
if (this.managesModels) {
|
|
136
|
+
createPiModels(options.snapshot.bindings.provider);
|
|
137
|
+
}
|
|
126
138
|
return preparePiRuntime(options, this.owner);
|
|
127
139
|
}
|
|
128
140
|
|
|
@@ -132,8 +144,7 @@ export class PiRuntimeAdapter {
|
|
|
132
144
|
* @remarks
|
|
133
145
|
* 调用方:Runtime 的 `initConfig()` 在 Prepared Runtime 和 revision 检查成功后、发布新快照前调用。
|
|
134
146
|
*
|
|
135
|
-
*
|
|
136
|
-
* 这解释了为什么一个未被选中的无效模型也会让配置启动失败;不要把激活误当成首次推理请求。
|
|
147
|
+
* 实现理由:`prepare()` 已校验所有 endpoint 模型,这里只把同一份部署目录切换到长期 Models 实例。
|
|
137
148
|
* 注入模型目录时跳过此步骤,以保留调用方对目录的配置权。
|
|
138
149
|
*/
|
|
139
150
|
activate(snapshot: PreparePiRuntimeOptions["snapshot"]): void {
|
|
@@ -148,7 +159,7 @@ export class PiRuntimeAdapter {
|
|
|
148
159
|
* @remarks
|
|
149
160
|
* 调用方:Runtime 的准入流程为每个新 Submission 调用,并把 descriptor 与其哈希一起持久化。
|
|
150
161
|
*
|
|
151
|
-
* 实现理由:memory、workspace 和 Extension context
|
|
162
|
+
* 实现理由:memory、workspace 和 Extension context 可能随时间变化,所以把准入时的 Turn 字段保存在 descriptor 中。
|
|
152
163
|
* 不要把 pin 推迟到 Turn 启动后,否则已持久化的 Submission 将失去可验证的执行基线。
|
|
153
164
|
*/
|
|
154
165
|
pin(options: PinPiRuntimeOptions): Promise<PinnedPiRuntime> {
|
|
@@ -187,7 +198,7 @@ export class PiRuntimeAdapter {
|
|
|
187
198
|
* @remarks
|
|
188
199
|
* 调用方:Runtime 在首次执行和恢复继续时各创建一个新 Turn,并提供 canonical messages、durable Tool 回调和流事件回调。
|
|
189
200
|
*
|
|
190
|
-
* 实现理由:构造时会校验 Prepared Runtime 的 owner
|
|
201
|
+
* 实现理由:构造时会校验 Prepared Runtime 的 owner 和 pinned descriptor 格式,再建立本 Turn 独享的 Tool governance 与执行状态。
|
|
191
202
|
* 不要跨 Turn 复用返回对象;abort、steer、assistant ordinal 和 Tool governance 都是单次执行状态。
|
|
192
203
|
*/
|
|
193
204
|
createTurn(
|
|
@@ -201,9 +212,14 @@ export class PiRuntimeAdapter {
|
|
|
201
212
|
}
|
|
202
213
|
|
|
203
214
|
export type {
|
|
215
|
+
RequestedCapability,
|
|
204
216
|
UIChatRequestBody,
|
|
205
217
|
} from "../message";
|
|
206
218
|
export type { PiToolApproval } from "../turn";
|
|
219
|
+
export type {
|
|
220
|
+
PiToolInteraction,
|
|
221
|
+
PiToolInteractionCancelReason,
|
|
222
|
+
} from "../turn";
|
|
207
223
|
export type {
|
|
208
224
|
CreatePreparedPiTurnOptions,
|
|
209
225
|
PiCanonicalUserInput,
|