@springbrand/agent-runtime 0.2.0-alpha.41 → 0.2.0-alpha.43

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.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/src/adapter/cloudflare/resources/r2-skill-source.ts +215 -0
  3. package/src/adapter/cloudflare/resources/runtime-resources.ts +1 -18
  4. package/src/adapter/cloudflare/subagent/tools.ts +2 -5
  5. package/src/adapter/cloudflare/universal-agent/preparation.ts +21 -9
  6. package/src/adapter/cloudflare/universal-agent/tools.ts +3 -2
  7. package/src/adapter/cloudflare/workspace/scoped-workspace.ts +15 -0
  8. package/src/index.ts +3 -0
  9. package/src/kernel/bindings.ts +38 -6
  10. package/src/layers/context/budget/gate.ts +3 -3
  11. package/src/layers/orchestration/temporary-agent/workspace.ts +2 -0
  12. package/src/lib/prompt.ts +30 -14
  13. package/src/pi/assembly/snapshot.ts +7 -1
  14. package/src/pi/runtime-adapter/assembly.ts +8 -3
  15. package/src/pi/runtime-adapter/execution.ts +108 -13
  16. package/src/pi/runtime-adapter/models.ts +9 -6
  17. package/src/pi/runtime-adapter/openrouter-messages.ts +10 -3
  18. package/src/pi/tool/base.ts +42 -14
  19. package/src/pi/tool/compiler.ts +15 -2
  20. package/src/pi/tool/core-host.ts +228 -1
  21. package/src/pi/tool/core.ts +37 -5
  22. package/src/pi/tool/declared.ts +3 -0
  23. package/src/pi/tool/nested-tools.ts +5 -1
  24. package/src/pi/tool/schedule.ts +12 -10
  25. package/src/pi/tool/skill.ts +240 -86
  26. package/src/pi/tool/subagent.ts +2 -0
  27. package/src/pi/tool/time.ts +1 -1
  28. package/src/pi/tool/web-fetch.ts +1 -1
  29. package/src/pi/tool/web-search/web-search.ts +2 -1
  30. package/src/pi/tool/workspace-revision.ts +2 -1
  31. package/src/pi/tool/workspace-sandbox.ts +10 -21
  32. package/src/runtime-agent.ts +3 -0
  33. package/src/runtime-assembler.ts +105 -7
  34. package/src/runtime-definition.ts +1 -0
  35. package/src/runtime.ts +50 -0
@@ -1,15 +1,102 @@
1
+ import {
2
+ createWorkspaceStateBackend,
3
+ type StateBackend,
4
+ type WorkspaceFsLike,
5
+ } from "@cloudflare/shell";
6
+ import {
7
+ truncateResponse,
8
+ truncateResult,
9
+ type ProxyToolOutput,
10
+ } from "@cloudflare/codemode";
1
11
  import { createBrowserTools } from "@cloudflare/think/tools/browser";
12
+ import { createExecuteRuntime } from "@cloudflare/think/tools/execute";
2
13
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
14
+ import type { Usage } from "@earendil-works/pi-ai";
3
15
  import type { ToolSet } from "ai";
4
16
  import type {
5
17
  RuntimeBrowserPort,
6
18
  RuntimeCodeExecutionPort,
19
+ WorkspacePort,
7
20
  } from "../../kernel/bindings";
21
+ import type { RuntimeCodeExecutionFactory } from "../../kernel/bindings";
8
22
  import { serializeOutput } from "../../lib/artifacts";
23
+ import type { PiToolCandidate } from "./compiler";
24
+ import { piCandidatesToAiTools } from "./nested-tools";
9
25
 
10
- // 本文件沿用 `../../index.ts` 入口定义的 BrowserPort 术语。
26
+ // 本文件沿用 `../../index.ts` 入口定义的 Workspace、PortTool Candidate 术语。
11
27
 
12
28
  const CODEMODE_SANDBOX_TIMEOUT_MS = 55_000;
