@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/lib/prompt.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stable system-prompt sections shared by every Runtime profile.
|
|
3
3
|
*
|
|
4
|
-
* Only `PERSONA` is replaceable by `RuntimeProfile.systemPrompt`.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* not actually provide.
|
|
4
|
+
* Only `PERSONA` is replaceable by `RuntimeProfile.systemPrompt`. Environment,
|
|
5
|
+
* tool and safety facts remain because a custom Agent persona must not redefine
|
|
6
|
+
* capabilities that the running kernel does not actually provide.
|
|
8
7
|
* Dynamic skills, memory and context blocks are injected by their own modules
|
|
9
8
|
* so this static prefix remains cacheable.
|
|
10
9
|
*/
|
|
@@ -15,13 +14,6 @@ export const PERSONA =
|
|
|
15
14
|
"You can recall user-managed cold memory across sessions and keep working memory within the current Session, " +
|
|
16
15
|
"manage files in your workspace, and use execute Code Mode for network requests and tool composition.";
|
|
17
16
|
|
|
18
|
-
// Base constraint injected alongside every Agent-specific persona.
|
|
19
|
-
export const SYSTEM_PROMPT =
|
|
20
|
-
"Web development: when writing a website or web page, use browser-ready mode: emit browser-ready static files " +
|
|
21
|
-
"that run directly in Workspace preview. Use ./ or ../ for every Workspace asset, never a root-relative path; " +
|
|
22
|
-
"load CSS from HTML instead of importing it from JavaScript. Resolve third-party ESM with a full pinned HTTPS URL " +
|
|
23
|
-
"or import map to a pinned CORS-enabled CDN. Do not require Vite, Node.js, bundling, or Sandbox at preview time.";
|
|
24
|
-
|
|
25
17
|
// Explains how to reach capabilities instead of listing unsupported substitutes.
|
|
26
18
|
export const RUNTIME =
|
|
27
19
|
"Runtime: this agent runs on Cloudflare Workers. When execute is present, its Code Mode Dynamic Worker is your " +
|
|
@@ -56,7 +48,8 @@ export const TOOLS =
|
|
|
56
48
|
"Tools: When execute is present, call a top-level Tool directly only for a single standalone Tool call or when that Tool is unavailable " +
|
|
57
49
|
"inside execute. Any operation that needs the same Tool more than once, two or more related file, Skill, Extension, MCP, or Host Tool calls, " +
|
|
58
50
|
"or branching or repetition MUST use one execute. Do not make repeated top-level calls for that work. Inside execute, use state.* for " +
|
|
59
|
-
"workspace file operations, tools.* for other host capabilities, a loop for repeated calls, and Promise.all for independent
|
|
51
|
+
"workspace file operations, tools.* for other host capabilities, a loop for repeated calls, and Promise.all for independent calls. " +
|
|
52
|
+
"For materialize_skill_resource, materialize every known set of Skill resources in one execute; never start one execute per resource. " +
|
|
60
53
|
"Tools available only at the top level must stay Direct. " +
|
|
61
54
|
"For web tasks, use web_search for web discovery, current facts, cited research, " +
|
|
62
55
|
"and public URL analysis. For web access specifically, use execute Code Mode only for raw or customized network requests, structured " +
|
|
@@ -75,6 +68,30 @@ export const TOOLS =
|
|
|
75
68
|
"only independent top-level-only Direct Tools should be issued together in one turn. When you reference " +
|
|
76
69
|
"code, cite it as file_path:line_number.";
|
|
77
70
|
|
|
71
|
+
// The browser tool is the only way to observe what a page actually does; its
|
|
72
|
+
// failure mode is returning nothing and looking clean, so the shape is fixed here.
|
|
73
|
+
export const BROWSER =
|
|
74
|
+
"Browser: When browser_execute is present, it is for one thing execute cannot do — what a page actually " +
|
|
75
|
+
"does in a real browser: rendered result, console output, runtime exceptions, failed resource loads, CSP " +
|
|
76
|
+
"blocks, and state after an interaction. Everything else stays with execute: computation, files, and " +
|
|
77
|
+
"ordinary HTTP requests (fetching HTML is execute's job, not the browser's). " +
|
|
78
|
+
"After you write or change a web page in the workspace, open it once with browser_execute before you call " +
|
|
79
|
+
"the work done, and say in your reply what you checked and what you saw. " +
|
|
80
|
+
"The cdp connector is request/response only: it delivers no CDP events. Enabling Runtime/Log/Network and " +
|
|
81
|
+
"then reading cdp.getDebugLog() returns method names with no payload — a page full of errors reads as clean. " +
|
|
82
|
+
"So the fixed shape is: create a target and attach for a sessionId; BEFORE Page.navigate, install a page-side " +
|
|
83
|
+
"collector via Page.addScriptToEvaluateOnNewDocument that buffers console.*, a capture-phase window 'error' " +
|
|
84
|
+
"listener (it catches both uncaught exceptions and failed <script>/<img>/<link> loads), 'unhandledrejection' " +
|
|
85
|
+
"and 'securitypolicyviolation' into one page global; then navigate, poll Runtime.evaluate for " +
|
|
86
|
+
"document.readyState === 'complete' (you cannot await an event), then Runtime.evaluate that global back with " +
|
|
87
|
+
"returnByValue. When visual appearance matters, call Page.captureScreenshot and return " +
|
|
88
|
+
"`{ image: { mimeType: 'image/png', data: screenshot.data }, observations }`; the Runtime projects that image " +
|
|
89
|
+
"back to you, so evaluate the visible hierarchy, spacing, clipping and overlap against the request as well as " +
|
|
90
|
+
"the textual diagnostics. Issue CDP calls sequentially — never Promise.all — because call order is recorded for replay. " +
|
|
91
|
+
"Fix what you find and re-check; when the page comes back clean, stop — do not re-verify a page that is " +
|
|
92
|
+
"already clean. If browser_execute is absent, say plainly that you could not verify the page in a browser " +
|
|
93
|
+
"instead of implying you did.";
|
|
94
|
+
|
|
78
95
|
// Uploaded-file routing prevents lossy markdown conversions from becoming data sources.
|
|
79
96
|
export const FILES =
|
|
80
97
|
"Uploaded files live under /uploads/ in your workspace; convertible formats have a companion " +
|
|
@@ -112,7 +129,7 @@ export const MEMORY =
|
|
|
112
129
|
* @remarks
|
|
113
130
|
* Pi 快照装配在生成每次 Runtime Snapshot 时调用;调用方只替换角色文本,不要重复拼接其他段落。
|
|
114
131
|
*
|
|
115
|
-
*
|
|
132
|
+
* Runtime、行为、规划、Tool、浏览器、文件、交互和 Memory 按固定顺序追加,
|
|
116
133
|
* 因为自定义角色不应重新定义实际 Kernel 没有的能力。
|
|
117
134
|
*
|
|
118
135
|
* 各段保持静态,动态 Skill、Context 和 Memory 由各自模块注入,避免这个可缓存前缀每回合改变。
|
|
@@ -121,12 +138,12 @@ export const MEMORY =
|
|
|
121
138
|
*/
|
|
122
139
|
export function assembleSystemPrompt(base?: string | null): string {
|
|
123
140
|
return [
|
|
124
|
-
SYSTEM_PROMPT,
|
|
125
141
|
base ?? PERSONA,
|
|
126
142
|
RUNTIME,
|
|
127
143
|
BEHAVIOR,
|
|
128
144
|
PLANNING,
|
|
129
145
|
TOOLS,
|
|
146
|
+
BROWSER,
|
|
130
147
|
FILES,
|
|
131
148
|
INTERACTION,
|
|
132
149
|
MEMORY,
|
|
@@ -139,7 +156,7 @@ export function assembleSystemPrompt(base?: string | null): string {
|
|
|
139
156
|
* @remarks
|
|
140
157
|
* Pi SubAgent 在启动有界子任务前调用;调用方应传入该子代理类型自己的 persona。
|
|
141
158
|
*
|
|
142
|
-
* 子代理共享真实执行环境和基本行为,但不获得主 Agent
|
|
159
|
+
* 子代理共享真实执行环境和基本行为,但不获得主 Agent 专属的 Skill、规划、Memory 和用户交互指令,因为这些责任留在主循环。
|
|
143
160
|
*
|
|
144
161
|
* 不要直接改成复用 `assembleSystemPrompt`,否则子代理会收到它没有的 Session 记忆和对话交互责任。
|
|
145
162
|
*
|
|
@@ -15,6 +15,8 @@ import { USER_STOP_REASON } from "../../kernel/receipts";
|
|
|
15
15
|
|
|
16
16
|
const MAX_OUTPUT_BYTES = 256 * 1024;
|
|
17
17
|
const MAX_OUTPUT_PREVIEW = 16 * 1024;
|
|
18
|
+
const PROVIDER_CREDIT_ERROR =
|
|
19
|
+
"The AI service is temporarily unavailable. Please try again later.";
|
|
18
20
|
|
|
19
21
|
export interface PiToolApprovalView {
|
|
20
22
|
readonly id: string;
|
|
@@ -101,6 +103,16 @@ function errorText(result: unknown): string {
|
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
105
|
|
|
106
|
+
/** Keep provider account details out of browser-facing error messages. */
|
|
107
|
+
export function publicAssistantError(message?: string): string | undefined {
|
|
108
|
+
if (!message) return undefined;
|
|
109
|
+
return /"limit_source"\s*:\s*"openrouter_credits"/.test(message) ||
|
|
110
|
+
(message.includes("Insufficient credits") &&
|
|
111
|
+
message.includes("openrouter.ai/settings/credits"))
|
|
112
|
+
? PROVIDER_CREDIT_ERROR
|
|
113
|
+
: message;
|
|
114
|
+
}
|
|
115
|
+
|
|
104
116
|
function toolCallAt(
|
|
105
117
|
update: AssistantMessageEvent,
|
|
106
118
|
): Partial<ToolCall> | undefined {
|
|
@@ -318,7 +330,12 @@ export class PiChunkEncoder {
|
|
|
318
330
|
case "toolcall_end":
|
|
319
331
|
return this.encodeToolInput(update);
|
|
320
332
|
case "error":
|
|
321
|
-
return [{
|
|
333
|
+
return [{
|
|
334
|
+
type: "error",
|
|
335
|
+
errorText: publicAssistantError(
|
|
336
|
+
update.error.errorMessage ?? update.reason,
|
|
337
|
+
) ?? "SpringBrand turn failed",
|
|
338
|
+
}];
|
|
322
339
|
default:
|
|
323
340
|
return [];
|
|
324
341
|
}
|
|
@@ -421,6 +438,7 @@ export class PiChunkEncoder {
|
|
|
421
438
|
return chunks;
|
|
422
439
|
}
|
|
423
440
|
this.finished = true;
|
|
441
|
+
const publicError = publicAssistantError(message.errorMessage);
|
|
424
442
|
if (this.startedAt !== undefined) {
|
|
425
443
|
const turnStatus = message.stopReason === "aborted"
|
|
426
444
|
? "aborted"
|
|
@@ -434,10 +452,10 @@ export class PiChunkEncoder {
|
|
|
434
452
|
completedAt: message.timestamp,
|
|
435
453
|
turnDurationMs: Math.max(0, message.timestamp - this.startedAt),
|
|
436
454
|
turnStatus,
|
|
437
|
-
...(
|
|
455
|
+
...(publicError &&
|
|
438
456
|
(message.stopReason !== "aborted" ||
|
|
439
457
|
message.errorMessage !== USER_STOP_REASON)
|
|
440
|
-
? { error:
|
|
458
|
+
? { error: publicError }
|
|
441
459
|
: {}),
|
|
442
460
|
},
|
|
443
461
|
});
|
|
@@ -449,7 +467,7 @@ export class PiChunkEncoder {
|
|
|
449
467
|
if (message.stopReason === "error") {
|
|
450
468
|
chunks.push({
|
|
451
469
|
type: "error",
|
|
452
|
-
errorText:
|
|
470
|
+
errorText: publicError ?? "SpringBrand turn failed",
|
|
453
471
|
});
|
|
454
472
|
return chunks;
|
|
455
473
|
}
|
|
@@ -53,8 +53,31 @@ import { toolRegistryFromPiCandidates } from "../../tool-registry";
|
|
|
53
53
|
|
|
54
54
|
// #region Single-run Pi bridge
|
|
55
55
|
|
|
56
|
+
// 一片执行最多跑多少个模型回合。这是 CPU 预算边界,不是任务边界:DO 的
|
|
57
|
+
// CPU 上限按 invocation 计,一条长回合跑在一次 invocation 里会把预算吃满并被驱逐,
|
|
58
|
+
// 让步换来的是一次全新 invocation 和全新预算。
|
|
56
59
|
const MAX_MODEL_TURNS_PER_SLICE = 30;
|
|
57
60
|
|
|
61
|
+
// 一条 Submission 跨所有执行片最多跑多少个模型回合。这是任务边界:
|
|
62
|
+
// 没有它,一个不收敛的工具循环会无限让步、无限唤醒、无限烧 token。
|
|
63
|
+
export const MAX_MODEL_TURNS_PER_SUBMISSION = 300;
|
|
64
|
+
|
|
65
|
+
// 预算耗尽时注入的收尾指令。跟着它一起把工具表清空,模型只能出文本,
|
|
66
|
+
// 于是这一轮以一条正常的终止助手消息结束 —— 而不是硬判失败或继续让步。
|
|
67
|
+
// 走 user 角色而非 assistant:多个 provider 拒绝以模型回合结尾的请求。
|
|
68
|
+
const WRAP_UP_MESSAGE = "You have reached this run's model-turn budget. Stop " +
|
|
69
|
+
"calling tools now. Reply with a final message that states what you " +
|
|
70
|
+
"completed, what you did not finish, and the exact next step you would " +
|
|
71
|
+
"take. Do not start new work.";
|
|
72
|
+
|
|
73
|
+
const TOOL_FAILURE_WRAP_UP_MESSAGE = "This turn reached its tool failure limit. " +
|
|
74
|
+
"Stop calling tools now. Reply with a final message that explains what " +
|
|
75
|
+
"failed and the exact next step the user should take. Do not start new work.";
|
|
76
|
+
|
|
77
|
+
// 收尾开始后最多再放行几个模型回合。工具已经清空,正常一轮就结束了;
|
|
78
|
+
// 留出余量是给 steering 消息,但不能因此把本片的 CPU 上限一起取消。
|
|
79
|
+
const MAX_WRAP_UP_TURNS = 2;
|
|
80
|
+
|
|
58
81
|
export class RetryableModelError extends Error {}
|
|
59
82
|
|
|
60
83
|
function normalizePlanToolCalls(message: AgentMessage): void {
|
|
@@ -136,14 +159,41 @@ interface PiTurnAdapterOptions {
|
|
|
136
159
|
// 直接复用 Pi 的回调类型可避免这里维护另一套上下文转换约定。
|
|
137
160
|
readonly transformContext: NonNullable<AgentOptions["transformContext"]>;
|
|
138
161
|
readonly workspace?: SpillWorkspace;
|
|
162
|
+
// 之前的执行片已经用掉的模型回合数。首次执行传 0。
|
|
163
|
+
readonly modelTurnsConsumed: number;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 收集那些结果已经由工具自己收口的 Tool 名字。
|
|
168
|
+
*
|
|
169
|
+
* @remarks
|
|
170
|
+
* `run` 在建 PiCore 前调用,把结果交给 `projectToolResultsForModel`。
|
|
171
|
+
*
|
|
172
|
+
* `outputBudget: "structure"` 的语义就是“这个结果不需要 Runtime 再替它省”:read
|
|
173
|
+
* 按页返回并在页脚写明覆盖的行号,activate_skill 返回的是模型接下来要照做的指令,
|
|
174
|
+
* Code Mode 的结果由它自己收口。对这些再套一次叶子上限,模型看到的正文会和结果里
|
|
175
|
+
* 写明的范围对不上 —— 页脚说 `lines 1-307`,正文却被从中间挖空。
|
|
176
|
+
*
|
|
177
|
+
* 其余工具仍然照常压缩:这道闸按分类放行,不是整体放宽。
|
|
178
|
+
*/
|
|
179
|
+
function selfBoundedToolNames(
|
|
180
|
+
candidates: readonly PiToolCandidate[],
|
|
181
|
+
): ReadonlySet<string> {
|
|
182
|
+
return new Set(
|
|
183
|
+
candidates
|
|
184
|
+
.filter((candidate) => candidate.outputBudget?.kind === "structure")
|
|
185
|
+
.map((candidate) => candidate.tool.name),
|
|
186
|
+
);
|
|
139
187
|
}
|
|
140
188
|
|
|
141
189
|
function projectToolResultsForModel(
|
|
142
190
|
messages: readonly AgentMessage[],
|
|
191
|
+
selfBounded: ReadonlySet<string>,
|
|
143
192
|
): AgentMessage[] {
|
|
144
193
|
let changed = false;
|
|
145
194
|
const projected = messages.map((message) => {
|
|
146
195
|
if (message.role !== "toolResult") return message;
|
|
196
|
+
if (selfBounded.has(message.toolName)) return message;
|
|
147
197
|
const payload = {
|
|
148
198
|
content: message.content,
|
|
149
199
|
details: message.details,
|
|
@@ -231,8 +281,14 @@ class PiTurnAdapter {
|
|
|
231
281
|
signal?.throwIfAborted();
|
|
232
282
|
const canonicalMessages = await this.opts.canonicalMessages();
|
|
233
283
|
const tools = this.compile(opts.tools);
|
|
234
|
-
|
|
284
|
+
const selfBounded = selfBoundedToolNames(opts.tools);
|
|
285
|
+
// `modelTurns` 是跨执行片的累计数,`sliceTurns` 只数本片 —— 前者管任务预算,
|
|
286
|
+
// 后者管 CPU 预算,两个边界互不替代。
|
|
287
|
+
let modelTurns = this.opts.modelTurnsConsumed;
|
|
288
|
+
let sliceTurns = 0;
|
|
235
289
|
let reachedModelTurnLimit = false;
|
|
290
|
+
let wrappingUp = false;
|
|
291
|
+
let wrapUpTurns = 0;
|
|
236
292
|
const pi = new PiCore({
|
|
237
293
|
convertToLlm,
|
|
238
294
|
streamFn: withProviderRetry(
|
|
@@ -248,14 +304,59 @@ class PiTurnAdapter {
|
|
|
248
304
|
transformContext: async (messages, signal) => {
|
|
249
305
|
const ctx = await this.opts.transformContext(messages, signal);
|
|
250
306
|
return transformMessages(
|
|
251
|
-
projectToolResultsForModel(ctx) as Message[],
|
|
307
|
+
projectToolResultsForModel(ctx, selfBounded) as Message[],
|
|
252
308
|
this.opts.pi.model,
|
|
253
309
|
) as AgentMessage[];
|
|
254
310
|
},
|
|
255
311
|
afterToolCall: this.governance.afterToolCall,
|
|
312
|
+
// Pi 在每个 turn_end 之后先调 prepareNextTurn、再调 shouldStopAfterTurn,
|
|
313
|
+
// 所以回合计数记在这里,停止判据只读不写。
|
|
314
|
+
// 用 `WithContext` 变体:只有它拿得到当前上下文,也就拿得到工具表和消息。
|
|
315
|
+
prepareNextTurnWithContext: ({ context }) => {
|
|
316
|
+
modelTurns += 1;
|
|
317
|
+
sliceTurns += 1;
|
|
318
|
+
if (wrappingUp) {
|
|
319
|
+
wrapUpTurns += 1;
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
const toolFailureLimitReached = this.governance.wrapUpRequested();
|
|
323
|
+
if (
|
|
324
|
+
!toolFailureLimitReached &&
|
|
325
|
+
modelTurns < MAX_MODEL_TURNS_PER_SUBMISSION - 1
|
|
326
|
+
) return undefined;
|
|
327
|
+
// 预算见底或工具熔断:清空工具表并注入收尾指令,逼出一条真正的
|
|
328
|
+
// 终止助手消息。直接判失败的话用户拿不到任何交代。
|
|
329
|
+
// 这条指令不进 newMessages,因此不会写进 transcript —— 它是控制指令,不是历史。
|
|
330
|
+
wrappingUp = true;
|
|
331
|
+
return {
|
|
332
|
+
context: {
|
|
333
|
+
...context,
|
|
334
|
+
tools: [],
|
|
335
|
+
messages: [
|
|
336
|
+
...context.messages,
|
|
337
|
+
{
|
|
338
|
+
role: "user" as const,
|
|
339
|
+
content: [{
|
|
340
|
+
type: "text" as const,
|
|
341
|
+
text: toolFailureLimitReached
|
|
342
|
+
? TOOL_FAILURE_WRAP_UP_MESSAGE
|
|
343
|
+
: WRAP_UP_MESSAGE,
|
|
344
|
+
}],
|
|
345
|
+
timestamp: Date.now(),
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
},
|
|
256
351
|
shouldStopAfterTurn: () => {
|
|
257
|
-
|
|
258
|
-
|
|
352
|
+
if (signal?.aborted === true) return true;
|
|
353
|
+
// 收尾那一轮必须跑完,否则让步会把唯一的终态机会顶掉。但「必须跑完」
|
|
354
|
+
// 不等于「不再有上限」:Pi 在这之后还会取 steering 和 follow-up 消息,
|
|
355
|
+
// 一直有新消息进来就会一直发模型请求,而且全在同一个 invocation 里 ——
|
|
356
|
+
// 那正是 MAX_MODEL_TURNS_PER_SLICE 要挡的 CPU 驱逐。
|
|
357
|
+
if (wrappingUp) return wrapUpTurns >= MAX_WRAP_UP_TURNS;
|
|
358
|
+
reachedModelTurnLimit = sliceTurns >= MAX_MODEL_TURNS_PER_SLICE;
|
|
359
|
+
return reachedModelTurnLimit;
|
|
259
360
|
},
|
|
260
361
|
initialState: {
|
|
261
362
|
model: this.opts.pi.model,
|
|
@@ -286,8 +387,8 @@ class PiTurnAdapter {
|
|
|
286
387
|
}
|
|
287
388
|
await running;
|
|
288
389
|
return reachedModelTurnLimit
|
|
289
|
-
? { kind: "yielded" }
|
|
290
|
-
: { kind: "terminal" };
|
|
390
|
+
? { kind: "yielded", modelTurns }
|
|
391
|
+
: { kind: "terminal", modelTurns };
|
|
291
392
|
} finally {
|
|
292
393
|
signal?.removeEventListener("abort", abort);
|
|
293
394
|
unsubscribe();
|
|
@@ -393,6 +494,8 @@ export interface CreatePreparedPiTurnOptions {
|
|
|
393
494
|
readonly startedAt: number;
|
|
394
495
|
readonly continuation: boolean;
|
|
395
496
|
readonly assistantOrdinal: number;
|
|
497
|
+
/** 之前的执行片已用掉的模型回合数;省略按 0 处理。 */
|
|
498
|
+
readonly modelTurnsConsumed?: number;
|
|
396
499
|
};
|
|
397
500
|
/**
|
|
398
501
|
* 在 Pi 启动前读取最新 canonical transcript。
|
|
@@ -474,8 +577,10 @@ export interface PiPreparedTurnRunOptions {
|
|
|
474
577
|
}
|
|
475
578
|
|
|
476
579
|
export type PiTurnRunResult =
|
|
477
|
-
|
|
478
|
-
| { readonly kind: "
|
|
580
|
+
/** Turn 已产生权威结果,或已经收尾。 */
|
|
581
|
+
| { readonly kind: "terminal"; readonly modelTurns: number }
|
|
582
|
+
/** 本片让出执行,Turn 未结束;调用方必须排出续跑。 */
|
|
583
|
+
| { readonly kind: "yielded"; readonly modelTurns: number };
|
|
479
584
|
|
|
480
585
|
// #endregion
|
|
481
586
|
|
|
@@ -724,6 +829,7 @@ export class PreparedPiTurnAdapter {
|
|
|
724
829
|
this.interrupted ? undefined : options.durability.settleTool(call),
|
|
725
830
|
transformContext: options.transformContext,
|
|
726
831
|
workspace: state.snapshot.bindings.workspace,
|
|
832
|
+
modelTurnsConsumed: options.submission.modelTurnsConsumed ?? 0,
|
|
727
833
|
});
|
|
728
834
|
this.systemPrompt = descriptor.systemPrompt;
|
|
729
835
|
this.options = options;
|
|
@@ -835,7 +941,7 @@ export class PreparedPiTurnAdapter {
|
|
|
835
941
|
);
|
|
836
942
|
if (this.terminalIntent) {
|
|
837
943
|
await this.options.onTerminal(this.terminalIntent);
|
|
838
|
-
return { kind: "terminal" };
|
|
944
|
+
return { kind: "terminal", modelTurns: result.modelTurns };
|
|
839
945
|
}
|
|
840
946
|
return result;
|
|
841
947
|
}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
applyPiToolResult,
|
|
18
18
|
captureUIUserSidecar,
|
|
19
19
|
piAssistantToUIMessage,
|
|
20
|
+
publicAssistantError,
|
|
20
21
|
type PiToolApprovalView,
|
|
21
22
|
type UIUserSidecar,
|
|
22
23
|
} from "../message";
|
|
@@ -655,7 +656,9 @@ export class PiRuntimeTranscript {
|
|
|
655
656
|
completedAt - submission.createdAt,
|
|
656
657
|
),
|
|
657
658
|
turnStatus: "error",
|
|
658
|
-
...(submission.error
|
|
659
|
+
...(submission.error
|
|
660
|
+
? { error: publicAssistantError(submission.error) }
|
|
661
|
+
: {}),
|
|
659
662
|
},
|
|
660
663
|
});
|
|
661
664
|
seenAssistantSubmissions.add(submissionId);
|
|
@@ -761,7 +764,9 @@ export class PiRuntimeTranscript {
|
|
|
761
764
|
),
|
|
762
765
|
}),
|
|
763
766
|
turnStatus: submission?.status,
|
|
764
|
-
...(submission?.error
|
|
767
|
+
...(submission?.error
|
|
768
|
+
? { error: publicAssistantError(submission.error) }
|
|
769
|
+
: {}),
|
|
765
770
|
},
|
|
766
771
|
approvals: this.approvalViews(entry.submissionId),
|
|
767
772
|
});
|
package/src/pi/tool/compiler.ts
CHANGED
|
@@ -48,6 +48,8 @@ export interface PiToolCandidate {
|
|
|
48
48
|
readonly tool: AgentTool<any, any>;
|
|
49
49
|
/** Keep this Tool Direct-only instead of also offering it through Code Mode. */
|
|
50
50
|
readonly direct?: true;
|
|
51
|
+
/** Offer this Tool only through Code Mode, never as a top-level Tool. */
|
|
52
|
+
readonly codeExecutionOnly?: true;
|
|
51
53
|
/** @internal Tools also callable through this Code Mode candidate. */
|
|
52
54
|
readonly codeExecutionTools?: readonly PiToolCandidate[];
|
|
53
55
|
/** Conservative maximum used in the stable Runtime descriptor. */
|
|
@@ -85,6 +87,7 @@ const governanceState = Symbol("PiToolGovernanceState");
|
|
|
85
87
|
/** 向 Pi Agent 提供工具后置钩子,并为编译器保留同一 Turn 的治理状态。 */
|
|
86
88
|
export interface PiToolGovernance {
|
|
87
89
|
readonly afterToolCall: NonNullable<AgentOptions["afterToolCall"]>;
|
|
90
|
+
readonly wrapUpRequested: () => boolean;
|
|
88
91
|
readonly [governanceState]: PiToolGovernanceState;
|
|
89
92
|
}
|
|
90
93
|
|
|
@@ -222,7 +225,7 @@ function recordFailure(
|
|
|
222
225
|
|
|
223
226
|
// 在执行前判断这次工具调用是否已超过重试或 Turn 上限。
|
|
224
227
|
// 受治理 execute 方法对每个调用首先调用它。
|
|
225
|
-
// 被阻断的 call id 必须记入 terminalCalls,才能让 Pi
|
|
228
|
+
// 被阻断的 call id 必须记入 terminalCalls,才能让 Pi 在返回错误后进入无工具收尾回合。
|
|
226
229
|
function blockReason(
|
|
227
230
|
state: PiToolGovernanceState | undefined,
|
|
228
231
|
toolCallId: string,
|
|
@@ -263,15 +266,16 @@ export function createPiToolGovernance(): PiToolGovernance {
|
|
|
263
266
|
};
|
|
264
267
|
const governance: PiToolGovernance = {
|
|
265
268
|
[governanceState]: state,
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
+
// Pi Agent 在每次工具执行结束后调用,并传入实际 toolCall id。只消费
|
|
270
|
+
// terminalCalls 中的 id,可避免一次失败误触发收尾;不能在这里直接
|
|
271
|
+
// terminate,否则模型没有机会产出权威 assistant 消息。
|
|
269
272
|
async afterToolCall(context) {
|
|
270
273
|
if (!state.terminalCalls.delete(context.toolCall.id)) {
|
|
271
274
|
return;
|
|
272
275
|
}
|
|
273
|
-
|
|
276
|
+
state.aborted = true;
|
|
274
277
|
},
|
|
278
|
+
wrapUpRequested: () => state.aborted,
|
|
275
279
|
};
|
|
276
280
|
return governance;
|
|
277
281
|
}
|
package/src/pi/tool/core-host.ts
CHANGED
|
@@ -2,9 +2,12 @@ import {
|
|
|
2
2
|
createWorkspaceStateBackend,
|
|
3
3
|
type WorkspaceFsLike,
|
|
4
4
|
} from "@cloudflare/shell";
|
|
5
|
+
import { createBrowserTools } from "@cloudflare/think/tools/browser";
|
|
5
6
|
import { createExecuteRuntime } from "@cloudflare/think/tools/execute";
|
|
6
7
|
import { jsonSchema, tool as aiTool, type ToolSet } from "ai";
|
|
7
8
|
import type {
|
|
9
|
+
RuntimeBrowserPort,
|
|
10
|
+
RuntimeCodeExecutionPort,
|
|
8
11
|
WorkspacePort,
|
|
9
12
|
} from "../../kernel/bindings";
|
|
10
13
|
import type {
|
|
@@ -16,6 +19,16 @@ import type {
|
|
|
16
19
|
|
|
17
20
|
const CODEMODE_SANDBOX_TIMEOUT_MS = 55_000;
|
|
18
21
|
|
|
22
|
+
/**
|
|
23
|
+
* 宿主的 Browser Rendering 绑定,只取本仓真正用到的那一面。
|
|
24
|
+
*
|
|
25
|
+
* 结构化声明而不是 `Fetcher`:`wrangler types` 现在为这个绑定生成 `BrowserRun` 而不是 `Fetcher`,
|
|
26
|
+
* 两种声明都得能传进来(上游 `BrowserBinding` 出于同样理由也是结构化的)。
|
|
27
|
+
*/
|
|
28
|
+
export interface RuntimeBrowserBinding {
|
|
29
|
+
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
30
|
+
}
|
|
31
|
+
|
|
19
32
|
/**
|
|
20
33
|
* 把宿主的 Worker Loader、出站网络和 Workspace 组装成代码执行 Port。
|
|
21
34
|
*
|
|
@@ -37,6 +50,89 @@ function codeExecutionTools(tools: ToolRegistry): ToolSet {
|
|
|
37
50
|
]));
|
|
38
51
|
}
|
|
39
52
|
|
|
53
|
+
// 上游把 Code Mode 类工具交付为 AI SDK tool;本仓只取 description 和 execute 两件,
|
|
54
|
+
// 并在这里就地校验,使装配期缺件立刻失败,而不是等模型调用时才炸。
|
|
55
|
+
function toCodeExecutionPort(
|
|
56
|
+
tool: ToolSet[string],
|
|
57
|
+
name: string,
|
|
58
|
+
): RuntimeCodeExecutionPort {
|
|
59
|
+
const { description, execute } = tool;
|
|
60
|
+
if (typeof description !== "string" || description.length === 0) {
|
|
61
|
+
throw new Error(`Upstream ${name} runtime returned a tool without a description`);
|
|
62
|
+
}
|
|
63
|
+
if (typeof execute !== "function") {
|
|
64
|
+
throw new Error(`Upstream ${name} runtime returned a non-executable tool`);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
description,
|
|
68
|
+
execute: (input) => Promise.resolve(execute(input, {
|
|
69
|
+
toolCallId: name,
|
|
70
|
+
messages: [],
|
|
71
|
+
context: undefined,
|
|
72
|
+
})),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 只有这一条挂在 Tool description 上:preview URL 是 per-Session 的,
|
|
77
|
+
// 放进静态 system prompt 前缀会让它每轮都变,破坏 prompt cache。
|
|
78
|
+
// 怎么用 CDP 才不会静默读空,是与 Session 无关的固定动作,住在静态提示词里。
|
|
79
|
+
function previewNote(base: string): string {
|
|
80
|
+
return "Open the current Workspace app with this exact preview URL, unchanged: " +
|
|
81
|
+
`${base} Keep its path and query string intact; do not append a workspace file path. ` +
|
|
82
|
+
"It is the exact URL and Content-Security-Policy (sandbox allow-scripts) the user sees. " +
|
|
83
|
+
"file:// cannot reach the workspace, and there is no other preview host — do not guess one.";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 把宿主的 Durable Object state、Worker Loader 和 Browser Run 绑定组装成浏览器执行 Port。
|
|
88
|
+
*
|
|
89
|
+
* Worker 宿主在具备 DO state 与 Browser 绑定时调用,返回值经 Platform Port 交给 Runtime 工具组装。
|
|
90
|
+
*
|
|
91
|
+
* 形状与 `createWorkspaceCodeExecutionFactory` 同构:宿主提供 DO state 与平台绑定,Runtime 只拿到一个可选装配输入。
|
|
92
|
+
* `create()` 延迟到 Tool Surface 真的要注册时才调,被 deny 的装配不会白建连接器。
|
|
93
|
+
*/
|
|
94
|
+
export function createBrowserExecutionFactory(options: {
|
|
95
|
+
readonly ctx: DurableObjectState;
|
|
96
|
+
readonly loader: WorkerLoader;
|
|
97
|
+
readonly browser: RuntimeBrowserBinding;
|
|
98
|
+
/**
|
|
99
|
+
* 本 Session 的 Workspace preview URL(可以带短期签名 query)。
|
|
100
|
+
*
|
|
101
|
+
* 宿主能确定公开 origin 并签发当前 Session 地址时提供;只有它能给出浏览器真的打得开的地址。
|
|
102
|
+
* 拿不到就不提供 —— 宁可让模型知道自己没有这条信息,也不要给一个猜的、会 404 的地址。
|
|
103
|
+
*/
|
|
104
|
+
readonly previewBaseUrl?: string;
|
|
105
|
+
}): RuntimeBrowserPort {
|
|
106
|
+
return {
|
|
107
|
+
create() {
|
|
108
|
+
const tools = createBrowserTools({
|
|
109
|
+
ctx: options.ctx,
|
|
110
|
+
loader: options.loader,
|
|
111
|
+
browser: options.browser,
|
|
112
|
+
// 沙箱超时沿用 Code Mode 的同一个常量,先于 60s 外层截止结束。
|
|
113
|
+
timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
|
|
114
|
+
// 会话用上游默认的 one-shot:每次执行一个全新浏览器,两次自检互不污染。
|
|
115
|
+
// Quick Action 显式关闭:2026-08-13 复测本地绑定仍不实现 quickAction(),
|
|
116
|
+
// 让上游默认值把那四个工具带回来只会注册出本地必然失败的 Tool(knowledge F22)。
|
|
117
|
+
quickActions: false,
|
|
118
|
+
name: "browser",
|
|
119
|
+
});
|
|
120
|
+
const tool = tools.browser_execute;
|
|
121
|
+
if (!tool) {
|
|
122
|
+
throw new Error("Upstream createBrowserTools did not return browser_execute");
|
|
123
|
+
}
|
|
124
|
+
const port = toCodeExecutionPort(tool, "browser_execute");
|
|
125
|
+
if (!options.previewBaseUrl) return port;
|
|
126
|
+
// 前置而不是追加:上游的 codemode 描述很长,缀在末尾的一句会被读丢 ——
|
|
127
|
+
// 2026-08-13 手动冒烟里模型就没看见它,转而去猜 `file://` 和 `https://workspace.local/`。
|
|
128
|
+
return {
|
|
129
|
+
...port,
|
|
130
|
+
description: `${previewNote(options.previewBaseUrl)}\n\n${port.description}`,
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
40
136
|
export function createWorkspaceCodeExecutionFactory(options: {
|
|
41
137
|
readonly ctx: DurableObjectState;
|
|
42
138
|
readonly loader: WorkerLoader;
|
|
@@ -57,20 +153,13 @@ export function createWorkspaceCodeExecutionFactory(options: {
|
|
|
57
153
|
),
|
|
58
154
|
name: "execute",
|
|
59
155
|
});
|
|
60
|
-
const
|
|
61
|
-
if (typeof description !== "string" || description.length === 0) {
|
|
62
|
-
throw new Error("Think createExecuteRuntime returned a tool without a description");
|
|
63
|
-
}
|
|
64
|
-
if (typeof execute !== "function") {
|
|
65
|
-
throw new Error("Think createExecuteRuntime returned a non-executable tool");
|
|
66
|
-
}
|
|
156
|
+
const port = toCodeExecutionPort(tool, "execute");
|
|
67
157
|
return {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
})),
|
|
158
|
+
...port,
|
|
159
|
+
description: port.description.replace(
|
|
160
|
+
"- Do not use `fetch` — use connector SDKs.",
|
|
161
|
+
"- Raw `fetch` is available inside `codemode.step(...)`; prefer connector SDKs when one owns the target.",
|
|
162
|
+
),
|
|
74
163
|
};
|
|
75
164
|
},
|
|
76
165
|
};
|