@springbrand/agent-runtime 0.2.0-alpha.13 → 0.2.0-alpha.15
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/universal-agent/preparation.ts +2 -2
- package/src/adapter/cloudflare/universal-agent/tools.ts +3 -6
- package/src/index.ts +2 -2
- package/src/kernel/submission-lifecycle.ts +18 -5
- package/src/layers/context/budget/gate.ts +99 -0
- package/src/lib/prompt.ts +4 -3
- package/src/pi/message/projection.ts +2 -2
- package/src/pi/runtime-adapter/assembly.ts +2 -2
- package/src/pi/runtime-adapter/execution.ts +47 -15
- package/src/pi/runtime-adapter/index.ts +1 -0
- package/src/pi/runtime-adapter/models.ts +1 -1
- package/src/pi/runtime-adapter/recovery.ts +17 -17
- package/src/pi/runtime-adapter/transcript.ts +2 -2
- package/src/pi/tool/base.ts +113 -27
- package/src/pi/tool/compiler.ts +66 -6
- package/src/pi/tool/core-host.ts +50 -27
- package/src/pi/tool/core.ts +4 -16
- package/src/pi/tool/skill.ts +78 -3
- package/src/pi/tool/workspace-sandbox.ts +4 -4
- package/src/pi/turn/tool-recovery.ts +7 -7
- package/src/runtime-agent.ts +8 -3
- package/src/runtime-assembler.ts +45 -8
- package/src/runtime.ts +58 -20
- package/src/tool-registry.ts +8 -1
package/package.json
CHANGED
|
@@ -15,7 +15,7 @@ import type { RuntimeDegradation } from "../../../kernel/degradation";
|
|
|
15
15
|
import type { RuntimeMemoryProfile } from "../../../kernel/profile";
|
|
16
16
|
import { AGENT_TYPES } from "../../../layers/orchestration/subagents/agent-types/registry";
|
|
17
17
|
import type { RuntimeAgentConfigContext } from "../../../runtime-agent-context";
|
|
18
|
-
import {
|
|
18
|
+
import { createWorkspaceCodeExecutionFactory } from "../../../pi/tool/core-host";
|
|
19
19
|
import {
|
|
20
20
|
createCloudflareSandboxAdapter,
|
|
21
21
|
type SandboxAdmission,
|
|
@@ -127,7 +127,7 @@ export async function prepareWorkspace<Env extends Cloudflare.Env>(
|
|
|
127
127
|
if (!workspace.value) return { degradations: workspace.degradations };
|
|
128
128
|
return {
|
|
129
129
|
workspace: workspace.value,
|
|
130
|
-
codeExecution:
|
|
130
|
+
codeExecution: createWorkspaceCodeExecutionFactory({
|
|
131
131
|
ctx: context.ctx,
|
|
132
132
|
loader: platform.loader,
|
|
133
133
|
outbound: platform.outbound(),
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
RuntimeCodeExecutionPort,
|
|
3
2
|
RuntimeMemoryPort,
|
|
4
3
|
RuntimeSandboxPort,
|
|
5
4
|
RuntimeSchedulePort,
|
|
@@ -9,7 +8,6 @@ import type {
|
|
|
9
8
|
import type { RuntimeDegradation } from "../../../kernel/degradation";
|
|
10
9
|
import type { RuntimeMemoryProfile } from "../../../kernel/profile";
|
|
11
10
|
import type { WorkspaceRevisionRestorePort } from "../../../workspace-versioning";
|
|
12
|
-
import { createCodeExecutionTool } from "../../../pi/tool/core";
|
|
13
11
|
import { createScheduleTools } from "../../../pi/tool/schedule";
|
|
14
12
|
import { createWorkspaceRevisionTools } from "../../../pi/tool/workspace-revision";
|
|
15
13
|
import {
|
|
@@ -18,6 +16,7 @@ import {
|
|
|
18
16
|
} from "../../../pi/tool/workspace-sandbox";
|
|
19
17
|
import {
|
|
20
18
|
mergeToolRegistries,
|
|
19
|
+
type RuntimeCodeExecutionFactory,
|
|
21
20
|
type ToolAssemblyResult,
|
|
22
21
|
type ToolRegistry,
|
|
23
22
|
} from "../../../tool-registry";
|
|
@@ -36,7 +35,7 @@ export function assembleUniversalAgentTools(options: {
|
|
|
36
35
|
hostTools?: ToolRegistry;
|
|
37
36
|
workspace?: WorkspacePort;
|
|
38
37
|
workspaceRevisions?: WorkspaceRevisionRestorePort;
|
|
39
|
-
codeExecution?:
|
|
38
|
+
codeExecution?: RuntimeCodeExecutionFactory;
|
|
40
39
|
sandbox?: RuntimeSandboxPort;
|
|
41
40
|
schedule?: RuntimeSchedulePort;
|
|
42
41
|
subagents?: RuntimeSubagentPort;
|
|
@@ -52,9 +51,6 @@ export function assembleUniversalAgentTools(options: {
|
|
|
52
51
|
options.workspaceRevisions
|
|
53
52
|
? createWorkspaceRevisionTools(options.workspaceRevisions)
|
|
54
53
|
: {},
|
|
55
|
-
options.codeExecution
|
|
56
|
-
? createCodeExecutionTool(options.codeExecution)
|
|
57
|
-
: {},
|
|
58
54
|
options.sandbox ? createSandboxTools(options.sandbox) : {},
|
|
59
55
|
options.schedule ? createScheduleTools(options.schedule) : {},
|
|
60
56
|
);
|
|
@@ -63,6 +59,7 @@ export function assembleUniversalAgentTools(options: {
|
|
|
63
59
|
tools,
|
|
64
60
|
bindings: {
|
|
65
61
|
...(options.workspace ? { workspace: options.workspace } : {}),
|
|
62
|
+
...(options.codeExecution ? { codeExecution: options.codeExecution } : {}),
|
|
66
63
|
...(options.memory ? { memory: options.memory } : {}),
|
|
67
64
|
...(options.subagents ? { subagents: options.subagents } : {}),
|
|
68
65
|
},
|
package/src/index.ts
CHANGED
|
@@ -86,6 +86,7 @@ export {
|
|
|
86
86
|
toolRegistryFromPiCandidates,
|
|
87
87
|
} from "./tool-registry";
|
|
88
88
|
export type { ModelOption } from "./lib/model-catalog";
|
|
89
|
+
export type { RuntimeCodeExecutionFactory } from "./tool-registry";
|
|
89
90
|
export {
|
|
90
91
|
assembleSubagentPrompt,
|
|
91
92
|
assembleSystemPrompt,
|
|
@@ -134,11 +135,10 @@ export { skillPiToolCandidates } from "./pi/tool";
|
|
|
134
135
|
export type { PiSkillBinding } from "./pi/tool";
|
|
135
136
|
export {
|
|
136
137
|
browserQuickActionPiToolCandidates,
|
|
137
|
-
createCodeExecutionTool,
|
|
138
138
|
codeExecutionPiToolCandidate,
|
|
139
139
|
} from "./pi/tool";
|
|
140
140
|
export {
|
|
141
|
-
|
|
141
|
+
createWorkspaceCodeExecutionFactory,
|
|
142
142
|
} from "./pi/tool";
|
|
143
143
|
export {
|
|
144
144
|
assemblePiExtensions,
|
|
@@ -117,7 +117,7 @@ export class SubmissionQueueFullError extends Error {
|
|
|
117
117
|
readonly code = "queue_full";
|
|
118
118
|
|
|
119
119
|
constructor() {
|
|
120
|
-
super(`
|
|
120
|
+
super(`SpringBrand submission queue is full (${MAX_PENDING_SUBMISSIONS})`);
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
@@ -303,6 +303,16 @@ export class SubmissionLifecycle<
|
|
|
303
303
|
return this.start(submissionId, true);
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
/** 等当前切片退出后恢复同一条非终态 Submission,避免 planned wake 被实例内去重吞掉。 */
|
|
307
|
+
async recoverAfterCurrent(submissionId: string): Promise<TSubmission> {
|
|
308
|
+
const current = this.executions.get(submissionId);
|
|
309
|
+
if (current) {
|
|
310
|
+
const outcome = await current;
|
|
311
|
+
if (isTerminalSubmissionStatus(outcome.status)) return outcome;
|
|
312
|
+
}
|
|
313
|
+
return this.start(submissionId, true);
|
|
314
|
+
}
|
|
315
|
+
|
|
306
316
|
recoverHead(): Promise<TSubmission | null> {
|
|
307
317
|
const running = this.options.store.findRunning();
|
|
308
318
|
if (running) return this.start(running.submissionId, true);
|
|
@@ -419,7 +429,7 @@ export class SubmissionLifecycle<
|
|
|
419
429
|
}
|
|
420
430
|
const submission = this.options.store.find(submissionId);
|
|
421
431
|
if (!submission) {
|
|
422
|
-
throw new Error(`Unknown
|
|
432
|
+
throw new Error(`Unknown SpringBrand submission: ${submissionId}`);
|
|
423
433
|
}
|
|
424
434
|
shouldPump = isTerminalSubmissionStatus(submission.status);
|
|
425
435
|
return submission;
|
|
@@ -450,10 +460,13 @@ export class SubmissionLifecycle<
|
|
|
450
460
|
// TODO(待确认): 持久层若长期保持非终态且没有执行者,这里没有超时或外部唤醒上限。
|
|
451
461
|
for (;;) {
|
|
452
462
|
const execution = this.executions.get(submissionId);
|
|
453
|
-
if (execution)
|
|
463
|
+
if (execution) {
|
|
464
|
+
await execution;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
454
467
|
const submission = this.options.store.find(submissionId);
|
|
455
468
|
if (!submission) {
|
|
456
|
-
throw new Error(`Unknown
|
|
469
|
+
throw new Error(`Unknown SpringBrand submission: ${submissionId}`);
|
|
457
470
|
}
|
|
458
471
|
if (isTerminalSubmissionStatus(submission.status)) return submission;
|
|
459
472
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
@@ -491,7 +504,7 @@ export class SubmissionLifecycle<
|
|
|
491
504
|
): Promise<TSubmission> {
|
|
492
505
|
const latest = this.options.store.find(submission.submissionId);
|
|
493
506
|
if (!latest) {
|
|
494
|
-
throw new Error(`Unknown
|
|
507
|
+
throw new Error(`Unknown SpringBrand submission: ${submission.submissionId}`);
|
|
495
508
|
}
|
|
496
509
|
if (isTerminalSubmissionStatus(latest.status)) return latest;
|
|
497
510
|
return this.options.commitTerminal(latest, outcome, message);
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ArtifactRef,
|
|
3
|
+
serializeOutput,
|
|
4
|
+
sha256Hex,
|
|
5
|
+
type SpillWorkspace,
|
|
6
|
+
} from "../../../lib/artifacts";
|
|
7
|
+
|
|
1
8
|
/**
|
|
2
9
|
* Durable history needs bounded values so one Tool response cannot make a
|
|
3
10
|
* session impossible to restore.
|
|
@@ -11,7 +18,15 @@
|
|
|
11
18
|
/** Maximum string leaf retained in durable tool output. */
|
|
12
19
|
const STORAGE_LEAF_MAX_CHARS = 32 * 1024;
|
|
13
20
|
|
|
21
|
+
/** Serialized outputs above this threshold get a narrower model view. */
|
|
22
|
+
const MODEL_VIEW_THRESHOLD = 8 * 1024;
|
|
23
|
+
|
|
24
|
+
/** Maximum string leaf exposed in a structure-preserving model view. */
|
|
25
|
+
const MODEL_LEAF_MAX_CHARS = 500;
|
|
26
|
+
|
|
14
27
|
const ELISION_RESERVE = 40;
|
|
28
|
+
const PREVIEW_CHARS = 600;
|
|
29
|
+
const DEFAULT_SPILL_DIR = "/scratch/tool-output";
|
|
15
30
|
|
|
16
31
|
// 作用:把过长文本从中间截短,同时保留开头和结尾。
|
|
17
32
|
// 调用:`truncateStringLeaves` 遇到字符串叶子时调用,传入该叶子的上限。
|
|
@@ -86,3 +101,87 @@ export function truncateStringLeaves(value: unknown, maxChars: number): unknown
|
|
|
86
101
|
export function boundDurableToolOutput(value: unknown): unknown {
|
|
87
102
|
return truncateStringLeaves(value, STORAGE_LEAF_MAX_CHARS);
|
|
88
103
|
}
|
|
104
|
+
|
|
105
|
+
/** Selects whether a Tool result is externalized or kept structurally durable. */
|
|
106
|
+
export type BudgetPolicy = { kind: "spill" } | { kind: "structure" };
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Writes one large payload to Workspace and returns its narrow durable reference.
|
|
110
|
+
*
|
|
111
|
+
* Small values and failed/unavailable Workspace writes return `null`; callers
|
|
112
|
+
* must then retain the existing bounded-structure fallback.
|
|
113
|
+
*/
|
|
114
|
+
export async function spillDurableToolOutput(
|
|
115
|
+
value: unknown,
|
|
116
|
+
options: {
|
|
117
|
+
readonly workspace?: SpillWorkspace;
|
|
118
|
+
},
|
|
119
|
+
): Promise<ArtifactRef | null> {
|
|
120
|
+
const { text, ext } = serializeOutput(value);
|
|
121
|
+
if (text.length <= MODEL_VIEW_THRESHOLD || !options.workspace) return null;
|
|
122
|
+
try {
|
|
123
|
+
const hash = await sha256Hex(text);
|
|
124
|
+
const path = `${DEFAULT_SPILL_DIR}/${hash.slice(0, 24)}.${ext}`;
|
|
125
|
+
await options.workspace.writeFile(
|
|
126
|
+
path,
|
|
127
|
+
text,
|
|
128
|
+
ext === "json" ? "application/json" : "text/plain",
|
|
129
|
+
);
|
|
130
|
+
return {
|
|
131
|
+
kind: "artifact_ref",
|
|
132
|
+
path,
|
|
133
|
+
bytes: new TextEncoder().encode(text).byteLength,
|
|
134
|
+
hash: hash.slice(0, 16),
|
|
135
|
+
preview: text.slice(0, PREVIEW_CHARS),
|
|
136
|
+
note:
|
|
137
|
+
"Output was large and has been saved to the Workspace file above. " +
|
|
138
|
+
"Use the read tool with that path to retrieve the full content when needed.",
|
|
139
|
+
};
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Bounds leaves first, then removes oldest array entries until the view fits.
|
|
146
|
+
function structureView(output: unknown): unknown {
|
|
147
|
+
const capped = truncateStringLeaves(output, MODEL_LEAF_MAX_CHARS);
|
|
148
|
+
if (serializeOutput(capped).text.length <= MODEL_VIEW_THRESHOLD) return capped;
|
|
149
|
+
if (typeof capped !== "object" || capped === null) return capped;
|
|
150
|
+
|
|
151
|
+
const record = { ...(capped as Record<string, unknown>) };
|
|
152
|
+
const arrayKey = Object.entries(record)
|
|
153
|
+
.filter((entry): entry is [string, unknown[]] => Array.isArray(entry[1]))
|
|
154
|
+
.sort((left, right) => right[1].length - left[1].length)[0]?.[0];
|
|
155
|
+
if (!arrayKey) return capped;
|
|
156
|
+
|
|
157
|
+
const items = [...(record[arrayKey] as unknown[])];
|
|
158
|
+
let dropped = 0;
|
|
159
|
+
while (
|
|
160
|
+
items.length > 1 &&
|
|
161
|
+
serializeOutput({ ...record, [arrayKey]: items }).text.length >
|
|
162
|
+
MODEL_VIEW_THRESHOLD
|
|
163
|
+
) {
|
|
164
|
+
items.shift();
|
|
165
|
+
dropped += 1;
|
|
166
|
+
}
|
|
167
|
+
if (dropped === 0) return capped;
|
|
168
|
+
record[arrayKey] = items;
|
|
169
|
+
record[`${arrayKey}Omitted`] = {
|
|
170
|
+
count: dropped,
|
|
171
|
+
note:
|
|
172
|
+
`${dropped} earlier entr(ies) omitted to fit the model context budget; ` +
|
|
173
|
+
"the most recent are kept. The durable record retains the bounded value.",
|
|
174
|
+
};
|
|
175
|
+
return record;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Creates a non-destructive, smaller Tool-result projection for the model. */
|
|
179
|
+
export function projectToolOutputForModel(output: unknown): unknown {
|
|
180
|
+
try {
|
|
181
|
+
return serializeOutput(output).text.length <= MODEL_VIEW_THRESHOLD
|
|
182
|
+
? output
|
|
183
|
+
: structureView(output);
|
|
184
|
+
} catch {
|
|
185
|
+
return truncateStringLeaves(output, MODEL_LEAF_MAX_CHARS);
|
|
186
|
+
}
|
|
187
|
+
}
|
package/src/lib/prompt.ts
CHANGED
|
@@ -80,9 +80,10 @@ export const FILES =
|
|
|
80
80
|
|
|
81
81
|
// User-interaction actions are called only when their UI semantics are useful.
|
|
82
82
|
export const INTERACTION =
|
|
83
|
-
"Interaction: When
|
|
84
|
-
"instead of writing 'please choose A/B/C' as text
|
|
85
|
-
"
|
|
83
|
+
"Interaction: When progress requires user decisions, put all 1-4 necessary questions in one ask_user call " +
|
|
84
|
+
"instead of asking each question separately or writing 'please choose A/B/C' as text. Use 2-6 options for " +
|
|
85
|
+
"a bounded choice, or no options when a required free-text answer is necessary; continue the same turn when its result arrives. " +
|
|
86
|
+
"Don't use it when a sensible default lets you proceed. " +
|
|
86
87
|
"After completing a substantive task (report, analysis, multi-step job): write your full answer " +
|
|
87
88
|
"FIRST, then call suggest_followups (2-4 directions) as the very last action and end the turn — " +
|
|
88
89
|
"write no text after it (the tool renders its own closing block). Never for small talk or while a " +
|
|
@@ -282,7 +282,7 @@ export class PiChunkEncoder {
|
|
|
282
282
|
return [];
|
|
283
283
|
default:
|
|
284
284
|
throw new Error(
|
|
285
|
-
`Unsupported
|
|
285
|
+
`Unsupported SpringBrand AgentEvent: ${String((event as { type?: unknown }).type)}`,
|
|
286
286
|
);
|
|
287
287
|
}
|
|
288
288
|
}
|
|
@@ -449,7 +449,7 @@ export class PiChunkEncoder {
|
|
|
449
449
|
if (message.stopReason === "error") {
|
|
450
450
|
chunks.push({
|
|
451
451
|
type: "error",
|
|
452
|
-
errorText: message.errorMessage ?? "
|
|
452
|
+
errorText: message.errorMessage ?? "SpringBrand turn failed",
|
|
453
453
|
});
|
|
454
454
|
return chunks;
|
|
455
455
|
}
|
|
@@ -321,7 +321,7 @@ class PreparedRuntime implements PreparedPiRuntime {
|
|
|
321
321
|
read(owner: object): PreparedPiRuntimeState {
|
|
322
322
|
if (owner !== this.owner) {
|
|
323
323
|
throw new Error(
|
|
324
|
-
"Prepared
|
|
324
|
+
"Prepared SpringBrand Runtime belongs to another runtime adapter",
|
|
325
325
|
);
|
|
326
326
|
}
|
|
327
327
|
return this.state;
|
|
@@ -341,7 +341,7 @@ export function readPreparedPiRuntime(
|
|
|
341
341
|
owner: object,
|
|
342
342
|
): PreparedPiRuntimeState {
|
|
343
343
|
if (!(prepared instanceof PreparedRuntime)) {
|
|
344
|
-
throw new Error("Prepared
|
|
344
|
+
throw new Error("Prepared SpringBrand Runtime handle is invalid");
|
|
345
345
|
}
|
|
346
346
|
return prepared.read(owner);
|
|
347
347
|
}
|
|
@@ -46,10 +46,12 @@ import {
|
|
|
46
46
|
withProviderRetry,
|
|
47
47
|
} from "./models";
|
|
48
48
|
import { EXECUTION_LEVELS } from "../../lib/execution-level";
|
|
49
|
+
import { projectToolOutputForModel } from "../../layers/context/budget/gate";
|
|
50
|
+
import type { SpillWorkspace } from "../../lib/artifacts";
|
|
49
51
|
|
|
50
52
|
// #region Single-run Pi bridge
|
|
51
53
|
|
|
52
|
-
const
|
|
54
|
+
const MAX_MODEL_TURNS_PER_SLICE = 30;
|
|
53
55
|
|
|
54
56
|
export class RetryableModelError extends Error {}
|
|
55
57
|
|
|
@@ -91,7 +93,7 @@ function classifyPiStopReason(stopReason: string | undefined): {
|
|
|
91
93
|
return {
|
|
92
94
|
outcome: "failed",
|
|
93
95
|
turnStatus: "error",
|
|
94
|
-
message: `
|
|
96
|
+
message: `SpringBrand ended the turn with an unrecognized stop reason: ${
|
|
95
97
|
stopReason ?? "(none)"
|
|
96
98
|
}`,
|
|
97
99
|
};
|
|
@@ -134,6 +136,29 @@ interface PiTurnAdapterOptions {
|
|
|
134
136
|
// PiCore 通过 AgentOptions.transformContext 调用它,Runtime 用它接入现有上下文处理。
|
|
135
137
|
// 直接复用 Pi 的回调类型可避免这里维护另一套上下文转换约定。
|
|
136
138
|
readonly transformContext: NonNullable<AgentOptions["transformContext"]>;
|
|
139
|
+
readonly workspace?: SpillWorkspace;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function projectToolResultsForModel(
|
|
143
|
+
messages: readonly AgentMessage[],
|
|
144
|
+
): AgentMessage[] {
|
|
145
|
+
let changed = false;
|
|
146
|
+
const projected = messages.map((message) => {
|
|
147
|
+
if (message.role !== "toolResult") return message;
|
|
148
|
+
const payload = {
|
|
149
|
+
content: message.content,
|
|
150
|
+
details: message.details,
|
|
151
|
+
};
|
|
152
|
+
const next = projectToolOutputForModel(payload) as typeof payload;
|
|
153
|
+
if (next === payload) return message;
|
|
154
|
+
changed = true;
|
|
155
|
+
return {
|
|
156
|
+
...message,
|
|
157
|
+
content: next.content,
|
|
158
|
+
details: next.details,
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
return changed ? projected : [...messages];
|
|
137
162
|
}
|
|
138
163
|
|
|
139
164
|
class PiTurnAdapter {
|
|
@@ -155,6 +180,7 @@ class PiTurnAdapter {
|
|
|
155
180
|
private compile(candidates: readonly PiToolCandidate[]) {
|
|
156
181
|
return compilePiTools(candidates, {
|
|
157
182
|
governance: this.governance,
|
|
183
|
+
workspace: this.opts.workspace,
|
|
158
184
|
settle: this.opts.settle,
|
|
159
185
|
});
|
|
160
186
|
}
|
|
@@ -201,7 +227,7 @@ class PiTurnAdapter {
|
|
|
201
227
|
readonly signal?: AbortSignal;
|
|
202
228
|
},
|
|
203
229
|
listener: (event: AgentEvent) => void | Promise<void>,
|
|
204
|
-
): Promise<
|
|
230
|
+
): Promise<PiTurnRunResult> {
|
|
205
231
|
const { systemPrompt, signal } = opts;
|
|
206
232
|
signal?.throwIfAborted();
|
|
207
233
|
const canonicalMessages = await this.opts.canonicalMessages();
|
|
@@ -221,11 +247,14 @@ class PiTurnAdapter {
|
|
|
221
247
|
// 切换 provider 或消息跨 provider 回放时若不规范化,provider 会静默拒绝。
|
|
222
248
|
transformContext: async (messages, signal) => {
|
|
223
249
|
const ctx = await this.opts.transformContext(messages, signal);
|
|
224
|
-
return transformMessages(
|
|
250
|
+
return transformMessages(
|
|
251
|
+
projectToolResultsForModel(ctx) as Message[],
|
|
252
|
+
this.opts.pi.model,
|
|
253
|
+
) as AgentMessage[];
|
|
225
254
|
},
|
|
226
255
|
afterToolCall: this.governance.afterToolCall,
|
|
227
256
|
shouldStopAfterTurn: () => {
|
|
228
|
-
reachedModelTurnLimit = ++modelTurns >=
|
|
257
|
+
reachedModelTurnLimit = ++modelTurns >= MAX_MODEL_TURNS_PER_SLICE;
|
|
229
258
|
return signal?.aborted === true || reachedModelTurnLimit;
|
|
230
259
|
},
|
|
231
260
|
initialState: {
|
|
@@ -256,7 +285,9 @@ class PiTurnAdapter {
|
|
|
256
285
|
pi.abort();
|
|
257
286
|
}
|
|
258
287
|
await running;
|
|
259
|
-
return reachedModelTurnLimit
|
|
288
|
+
return reachedModelTurnLimit
|
|
289
|
+
? { kind: "yielded" }
|
|
290
|
+
: { kind: "terminal" };
|
|
260
291
|
} finally {
|
|
261
292
|
signal?.removeEventListener("abort", abort);
|
|
262
293
|
unsubscribe();
|
|
@@ -447,6 +478,10 @@ export interface PiPreparedTurnRunOptions {
|
|
|
447
478
|
readonly signal?: AbortSignal;
|
|
448
479
|
}
|
|
449
480
|
|
|
481
|
+
export type PiTurnRunResult =
|
|
482
|
+
| { readonly kind: "terminal" }
|
|
483
|
+
| { readonly kind: "yielded" };
|
|
484
|
+
|
|
450
485
|
// #endregion
|
|
451
486
|
|
|
452
487
|
// #region Durable prepared-Turn execution
|
|
@@ -505,7 +540,7 @@ export class PreparedPiTurnAdapter {
|
|
|
505
540
|
: candidate.requiredExecutionLevel;
|
|
506
541
|
if (!EXECUTION_LEVELS.includes(requiredExecutionLevel)) {
|
|
507
542
|
throw new Error(
|
|
508
|
-
`
|
|
543
|
+
`SpringBrand Tool "${candidate.tool.name}" resolved an invalid execution level`,
|
|
509
544
|
);
|
|
510
545
|
}
|
|
511
546
|
await state.snapshot.bindings.platform.gateTool?.({
|
|
@@ -652,6 +687,7 @@ export class PreparedPiTurnAdapter {
|
|
|
652
687
|
this.interrupted ? undefined : options.durability.settleTool(call),
|
|
653
688
|
onToolTelemetry: options.onToolTelemetry,
|
|
654
689
|
transformContext: options.transformContext,
|
|
690
|
+
workspace: state.snapshot.bindings.workspace,
|
|
655
691
|
});
|
|
656
692
|
this.systemPrompt = descriptor.systemPrompt;
|
|
657
693
|
this.options = options;
|
|
@@ -751,9 +787,9 @@ export class PreparedPiTurnAdapter {
|
|
|
751
787
|
*/
|
|
752
788
|
async run(
|
|
753
789
|
options: PiPreparedTurnRunOptions,
|
|
754
|
-
): Promise<
|
|
790
|
+
): Promise<PiTurnRunResult> {
|
|
755
791
|
this.terminalIntent = undefined;
|
|
756
|
-
const
|
|
792
|
+
const result = await this.turn.run(
|
|
757
793
|
{
|
|
758
794
|
systemPrompt: this.systemPrompt,
|
|
759
795
|
tools: this.candidates,
|
|
@@ -761,15 +797,11 @@ export class PreparedPiTurnAdapter {
|
|
|
761
797
|
},
|
|
762
798
|
(event) => this.handleEvent(event),
|
|
763
799
|
);
|
|
764
|
-
if (reachedModelTurnLimit && !this.terminalIntent) {
|
|
765
|
-
this.terminalIntent = {
|
|
766
|
-
outcome: "failed",
|
|
767
|
-
message: `Pi stopped after ${MAX_MODEL_TURNS} model turns`,
|
|
768
|
-
};
|
|
769
|
-
}
|
|
770
800
|
if (this.terminalIntent) {
|
|
771
801
|
await this.options.onTerminal(this.terminalIntent);
|
|
802
|
+
return { kind: "terminal" };
|
|
772
803
|
}
|
|
804
|
+
return result;
|
|
773
805
|
}
|
|
774
806
|
|
|
775
807
|
// 提交完成的 Pi 消息、推导终态意图,并把事件投影给可恢复流。
|
|
@@ -498,7 +498,7 @@ function configuredModel(
|
|
|
498
498
|
);
|
|
499
499
|
if (!catalogModel) {
|
|
500
500
|
throw new Error(
|
|
501
|
-
`Model is not in
|
|
501
|
+
`Model is not in SpringBrand's built-in catalog for ${endpoint.protocol}: ${modelId}`,
|
|
502
502
|
);
|
|
503
503
|
}
|
|
504
504
|
const {
|
|
@@ -201,7 +201,7 @@ function milestoneMutation(
|
|
|
201
201
|
// 缺少身份时立即失败,避免把无归属的记录混入另一次 Turn 的恢复日志。
|
|
202
202
|
function recoveryIdentity(input: PiRecoveryInput) {
|
|
203
203
|
if (!input.identity) {
|
|
204
|
-
throw new Error("
|
|
204
|
+
throw new Error("SpringBrand recovery command is missing its Turn revision");
|
|
205
205
|
}
|
|
206
206
|
return { version: 1 as const, ...input.identity };
|
|
207
207
|
}
|
|
@@ -255,7 +255,7 @@ function interactionToolResult(
|
|
|
255
255
|
timestamp: number,
|
|
256
256
|
): ToolResultMessage {
|
|
257
257
|
if (!interaction) {
|
|
258
|
-
throw new Error("
|
|
258
|
+
throw new Error("SpringBrand Tool interaction is missing for its response");
|
|
259
259
|
}
|
|
260
260
|
return {
|
|
261
261
|
role: "toolResult",
|
|
@@ -417,7 +417,7 @@ export function decidePiRecovery(
|
|
|
417
417
|
(milestone) => milestone.type === "approval",
|
|
418
418
|
);
|
|
419
419
|
if (!approval || approval.type !== "approval") {
|
|
420
|
-
throw new Error("
|
|
420
|
+
throw new Error("SpringBrand approval decision milestone is missing");
|
|
421
421
|
}
|
|
422
422
|
mutations.push({
|
|
423
423
|
kind: "decide-approval",
|
|
@@ -453,7 +453,7 @@ export function decidePiRecovery(
|
|
|
453
453
|
JSON.stringify(existing.input) !== JSON.stringify(record.input)
|
|
454
454
|
) {
|
|
455
455
|
throw new Error(
|
|
456
|
-
`Conflicting
|
|
456
|
+
`Conflicting SpringBrand Tool input for ${record.toolCallId}`,
|
|
457
457
|
);
|
|
458
458
|
}
|
|
459
459
|
} else {
|
|
@@ -482,7 +482,7 @@ export function decidePiRecovery(
|
|
|
482
482
|
existing.inputJson !== approval.inputJson
|
|
483
483
|
) {
|
|
484
484
|
throw new Error(
|
|
485
|
-
`Conflicting
|
|
485
|
+
`Conflicting SpringBrand Tool approval: ${approval.executionId}`,
|
|
486
486
|
);
|
|
487
487
|
}
|
|
488
488
|
} else {
|
|
@@ -530,7 +530,7 @@ export function decidePiRecovery(
|
|
|
530
530
|
existing.inputJson !== interaction.inputJson
|
|
531
531
|
) {
|
|
532
532
|
throw new Error(
|
|
533
|
-
`Conflicting
|
|
533
|
+
`Conflicting SpringBrand Tool interaction: ${interaction.interactionId}`,
|
|
534
534
|
);
|
|
535
535
|
}
|
|
536
536
|
} else {
|
|
@@ -580,7 +580,7 @@ export function decidePiRecovery(
|
|
|
580
580
|
(milestone) => milestone.type === "interaction",
|
|
581
581
|
);
|
|
582
582
|
if (!record || record.type !== "interaction") {
|
|
583
|
-
throw new Error("
|
|
583
|
+
throw new Error("SpringBrand interaction settlement milestone is missing");
|
|
584
584
|
}
|
|
585
585
|
mutations.push({
|
|
586
586
|
kind: "settle-interaction",
|
|
@@ -621,7 +621,7 @@ export function decidePiRecovery(
|
|
|
621
621
|
if (existing) {
|
|
622
622
|
if (JSON.stringify(existing) !== JSON.stringify(toolResult)) {
|
|
623
623
|
throw new Error(
|
|
624
|
-
`Conflicting
|
|
624
|
+
`Conflicting SpringBrand Tool result for ${command.toolCallId}`,
|
|
625
625
|
);
|
|
626
626
|
}
|
|
627
627
|
} else {
|
|
@@ -676,7 +676,7 @@ export function decidePiRecovery(
|
|
|
676
676
|
plan.continuationKey !== input.command.continuationKey
|
|
677
677
|
) {
|
|
678
678
|
throw new Error(
|
|
679
|
-
`
|
|
679
|
+
`SpringBrand continuation is not ready: ${input.command.continuationKey}`,
|
|
680
680
|
);
|
|
681
681
|
}
|
|
682
682
|
const committed = commitPiRecoveryContinuation(
|
|
@@ -686,7 +686,7 @@ export function decidePiRecovery(
|
|
|
686
686
|
);
|
|
687
687
|
if (!committed) {
|
|
688
688
|
throw new Error(
|
|
689
|
-
`
|
|
689
|
+
`SpringBrand continuation could not be committed: ${plan.continuationKey}`,
|
|
690
690
|
);
|
|
691
691
|
}
|
|
692
692
|
const mutation = milestoneMutation(
|
|
@@ -809,13 +809,13 @@ export class PiRuntimeRecoveryAdapter
|
|
|
809
809
|
if (!durable) {
|
|
810
810
|
return {
|
|
811
811
|
kind: "fail",
|
|
812
|
-
reason: "Recovered
|
|
812
|
+
reason: "Recovered SpringBrand submission is missing",
|
|
813
813
|
} as const;
|
|
814
814
|
}
|
|
815
815
|
if (durable.terminal) {
|
|
816
816
|
return {
|
|
817
817
|
kind: "ignore",
|
|
818
|
-
reason: "
|
|
818
|
+
reason: "SpringBrand submission is already terminal",
|
|
819
819
|
} as const;
|
|
820
820
|
}
|
|
821
821
|
const decision = decidePiRecovery({
|
|
@@ -827,20 +827,20 @@ export class PiRuntimeRecoveryAdapter
|
|
|
827
827
|
if (decision.effect.reason === "approval") {
|
|
828
828
|
return {
|
|
829
829
|
kind: "park",
|
|
830
|
-
reason: "
|
|
830
|
+
reason: "SpringBrand Turn is waiting for Tool approval",
|
|
831
831
|
} as const;
|
|
832
832
|
}
|
|
833
833
|
if (decision.effect.reason === "interaction") {
|
|
834
834
|
return {
|
|
835
835
|
kind: "park",
|
|
836
|
-
reason: "
|
|
836
|
+
reason: "SpringBrand Turn is waiting for a client Tool interaction response",
|
|
837
837
|
} as const;
|
|
838
838
|
}
|
|
839
839
|
if (decision.effect.reason === "uncertain-tool") {
|
|
840
840
|
return {
|
|
841
841
|
kind: "park",
|
|
842
842
|
reason:
|
|
843
|
-
"
|
|
843
|
+
"SpringBrand Turn is parked on an uncertain non-idempotent Tool",
|
|
844
844
|
incidentStatus: "failed",
|
|
845
845
|
} as const;
|
|
846
846
|
}
|
|
@@ -848,12 +848,12 @@ export class PiRuntimeRecoveryAdapter
|
|
|
848
848
|
return {
|
|
849
849
|
kind: "fail",
|
|
850
850
|
reason:
|
|
851
|
-
"
|
|
851
|
+
"SpringBrand recovery is complete but the Submission is not terminal",
|
|
852
852
|
} as const;
|
|
853
853
|
}
|
|
854
854
|
return {
|
|
855
855
|
kind: "park",
|
|
856
|
-
reason: "
|
|
856
|
+
reason: "SpringBrand recovery is waiting for durable input",
|
|
857
857
|
} as const;
|
|
858
858
|
}
|
|
859
859
|
return {
|
|
@@ -526,7 +526,7 @@ export class PiRuntimeTranscript {
|
|
|
526
526
|
*/
|
|
527
527
|
importSnapshot(snapshot: PiCanonicalTranscriptSnapshot): void {
|
|
528
528
|
if (this.hasActiveTurn()) {
|
|
529
|
-
throw new Error("Cannot import messages while a
|
|
529
|
+
throw new Error("Cannot import messages while a SpringBrand Turn is active");
|
|
530
530
|
}
|
|
531
531
|
this.durability.transaction(() => {
|
|
532
532
|
let currentSubmissionId: string | undefined;
|
|
@@ -551,7 +551,7 @@ export class PiRuntimeTranscript {
|
|
|
551
551
|
: {}),
|
|
552
552
|
});
|
|
553
553
|
if (!appended) {
|
|
554
|
-
throw new Error(`
|
|
554
|
+
throw new Error(`SpringBrand Session message "${entry.id}" already exists`);
|
|
555
555
|
}
|
|
556
556
|
}
|
|
557
557
|
});
|