29
+ export type WorkspaceAccessMode = "none" | "read" | "write";
30
+ const WRITE_STATE_METHODS = new Set([
31
+ "writeFile",
32
+ "writeFileBytes",
33
+ "appendFile",
34
+ "writeJson",
35
+ "updateJson",
36
+ "mkdir",
37
+ "replaceInFile",
38
+ "replaceInFiles",
39
+ "rm",
40
+ "cp",
41
+ "mv",
42
+ "symlink",
43
+ "createArchive",
44
+ "extractArchive",
45
+ "compressFile",
46
+ "decompressFile",
47
+ "removeTree",
48
+ "copyTree",
49
+ "moveTree",
50
+ "planEdits",
51
+ "applyEditPlan",
52
+ "applyEdits",
53
+ ]);
54
+
55
+ export function workspaceStateBackend(
56
+ workspace: WorkspacePort,
57
+ mode: WorkspaceAccessMode,
58
+ ): StateBackend | undefined {
59
+ if (mode === "none") return undefined;
60
+ const backend = createWorkspaceStateBackend(
61
+ workspace as unknown as WorkspaceFsLike,
62
+ );
63
+ return new Proxy(backend, {
64
+ get(target, property, receiver) {
65
+ if (property === "writeFileBytes") return undefined;
66
+ if (
67
+ mode === "read" &&
68
+ typeof property === "string" &&
69
+ WRITE_STATE_METHODS.has(property)
70
+ ) {
71
+ return async () => {
72
+ throw new Error("Workspace state is read-only for this execution");
73
+ };
74
+ }
75
+ const value = Reflect.get(target, property, receiver);
76
+ return typeof value === "function" ? value.bind(target) : value;
77
+ },
78
+ }) as StateBackend;
79
+ }
80
+
81
+ const SPRINGBRAND_CODE_MODE_POLICY = `## SpringBrand Workspace and Asset Policy
82
+
83
+ Use state.* for every Workspace filesystem operation. Workspace file methods do not exist under tools.*.
84
+
85
+ state.writeFileBytes is not available. Use state.writeFile or state.appendFile for sandbox-created text.
86
+
87
+ Bundled Skill resources are not Workspace files. Read text with tools.read_skill_resource, run bundled scripts with tools.run_skill_script, and copy templates or assets with tools.materialize_skill_resource.
88
+
89
+ User Attachments are not Workspace files until copied with tools.materialize_attachment.
90
+
91
+ Never create or write data URLs, base64 image strings, or embedded binary payloads in Code Mode.
92
+
93
+ Copy a remote asset into Workspace with tools.materialize_url({ url, destination }). Raw fetch remains available. Do not carry large responses, binary payloads, data URLs, or base64 assets through Code Mode.
94
+
95
+ After materialization, reference the Workspace file by a relative path. Website HTML and CSS must reference relative Workspace assets, never temporary external URLs or data URLs.
96
+
97
+ Keep large content out of the execute result. Write it to Workspace and return only path, mediaType, bytes, summary, and warnings.
98
+
99
+ This execute tool cannot ask the user, inspect a rendered page, capture a screenshot, save or publish a Creation, or authorize an external action. Use the corresponding top-level Tool for those operations.`;
13
100
 
