@springbrand/agent-runtime 0.1.3-alpha.0 → 0.1.3-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/db/schema.ts +10 -1
- package/src/db/submission.repo.ts +34 -1
- package/src/index.ts +9 -11
- package/src/kernel/bindings.ts +27 -6
- package/src/kernel/extensions.ts +1 -1
- package/src/kernel/profile.ts +3 -4
- package/src/kernel/recoverable-chat-agent.ts +82 -4
- package/src/kernel/state.ts +4 -0
- package/src/kernel/submission-lifecycle.ts +3 -2
- package/src/layers/orchestration/temporary-agent/runner.ts +0 -65
- package/src/lib/prompt.ts +1 -1
- package/src/pi/assembly/context.ts +1 -3
- package/src/pi/assembly/snapshot.ts +17 -9
- package/src/pi/runtime-adapter/assembly.ts +25 -86
- package/src/pi/runtime-adapter/execution.ts +107 -12
- package/src/pi/runtime-adapter/index.ts +8 -3
- package/src/pi/runtime-adapter/models.ts +221 -29
- package/src/pi/runtime-adapter/transcript.ts +61 -3
- package/src/pi/tool/ai-adapter.ts +58 -1
- package/src/pi/tool/base.ts +112 -2
- package/src/pi/tool/compiler.ts +5 -32
- package/src/pi/tool/core-host.ts +28 -30
- package/src/pi/tool/core.ts +25 -122
- package/src/pi/tool/mcp.ts +2 -2
- package/src/pi/tool/schedule.ts +46 -2
- package/src/pi/tool/skill.ts +112 -420
- package/src/pi/tool/web-fetch.ts +282 -0
- package/src/pi/tool/web-search/api.ts +34 -18
- package/src/pi/tool/workspace-sandbox.ts +92 -258
- package/src/plugins.ts +358 -531
- package/src/runtime.ts +195 -44
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
type PiLoadedExtension,
|
|
20
20
|
} from "../assembly";
|
|
21
21
|
import {
|
|
22
|
-
compilePiTools,
|
|
23
22
|
createPiMcpToolCandidates,
|
|
24
23
|
listExtensionsPiToolCandidate,
|
|
25
24
|
type PiMcpHost,
|
|
@@ -41,7 +40,7 @@ import {
|
|
|
41
40
|
interface PiAssemblySnapshot {
|
|
42
41
|
readonly pi: Pick<
|
|
43
42
|
PiRuntimeAssembly,
|
|
44
|
-
"extensions" | "
|
|
43
|
+
"extensions" | "toolSurface"
|
|
45
44
|
>;
|
|
46
45
|
readonly profile: Pick<
|
|
47
46
|
RuntimeProfile,
|
|
@@ -75,10 +74,7 @@ interface PreparedPiAssembly {
|
|
|
75
74
|
readonly loaded: readonly PiLoadedExtension[];
|
|
76
75
|
readonly context: readonly PiExtensionContextContribution[];
|
|
77
76
|
readonly degradations: PiExtensionAssembly["degradations"];
|
|
78
|
-
|
|
79
|
-
// preparePiAssembly 用它预检,preparePiRuntime 再读取并冻结最终列表。
|
|
80
|
-
// MCP 就绪状态由 Host 提供,因此这里不能只拼接初始化时的静态数组。
|
|
81
|
-
toolCandidates(): PiToolCandidate[];
|
|
77
|
+
readonly candidates: readonly PiToolCandidate[];
|
|
82
78
|
}
|
|
83
79
|
|
|
84
80
|
/**
|
|
@@ -164,7 +160,7 @@ export interface PinnedPiRuntime {
|
|
|
164
160
|
* @remarks
|
|
165
161
|
* `PreparedPiTurnAdapter` 在重建 Turn 前读取这个 JSON 结构。
|
|
166
162
|
*
|
|
167
|
-
*
|
|
163
|
+
* 基础版本作为准入时元数据保留,创建 Turn 时不与当前 Runtime 比较。
|
|
168
164
|
*/
|
|
169
165
|
export interface PinnedPiRuntimeDescriptor {
|
|
170
166
|
readonly version: 1;
|
|
@@ -204,6 +200,8 @@ const IDEMPOTENT_TOOL_NAMES = new Set([
|
|
|
204
200
|
"browser_markdown",
|
|
205
201
|
"browser_scrape",
|
|
206
202
|
"bind_resource",
|
|
203
|
+
"delete",
|
|
204
|
+
"edit",
|
|
207
205
|
"find",
|
|
208
206
|
"get_time",
|
|
209
207
|
"grep",
|
|
@@ -215,6 +213,7 @@ const IDEMPOTENT_TOOL_NAMES = new Set([
|
|
|
215
213
|
"read_skill_resource",
|
|
216
214
|
"sandbox_process_logs",
|
|
217
215
|
"web_search",
|
|
216
|
+
"write",
|
|
218
217
|
"unbind_resource",
|
|
219
218
|
]);
|
|
220
219
|
|
|
@@ -255,7 +254,7 @@ function describeTools(candidates: readonly PiToolCandidate[]) {
|
|
|
255
254
|
|
|
256
255
|
// 把本模块认定会影响版本的准备结果序列化出来。
|
|
257
256
|
// preparePiRuntime 创建一次,Runtime 接受快照前再对结果求哈希。
|
|
258
|
-
//
|
|
257
|
+
// 集合排序保证等价输入稳定;增删字段会改变版本哈希。
|
|
259
258
|
function describeRuntime(
|
|
260
259
|
snapshot: RuntimeSnapshot,
|
|
261
260
|
candidates: readonly PiToolCandidate[],
|
|
@@ -338,18 +337,17 @@ export function readPreparedPiRuntime(
|
|
|
338
337
|
}
|
|
339
338
|
|
|
340
339
|
/**
|
|
341
|
-
*
|
|
340
|
+
* 读取已存的固定描述。
|
|
342
341
|
*
|
|
343
342
|
* @remarks
|
|
344
343
|
* `PreparedPiTurnAdapter` 在新建或恢复 Pi Turn 前调用它。
|
|
345
344
|
*
|
|
346
|
-
*
|
|
345
|
+
* Prepared Runtime 提供当前执行能力,descriptor 仅保留 Submission 固定的 Turn 字段。
|
|
347
346
|
*/
|
|
348
347
|
export function readPinnedPiRuntime(
|
|
349
348
|
prepared: PreparedPiRuntime,
|
|
350
349
|
owner: object,
|
|
351
350
|
descriptorBody: string,
|
|
352
|
-
baseRevision: string,
|
|
353
351
|
): {
|
|
354
352
|
readonly state: PreparedPiRuntimeState;
|
|
355
353
|
readonly descriptor: PinnedPiRuntimeDescriptor;
|
|
@@ -365,24 +363,11 @@ export function readPinnedPiRuntime(
|
|
|
365
363
|
throw new Error("Pinned Runtime assembly descriptor is invalid");
|
|
366
364
|
}
|
|
367
365
|
const descriptor = value as PinnedPiRuntimeDescriptor;
|
|
368
|
-
const expected = JSON.parse(prepared.revisionDescriptor) as Record<
|
|
369
|
-
string,
|
|
370
|
-
unknown
|
|
371
|
-
>;
|
|
372
|
-
const actual = descriptor as unknown as Record<string, unknown>;
|
|
373
|
-
const incompatible = Object.keys(expected).some((key) =>
|
|
374
|
-
key !== "systemPrompt" &&
|
|
375
|
-
JSON.stringify(actual[key]) !== JSON.stringify(expected[key])
|
|
376
|
-
);
|
|
377
366
|
if (
|
|
378
367
|
descriptor.version !== 1 ||
|
|
379
|
-
descriptor.
|
|
380
|
-
typeof descriptor.systemPrompt !== "string" ||
|
|
381
|
-
incompatible
|
|
368
|
+
typeof descriptor.systemPrompt !== "string"
|
|
382
369
|
) {
|
|
383
|
-
throw new Error(
|
|
384
|
-
"Pinned Runtime revision is unavailable for Pi recovery",
|
|
385
|
-
);
|
|
370
|
+
throw new Error("Pinned Runtime assembly descriptor is invalid");
|
|
386
371
|
}
|
|
387
372
|
return { state, descriptor };
|
|
388
373
|
}
|
|
@@ -391,54 +376,7 @@ export function readPinnedPiRuntime(
|
|
|
391
376
|
|
|
392
377
|
// #region 扩展与工具装配
|
|
393
378
|
|
|
394
|
-
|
|
395
|
-
// 把准备快照、已加载扩展和 MCP Host 放在一个装配对象里。
|
|
396
|
-
// preparePiAssembly 在扩展加载完成后构造它。
|
|
397
|
-
// 这里保留源对象而不复制列表,让每个访问器都读取同一份结果。
|
|
398
|
-
constructor(
|
|
399
|
-
private readonly snapshot: PiAssemblySnapshot,
|
|
400
|
-
private readonly extensions: PiExtensionAssembly,
|
|
401
|
-
private readonly mcpHost: PiMcpHost,
|
|
402
|
-
) {}
|
|
403
|
-
|
|
404
|
-
// 返回成功加载的扩展。
|
|
405
|
-
// preparePiRuntime 在生成版本描述时读取这个列表。
|
|
406
|
-
// 直接代理扩展装配,避免维护可能漂移的第二份列表。
|
|
407
|
-
get loaded(): readonly PiLoadedExtension[] {
|
|
408
|
-
return this.extensions.loaded;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// 返回已加载扩展声明的上下文贡献。
|
|
412
|
-
// pinPiRuntime 在提交准入时把它们交给系统上下文装配器。
|
|
413
|
-
// 上下文值在固定阶段才读取,所以不能提前写死在准备阶段的提示词中。
|
|
414
|
-
get context(): readonly PiExtensionContextContribution[] {
|
|
415
|
-
return this.extensions.context;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// 返回准备阶段被降级处理、而不是继续抛出的扩展失败。
|
|
419
|
-
// preparePiRuntime 把它们与快照降级合并,用于报告 Runtime 加载结果。
|
|
420
|
-
// 直接代理可保留扩展装配器生成的原始降级记录。
|
|
421
|
-
get degradations(): PiExtensionAssembly["degradations"] {
|
|
422
|
-
return this.extensions.degradations;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// 汇总内置、扩展列表、扩展和当前已就绪的 MCP 工具。
|
|
426
|
-
// 准备流程先调用它做校验,再冻结一份列表供后续 Turn 使用。
|
|
427
|
-
// 每次重新生成是因为 MCP 候选来自 Host 当前的连接状态。
|
|
428
|
-
toolCandidates(): PiToolCandidate[] {
|
|
429
|
-
return [
|
|
430
|
-
...this.snapshot.pi.toolCandidates,
|
|
431
|
-
...this.extensions.candidates,
|
|
432
|
-
listExtensionsPiToolCandidate(this.extensions.loaded),
|
|
433
|
-
...createPiMcpToolCandidates(
|
|
434
|
-
this.mcpHost,
|
|
435
|
-
this.snapshot.profile.mcpServers,
|
|
436
|
-
),
|
|
437
|
-
];
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// 加载已启用扩展,并确认汇总后的工具可以正常编译。
|
|
379
|
+
// 加载已启用扩展,并把 Extension/MCP 候选交给唯一 Tool Surface 最终化。
|
|
442
380
|
// preparePiRuntime 为每份待配置的 Runtime 快照调用它一次。
|
|
443
381
|
// 工作区能力在共享加载边界校验,单个扩展加载失败则保留为降级结果。
|
|
444
382
|
async function preparePiAssembly(
|
|
@@ -469,18 +407,19 @@ async function preparePiAssembly(
|
|
|
469
407
|
});
|
|
470
408
|
},
|
|
471
409
|
});
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
extensions,
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
410
|
+
return Object.freeze({
|
|
411
|
+
loaded: extensions.loaded,
|
|
412
|
+
context: extensions.context,
|
|
413
|
+
degradations: extensions.degradations,
|
|
414
|
+
candidates: snapshot.pi.toolSurface.finalize([
|
|
415
|
+
...extensions.candidates,
|
|
416
|
+
listExtensionsPiToolCandidate(extensions.loaded),
|
|
417
|
+
...createPiMcpToolCandidates(
|
|
418
|
+
options.mcpHost,
|
|
419
|
+
snapshot.profile.mcpServers,
|
|
420
|
+
),
|
|
421
|
+
]),
|
|
482
422
|
});
|
|
483
|
-
return prepared;
|
|
484
423
|
}
|
|
485
424
|
|
|
486
425
|
// #endregion
|
|
@@ -500,7 +439,7 @@ export async function preparePiRuntime(
|
|
|
500
439
|
owner: object,
|
|
501
440
|
): Promise<PreparedPiRuntime> {
|
|
502
441
|
const assembly = await preparePiAssembly(options);
|
|
503
|
-
const candidates = assembly.
|
|
442
|
+
const candidates = assembly.candidates;
|
|
504
443
|
return Object.freeze(new PreparedRuntime(
|
|
505
444
|
describeRuntime(options.snapshot, candidates, assembly.loaded),
|
|
506
445
|
Object.freeze([
|
|
@@ -9,17 +9,21 @@ 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";
|
|
18
|
+
import { transformMessages } from "@earendil-works/pi-ai/api/transform-messages";
|
|
17
19
|
import { requiresPiToolApproval, parkPiToolApproval } from "../turn";
|
|
18
20
|
import { PiChunkEncoder } from "../message";
|
|
19
21
|
import type { UIMessageChunk } from "ai";
|
|
22
|
+
import { ChatStreamStalledError } from "agents/chat";
|
|
20
23
|
import {
|
|
21
24
|
compilePiTools,
|
|
22
25
|
createPiToolGovernance,
|
|
26
|
+
normalizeUpdatePlanArguments,
|
|
23
27
|
type PiToolCandidate,
|
|
24
28
|
type PiToolGovernance,
|
|
25
29
|
type PiToolTelemetry,
|
|
@@ -30,10 +34,28 @@ import {
|
|
|
30
34
|
readPinnedPiRuntime,
|
|
31
35
|
type PreparedPiRuntime,
|
|
32
36
|
} from "./assembly";
|
|
33
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
isRecoverableAssistantError,
|
|
39
|
+
isModelStreamStallMessage,
|
|
40
|
+
resolvePiApiKey,
|
|
41
|
+
withProviderRetry,
|
|
42
|
+
} from "./models";
|
|
34
43
|
|
|
35
44
|
// #region Single-run Pi bridge
|
|
36
45
|
|
|
46
|
+
const MAX_MODEL_TURNS = 30;
|
|
47
|
+
|
|
48
|
+
export class RetryableModelError extends Error {}
|
|
49
|
+
|
|
50
|
+
function normalizePlanToolCalls(message: AgentMessage): void {
|
|
51
|
+
if (message.role !== "assistant") return;
|
|
52
|
+
for (const part of message.content) {
|
|
53
|
+
if (part.type === "toolCall" && part.name === "update_plan") {
|
|
54
|
+
part.arguments = normalizeUpdatePlanArguments(part.arguments);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
37
59
|
/**
|
|
38
60
|
* 把 Pi 的终止原因翻译成本仓的回合结局。
|
|
39
61
|
*
|
|
@@ -70,6 +92,16 @@ function classifyPiStopReason(stopReason: string | undefined): {
|
|
|
70
92
|
}
|
|
71
93
|
}
|
|
72
94
|
|
|
95
|
+
function visibleAssistantContent(message: AssistantMessage) {
|
|
96
|
+
return message.content.filter((part) =>
|
|
97
|
+
part.type === "text"
|
|
98
|
+
? part.text.trim()
|
|
99
|
+
: part.type === "thinking"
|
|
100
|
+
? part.thinking.trim()
|
|
101
|
+
: false
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
73
105
|
interface PiTurnAdapterOptions {
|
|
74
106
|
readonly pi: {
|
|
75
107
|
readonly model: Model<Api>;
|
|
@@ -77,11 +109,11 @@ interface PiTurnAdapterOptions {
|
|
|
77
109
|
};
|
|
78
110
|
readonly models: Models;
|
|
79
111
|
readonly apiKey: string;
|
|
112
|
+
readonly modelSessionId: string;
|
|
80
113
|
// 读取这次执行真正要交给 Pi 的 canonical transcript。
|
|
81
114
|
// PiTurnAdapter.run 在创建 PiCore 前调用它,宿主应返回当时最新的 transcript。
|
|
82
115
|
// 这里保留为延迟读取,是为了避免适配器创建后新增的恢复消息被旧快照漏掉。
|
|
83
116
|
readonly canonicalMessages: () => Promise<readonly AgentMessage[]>;
|
|
84
|
-
readonly denyTools?: readonly string[];
|
|
85
117
|
// 把工具的最终结果交回宿主持久化。
|
|
86
118
|
// 共享工具编译器在向 Pi 返回成功或规范化失败前调用它。
|
|
87
119
|
// 这个先落库再发布结果的顺序是恢复边界,不能改成事后异步补写。
|
|
@@ -113,10 +145,9 @@ class PiTurnAdapter {
|
|
|
113
145
|
|
|
114
146
|
// 把 Tool candidate 变成 Pi 可以执行的受治理 Tool。
|
|
115
147
|
// 正常运行会编译 Pinned Runtime 的整份列表,恢复时只编译指定 Tool candidate。
|
|
116
|
-
//
|
|
148
|
+
// 输出限额和持久化结算统一走共享编译器,避免恢复另开旁路。
|
|
117
149
|
private compile(candidates: readonly PiToolCandidate[]) {
|
|
118
150
|
return compilePiTools(candidates, {
|
|
119
|
-
deny: this.opts.denyTools,
|
|
120
151
|
governance: this.governance,
|
|
121
152
|
settle: this.opts.settle,
|
|
122
153
|
});
|
|
@@ -129,10 +160,12 @@ class PiTurnAdapter {
|
|
|
129
160
|
candidate: PiToolCandidate,
|
|
130
161
|
toolCallId: string,
|
|
131
162
|
input: unknown,
|
|
163
|
+
signal?: AbortSignal,
|
|
132
164
|
): Promise<boolean> {
|
|
133
165
|
const [tool] = this.compile([candidate]);
|
|
134
166
|
if (!tool) return false;
|
|
135
|
-
|
|
167
|
+
signal?.throwIfAborted();
|
|
168
|
+
await tool.execute(toolCallId, input, signal);
|
|
136
169
|
return true;
|
|
137
170
|
}
|
|
138
171
|
|
|
@@ -162,19 +195,33 @@ class PiTurnAdapter {
|
|
|
162
195
|
readonly signal?: AbortSignal;
|
|
163
196
|
},
|
|
164
197
|
listener: (event: AgentEvent) => void | Promise<void>,
|
|
165
|
-
): Promise<
|
|
198
|
+
): Promise<boolean> {
|
|
166
199
|
const { systemPrompt, signal } = opts;
|
|
167
200
|
signal?.throwIfAborted();
|
|
168
201
|
const canonicalMessages = await this.opts.canonicalMessages();
|
|
169
202
|
const tools = this.compile(opts.tools);
|
|
203
|
+
let modelTurns = 0;
|
|
204
|
+
let reachedModelTurnLimit = false;
|
|
170
205
|
const pi = new PiCore({
|
|
171
206
|
convertToLlm,
|
|
172
207
|
streamFn: withProviderRetry(
|
|
173
208
|
this.opts.models.streamSimple.bind(this.opts.models),
|
|
209
|
+
0,
|
|
210
|
+
this.opts.modelSessionId,
|
|
174
211
|
),
|
|
175
212
|
getApiKey: () => this.opts.apiKey,
|
|
176
|
-
|
|
213
|
+
// transformMessages 在宿主上下文变换之后运行,确保跨 provider 的 tool call ID 格式兼容。
|
|
214
|
+
// OpenAI Responses API 生成含 `|` 的 450+ 字符 ID,Anthropic 只接受 ^[a-zA-Z0-9_-]+$(64 字符上限);
|
|
215
|
+
// 切换 provider 或消息跨 provider 回放时若不规范化,provider 会静默拒绝。
|
|
216
|
+
transformContext: async (messages, signal) => {
|
|
217
|
+
const ctx = await this.opts.transformContext(messages, signal);
|
|
218
|
+
return transformMessages(ctx as Message[], this.opts.pi.model) as AgentMessage[];
|
|
219
|
+
},
|
|
177
220
|
afterToolCall: this.governance.afterToolCall,
|
|
221
|
+
shouldStopAfterTurn: () => {
|
|
222
|
+
reachedModelTurnLimit = ++modelTurns >= MAX_MODEL_TURNS;
|
|
223
|
+
return signal?.aborted === true || reachedModelTurnLimit;
|
|
224
|
+
},
|
|
178
225
|
initialState: {
|
|
179
226
|
model: this.opts.pi.model,
|
|
180
227
|
systemPrompt,
|
|
@@ -203,6 +250,7 @@ class PiTurnAdapter {
|
|
|
203
250
|
pi.abort();
|
|
204
251
|
}
|
|
205
252
|
await running;
|
|
253
|
+
return reachedModelTurnLimit;
|
|
206
254
|
} finally {
|
|
207
255
|
signal?.removeEventListener("abort", abort);
|
|
208
256
|
unsubscribe();
|
|
@@ -288,7 +336,6 @@ export interface PiTurnDurability {
|
|
|
288
336
|
*/
|
|
289
337
|
export interface CreatePreparedPiTurnOptions {
|
|
290
338
|
readonly prepared: PreparedPiRuntime;
|
|
291
|
-
readonly baseRevision: string;
|
|
292
339
|
readonly pinnedDescriptor: string;
|
|
293
340
|
readonly submission: {
|
|
294
341
|
readonly id: string;
|
|
@@ -391,6 +438,7 @@ export interface PiPreparedTurnRunOptions {
|
|
|
391
438
|
*/
|
|
392
439
|
export class PreparedPiTurnAdapter {
|
|
393
440
|
private readonly turn: PiTurnAdapter;
|
|
441
|
+
private readonly abortController = new AbortController();
|
|
394
442
|
private readonly candidates: readonly PiToolCandidate[];
|
|
395
443
|
private assistantOrdinal: number;
|
|
396
444
|
private readonly encoder: PiChunkEncoder;
|
|
@@ -415,7 +463,6 @@ export class PreparedPiTurnAdapter {
|
|
|
415
463
|
options.prepared,
|
|
416
464
|
dependencies.owner,
|
|
417
465
|
options.pinnedDescriptor,
|
|
418
|
-
options.baseRevision,
|
|
419
466
|
);
|
|
420
467
|
this.assistantOrdinal = options.submission.assistantOrdinal;
|
|
421
468
|
this.encoder = new PiChunkEncoder({
|
|
@@ -523,8 +570,8 @@ export class PreparedPiTurnAdapter {
|
|
|
523
570
|
state.snapshot.bindings.provider,
|
|
524
571
|
state.snapshot.pi.model.id,
|
|
525
572
|
),
|
|
573
|
+
modelSessionId: options.submission.requestId,
|
|
526
574
|
canonicalMessages: options.canonicalMessages,
|
|
527
|
-
denyTools: descriptor.deny,
|
|
528
575
|
settle: options.durability.settleTool,
|
|
529
576
|
onToolTelemetry: options.onToolTelemetry,
|
|
530
577
|
transformContext: options.transformContext,
|
|
@@ -544,6 +591,7 @@ export class PreparedPiTurnAdapter {
|
|
|
544
591
|
* 转交给 PiTurnAdapter 可保留启动前取消标记;run() 之前还没有可直接调用的 PiCore。
|
|
545
592
|
*/
|
|
546
593
|
abort(): void {
|
|
594
|
+
this.abortController.abort();
|
|
547
595
|
this.turn.abort();
|
|
548
596
|
}
|
|
549
597
|
|
|
@@ -582,6 +630,7 @@ export class PreparedPiTurnAdapter {
|
|
|
582
630
|
candidate,
|
|
583
631
|
request.toolCallId,
|
|
584
632
|
request.input,
|
|
633
|
+
this.abortController.signal,
|
|
585
634
|
)
|
|
586
635
|
: false;
|
|
587
636
|
}
|
|
@@ -597,7 +646,7 @@ export class PreparedPiTurnAdapter {
|
|
|
597
646
|
options: PiPreparedTurnRunOptions,
|
|
598
647
|
): Promise<void> {
|
|
599
648
|
this.terminalIntent = undefined;
|
|
600
|
-
await this.turn.run(
|
|
649
|
+
const reachedModelTurnLimit = await this.turn.run(
|
|
601
650
|
{
|
|
602
651
|
systemPrompt: this.systemPrompt,
|
|
603
652
|
tools: this.candidates,
|
|
@@ -605,6 +654,12 @@ export class PreparedPiTurnAdapter {
|
|
|
605
654
|
},
|
|
606
655
|
(event) => this.handleEvent(event),
|
|
607
656
|
);
|
|
657
|
+
if (reachedModelTurnLimit && !this.terminalIntent) {
|
|
658
|
+
this.terminalIntent = {
|
|
659
|
+
outcome: "failed",
|
|
660
|
+
message: `Pi stopped after ${MAX_MODEL_TURNS} model turns`,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
608
663
|
if (this.terminalIntent) {
|
|
609
664
|
await this.options.onTerminal(this.terminalIntent);
|
|
610
665
|
}
|
|
@@ -614,9 +669,49 @@ export class PreparedPiTurnAdapter {
|
|
|
614
669
|
// PiTurnAdapter 会对每个 AgentEvent 调用它,PiCore 会等待该 Promise 后再越过订阅事件屏障。
|
|
615
670
|
// canonical message 先于流投影提交,恢复才不会依赖仅供浏览器消费的记录;调整顺序必须复核 recovery effect。
|
|
616
671
|
private async handleEvent(event: AgentEvent): Promise<void> {
|
|
672
|
+
if (
|
|
673
|
+
event.type === "message_start" ||
|
|
674
|
+
event.type === "message_update" ||
|
|
675
|
+
event.type === "message_end"
|
|
676
|
+
) {
|
|
677
|
+
normalizePlanToolCalls(event.message);
|
|
678
|
+
}
|
|
679
|
+
if (
|
|
680
|
+
event.type === "message_update" &&
|
|
681
|
+
event.assistantMessageEvent.type === "error" &&
|
|
682
|
+
(isModelStreamStallMessage(
|
|
683
|
+
event.assistantMessageEvent.error.errorMessage,
|
|
684
|
+
) ||
|
|
685
|
+
isRecoverableAssistantError(event.assistantMessageEvent.error))
|
|
686
|
+
) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
617
689
|
let projectedEvent = event;
|
|
618
690
|
if (event.type === "message_end") {
|
|
619
691
|
const message = event.message;
|
|
692
|
+
if (message.role === "assistant") {
|
|
693
|
+
const stalled = message.stopReason === "error" &&
|
|
694
|
+
isModelStreamStallMessage(message.errorMessage);
|
|
695
|
+
const retryable = isRecoverableAssistantError(message);
|
|
696
|
+
if (stalled || retryable) {
|
|
697
|
+
const content = visibleAssistantContent(message);
|
|
698
|
+
if (content.length > 0) {
|
|
699
|
+
this.assistantOrdinal += 1;
|
|
700
|
+
await this.options.onCanonicalMessage({
|
|
701
|
+
kind: "commit-turn",
|
|
702
|
+
ordinal: this.assistantOrdinal,
|
|
703
|
+
message: {
|
|
704
|
+
...message,
|
|
705
|
+
content,
|
|
706
|
+
},
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
if (stalled) {
|
|
710
|
+
throw new ChatStreamStalledError(message.errorMessage!);
|
|
711
|
+
}
|
|
712
|
+
throw new RetryableModelError(message.errorMessage);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
620
715
|
if (message.role === "user") {
|
|
621
716
|
await this.options.onCanonicalMessage({
|
|
622
717
|
kind: "append-user",
|
|
@@ -634,7 +729,7 @@ export class PreparedPiTurnAdapter {
|
|
|
634
729
|
abortReason
|
|
635
730
|
? {
|
|
636
731
|
...message,
|
|
637
|
-
content:
|
|
732
|
+
content: visibleAssistantContent(message),
|
|
638
733
|
stopReason: "aborted" as const,
|
|
639
734
|
errorMessage: abortReason,
|
|
640
735
|
}
|
|
@@ -2,13 +2,18 @@ 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
11
|
import { configurePiModels, resolvePiApiKey } from "./models";
|
|
11
|
-
export {
|
|
12
|
+
export {
|
|
13
|
+
MODEL_STREAM_STALL_TIMEOUT_MS,
|
|
14
|
+
readModelStreamStallDetails,
|
|
15
|
+
withProviderRetry,
|
|
16
|
+
} from "./models";
|
|
12
17
|
import {
|
|
13
18
|
pinPiRuntime,
|
|
14
19
|
preparePiRuntime,
|
|
@@ -148,7 +153,7 @@ export class PiRuntimeAdapter {
|
|
|
148
153
|
* @remarks
|
|
149
154
|
* 调用方:Runtime 的准入流程为每个新 Submission 调用,并把 descriptor 与其哈希一起持久化。
|
|
150
155
|
*
|
|
151
|
-
* 实现理由:memory、workspace 和 Extension context
|
|
156
|
+
* 实现理由:memory、workspace 和 Extension context 可能随时间变化,所以把准入时的 Turn 字段保存在 descriptor 中。
|
|
152
157
|
* 不要把 pin 推迟到 Turn 启动后,否则已持久化的 Submission 将失去可验证的执行基线。
|
|
153
158
|
*/
|
|
154
159
|
pin(options: PinPiRuntimeOptions): Promise<PinnedPiRuntime> {
|
|
@@ -187,7 +192,7 @@ export class PiRuntimeAdapter {
|
|
|
187
192
|
* @remarks
|
|
188
193
|
* 调用方:Runtime 在首次执行和恢复继续时各创建一个新 Turn,并提供 canonical messages、durable Tool 回调和流事件回调。
|
|
189
194
|
*
|
|
190
|
-
* 实现理由:构造时会校验 Prepared Runtime 的 owner
|
|
195
|
+
* 实现理由:构造时会校验 Prepared Runtime 的 owner 和 pinned descriptor 格式,再建立本 Turn 独享的 Tool governance 与执行状态。
|
|
191
196
|
* 不要跨 Turn 复用返回对象;abort、steer、assistant ordinal 和 Tool governance 都是单次执行状态。
|
|
192
197
|
*/
|
|
193
198
|
createTurn(
|