@springbrand/agent-runtime 0.2.0-alpha.13 → 0.2.0-alpha.14

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.
@@ -15,47 +15,122 @@ import {
15
15
  import { webSearchPiToolCandidate } from "./web-search";
16
16
  import type { WebSearch } from "./web-search/api";
17
17
 
18
- const askUserParameters = Type.Object({
18
+ const askUserQuestion = Type.Object({
19
19
  question: Type.String({
20
+ minLength: 1,
20
21
  description: "The question to put to the user.",
21
22
  }),
22
- options: Type.Array(Type.String(), {
23
+ options: Type.Optional(Type.Array(Type.String({ minLength: 1 }), {
23
24
  minItems: 2,
24
25
  maxItems: 6,
26
+ uniqueItems: true,
25
27
  description: "2-6 mutually distinct options for the user to choose from.",
26
- }),
28
+ })),
27
29
  multiSelect: Type.Optional(Type.Boolean({
28
30
  description:
29
31
  "Whether the user may select more than one option. Defaults to false.",
30
32
  })),
33
+ allowCustom: Type.Optional(Type.Boolean({
34
+ description:
35
+ "Whether the user may add free text. Defaults to false with options and true without options.",
36
+ })),
37
+ });
38
+
39
+ const askUserParameters = Type.Object({
40
+ questions: Type.Array(askUserQuestion, {
41
+ minItems: 1,
42
+ maxItems: 4,
43
+ description:
44
+ "All user decisions needed to continue. Ask them together in one call.",
45
+ }),
31
46
  });
32
47
 
33
48
  /**
34
49
  * `ask_user` 的客户端响应体。
35
50
  *
36
- * `selections` 是选中的选项原文(多选时多于一项),`text` 是可选的补充说明。
51
+ * `answers` 与 Tool 输入的 `questions` 按下标一一对应。
37
52
  * 刻意用结构化数组而不是拼好的字符串 —— 选项本身可能含分隔符,拼了再拆是有损的。
38
53
  */
39
54
  const askUserResponse = Type.Object({
40
- selections: Type.Array(Type.String()),
41
- text: Type.Optional(Type.String()),
55
+ answers: Type.Array(Type.Object({
56
+ selections: Type.Array(Type.String()),
57
+ text: Type.Optional(Type.String()),
58
+ })),
42
59
  });
43
60
 
44
- // 手写校验而不是拉 TypeBox compiler:这里只需要判真假,
45
- // 且校验必须在 Worker 冷启动路径上零成本。
46
- function isAskUserResponse(value: unknown): boolean {
47
- if (typeof value !== "object" || value === null) return false;
48
- const record = value as Record<string, unknown>;
49
- if (!Array.isArray(record.selections)) return false;
50
- if (!record.selections.every((item) => typeof item === "string")) {
51
- return false;
61
+ type AskUserInput = Static<typeof askUserParameters>;
62
+ type AskUserResponse = Static<typeof askUserResponse>;
63
+
64
+ function recordOf(value: unknown): Record<string, unknown> | null {
65
+ return typeof value === "object" && value !== null && !Array.isArray(value)
66
+ ? value as Record<string, unknown>
67
+ : null;
68
+ }
69
+
70
+ function questionsOf(value: unknown): AskUserInput["questions"] | null {
71
+ const questions = recordOf(value)?.questions;
72
+ if (!Array.isArray(questions) || questions.length < 1 || questions.length > 4) {
73
+ return null;
52
74
  }
53
- if (record.text !== undefined && typeof record.text !== "string") {
75
+ for (const value of questions) {
76
+ const question = recordOf(value);
77
+ if (!question || typeof question.question !== "string" || !question.question) {
78
+ return null;
79
+ }
80
+ if (
81
+ question.multiSelect !== undefined &&
82
+ typeof question.multiSelect !== "boolean"
83
+ ) return null;
84
+ if (
85
+ question.allowCustom !== undefined &&
86
+ typeof question.allowCustom !== "boolean"
87
+ ) return null;
88
+ if (question.options !== undefined) {
89
+ if (
90
+ !Array.isArray(question.options) ||
91
+ question.options.length < 2 ||
92
+ question.options.length > 6 ||
93
+ !question.options.every((option) =>
94
+ typeof option === "string" && option.length > 0
95
+ ) ||
96
+ new Set(question.options).size !== question.options.length
97
+ ) return null;
98
+ }
99
+ }
100
+ return questions as AskUserInput["questions"];
101
+ }
102
+
103
+ // 手写校验而不是拉 TypeBox compiler:这里只需要判真假,
104
+ // 且校验位于 Worker 响应信任边界,不应为每次回答编译 schema。
105
+ function isAskUserResponse(input: unknown, value: unknown): boolean {
106
+ const questions = questionsOf(input);
107
+ const answers = recordOf(value)?.answers;
108
+ if (!questions || !Array.isArray(answers) || answers.length !== questions.length) {
54
109
  return false;
55
110
  }
56
- // 什么都没选、也没写字,等于没回答 —— 不该被当成一次有效结算。
57
- return record.selections.length > 0 ||
58
- (typeof record.text === "string" && record.text.trim().length > 0);
111
+
112
+ return questions.every((question, index) => {
113
+ const answer = recordOf(answers[index]);
114
+ if (!answer || !Array.isArray(answer.selections)) return false;
115
+ if (!answer.selections.every((item) => typeof item === "string")) {
116
+ return false;
117
+ }
118
+ if (answer.text !== undefined && typeof answer.text !== "string") {
119
+ return false;
120
+ }
121
+
122
+ const options = question.options ?? [];
123
+ const text = typeof answer.text === "string" ? answer.text : "";
124
+ if (text.length > 2_000) return false;
125
+ if (answer.selections.some((selection) => !options.includes(selection))) {
126
+ return false;
127
+ }
128
+ if (new Set(answer.selections).size !== answer.selections.length) return false;
129
+ if (!question.multiSelect && answer.selections.length > 1) return false;
130
+ const allowCustom = options.length === 0 || question.allowCustom === true;
131
+ if (!allowCustom && text.trim()) return false;
132
+ return answer.selections.length > 0 || text.trim().length > 0;
133
+ });
59
134
  }
60
135
 
61
136
  const suggestFollowupsParameters = Type.Object({
@@ -162,7 +237,7 @@ export function basePiToolCandidates(
162
237
  name: "ask_user",
163
238
  label: "Ask user",
164
239
  description:
165
- "Ask the user to choose among a small set of options. Call this tool whenever you need the user to make a choice from limited options do NOT write plain text like 'please pick A / B / C'. The user's choice comes back to you as this tool's result, so just continue once you have it. Do NOT end your turn after calling this tool.",
240
+ "Ask all 1-4 user questions needed to continue in one call. Each question may offer 2-6 options, allow multiple selections, and/or allow a custom answer. Do not make one call per question or write plain text like 'please pick A / B / C'. The answers return as this tool's result, so continue the same turn after calling it.",
166
241
  parameters: askUserParameters,
167
242
  // 永远不会被调用:执行链在 interaction 闸就 park 住了。留一个失败关闭的
168
243
  // 实现,是为了万一哪次改动绕过了那道闸,能立刻炸出来而不是静默返回空答案。
@@ -174,18 +249,29 @@ export function basePiToolCandidates(
174
249
  }),
175
250
  interaction: {
176
251
  validateResponse: isAskUserResponse,
177
- settle: (_input, response) => {
178
- const answer = response as Static<typeof askUserResponse>;
179
- const parts = [
180
- ...answer.selections,
181
- ...(answer.text?.trim() ? [answer.text.trim()] : []),
182
- ];
252
+ settle: (input, response) => {
253
+ const questions = (input as AskUserInput).questions;
254
+ const responseAnswers = (response as AskUserResponse).answers;
255
+ const answers = questions.map((question, index) => {
256
+ const answer = responseAnswers[index]!;
257
+ return {
258
+ question: question.question,
259
+ selections: answer.selections,
260
+ ...(answer.text !== undefined ? { text: answer.text.trim() } : {}),
261
+ };
262
+ });
183
263
  return {
184
264
  content: [{
185
265
  type: "text",
186
- text: `The user answered: ${parts.join(" / ")}`,
266
+ text: `The user answered:\n${answers.map((answer, index) => {
267
+ const parts = [
268
+ ...answer.selections,
269
+ ...(answer.text ? [answer.text] : []),
270
+ ];
271
+ return `${index + 1}. ${answer.question}\n ${parts.join(" / ")}`;
272
+ }).join("\n")}`,
187
273
  }],
188
- details: answer,
274
+ details: { answers },
189
275
  };
190
276
  },
191
277
  },
@@ -4,7 +4,16 @@ import type {
4
4
  AgentToolResult,
5
5
  } from "@earendil-works/pi-agent-core";
6
6
  import type { ApprovalReceipt } from "../../kernel/receipts";
7
- import { boundDurableToolOutput } from "../../layers/context/budget/gate";
7
+ import {
8
+ boundDurableToolOutput,
9
+ type BudgetPolicy,
10
+ spillDurableToolOutput,
11
+ } from "../../layers/context/budget/gate";
12
+ import {
13
+ type ArtifactRef,
14
+ serializeOutput,
15
+ type SpillWorkspace,
16
+ } from "../../lib/artifacts";
8
17
  import {
9
18
  type ExecutionLevel,
10
19
  } from "../../lib/execution-level";
@@ -24,8 +33,8 @@ import {
24
33
  * 会让下一次模型调用非法。
25
34
  */
26
35
  export interface PiToolInteractionSpec {
27
- /** 校验客户端响应体;不通过则拒绝投递,park 保持不变。 */
28
- readonly validateResponse: (response: unknown) => boolean;
36
+ /** 结合 Tool 输入校验客户端响应体;不通过则拒绝投递,park 保持不变。 */
37
+ readonly validateResponse: (input: unknown, response: unknown) => boolean;
29
38
  /** 把通过校验的响应映射成 ToolResult;缺省是原样回传。 */
30
39
  readonly settle?: (
31
40
  input: unknown,
@@ -54,11 +63,14 @@ export interface PiToolCandidate {
54
63
  readonly summary?: string;
55
64
  readonly source?: ApprovalReceipt["source"];
56
65
  readonly interaction?: PiToolInteractionSpec;
66
+ /** Large payloads spill by default; protocol-shaped results opt into structure. */
67
+ readonly outputBudget?: BudgetPolicy;
57
68
  }
58
69
 
59
70
  /** 控制候选 Pi 工具如何被编译为可执行工具集。 */
60
71
  export interface CompilePiToolsOptions {
61
72
  readonly governance?: PiToolGovernance;
73
+ readonly workspace?: SpillWorkspace;
62
74
  readonly settle: (
63
75
  call: SettledPiToolCall,
64
76
  ) => void | Promise<void>;
@@ -200,6 +212,52 @@ function emitToolTelemetry(
200
212
  }
201
213
  }
202
214
 
215
+ // Most Pi adapters keep the original Tool value in details and its serialized
216
+ // model form in one text block. Spill that original once instead of duplicating
217
+ // it inside an AgentToolResult JSON envelope.
218
+ function spillPayload(result: AgentToolResult<unknown>): unknown {
219
+ const text = result.content.length === 1 && result.content[0]?.type === "text"
220
+ ? result.content[0].text
221
+ : undefined;
222
+ if (text !== undefined && serializeOutput(result.details).text === text) {
223
+ return result.details;
224
+ }
225
+ return result;
226
+ }
227
+
228
+ function artifactResult(
229
+ result: AgentToolResult<unknown>,
230
+ ref: ArtifactRef,
231
+ ): AgentToolResult<unknown> {
232
+ return {
233
+ ...result,
234
+ content: [{
235
+ type: "text",
236
+ text: [
237
+ ref.note,
238
+ `Path: ${ref.path}`,
239
+ `Bytes: ${ref.bytes}`,
240
+ `Preview:\n${ref.preview}`,
241
+ ].join("\n"),
242
+ }],
243
+ details: ref,
244
+ };
245
+ }
246
+
247
+ async function durableResult(
248
+ result: AgentToolResult<unknown>,
249
+ candidate: PiToolCandidate,
250
+ workspace: SpillWorkspace | undefined,
251
+ ): Promise<AgentToolResult<unknown>> {
252
+ if ((candidate.outputBudget ?? { kind: "spill" }).kind === "spill") {
253
+ const ref = await spillDurableToolOutput(spillPayload(result), {
254
+ workspace,
255
+ });
256
+ if (ref) return artifactResult(result, ref);
257
+ }
258
+ return boundDurableToolOutput(result) as AgentToolResult<unknown>;
259
+ }
260
+
203
261
  // 记录一次工具失败并在 Turn 达到上限时打开断路器。
204
262
  // 受治理 execute 方法在工具或结算失败后调用。
205
263
  // 同时记录单输入次数和 Turn 总数,是为了既阻止原样重试又防止换参数无限失败。
@@ -341,9 +399,11 @@ function governedTool(
341
399
 
342
400
  let result: AgentToolResult<unknown>;
343
401
  try {
344
- result = boundDurableToolOutput(
402
+ result = await durableResult(
345
403
  await execute(toolCallId, args, signal, onUpdate),
346
- ) as AgentToolResult<unknown>;
404
+ candidate,
405
+ options.workspace,
406
+ );
347
407
  } catch (cause) {
348
408
  const message = errorMessage(cause);
349
409
  const result = boundDurableToolOutput({
@@ -442,7 +502,7 @@ export function compilePiTools(
442
502
  options: CompilePiToolsOptions,
443
503
  ): AgentTool<any, any>[] {
444
504
  if (!options?.settle) {
445
- throw new Error("Pi Tool settlement port is required");
505
+ throw new Error("SpringBrand Tool settlement port is required");
446
506
  }
447
507
  return candidates.map((candidate) => governedTool(candidate, options));
448
508
  }
@@ -3,10 +3,14 @@ import {
3
3
  type WorkspaceFsLike,
4
4
  } from "@cloudflare/shell";
5
5
  import { createExecuteRuntime } from "@cloudflare/think/tools/execute";
6
+ import { jsonSchema, tool as aiTool, type ToolSet } from "ai";
6
7
  import type {
7
- RuntimeCodeExecutionPort,
8
8
  WorkspacePort,
9
9
  } from "../../kernel/bindings";
10
+ import type {
11
+ RuntimeCodeExecutionFactory,
12
+ ToolRegistry,
13
+ } from "../../tool-registry";
10
14
 
11
15
  // 本文件沿用 `../../index.ts` 入口定义的 Workspace、Port 和 Tool Candidate 术语。
12
16
 
@@ -19,36 +23,55 @@ const CODEMODE_SANDBOX_TIMEOUT_MS = 55_000;
19
23
  *
20
24
  * Think 的独立 execute factory 接受显式宿主参数,不要求 Agent 继承 Think;这里只借它组装 Codemode Runtime、Dynamic Worker executor 和已限定范围的 Workspace state connector。
21
25
  */
22
- export function createWorkspaceCodeExecutionPort(options: {
26
+ function codeExecutionTools(tools: ToolRegistry): ToolSet {
27
+ return Object.fromEntries(Object.entries(tools).map(([name, spec]) => [
28
+ name,
29
+ aiTool({
30
+ description: spec.description,
31
+ inputSchema: jsonSchema(spec.parameters as never),
32
+ execute: (input, options) => spec.execute(input, {
33
+ toolCallId: options?.toolCallId ?? name,
34
+ signal: options?.abortSignal ?? new AbortController().signal,
35
+ }),
36
+ }),
37
+ ]));
38
+ }
39
+
40
+ export function createWorkspaceCodeExecutionFactory(options: {
23
41
  readonly ctx: DurableObjectState;
24
42
  readonly loader: WorkerLoader;
25
43
  readonly outbound: Fetcher;
26
44
  readonly workspace: WorkspacePort;
27
- }): RuntimeCodeExecutionPort {
28
- const { tool } = createExecuteRuntime({
29
- ctx: options.ctx,
30
- loader: options.loader,
31
- globalOutbound: options.outbound,
32
- // 先于外层 60s 截止结束,给 Runtime RPC 结算和 Worker 释放留出时间。
33
- timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
34
- state: createWorkspaceStateBackend(
35
- options.workspace as unknown as WorkspaceFsLike,
36
- ),
37
- name: "execute",
38
- });
39
- const { description, execute } = tool;
40
- if (typeof description !== "string" || description.length === 0) {
41
- throw new Error("Think createExecuteRuntime returned a tool without a description");
42
- }
43
- if (typeof execute !== "function") {
44
- throw new Error("Think createExecuteRuntime returned a non-executable tool");
45
- }
45
+ }): RuntimeCodeExecutionFactory {
46
46
  return {
47
- description,
48
- execute: (input) => Promise.resolve(execute(input, {
49
- toolCallId: "execute",
50
- messages: [],
51
- context: undefined,
52
- })),
47
+ create(tools) {
48
+ const { tool } = createExecuteRuntime({
49
+ ctx: options.ctx,
50
+ loader: options.loader,
51
+ globalOutbound: options.outbound,
52
+ tools: codeExecutionTools(tools),
53
+ // 先于外层 60s 截止结束,给 Runtime RPC 结算和 Worker 释放留出时间。
54
+ timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
55
+ state: createWorkspaceStateBackend(
56
+ options.workspace as unknown as WorkspaceFsLike,
57
+ ),
58
+ name: "execute",
59
+ });
60
+ const { description, execute } = tool;
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
+ }
67
+ return {
68
+ description,
69
+ execute: (input) => Promise.resolve(execute(input, {
70
+ toolCallId: "execute",
71
+ messages: [],
72
+ context: undefined,
73
+ })),
74
+ };
75
+ },
53
76
  };
54
77
  }
@@ -12,10 +12,6 @@ import { serializeOutput } from "../../lib/artifacts";
12
12
  import type { PiLoadedExtension } from "../assembly/extensions";
13
13
  import { aiToolToPi } from "./ai-adapter";
14
14
  import type { PiToolCandidate } from "./compiler";
15
- import {
16
- toolRegistryFromPiCandidates,
17
- type ToolRegistry,
18
- } from "../../tool-registry";
19
15
 
20
16
  // 本文件沿用 `../../index.ts` 入口定义的 Extension、Port 和 Tool Candidate 术语。
21
17
 
@@ -164,19 +160,11 @@ export function codeExecutionPiToolCandidate(
164
160
  return {
165
161
  owner: "core:codemode",
166
162
  requiredExecutionLevel: "high",
163
+ outputBudget: { kind: "structure" },
167
164
  source: "codemode",
168
165
  summary: "Run JavaScript with network and configured connector access",
169
166
  tool,
170
167
  };
171
168
  }
172
169
 
173
- /** 从 Codemode Runtime Port 生成 `execute` Tool。 */
174
- export function createCodeExecutionTool(
175
- runtime: RuntimeCodeExecutionPort,
176
- ): ToolRegistry {
177
- return toolRegistryFromPiCandidates([
178
- codeExecutionPiToolCandidate(runtime),
179
- ]);
180
- }
181
-
182
170
  // #endregion
@@ -1,17 +1,20 @@
1
1
  import {
2
2
  runner as createSkillScriptRunner,
3
3
  SkillRegistry,
4
+ type SkillResource,
4
5
  type SkillScriptRequest,
5
6
  type SkillScriptRunner,
6
7
  type SkillSource,
7
8
  } from "agents/skills";
8
- import { jsonSchema, type ToolSet } from "ai";
9
+ import { jsonSchema, tool, type ToolSet } from "ai";
10
+ import { z } from "zod";
9
11
  import type {
10
12
  RuntimeSkillScriptPolicy,
11
13
  WorkspacePort,
12
14
  } from "../../kernel/bindings";
13
15
  import { aiToolToPi } from "./ai-adapter";
14
16
  import type { PiToolCandidate } from "./compiler";
17
+ import { serializeWorkspaceMutation } from "./workspace-sandbox";
15
18
 
16
19
  /** A configured Skill source and its script capabilities. */
17
20
  export interface PiSkillBinding {
@@ -103,11 +106,72 @@ const SKILL_TOOL_LABELS: Readonly<Record<string, string>> = {
103
106
  activate_skill: "Activate Skill",
104
107
  read_skill_resource: "Read Skill resource",
105
108
  run_skill_script: "Run Skill script",
109
+ materialize_skill_resource: "Materialize Skill resource",
106
110
  };
107
111
 
108
112
  const SKILL_ENTRY_READ_GUIDANCE =
109
113
  "SKILL.md contains the Skill instructions; use activate_skill instead.";
110
114
 
115
+ function resourceBytes(resource: SkillResource): Uint8Array {
116
+ if ((resource.encoding ?? "text") === "text") {
117
+ return new TextEncoder().encode(resource.content);
118
+ }
119
+ const binary = atob(resource.content);
120
+ const bytes = new Uint8Array(binary.length);
121
+ for (let index = 0; index < binary.length; index++) {
122
+ bytes[index] = binary.charCodeAt(index);
123
+ }
124
+ return bytes;
125
+ }
126
+
127
+ function materializeSkillResourceTool(
128
+ bindings: readonly PiSkillBinding[],
129
+ workspace: WorkspacePort,
130
+ ) {
131
+ const names = bindings.map(({ name }) => name) as [string, ...string[]];
132
+ const byName = new Map(bindings.map((binding) => [binding.name, binding]));
133
+ return tool({
134
+ description:
135
+ "Copy a complete bundled Skill resource directly into the Workspace without returning its contents to the model. " +
136
+ "Use this instead of read_skill_resource when a template, script, image, font, or other asset must become a Workspace file. " +
137
+ "The destination is created or overwritten, including parent directories.",
138
+ inputSchema: z.object({
139
+ name: z.enum(names).describe("Activated Skill name"),
140
+ path: z.string().min(1).describe("Bundled resource path listed by activate_skill"),
141
+ destination: z.string().min(1).describe("Absolute destination path in the Workspace"),
142
+ }),
143
+ execute: async ({ name, path, destination }, { abortSignal }) => {
144
+ abortSignal?.throwIfAborted();
145
+ const source = byName.get(name)?.source;
146
+ if (!source?.readResource) {
147
+ throw new Error(`Skill \"${name}\" has no readable resources.`);
148
+ }
149
+ const resource = await source.readResource(name, path);
150
+ if (!resource) throw new Error(`Resource not found: ${name}/${path}`);
151
+ abortSignal?.throwIfAborted();
152
+ const bytes = resourceBytes(resource);
153
+
154
+ await serializeWorkspaceMutation(workspace, destination, async () => {
155
+ abortSignal?.throwIfAborted();
156
+ const parent = destination.replace(/\/[^/]+$/, "");
157
+ if (parent && parent !== "/") {
158
+ await workspace.mkdir(parent, { recursive: true });
159
+ }
160
+ await workspace.writeFileBytes(destination, bytes, resource.mimeType);
161
+ });
162
+
163
+ return {
164
+ name,
165
+ path: resource.path,
166
+ destination,
167
+ bytesWritten: bytes.byteLength,
168
+ encoding: resource.encoding ?? "text",
169
+ ...(resource.mimeType ? { mimeType: resource.mimeType } : {}),
170
+ };
171
+ },
172
+ });
173
+ }
174
+
111
175
  /** Create Pi candidates from the official Agents SDK SkillRegistry tools. */
112
176
  export async function skillPiToolCandidates(
113
177
  bindings: readonly PiSkillBinding[],
@@ -121,7 +185,15 @@ export async function skillPiToolCandidates(
121
185
  );
122
186
  await registry.load();
123
187
 
124
- return Object.entries(registry.tools()).map(([name, tool]) => {
188
+ const tools = registry.tools();
189
+ if (options.workspace) {
190
+ tools.materialize_skill_resource = materializeSkillResourceTool(
191
+ bindings,
192
+ options.workspace,
193
+ );
194
+ }
195
+
196
+ return Object.entries(tools).map(([name, tool]) => {
125
197
  const adapted = aiToolToPi(name, tool, {
126
198
  label: SKILL_TOOL_LABELS[name] ?? name,
127
199
  ...(name === "read_skill_resource"
@@ -154,7 +226,10 @@ export async function skillPiToolCandidates(
154
226
  }
155
227
  return {
156
228
  owner: "runtime-skill",
157
- requiredExecutionLevel: name === "run_skill_script" ? "high" : "safe",
229
+ requiredExecutionLevel:
230
+ name === "run_skill_script" || name === "materialize_skill_resource"
231
+ ? "high"
232
+ : "safe",
158
233
  tool: adapted,
159
234
  };
160
235
  });
@@ -111,7 +111,7 @@ const mutationQueues = new WeakMap<
111
111
  Map<string, Promise<void>>
112
112
  >();
113
113
 
114
- function serializeByPath<T>(
114
+ export function serializeWorkspaceMutation<T>(
115
115
  workspace: WorkspacePort,
116
116
  path: string,
117
117
  run: () => Promise<T>,
@@ -193,7 +193,7 @@ export function workspacePiToolCandidates(
193
193
  signal?.throwIfAborted();
194
194
  running(onUpdate, "Writing the Workspace file.");
195
195
 
196
- return serializeByPath(workspace, path, async () => {
196
+ return serializeWorkspaceMutation(workspace, path, async () => {
197
197
  // 排队期间可能已经被取消,拿到闸之后必须重新确认,否则会写一个已放弃的结果。
198
198
  signal?.throwIfAborted();
199
199
  return writeTool.execute(
@@ -221,7 +221,7 @@ export function workspacePiToolCandidates(
221
221
  running(onUpdate, "Editing the Workspace file.");
222
222
  // 读-比对-写整段都在闸内:闸只包住最后那次 writeFile 的话,基准内容仍然可能在
223
223
  // 比对之后被别的调用换掉,丢写照旧发生。
224
- return serializeByPath(workspace, params.path, async () => {
224
+ return serializeWorkspaceMutation(workspace, params.path, async () => {
225
225
  signal?.throwIfAborted();
226
226
  const output = await editTool.execute(
227
227
  toolCallId,
@@ -267,7 +267,7 @@ export function workspacePiToolCandidates(
267
267
  if (typeof path !== "string") {
268
268
  return removeTool.execute(toolCallId, params, signal, onUpdate);
269
269
  }
270
- return serializeByPath(workspace, path, () =>
270
+ return serializeWorkspaceMutation(workspace, path, () =>
271
271
  removeTool.execute(toolCallId, params, signal, onUpdate),
272
272
  );
273
273
  },
@@ -429,7 +429,7 @@ function assertIdentity(
429
429
  state.assemblyRevision !== milestone.assemblyRevision)
430
430
  ) {
431
431
  throw new Error(
432
- "Pi recovery milestone does not belong to the active Turn revision",
432
+ "SpringBrand recovery milestone does not belong to the active Turn revision",
433
433
  );
434
434
  }
435
435
  }
@@ -496,7 +496,7 @@ export function replayPiToolRecovery(
496
496
  const previous = tools[milestone.toolCallId];
497
497
  if (previous && !sameToolInput(previous, milestone)) {
498
498
  throw new Error(
499
- `Conflicting Pi Tool input for ${milestone.toolCallId}`,
499
+ `Conflicting SpringBrand Tool input for ${milestone.toolCallId}`,
500
500
  );
501
501
  }
502
502
  tools[milestone.toolCallId] = previous ?? {
@@ -891,7 +891,7 @@ export function applyRecoveredPiApprovalDecision(
891
891
  ): RecoveredPiApprovalDecision {
892
892
  const approval = state.approvals[input.executionId];
893
893
  if (!approval) {
894
- throw new Error(`Unknown Pi Tool approval ${input.executionId}`);
894
+ throw new Error(`Unknown SpringBrand Tool approval ${input.executionId}`);
895
895
  }
896
896
  const transition = decidePiToolApproval(
897
897
  approval,
@@ -908,7 +908,7 @@ export function applyRecoveredPiApprovalDecision(
908
908
  return { milestones: [], outcome: transition.outcome };
909
909
  }
910
910
  if (!state.turnId || !state.assemblyRevision) {
911
- throw new Error("Pi Tool approval is missing its Turn revision");
911
+ throw new Error("SpringBrand Tool approval is missing its Turn revision");
912
912
  }
913
913
 
914
914
  const identity: RecoveryIdentity = {
@@ -951,7 +951,7 @@ export function recordRecoveredPiToolInteraction(
951
951
  interaction: PiToolInteraction,
952
952
  ): PiToolRecoveryMilestone[] {
953
953
  if (!state.turnId || !state.assemblyRevision) {
954
- throw new Error("Pi Tool interaction is missing its Turn revision");
954
+ throw new Error("SpringBrand Tool interaction is missing its Turn revision");
955
955
  }
956
956
  return [{
957
957
  version: 1,
@@ -989,7 +989,7 @@ export function applyRecoveredPiToolInteractionSettlement(
989
989
  ): RecoveredPiToolInteractionSettlement {
990
990
  const interaction = state.interactions[input.interactionId];
991
991
  if (!interaction) {
992
- throw new Error(`Unknown Pi Tool interaction ${input.interactionId}`);
992
+ throw new Error(`Unknown SpringBrand Tool interaction ${input.interactionId}`);
993
993
  }
994
994
  const transition = input.kind === "respond"
995
995
  ? respondPiToolInteraction(interaction, {
@@ -1008,7 +1008,7 @@ export function applyRecoveredPiToolInteractionSettlement(
1008
1008
  return { milestones: [], outcome };
1009
1009
  }
1010
1010
  if (!state.turnId || !state.assemblyRevision) {
1011
- throw new Error("Pi Tool interaction is missing its Turn revision");
1011
+ throw new Error("SpringBrand Tool interaction is missing its Turn revision");
1012
1012
  }
1013
1013
 
1014
1014
  const identity: RecoveryIdentity = {
@@ -652,9 +652,14 @@ export function defineRuntimeAgent<
652
652
  const policy = assembly.surfacePolicy;
653
653
  return {
654
654
  ...assembly,
655
- bindings: assembly.bindings?.workspace
656
- ? { workspace: assembly.bindings.workspace }
657
- : {},
655
+ bindings: {
656
+ ...(assembly.bindings?.workspace
657
+ ? { workspace: assembly.bindings.workspace }
658
+ : {}),
659
+ ...(assembly.bindings?.codeExecution
660
+ ? { codeExecution: assembly.bindings.codeExecution }
661
+ : {}),
662
+ },
658
663
  memoryProfile: {
659
664
  enabled: false,
660
665
  memoryTokens: 2_000,