14
101
  /**
15
102
  * 宿主的 Browser Rendering 绑定,只取本仓真正用到的那一面。
@@ -21,6 +108,13 @@ export interface RuntimeBrowserBinding {
21
108
  fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
22
109
  }
23
110
 
111
+ /**
112
+ * 把宿主的 Worker Loader、出站网络和 Workspace 组装成代码执行 Port。
113
+ *
114
+ * Worker 宿主在具备 Durable Object state 和完整平台绑定时调用,然后把返回值交给 Runtime 工具组装。
115
+ *
116
+ * Think 的独立 execute factory 接受显式宿主参数,不要求 Agent 继承 Think;这里只借它组装 Codemode Runtime、Dynamic Worker executor 和已限定范围的 Workspace state connector。
117
+ */
24
118
  function result(details: unknown): AgentToolResult<unknown> {
25
119
  return {
26
120
  content: [{ type: "text", text: serializeOutput(details).text }],
@@ -28,6 +122,92 @@ function result(details: unknown): AgentToolResult<unknown> {
28
122
  };
29
123
  }
30
124
 
125
+ function sumUsage(results: readonly AgentToolResult<unknown>[]): Usage | undefined {
126
+ const usages = results.flatMap(({ usage }) => usage ? [usage] : []);
127
+ if (usages.length === 0) return undefined;
128
+ return usages.reduce<Usage>((total, usage) => ({
129
+ input: total.input + usage.input,
130
+ output: total.output + usage.output,
131
+ cacheRead: total.cacheRead + usage.cacheRead,
132
+ cacheWrite: total.cacheWrite + usage.cacheWrite,
133
+ ...(total.cacheWrite1h === undefined && usage.cacheWrite1h === undefined
134
+ ? {}
135
+ : { cacheWrite1h: (total.cacheWrite1h ?? 0) + (usage.cacheWrite1h ?? 0) }),
136
+ ...(total.reasoning === undefined && usage.reasoning === undefined
137
+ ? {}
138
+ : { reasoning: (total.reasoning ?? 0) + (usage.reasoning ?? 0) }),
139
+ totalTokens: total.totalTokens + usage.totalTokens,
140
+ cost: {
141
+ input: total.cost.input + usage.cost.input,
142
+ output: total.cost.output + usage.cost.output,
143
+ cacheRead: total.cost.cacheRead + usage.cost.cacheRead,
144
+ cacheWrite: total.cost.cacheWrite + usage.cost.cacheWrite,
145
+ total: total.cost.total + usage.cost.total,
146
+ },
147
+ }), {
148
+ input: 0,
149
+ output: 0,
150
+ cacheRead: 0,
151
+ cacheWrite: 0,
152
+ totalTokens: 0,
153
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
154
+ });
155
+ }
156
+
157
+ function codeExecutionResult(
158
+ details: unknown,
159
+ innerResults: readonly AgentToolResult<unknown>[],
160
+ ): AgentToolResult<unknown> {
161
+ const addedToolNames = [...new Set(
162
+ innerResults.flatMap(({ addedToolNames }) => addedToolNames ?? []),
163
+ )].sort();
164
+ const usage = sumUsage(innerResults);
165
+ return {
166
+ ...result(details),
167
+ ...(addedToolNames.length > 0 ? { addedToolNames } : {}),
168
+ ...(usage ? { usage } : {}),
169
+ ...(innerResults.length > 0 && innerResults.every(({ terminate }) => terminate === true)
170
+ ? { terminate: true }
171
+ : {}),
172
+ };
173
+ }
174
+
175
+ function projectCodeModeResult(details: unknown): unknown {
176
+ if (typeof details !== "object" || details === null) {
177
+ return { status: "error", executionId: "unknown", error: truncateResponse(String(details)) };
178
+ }
179
+ const output = details as ProxyToolOutput;
180
+ const calls = Array.isArray(output.calls) ? output.calls : [];
181
+ const callSummary = calls.length > 0
182
+ ? {
183
+ total: calls.length,
184
+ failed: calls.filter(({ state }) => state === "error").length,
185
+ methods: [...new Set(calls.map(({ method }) => method))]
186
+ .map((method) => truncateResponse(method, { maxChars: 256 }))
187
+ .slice(0, 100),
188
+ }
189
+ : undefined;
190
+ const common = {
191
+ status: output.status,
192
+ executionId: output.executionId,
193
+ ...(callSummary ? { callSummary } : {}),
194
+ };
195
+ if (output.status === "completed") {
196
+ return { ...common, result: truncateResult(output.result) };
197
+ }
198
+ if (output.status === "paused") {
199
+ return {
200
+ ...common,
201
+ pending: output.pending.slice(0, 100).map(({ connector, method, seq }) => ({
202
+ connector: truncateResponse(connector, { maxChars: 256 }),
203
+ method: truncateResponse(method, { maxChars: 256 }),
204
+ seq,
205
+ })),
206
+ };
207
+ }
208
+ return { ...common, error: truncateResponse(output.error) };
209
+ }
210
+
31
211
  // 上游把 Code Mode 类工具交付为 AI SDK tool;本仓只取 description 和 execute 两件,
32
212
  // 并在这里就地校验,使装配期缺件立刻失败,而不是等模型调用时才炸。
33
213
  function toCodeExecutionPort(
@@ -93,6 +273,7 @@ function browserToolDescription(): string {
93
273
  *
94
274
  * Worker 宿主在具备 DO state 与 Browser 绑定时调用,返回值经 Platform Port 交给 Runtime 工具组装。
95
275
  *
276
+ * 形状与 `createWorkspaceCodeExecutionFactory` 同构:宿主提供 DO state 与平台绑定,Runtime 只拿到一个可选装配输入。
96
277
  * `create()` 延迟到 Tool Surface 真的要注册时才调,被 deny 的装配不会白建连接器。
97
278
  */
98
279
  export function createBrowserExecutionFactory(options: {
@@ -137,3 +318,49 @@ export function createBrowserExecutionFactory(options: {
137
318
  },
138
319
  };
139
320
  }
321
+
322
+ export function createWorkspaceCodeExecutionFactory(options: {
323
+ readonly ctx: DurableObjectState;
324
+ readonly loader: WorkerLoader;
325
+ readonly outbound: Fetcher;
326
+ readonly workspace: WorkspacePort;
327
+ readonly workspaceAccessMode: WorkspaceAccessMode;
328
+ }): RuntimeCodeExecutionFactory {
329
+ return {
330
+ create(candidates) {
331
+ const state = workspaceStateBackend(
332
+ options.workspace,
333
+ options.workspaceAccessMode,
334
+ );
335
+ const createPort = (innerResults: AgentToolResult<unknown>[]) => {
336
+ let nestedCallOrdinal = 0;
337
+ const { tool } = createExecuteRuntime({
338
+ ctx: options.ctx,
339
+ loader: options.loader,
340
+ globalOutbound: options.outbound,
341
+ tools: piCandidatesToAiTools(candidates, {
342
+ fallbackToolCallId: (name) =>
343
+ `codemode:${++nestedCallOrdinal}:${name}`,
344
+ onResult: (value) => innerResults.push(value),
345
+ }),
346
+ timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
347
+ ...(state ? { state } : {}),
348
+ name: "execute",
349
+ });
350
+ return toCodeExecutionPort(
351
+ tool,
352
+ "execute",
353
+ (details) => codeExecutionResult(
354
+ projectCodeModeResult(details),
355
+ innerResults,
356
+ ),
357
+ );
358
+ };
359
+ const description = createPort([]).description;
360
+ return {
361
+ description: `${description}\n\n${SPRINGBRAND_CODE_MODE_POLICY}`,
362
+ execute: (input) => createPort([]).execute(input),
363
+ };
364
+ },
365
+ };
366
+ }
@@ -165,7 +165,7 @@ function browserResult(details: unknown): AgentToolResult<unknown> {
165
165
 
166
166
  // #region Extension discovery
167
167
 
168
- const noParameters = Type.Object({});
168
+ const noParameters = Type.Object({}, { additionalProperties: false });
169
169
 
170
170
  /**
171
171
  * 为当前已加载 Extension 列表创建一个 Pi 工具候选项。
@@ -202,19 +202,21 @@ export function listExtensionsPiToolCandidate(
202
202
 
203
203
  // #endregion
204
204
 
205
- // #region Browser Code Mode
205
+ // #region Code Mode
206
206
 
207
207
  const executeParameters = Type.Object({
208
208
  code: Type.String({
209
209
  minLength: 1,
210
+ maxLength: 131_072,
210
211
  description:
211
212
  "Plain JavaScript async function. TypeScript annotations are not supported.",
212
213
  }),
213
- });
214
+ }, { additionalProperties: false });
214
215
  const CODEMODE_EXECUTE_TIMEOUT_MS = 60_000;
215
216
 
216
- // 把模型提供的浏览器代码交给 Codemode Runtime 执行,并在外层再压一道截止。
217
- // `label` 只用于超时文案,因为模型看到的名字由候选项决定。
217
+ // 把模型提供的代码交给 Codemode Runtime 执行,并在外层再压一道截止。
218
+ // 两个 Code Mode 类工具(`execute``browser_execute`)共用同一段时序,
219
+ // 避免两套心智模型;`label` 只用于超时文案,因为模型看到的名字由候选项决定。
218
220
  function runCodemode(
219
221
  runtime: RuntimeCodeExecutionPort,
220
222
  label: string,
@@ -253,6 +255,36 @@ function runCodemode(
253
255
  };
254
256
  }
255
257
 
258
+ /**
259
+ * 把 Cloudflare Codemode Runtime handle 包装为 Pi 代码执行工具候选项。
260
+ *
261
+ * Tool Surface 收到 Host 已组装的 Code Execution Port 后调用,
262
+ * 模型再通过 `execute` 运行代码。
263
+ *
264
+ * Code Mode 作为一个完整的 safe 工具对外暴露,内部能力不再单独提权。
265
+ */
266
+ export function codeExecutionPiToolCandidate(
267
+ runtime: RuntimeCodeExecutionPort,
268
+ ): PiToolCandidate {
269
+ const tool: AgentTool<typeof executeParameters> = {
270
+ name: "execute",
271
+ label: "Execute JavaScript",
272
+ description: runtime.description,
273
+ parameters: executeParameters,
274
+ // Pi 工具循环在模型选择 `execute` 时调用,调用前允许 Turn 取消。
275
+ // 必须通过 Runtime handle 而不是直接调用 executor,因为 Cloudflare Codemode 把重放、审批和执行日志放在持久化 Runtime 层。
276
+ execute: runCodemode(runtime, "Code Mode execute"),
277
+ };
278
+ return {
279
+ owner: "core:codemode",
280
+ requiredExecutionLevel: "safe",
281
+ outputBudget: { kind: "structure" },
282
+ source: "codemode",
283
+ summary: "Run JavaScript with network and configured connector access",
284
+ tool,
285
+ };
286
+ }
287
+
256
288
  /** 模型可见的浏览器工具名;属外部契约,改名是破坏性变更。 */
257
289
  export const BROWSER_EXECUTE_TOOL_NAME = "browser_execute";
258
290
 
@@ -49,6 +49,7 @@ export interface PiDeclaredToolPolicy<Reply = unknown> {
49
49
  readonly modelName: string;
50
50
  readonly requiredExecutionLevel: PiToolCandidate["requiredExecutionLevel"];
51
51
  readonly requiredExecutionLevelForInput?: PiToolCandidate["requiredExecutionLevelForInput"];
52
+ readonly exposureMode?: PiToolCandidate["exposureMode"];
52
53
  readonly retry?: PiToolCandidate["retry"];
53
54
  readonly source?: PiToolCandidate["source"];
54
55
  readonly summary?: string;
@@ -104,6 +105,7 @@ export function createPiDeclaredToolCandidate<Reply = unknown>(
104
105
  parameters: structuredClone(
105
106
  declared.inputSchema ?? { type: "object", additionalProperties: true },
106
107
  ) as AgentTool["parameters"],
108
+ constrainedSampling: false,
107
109
  execute: async (toolCallId, input, signal) =>
108
110
  project(await call(input as Record<string, unknown>, { toolCallId, signal })),
109
111
  };
@@ -115,6 +117,7 @@ export function createPiDeclaredToolCandidate<Reply = unknown>(
115
117
  ...(policy.requiredExecutionLevelForInput
116
118
  ? { requiredExecutionLevelForInput: policy.requiredExecutionLevelForInput }
117
119
  : {}),
120
+ ...(policy.exposureMode ? { exposureMode: policy.exposureMode } : {}),
118
121
  ...(policy.retry ? { retry: policy.retry } : {}),
119
122
  ...(policy.source ? { source: policy.source } : {}),
120
123
  };
@@ -35,7 +35,11 @@ export function piCandidatesToAiTools(
35
35
  call?.abortSignal ?? new AbortController().signal,
36
36
  );
37
37
  options.onResult?.(result);
38
- return result;
38
+ if (result.details !== undefined) return result.details;
39
+ const [only] = result.content;
40
+ return result.content.length === 1 && only?.type === "text"
41
+ ? only.text
42
+ : result.content;
39
43
  },
40
44
  }),
41
45
  ]));
