@springbrand/agent-runtime 0.2.0-alpha.18 → 0.2.0-alpha.20
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/adapter/cloudflare/sandbox/adapter.ts +20 -5
- package/src/adapter/cloudflare/universal-agent/preparation.ts +32 -5
- package/src/db/schema.ts +10 -0
- package/src/db/submission.repo.ts +44 -1
- package/src/index.ts +3 -1
- package/src/kernel/bindings.ts +22 -13
- package/src/kernel/recoverable-chat-agent.ts +37 -11
- package/src/kernel/submission-lifecycle.ts +53 -10
- package/src/kernel/tool-surface.ts +41 -0
- package/src/layers/context/budget/gate.ts +57 -11
- package/src/lib/prompt.ts +32 -15
- package/src/pi/message/projection.ts +22 -4
- package/src/pi/runtime-adapter/execution.ts +115 -9
- package/src/pi/runtime-adapter/transcript.ts +7 -2
- package/src/pi/tool/compiler.ts +9 -5
- package/src/pi/tool/core-host.ts +102 -13
- package/src/pi/tool/core.ts +209 -61
- package/src/pi/tool/schedule.ts +3 -1
- package/src/pi/tool/skill.ts +88 -1
- package/src/pi/tool/workspace-sandbox.ts +136 -4
- package/src/runtime-assembler.ts +32 -8
- package/src/runtime.ts +251 -44
- package/src/skills/index.ts +14 -0
- package/src/skills/springbrand-worker-website/index.ts +73 -0
- package/src/tool-registry.ts +15 -32
package/src/runtime-assembler.ts
CHANGED
|
@@ -54,7 +54,8 @@ import {
|
|
|
54
54
|
memoryPiToolCandidate,
|
|
55
55
|
} from "./pi/tool/base";
|
|
56
56
|
import {
|
|
57
|
-
|
|
57
|
+
BROWSER_EXECUTE_TOOL_NAME,
|
|
58
|
+
browserExecutionPiToolCandidate,
|
|
58
59
|
codeExecutionPiToolCandidate,
|
|
59
60
|
} from "./pi/tool/core";
|
|
60
61
|
import { skillPiToolCandidates } from "./pi/tool/skill";
|
|
@@ -65,6 +66,7 @@ import {
|
|
|
65
66
|
normalizeAgentTelemetryBinding,
|
|
66
67
|
type AgentTelemetryBinding,
|
|
67
68
|
} from "./telemetry/contract";
|
|
69
|
+
import { BUILT_IN_RUNTIME_SKILLS } from "./skills";
|
|
68
70
|
|
|
69
71
|
/** Runtime Assembler:校验一份扁平输入并生成可原子提交的 Snapshot。 */
|
|
70
72
|
|
|
@@ -192,15 +194,23 @@ async function createToolSurface(
|
|
|
192
194
|
const visible = (candidate: PiToolCandidate) =>
|
|
193
195
|
!deny.has(candidate.tool.name) &&
|
|
194
196
|
allowsTool?.(candidate.tool.name) !== false;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
+
// 平台没有浏览器能力时注册空集:宁可没有这个 Tool,也不注册一个必然失败的 Tool 误导模型。
|
|
198
|
+
// `create()` 推迟到确认这个名字真的可见之后,被 deny 的装配不白建一个浏览器连接器。
|
|
199
|
+
// `direct` 使它只走 Direct 调用,不再被并进 `execute` 的工具集——两个 Code Mode 类工具互相嵌套
|
|
200
|
+
// 会让「哪次执行被记录、被重放」变得无法解释,而它们的分工本就由 System Prompt 划清。
|
|
201
|
+
const browserVisible = !deny.has(BROWSER_EXECUTE_TOOL_NAME) &&
|
|
202
|
+
allowsTool?.(BROWSER_EXECUTE_TOOL_NAME) !== false;
|
|
203
|
+
const browserCandidates = input.platform.browser && browserVisible
|
|
204
|
+
? [{
|
|
205
|
+
...browserExecutionPiToolCandidate(input.platform.browser.create()),
|
|
206
|
+
direct: true as const,
|
|
207
|
+
}]
|
|
197
208
|
: [];
|
|
198
209
|
const baseCandidates = basePiToolCandidates(input.webSearch);
|
|
199
210
|
const memoryCandidates = input.memory
|
|
200
211
|
? [memoryPiToolCandidate(input.memory.port, input.memory.profile)]
|
|
201
212
|
: [];
|
|
202
213
|
const scriptCandidates = [
|
|
203
|
-
...browserCandidates,
|
|
204
214
|
...input.hostTools,
|
|
205
215
|
...baseCandidates,
|
|
206
216
|
...memoryCandidates,
|
|
@@ -238,6 +248,9 @@ async function createToolSurface(
|
|
|
238
248
|
}
|
|
239
249
|
|
|
240
250
|
const finalized = [...tools.values()];
|
|
251
|
+
const directVisible = finalized.filter(
|
|
252
|
+
(candidate) => !candidate.codeExecutionOnly,
|
|
253
|
+
);
|
|
241
254
|
if (tools.has("execute")) {
|
|
242
255
|
throw new Error("SpringBrand reserved Runtime Tool name: execute");
|
|
243
256
|
}
|
|
@@ -246,7 +259,7 @@ async function createToolSurface(
|
|
|
246
259
|
deny.has("execute") ||
|
|
247
260
|
allowsTool?.("execute") === false
|
|
248
261
|
) {
|
|
249
|
-
return Object.freeze(
|
|
262
|
+
return Object.freeze(directVisible);
|
|
250
263
|
}
|
|
251
264
|
const mergeable = finalized.filter(
|
|
252
265
|
(candidate) =>
|
|
@@ -264,14 +277,22 @@ async function createToolSurface(
|
|
|
264
277
|
),
|
|
265
278
|
codeExecutionTools,
|
|
266
279
|
}),
|
|
267
|
-
...
|
|
280
|
+
...directVisible,
|
|
268
281
|
]);
|
|
269
282
|
},
|
|
270
283
|
}),
|
|
271
284
|
extensions: input.extensions.filter(
|
|
272
285
|
(extension) => allowsExtension?.(extension) !== false,
|
|
273
286
|
),
|
|
274
|
-
|
|
287
|
+
// 浏览器是可选能力:平台没给就只记诊断,不让整个装配失败。
|
|
288
|
+
// 被 policy deny 不算降级——那是有人主动关的,不是平台缺件。
|
|
289
|
+
degradations: input.platform.browser
|
|
290
|
+
? []
|
|
291
|
+
: [{
|
|
292
|
+
capability: "browser" as const,
|
|
293
|
+
reason: "unavailable" as const,
|
|
294
|
+
detail: "browser_binding_missing",
|
|
295
|
+
}],
|
|
275
296
|
};
|
|
276
297
|
}
|
|
277
298
|
|
|
@@ -827,7 +848,10 @@ export async function assembleRuntimeSnapshot<
|
|
|
827
848
|
? { subagents: toolAssembly.bindings.subagents }
|
|
828
849
|
: {}),
|
|
829
850
|
hostTools,
|
|
830
|
-
skills:
|
|
851
|
+
skills:
|
|
852
|
+
input.ctx.role === "primary" && toolAssembly.bindings?.workspace
|
|
853
|
+
? [...BUILT_IN_RUNTIME_SKILLS, ...resources.skills]
|
|
854
|
+
: resources.skills,
|
|
831
855
|
connectors: resources.connectors.servers,
|
|
832
856
|
...(resources.connectors.gateway
|
|
833
857
|
? { gateway: resources.connectors.gateway }
|
package/src/runtime.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
SubmissionLifecycle,
|
|
23
23
|
MAX_PENDING_SUBMISSIONS,
|
|
24
24
|
SubmissionQueueFullError,
|
|
25
|
+
type SubmissionExecutionMode,
|
|
25
26
|
type SubmissionInput,
|
|
26
27
|
type SubmissionStore,
|
|
27
28
|
type SubmissionHandle,
|
|
@@ -128,6 +129,10 @@ import {
|
|
|
128
129
|
|
|
129
130
|
const SCHEDULED_STABLE_TIMEOUT_MS = 30_000;
|
|
130
131
|
const TURN_EVENT_RETRY_SECONDS = 10;
|
|
132
|
+
|
|
133
|
+
// 计划续跑丢失多久后由看门狗接手。必须显著长于一个正常执行片的时长,
|
|
134
|
+
// 否则会在健康的长执行片上误判并派发出重复的一片。
|
|
135
|
+
const CONTINUATION_WATCHDOG_SECONDS = 300;
|
|
131
136
|
// 一次 Host admission 调用最多被视为「在途」多久。停在 pending 的准入行永久保留
|
|
132
137
|
// —— 它固定了同一 identity 重投时必须复用的 runId,不能靠删行来让容量回收 ——
|
|
133
138
|
// 但一次远端失败或崩溃留下的行超过这个窗口后就不再占用 pending 容量,否则
|
|
@@ -169,6 +174,10 @@ interface StoredSubmission extends SubmissionReceipt {
|
|
|
169
174
|
accountId: string | null;
|
|
170
175
|
rateVersion: number | null;
|
|
171
176
|
slotIdentity: string | null;
|
|
177
|
+
/** 跨执行片累计的模型回合数,全局回合预算据此判断。 */
|
|
178
|
+
modelTurns: number;
|
|
179
|
+
/** 当前让步执行片的标识;没有未完成让步时为 null。 */
|
|
180
|
+
continuationId: string | null;
|
|
172
181
|
}
|
|
173
182
|
|
|
174
183
|
interface SubmitMessageOptions {
|
|
@@ -196,6 +205,35 @@ interface PlannedContinuationData {
|
|
|
196
205
|
readonly continuationId: string;
|
|
197
206
|
}
|
|
198
207
|
|
|
208
|
+
/**
|
|
209
|
+
* 说明一次恢复准备的结局。
|
|
210
|
+
*
|
|
211
|
+
* @remarks
|
|
212
|
+
* `prepareRecoveredTurn` 返回它,`executeNonTerminalSubmission` 据此决定重起 Turn、
|
|
213
|
+
* 保持停靠还是落终态。
|
|
214
|
+
*
|
|
215
|
+
* `parked` 与 `unresumable` 必须分开:前者有人(审批、客户端交互)或有已排的调度会来解开它,
|
|
216
|
+
* 后者没有任何东西会再唤醒 —— 把两者混成一个「不继续」,非终态记录就会永久停在 running。
|
|
217
|
+
*/
|
|
218
|
+
type RecoveredTurnPreparation =
|
|
219
|
+
/** Transcript 完好,可以重起 Pi Turn。 */
|
|
220
|
+
| { readonly kind: "resume" }
|
|
221
|
+
/** 正在等人或等已排的续跑;保持非终态是正确的。 */
|
|
222
|
+
| { readonly kind: "parked" }
|
|
223
|
+
/** 准备过程自己已经写好了终态。 */
|
|
224
|
+
| { readonly kind: "settled" }
|
|
225
|
+
/** 续不下去,调用方必须落一条用户看得见的失败终态。 */
|
|
226
|
+
| { readonly kind: "unresumable"; readonly reason: string };
|
|
227
|
+
|
|
228
|
+
// 作用:为一个没人会来解开的 `wait` 生成用户能看懂的终态原因。
|
|
229
|
+
// 调用:`prepareRecoveredTurn` 判定 wait 不是 approval/interaction 时调用。
|
|
230
|
+
// 原因:终态文本会直接呈现给用户,必须说明发生了什么以及能不能重试。
|
|
231
|
+
function unresumableWaitMessage(reason: string | undefined): string {
|
|
232
|
+
return reason === "uncertain-tool"
|
|
233
|
+
? "SpringBrand stopped this turn: a non-idempotent Tool was interrupted and its result cannot be confirmed, so it was not retried. Re-send the request if you want to try again."
|
|
234
|
+
: "SpringBrand could not resume this turn after an interruption.";
|
|
235
|
+
}
|
|
236
|
+
|
|
199
237
|
// 作用:把任意异常整理成可以持久化或发给客户端的文字。
|
|
200
238
|
// 调用:Turn 执行、恢复和业务投影捕获 `unknown` 异常时调用。
|
|
201
239
|
// 原因:错误边界不能假定抛出值一定是 `Error`。
|
|
@@ -494,8 +532,8 @@ export abstract class AgentRuntimeKernel<
|
|
|
494
532
|
this.submissions = new SubmissionLifecycle({
|
|
495
533
|
store: submissionStore,
|
|
496
534
|
clearTerminal: () => clearChatTerminal(this.ctx.storage),
|
|
497
|
-
execute: (submissionId,
|
|
498
|
-
this.executeSubmission(submissionId,
|
|
535
|
+
execute: (submissionId, mode) =>
|
|
536
|
+
this.executeSubmission(submissionId, mode),
|
|
499
537
|
appendAbortIntent: (submission, reason) =>
|
|
500
538
|
this.appendTerminalIntent(submission, "aborted", reason),
|
|
501
539
|
commitTerminal: (submission, outcome, message) =>
|
|
@@ -853,6 +891,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
853
891
|
startedAt,
|
|
854
892
|
continuation: output.continuation ?? false,
|
|
855
893
|
assistantOrdinal: output.assistantOrdinal ?? 0,
|
|
894
|
+
modelTurnsConsumed: submission.modelTurns,
|
|
856
895
|
},
|
|
857
896
|
canonicalMessages: () => this.transcript.canonicalMessages(),
|
|
858
897
|
durability: {
|
|
@@ -1037,11 +1076,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
1037
1076
|
const running = this.db.submissions.findRunning() as StoredSubmission | null;
|
|
1038
1077
|
if (running) {
|
|
1039
1078
|
const effect = this.decidePiRecovery(running).effect;
|
|
1040
|
-
if (
|
|
1041
|
-
effect.kind === "wait" &&
|
|
1042
|
-
(effect.reason === "approval" ||
|
|
1043
|
-
effect.reason === "uncertain-tool")
|
|
1044
|
-
) {
|
|
1079
|
+
if (effect.kind === "wait" && effect.reason === "approval") {
|
|
1045
1080
|
return;
|
|
1046
1081
|
}
|
|
1047
1082
|
}
|
|
@@ -2422,21 +2457,26 @@ export abstract class AgentRuntimeKernel<
|
|
|
2422
2457
|
// 作用:按持久事实把一个中断 Turn 推进到可继续或已收尾状态。
|
|
2423
2458
|
// 调用:`executeNonTerminalSubmission` 以 recovery 模式重起 Pi Turn 前调用。
|
|
2424
2459
|
// 原因:用有界循环执行状态机效果,既允许 Tool 重试产生新事实,也防止错误规则无限自旋。
|
|
2460
|
+
//
|
|
2461
|
+
// 返回值必须区分「在等人」和「续不下去」:以前两者都压成 false,调用方一律原样返回
|
|
2462
|
+
// 非终态记录,于是「续不下去」这一类会让 Submission 永久停在 running。
|
|
2425
2463
|
private async prepareRecoveredTurn(
|
|
2426
2464
|
submission: StoredSubmission,
|
|
2427
2465
|
adapter: PreparedPiTurnAdapter,
|
|
2428
|
-
): Promise<
|
|
2466
|
+
): Promise<RecoveredTurnPreparation> {
|
|
2429
2467
|
let decision = await this.materializeRecoveredToolResults(submission);
|
|
2430
2468
|
for (let step = 0; step < 32; step += 1) {
|
|
2431
2469
|
const latest = this.readSubmission(submission.submissionId);
|
|
2432
|
-
if (!latest || isTerminalSubmissionStatus(latest.status))
|
|
2470
|
+
if (!latest || isTerminalSubmissionStatus(latest.status)) {
|
|
2471
|
+
return { kind: "settled" };
|
|
2472
|
+
}
|
|
2433
2473
|
if (latest.abortReason) {
|
|
2434
2474
|
await this.submissions.finish(
|
|
2435
2475
|
latest,
|
|
2436
2476
|
"aborted",
|
|
2437
2477
|
latest.abortReason,
|
|
2438
2478
|
);
|
|
2439
|
-
return
|
|
2479
|
+
return { kind: "settled" };
|
|
2440
2480
|
}
|
|
2441
2481
|
this.db.transaction(() => {
|
|
2442
2482
|
this.applyPiRecoveryMutations(
|
|
@@ -2446,7 +2486,19 @@ export abstract class AgentRuntimeKernel<
|
|
|
2446
2486
|
});
|
|
2447
2487
|
switch (decision.effect.kind) {
|
|
2448
2488
|
case "wait":
|
|
2449
|
-
|
|
2489
|
+
// 这里必须逐个 reason 判,不能按「不是 approval 就算续不下去」一刀切:
|
|
2490
|
+
// - approval / interaction:等人,用户迟早会点。
|
|
2491
|
+
// - complete:终态里程碑已提交,行状态同事务写入,上层的终态检查会收掉。
|
|
2492
|
+
// - undefined:续跑里程碑已暂存待派发,`dispatchPendingContinuations`
|
|
2493
|
+
// 会来接手 —— 判它失败等于杀掉一轮本来有人管的 Turn。
|
|
2494
|
+
// - uncertain-tool:**没有任何人会来解开**。非幂等工具结果不确定,恢复
|
|
2495
|
+
// 拒绝重放;停在这里就是永久挂死。
|
|
2496
|
+
return decision.effect.reason === "uncertain-tool"
|
|
2497
|
+
? {
|
|
2498
|
+
kind: "unresumable",
|
|
2499
|
+
reason: unresumableWaitMessage(decision.effect.reason),
|
|
2500
|
+
}
|
|
2501
|
+
: { kind: "parked" };
|
|
2450
2502
|
case "retry-tool": {
|
|
2451
2503
|
try {
|
|
2452
2504
|
if (
|
|
@@ -2456,7 +2508,11 @@ export abstract class AgentRuntimeKernel<
|
|
|
2456
2508
|
input: decision.effect.input,
|
|
2457
2509
|
})
|
|
2458
2510
|
) {
|
|
2459
|
-
return
|
|
2511
|
+
return {
|
|
2512
|
+
kind: "unresumable",
|
|
2513
|
+
reason:
|
|
2514
|
+
`SpringBrand could not resume this turn: Tool "${decision.effect.toolName}" is no longer available.`,
|
|
2515
|
+
};
|
|
2460
2516
|
}
|
|
2461
2517
|
} catch {
|
|
2462
2518
|
// The governed Tool persisted its bounded error ToolResult.
|
|
@@ -2471,17 +2527,25 @@ export abstract class AgentRuntimeKernel<
|
|
|
2471
2527
|
submission,
|
|
2472
2528
|
decision.effect.approvalExecutionId,
|
|
2473
2529
|
);
|
|
2474
|
-
return
|
|
2530
|
+
return { kind: "parked" };
|
|
2475
2531
|
case "finish":
|
|
2476
2532
|
await this.submissions.finish(
|
|
2477
2533
|
submission,
|
|
2478
2534
|
decision.effect.outcome,
|
|
2479
2535
|
decision.effect.message,
|
|
2480
2536
|
);
|
|
2481
|
-
return
|
|
2537
|
+
return { kind: "settled" };
|
|
2482
2538
|
case "resume-turn": {
|
|
2483
2539
|
const last = (await this.transcript.canonicalMessages()).at(-1);
|
|
2484
|
-
|
|
2540
|
+
// Pi 的 agentLoopContinue 拒绝从 assistant 消息继续。悬空的尾部 assistant
|
|
2541
|
+
// 意味着这条 transcript 续不下去了 —— 判断出来就必须收尾,不能默默退场。
|
|
2542
|
+
return last?.role === "user" || last?.role === "toolResult"
|
|
2543
|
+
? { kind: "resume" }
|
|
2544
|
+
: {
|
|
2545
|
+
kind: "unresumable",
|
|
2546
|
+
reason:
|
|
2547
|
+
"SpringBrand could not resume this turn: the transcript ends on an assistant message.",
|
|
2548
|
+
};
|
|
2485
2549
|
}
|
|
2486
2550
|
}
|
|
2487
2551
|
}
|
|
@@ -2611,7 +2675,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
2611
2675
|
// 原因:把失败帧、可续传流和 pre-stream 清理放在最外层,避免内部分支遗留半开传输状态。
|
|
2612
2676
|
private async executeSubmission(
|
|
2613
2677
|
submissionId: string,
|
|
2614
|
-
|
|
2678
|
+
mode: SubmissionExecutionMode = "fresh",
|
|
2615
2679
|
): Promise<StoredSubmission> {
|
|
2616
2680
|
const submission = this.readSubmission(submissionId);
|
|
2617
2681
|
if (!submission) {
|
|
@@ -2627,7 +2691,9 @@ export abstract class AgentRuntimeKernel<
|
|
|
2627
2691
|
submission.abortReason,
|
|
2628
2692
|
);
|
|
2629
2693
|
}
|
|
2630
|
-
if (recovery) {
|
|
2694
|
+
if (mode === "recovery") {
|
|
2695
|
+
// 只有真中断才记恢复事实。计划让步也走这里的话,每个执行片都会伪造一条
|
|
2696
|
+
// `runtime_restart`,并且抹掉真实的 stall 原因 —— 真崩溃和主动让出在观测上就分不开了。
|
|
2631
2697
|
this.telemetry.capture("turnRecovering", {
|
|
2632
2698
|
submissionId,
|
|
2633
2699
|
reason: submission.recoveryReason ?? "runtime_restart",
|
|
@@ -2635,16 +2701,16 @@ export abstract class AgentRuntimeKernel<
|
|
|
2635
2701
|
});
|
|
2636
2702
|
this.db.submissions.clearRecoveryReason(submissionId);
|
|
2637
2703
|
await this.broadcastApprovals();
|
|
2638
|
-
await this.ensureRuntimeReady();
|
|
2639
2704
|
}
|
|
2705
|
+
if (mode !== "fresh") await this.ensureRuntimeReady();
|
|
2640
2706
|
try {
|
|
2641
2707
|
return await this.executeNonTerminalSubmission(
|
|
2642
2708
|
submission,
|
|
2643
|
-
|
|
2709
|
+
mode,
|
|
2644
2710
|
);
|
|
2645
2711
|
} catch (error) {
|
|
2646
2712
|
if (
|
|
2647
|
-
recovery &&
|
|
2713
|
+
mode === "recovery" &&
|
|
2648
2714
|
(error instanceof ChatStreamStalledError ||
|
|
2649
2715
|
error instanceof RetryableModelError)
|
|
2650
2716
|
) {
|
|
@@ -2665,7 +2731,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
2665
2731
|
failed.status === "error"
|
|
2666
2732
|
? failed.error ?? "SpringBrand turn failed"
|
|
2667
2733
|
: undefined,
|
|
2668
|
-
|
|
2734
|
+
mode !== "fresh",
|
|
2669
2735
|
);
|
|
2670
2736
|
return failed;
|
|
2671
2737
|
} finally {
|
|
@@ -2683,8 +2749,11 @@ export abstract class AgentRuntimeKernel<
|
|
|
2683
2749
|
// 原因:恢复补全、状态迁移、fiber、流式记录与终态投影必须保持固定顺序,否则唤醒后会重复执行或丢失回答。
|
|
2684
2750
|
private async executeNonTerminalSubmission(
|
|
2685
2751
|
submission: StoredSubmission,
|
|
2686
|
-
|
|
2752
|
+
mode: SubmissionExecutionMode,
|
|
2687
2753
|
): Promise<StoredSubmission> {
|
|
2754
|
+
// 续跑和恢复一样要给客户端打 continuation 标记(否则前端会另起一个空累加器,
|
|
2755
|
+
// 把已经渲染出来的半条回答整条换掉),但只有恢复需要重建持久事实。
|
|
2756
|
+
const continuation = mode !== "fresh";
|
|
2688
2757
|
const submissionId = submission.submissionId;
|
|
2689
2758
|
if (submission.abortReason) {
|
|
2690
2759
|
return this.submissions.finish(
|
|
@@ -2695,11 +2764,16 @@ export abstract class AgentRuntimeKernel<
|
|
|
2695
2764
|
}
|
|
2696
2765
|
submission = await this.pinSubmissionAssembly(submission);
|
|
2697
2766
|
await this.assertPinnedSubmission(submission);
|
|
2698
|
-
if (submission.status === "pending" &&
|
|
2767
|
+
if (submission.status === "pending" && mode === "fresh") {
|
|
2699
2768
|
submission = await this.activateQueuedSubmission(submission);
|
|
2700
2769
|
}
|
|
2701
2770
|
const recoveryAdapter = this.createSubmissionExecutionAdapter(submission);
|
|
2702
|
-
|
|
2771
|
+
let awaitingHumanInput = false;
|
|
2772
|
+
// 让步续跑跳过整套里程碑重放:这一片是主动让出的,transcript 完好、没有半路的
|
|
2773
|
+
// Tool 要重建。走恢复通道不只是白跑,重放成本还随让步次数增长 —— 第 N 片要
|
|
2774
|
+
// 重放前 N-1 片积累的全部里程碑。但调度回调可能跨过等人的停靠期,所以下面仍需检查
|
|
2775
|
+
// 恢复决策,不能把尾部 assistant Tool Use 直接交给 Pi 续跑。
|
|
2776
|
+
if (mode === "recovery") {
|
|
2703
2777
|
const deactivatePreparation = this.submissions.activate(submission, {
|
|
2704
2778
|
submissionId,
|
|
2705
2779
|
requestId: submission.requestId,
|
|
@@ -2708,27 +2782,44 @@ export abstract class AgentRuntimeKernel<
|
|
|
2708
2782
|
continuation: true,
|
|
2709
2783
|
agent: recoveryAdapter,
|
|
2710
2784
|
});
|
|
2711
|
-
let
|
|
2785
|
+
let preparation: RecoveredTurnPreparation;
|
|
2712
2786
|
try {
|
|
2713
|
-
|
|
2787
|
+
preparation = await this.prepareRecoveredTurn(
|
|
2714
2788
|
submission,
|
|
2715
2789
|
recoveryAdapter,
|
|
2716
2790
|
);
|
|
2717
2791
|
} finally {
|
|
2718
2792
|
deactivatePreparation();
|
|
2719
2793
|
}
|
|
2720
|
-
if (
|
|
2794
|
+
if (preparation.kind !== "resume") {
|
|
2721
2795
|
const latest = this.readSubmission(submissionId)!;
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2796
|
+
if (latest.abortReason) {
|
|
2797
|
+
return this.submissions.finish(
|
|
2798
|
+
latest,
|
|
2799
|
+
"aborted",
|
|
2800
|
+
latest.abortReason,
|
|
2801
|
+
);
|
|
2802
|
+
}
|
|
2803
|
+
if (isTerminalSubmissionStatus(latest.status)) return latest;
|
|
2804
|
+
// 非终态还想退场,只有「在等人」这一种是合法的。其余一律收成失败终态:
|
|
2805
|
+
// 停在 running 会让 `pump()` 的 `findRunning()` 永远有值,这个 Session
|
|
2806
|
+
// 之后所有消息都排不进去,而且没有任何东西会再唤醒它。
|
|
2807
|
+
if (preparation.kind === "parked") return latest;
|
|
2808
|
+
// `settled` 走到这里说明它没真的写成终态 —— 契约破了也不能放它挂着。
|
|
2809
|
+
return this.submissions.finish(
|
|
2810
|
+
latest,
|
|
2811
|
+
"failed",
|
|
2812
|
+
preparation.kind === "unresumable"
|
|
2813
|
+
? preparation.reason
|
|
2814
|
+
: unresumableWaitMessage(undefined),
|
|
2815
|
+
);
|
|
2729
2816
|
}
|
|
2730
2817
|
} else {
|
|
2731
|
-
await this.materializeRecoveredToolResults(submission);
|
|
2818
|
+
const decision = await this.materializeRecoveredToolResults(submission);
|
|
2819
|
+
awaitingHumanInput = mode === "continuation" &&
|
|
2820
|
+
decision.effect.kind === "wait" &&
|
|
2821
|
+
(decision.effect.reason === "approval" ||
|
|
2822
|
+
decision.effect.reason === "interaction");
|
|
2732
2823
|
}
|
|
2733
2824
|
|
|
2734
2825
|
const ready = this.readSubmission(submissionId);
|
|
@@ -2743,6 +2834,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
2743
2834
|
ready.abortReason,
|
|
2744
2835
|
);
|
|
2745
2836
|
}
|
|
2837
|
+
if (awaitingHumanInput) return ready;
|
|
2746
2838
|
if (ready.status === "pending") {
|
|
2747
2839
|
if (this.db.submissions.transition(submissionId, "running", ["pending"])) {
|
|
2748
2840
|
this.telemetry.capture("turnStarted", {
|
|
@@ -2767,7 +2859,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
2767
2859
|
let turn!: ActiveTurn;
|
|
2768
2860
|
const adapter = this.createSubmissionExecutionAdapter(submission, {
|
|
2769
2861
|
startedAt,
|
|
2770
|
-
continuation
|
|
2862
|
+
continuation,
|
|
2771
2863
|
assistantOrdinal,
|
|
2772
2864
|
onRecord: (record) =>
|
|
2773
2865
|
this.migratedSubmissions.has(submissionId)
|
|
@@ -2796,7 +2888,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
2796
2888
|
requestId: submission.requestId,
|
|
2797
2889
|
messageId: submission.assistantMessageId,
|
|
2798
2890
|
startedAt,
|
|
2799
|
-
continuation
|
|
2891
|
+
continuation,
|
|
2800
2892
|
agent: adapter,
|
|
2801
2893
|
};
|
|
2802
2894
|
let streamId: string | undefined;
|
|
@@ -2809,7 +2901,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
2809
2901
|
{
|
|
2810
2902
|
requestId: submission.requestId,
|
|
2811
2903
|
messageId: submission.assistantMessageId,
|
|
2812
|
-
continuation
|
|
2904
|
+
continuation,
|
|
2813
2905
|
messages: await this.transcript.snapshotMessages(),
|
|
2814
2906
|
recoveryData: {
|
|
2815
2907
|
submissionId,
|
|
@@ -2844,6 +2936,19 @@ export abstract class AgentRuntimeKernel<
|
|
|
2844
2936
|
if (!streamId) {
|
|
2845
2937
|
throw new Error("Yielded SpringBrand Turn has no recoverable stream");
|
|
2846
2938
|
}
|
|
2939
|
+
// 回合数和续跑标识必须先落库、再排调度:调度一旦到期就会读这两个值,
|
|
2940
|
+
// 反过来写会让续跑看到上一片的状态。
|
|
2941
|
+
this.db.transaction(() =>
|
|
2942
|
+
this.db.submissions.recordYieldedSlice(
|
|
2943
|
+
submissionId,
|
|
2944
|
+
runResult!.modelTurns,
|
|
2945
|
+
streamId!,
|
|
2946
|
+
)
|
|
2947
|
+
);
|
|
2948
|
+
// 让步必须密封本切片的流,但不能发 done —— 这一轮还没有权威结果。
|
|
2949
|
+
// 不密封的话下一个切片的 start() 会直接顶掉 activeStreamId,旧行永远
|
|
2950
|
+
// 停在 streaming,元数据和 chunk 要等 abandoned 保留期才回收。
|
|
2951
|
+
this.sealRecoverableStreamSlice(streamId);
|
|
2847
2952
|
await this.schedule(
|
|
2848
2953
|
0,
|
|
2849
2954
|
"_piPlannedContinuation",
|
|
@@ -2854,8 +2959,21 @@ export abstract class AgentRuntimeKernel<
|
|
|
2854
2959
|
},
|
|
2855
2960
|
{ idempotent: true },
|
|
2856
2961
|
);
|
|
2962
|
+
await this.scheduleContinuationWatchdog(
|
|
2963
|
+
submissionId,
|
|
2964
|
+
submission.requestId,
|
|
2965
|
+
streamId,
|
|
2966
|
+
);
|
|
2857
2967
|
return latest;
|
|
2858
2968
|
}
|
|
2969
|
+
if (runResult) {
|
|
2970
|
+
this.db.transaction(() =>
|
|
2971
|
+
this.db.submissions.recordModelTurns(
|
|
2972
|
+
submissionId,
|
|
2973
|
+
runResult!.modelTurns,
|
|
2974
|
+
)
|
|
2975
|
+
);
|
|
2976
|
+
}
|
|
2859
2977
|
const intent = terminalIntent ?? {
|
|
2860
2978
|
outcome: "failed" as const,
|
|
2861
2979
|
message:
|
|
@@ -2929,7 +3047,11 @@ export abstract class AgentRuntimeKernel<
|
|
|
2929
3047
|
recoveryOutcome !== "disabled" &&
|
|
2930
3048
|
!this.readSubmission(submissionId)?.abortReason
|
|
2931
3049
|
) {
|
|
2932
|
-
|
|
3050
|
+
// 只有恢复通道的调用方(`_chatRecoveryRetry`)靠这次重抛判定 "scheduled"。
|
|
3051
|
+
// 让步续跑由 alarm 驱动,重抛只会把那次 alarm 变成一次失败。
|
|
3052
|
+
if (mode === "recovery" && recoveryOutcome === "scheduled") {
|
|
3053
|
+
throw error;
|
|
3054
|
+
}
|
|
2933
3055
|
return this.readSubmission(submissionId)!;
|
|
2934
3056
|
}
|
|
2935
3057
|
} else if (!stoppedDuringRecovery) {
|
|
@@ -4024,19 +4146,104 @@ export abstract class AgentRuntimeKernel<
|
|
|
4024
4146
|
}
|
|
4025
4147
|
}
|
|
4026
4148
|
|
|
4027
|
-
|
|
4028
|
-
|
|
4149
|
+
// 作用:为一个让步执行片排一次兜底唤醒。
|
|
4150
|
+
// 调用:`executeNonTerminalSubmission` 排出计划续跑之后立即调用。
|
|
4151
|
+
// 原因:计划续跑只有一次机会 —— schedule 抛错、alarm 回调抛错、实例中途被换掉,
|
|
4152
|
+
// 这一轮就再也没人唤醒了。看门狗是唯一能兜住未知丢失路径的东西;
|
|
4153
|
+
// 它按续跑标识判断有没有推进,所以正常情况下醒来即空转返回。
|
|
4154
|
+
private async scheduleContinuationWatchdog(
|
|
4155
|
+
submissionId: string,
|
|
4156
|
+
requestId: string,
|
|
4157
|
+
continuationId: string,
|
|
4029
4158
|
): Promise<void> {
|
|
4030
|
-
|
|
4159
|
+
try {
|
|
4160
|
+
await this.schedule(
|
|
4161
|
+
CONTINUATION_WATCHDOG_SECONDS,
|
|
4162
|
+
"_piContinuationWatchdog",
|
|
4163
|
+
{ submissionId, requestId, continuationId },
|
|
4164
|
+
{ idempotent: true },
|
|
4165
|
+
);
|
|
4166
|
+
} catch (error) {
|
|
4167
|
+
// 看门狗排不上不该反过来杀掉这一轮:计划续跑本身已经排好了。
|
|
4168
|
+
console.warn(
|
|
4169
|
+
"[pi-continuation-watchdog:degraded]",
|
|
4170
|
+
json({ submissionId, error: errorText(error) }),
|
|
4171
|
+
);
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
|
|
4175
|
+
// 判断一条让步提交是否仍停在 `continuationId` 这一片上。
|
|
4176
|
+
// 计划续跑和看门狗都用它决定该不该接手,避免和更新的执行片重复派发。
|
|
4177
|
+
//
|
|
4178
|
+
// `allowUnrecorded` 给看门狗用:让步的落库万一没生效(记录已不在
|
|
4179
|
+
// pending/running),`continuationId` 会是 null,此时严格比对会把续跑和看门狗
|
|
4180
|
+
// 一起丢掉 —— 那正是这次要根除的静默挂死。计划续跑不放宽,它的职责就是去重。
|
|
4181
|
+
private stalledOnContinuation(
|
|
4182
|
+
data: PlannedContinuationData,
|
|
4183
|
+
{ allowUnrecorded = false }: { allowUnrecorded?: boolean } = {},
|
|
4184
|
+
): StoredSubmission | null {
|
|
4031
4185
|
const submission = this.readSubmission(data.submissionId);
|
|
4032
4186
|
if (
|
|
4033
4187
|
!submission ||
|
|
4034
4188
|
submission.requestId !== data.requestId ||
|
|
4035
4189
|
isTerminalSubmissionStatus(submission.status)
|
|
4036
4190
|
) {
|
|
4037
|
-
return;
|
|
4191
|
+
return null;
|
|
4192
|
+
}
|
|
4193
|
+
const onThisSlice = submission.continuationId === data.continuationId ||
|
|
4194
|
+
(allowUnrecorded && submission.continuationId === null);
|
|
4195
|
+
return onThisSlice ? submission : null;
|
|
4196
|
+
}
|
|
4197
|
+
|
|
4198
|
+
async _piPlannedContinuation(
|
|
4199
|
+
data?: PlannedContinuationData,
|
|
4200
|
+
): Promise<void> {
|
|
4201
|
+
if (!data?.submissionId || !data.requestId || !data.continuationId) return;
|
|
4202
|
+
// 校验续跑标识:两条 alarm 同时到期时,晚到的那条对应的已经是上一片,
|
|
4203
|
+
// 放它进去会白跑一整个执行片。
|
|
4204
|
+
const submission = this.stalledOnContinuation(data);
|
|
4205
|
+
if (!submission) return;
|
|
4206
|
+
await this.submissions.continueAfterCurrent(submission.submissionId);
|
|
4207
|
+
}
|
|
4208
|
+
|
|
4209
|
+
/**
|
|
4210
|
+
* 在计划续跑丢失时接手一条让步提交。
|
|
4211
|
+
*
|
|
4212
|
+
* @remarks
|
|
4213
|
+
* Agents SDK 的调度器在 `scheduleContinuationWatchdog` 排的延时任务到期时按方法名调用。
|
|
4214
|
+
*
|
|
4215
|
+
* 正常情况下这一片早就推进了(`continuationId` 已换或已终态),此时直接返回。
|
|
4216
|
+
*
|
|
4217
|
+
* 走 `recover` 而不是 `continueAfterCurrent`,有两个都不能省的理由:
|
|
4218
|
+
* 一是续跑会丢通常就是实例中途没了,那属于崩溃,必须重放里程碑、补齐半路的
|
|
4219
|
+
* Tool,续跑模式恰恰跳过这些;二是这一轮可能正停在审批或客户端交互上等人
|
|
4220
|
+
* (被驱逐后内存里没有执行器,看起来就像「没人在跑」),只有恢复准备认得出
|
|
4221
|
+
* 这是合法停靠并原样放过 —— 续跑模式会直接在它上面重起一轮。
|
|
4222
|
+
*
|
|
4223
|
+
* `recover` 复用 `executions` 的实例内去重,因此和一片刚进序幕、还没登记执行器的
|
|
4224
|
+
* 竞态也一并合流,不会多跑一片。
|
|
4225
|
+
*/
|
|
4226
|
+
async _piContinuationWatchdog(
|
|
4227
|
+
data?: PlannedContinuationData,
|
|
4228
|
+
): Promise<void> {
|
|
4229
|
+
if (!data?.submissionId || !data.requestId || !data.continuationId) return;
|
|
4230
|
+
const submission = this.stalledOnContinuation(data, {
|
|
4231
|
+
allowUnrecorded: true,
|
|
4232
|
+
});
|
|
4233
|
+
if (!submission) return;
|
|
4234
|
+
try {
|
|
4235
|
+
await this.ensureRuntimeReady();
|
|
4236
|
+
await this.submissions.recover(submission.submissionId);
|
|
4237
|
+
} catch (error) {
|
|
4238
|
+
const latest = this.readSubmission(data.submissionId);
|
|
4239
|
+
if (latest && !isTerminalSubmissionStatus(latest.status)) {
|
|
4240
|
+
await this.submissions.finish(
|
|
4241
|
+
latest,
|
|
4242
|
+
"failed",
|
|
4243
|
+
`SpringBrand could not continue this turn: ${errorText(error)}`,
|
|
4244
|
+
);
|
|
4245
|
+
}
|
|
4038
4246
|
}
|
|
4039
|
-
await this.submissions.recoverAfterCurrent(submission.submissionId);
|
|
4040
4247
|
}
|
|
4041
4248
|
|
|
4042
4249
|
// 作用:Agent Tool 子运行开始后重算一次活动投影。
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { fromManifest } from "agents/skills";
|
|
2
|
+
import { SPRINGBRAND_WORKER_WEBSITE_SKILL } from "./springbrand-worker-website";
|
|
3
|
+
|
|
4
|
+
const source = fromManifest({
|
|
5
|
+
id: "springbrand-runtime-built-ins",
|
|
6
|
+
fingerprint: JSON.stringify(SPRINGBRAND_WORKER_WEBSITE_SKILL),
|
|
7
|
+
skills: [SPRINGBRAND_WORKER_WEBSITE_SKILL],
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const BUILT_IN_RUNTIME_SKILLS = Object.freeze([Object.freeze({
|
|
11
|
+
name: SPRINGBRAND_WORKER_WEBSITE_SKILL.name,
|
|
12
|
+
description: SPRINGBRAND_WORKER_WEBSITE_SKILL.description,
|
|
13
|
+
source,
|
|
14
|
+
})]);
|