@@ -18,28 +18,28 @@ const scheduleTriggerParameters = Type.Union([
18
18
  minimum: 1,
19
19
  description: "Fire once, this many seconds from now.",
20
20
  }),
21
- }),
21
+ }, { additionalProperties: false }),
22
22
  Type.Object({
23
23
  kind: Type.Literal("once-at"),
24
24
  at: Type.String({
25
25
  description:
26
26
  "Fire once at this absolute UTC ISO time, e.g. 2026-07-20T09:00:00Z.",
27
27
  }),
28
- }),
28
+ }, { additionalProperties: false }),
29
29
  Type.Object({
30
30
  kind: Type.Literal("cron"),
31
31
  cron: Type.String({
32
32
  description:
33
33
  "Repeat on a standard UTC cron, e.g. '0 9 * * *' = daily 09:00 UTC.",
34
34
  }),
35
- }),
35
+ }, { additionalProperties: false }),
36
36
  Type.Object({
37
37
  kind: Type.Literal("interval"),
38
38
  seconds: Type.Integer({
39
39
  minimum: 1,
40
40
  description: "Repeat every this many seconds.",
41
41
  }),
42
- }),
42
+ }, { additionalProperties: false }),
43
43
  ], { description: "When to fire." });
44
44
 
45
45
  const scheduleParameters = Type.Object({
@@ -56,21 +56,21 @@ const scheduleParameters = Type.Object({
56
56
  description:
57
57
  "IANA timezone the clock times in `trigger` should be interpreted in, e.g. 'Asia/Shanghai'. Pass the user's own timezone whenever they state a wall-clock time. Omit only for delays and intervals. Defaults to UTC.",
58
58
  })),
59
- });
59
+ }, { additionalProperties: false });
60
60
 
61
- const noParameters = Type.Object({});
61
+ const noParameters = Type.Object({}, { additionalProperties: false });
62
62
  const scheduleIdParameters = Type.Object({
63
63
  id: Type.String({
64
64
  description: "The schedule id, as returned by `list_schedules`.",
65
65
  }),
66
- });
66
+ }, { additionalProperties: false });
67
67
  const updateScheduleParameters = Type.Object({
68
68
  id: Type.String({ description: "The exact id returned by `list_schedules`." }),
69
69
  prompt: Type.Optional(Type.String()),
70
70
  trigger: Type.Optional(scheduleTriggerParameters),
71
71
  label: Type.Optional(Type.String()),
72
72
  timezone: Type.Optional(Type.String()),
73
- });
73
+ }, { additionalProperties: false });
74
74
  const changeScheduleAgentParameters = Type.Object({
75
75
  id: Type.String({
76
76
  description: "The exact schedule id returned by `list_schedules`.",
@@ -78,7 +78,7 @@ const changeScheduleAgentParameters = Type.Object({
78
78
  userAgentId: Type.String({
79
79
  description: "The exact User Agent id selected by the user.",
80
80
  }),
81
- });
81
+ }, { additionalProperties: false });
82
82
 
83
83
  function result<T>(details: T): AgentToolResult<T> {
84
84
  return {
@@ -92,12 +92,13 @@ function candidate<T extends TSchema>(
92
92
  options: Partial<
93
93
  Pick<
94
94
  PiToolCandidate,
95
- "alwaysRequiresApproval" | "owner" | "requiredExecutionLevel" | "summary"
95
+ "alwaysRequiresApproval" | "exposureMode" | "owner" | "requiredExecutionLevel" | "summary"
96
96
  >
97
97
  > = {},
98
98
  ): PiToolCandidate {
99
99
  return {
100
100
  owner: options.owner ?? "runtime-base",
101
+ exposureMode: options.exposureMode ?? "direct",
101
102
  requiredExecutionLevel: options.requiredExecutionLevel ?? "safe",
102
103
  source: "action",
103
104
  tool,
@@ -133,6 +134,7 @@ export function schedulePiToolCandidates(
133
134
  },
134
135
  {
135
136
  alwaysRequiresApproval: true,
137
+ exposureMode: "direct",
136
138
  summary: "Create a scheduled task",
137
139
  },
138
140
  ),