@springbrand/agent-runtime 0.2.0-alpha.34 → 0.2.0-alpha.36

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.
@@ -4,24 +4,57 @@ import {
4
4
  } from "@cloudflare/shell";
5
5
  import { createBrowserTools } from "@cloudflare/think/tools/browser";
6
6
  import { createExecuteRuntime } from "@cloudflare/think/tools/execute";
7
- import { jsonSchema, tool as aiTool, type ToolSet } from "ai";
7
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
8
+ import type { Usage } from "@earendil-works/pi-ai";
9
+ import type { ToolSet } from "ai";
8
10
  import type {
9
11
  RuntimeBrowserPort,
10
12
  RuntimeCodeExecutionPort,
11
13
  WorkspacePort,
12
14
  } from "../../kernel/bindings";
13
- import type {
14
- RuntimeCodeExecutionFactory,
15
- ToolRegistry,
16
- } from "../../tool-registry";
15
+ import type { RuntimeCodeExecutionFactory } from "../../kernel/bindings";
16
+ import { serializeOutput } from "../../lib/artifacts";
17
+ import type { PiToolCandidate } from "./compiler";
18
+ import { piCandidatesToAiTools } from "./nested-tools";
17
19
 
18
20
  // 本文件沿用 `../../index.ts` 入口定义的 Workspace、Port 和 Tool Candidate 术语。
19
21
 
20
22
  const CODEMODE_SANDBOX_TIMEOUT_MS = 55_000;
21
- const CODEMODE_TOOL_SURFACE_GUIDANCE =
22
- "The `tools.*` Available list below is exhaustive, not examples. Inside execute, call only methods in that list; " +
23
- "never guess or construct a `tools.*` method name. If a required Tool is absent, leave execute and call it at the top level, " +
24
- "using top-level Tool Search first when it is deferred. `codemode.search` cannot add methods to `tools.*`.";
23
+
24
+ function directoryEntry(name: string, label: string): string {
25
+ const singleLineLabel = label.replaceAll(/\s+/g, " ").trim().replaceAll("`", "'");
26
+ return `- \`${name}\` ${singleLineLabel}`;
27
+ }
28
+
29
+ function codeExecutionDescription(
30
+ candidates: readonly PiToolCandidate[],
31
+ ): string {
32
+ const list = (
33
+ entries: readonly { readonly name: string; readonly label: string }[],
34
+ ) => entries.length > 0
35
+ ? entries.map(({ name, label }) => directoryEntry(name, label)).join("\n")
36
+ : "- None.";
37
+
38
+ return [
39
+ "Execute plain JavaScript in a sandbox using the exact Tool directory below.",
40
+ "",
41
+ "## `tools.*` Available",
42
+ list(candidates.map(({ tool }) => ({
43
+ name: tool.name,
44
+ label: tool.label ?? tool.name,
45
+ }))),
46
+ "",
47
+ "Call only the methods listed above through `tools.*`; never guess or construct a method name.",
48
+ "Use `codemode.describe(\"tools.method\")` when you need the exact input type for a listed method.",
49
+ "`codemode.search` cannot add methods to `tools.*` or load top-level Tools; it searches only connector methods and snippets already installed in this Code Mode Runtime.",
50
+ "Use `state.*` for the Workspace filesystem. Every method takes one object argument, for example `state.readFile({ path })` and `state.writeFile({ path, content })`.",
51
+ "Wrap raw fetch, random values, time, and other nondeterministic work in `codemode.step(name, fn)` so replay runs them once.",
52
+ "Some connector methods pause for approval and resume automatically. Do not re-issue paused code.",
53
+ "Keep all code outside connector calls and `codemode.step` deterministic.",
54
+ "Raw `fetch` is available inside `codemode.step(...)`; prefer connector SDKs when one owns the target.",
55
+ "There is no Node.js `require`, `process`, package manager, or Python runtime.",
56
+ ].join("\n");
57
+ }
25
58
 
26
59
  /**
27
60
  * 宿主的 Browser Rendering 绑定,只取本仓真正用到的那一面。
@@ -40,26 +73,61 @@ export interface RuntimeBrowserBinding {
40
73
  *
41
74
  * Think 的独立 execute factory 接受显式宿主参数,不要求 Agent 继承 Think;这里只借它组装 Codemode Runtime、Dynamic Worker executor 和已限定范围的 Workspace state connector。
42
75
  */
43
- function codeExecutionTools(tools: ToolRegistry): ToolSet {
44
- return Object.fromEntries(Object.entries(tools).map(([name, spec]) => [
45
- name,
46
- aiTool({
47
- description: spec.description,
48
- inputSchema: jsonSchema(spec.parameters as never),
49
- execute: async (input, options) => {
50
- const result = await spec.execute(input, {
51
- toolCallId: options?.toolCallId ?? name,
52
- signal: options?.abortSignal ?? new AbortController().signal,
53
- });
54
- return result != null &&
55
- typeof result === "object" &&
56
- "content" in result &&
57
- Array.isArray((result as { content?: unknown }).content)
58
- ? (result as { details?: unknown }).details ?? result
59
- : result;
60
- },
61
- }),
62
- ]));
76
+ function result(details: unknown): AgentToolResult<unknown> {
77
+ return {
78
+ content: [{ type: "text", text: serializeOutput(details).text }],
79
+ details,
80
+ };
81
+ }
82
+
83
+ function sumUsage(results: readonly AgentToolResult<unknown>[]): Usage | undefined {
84
+ const usages = results.flatMap(({ usage }) => usage ? [usage] : []);
85
+ if (usages.length === 0) return undefined;
86
+ return usages.reduce<Usage>((total, usage) => ({
87
+ input: total.input + usage.input,
88
+ output: total.output + usage.output,
89
+ cacheRead: total.cacheRead + usage.cacheRead,
90
+ cacheWrite: total.cacheWrite + usage.cacheWrite,
91
+ ...(total.cacheWrite1h === undefined && usage.cacheWrite1h === undefined
92
+ ? {}
93
+ : { cacheWrite1h: (total.cacheWrite1h ?? 0) + (usage.cacheWrite1h ?? 0) }),
94
+ ...(total.reasoning === undefined && usage.reasoning === undefined
95
+ ? {}
96
+ : { reasoning: (total.reasoning ?? 0) + (usage.reasoning ?? 0) }),
97
+ totalTokens: total.totalTokens + usage.totalTokens,
98
+ cost: {
99
+ input: total.cost.input + usage.cost.input,
100
+ output: total.cost.output + usage.cost.output,
101
+ cacheRead: total.cost.cacheRead + usage.cost.cacheRead,
102
+ cacheWrite: total.cost.cacheWrite + usage.cost.cacheWrite,
103
+ total: total.cost.total + usage.cost.total,
104
+ },
105
+ }), {
106
+ input: 0,
107
+ output: 0,
108
+ cacheRead: 0,
109
+ cacheWrite: 0,
110
+ totalTokens: 0,
111
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
112
+ });
113
+ }
114
+
115
+ function codeExecutionResult(
116
+ details: unknown,
117
+ innerResults: readonly AgentToolResult<unknown>[],
118
+ ): AgentToolResult<unknown> {
119
+ const addedToolNames = [...new Set(
120
+ innerResults.flatMap(({ addedToolNames }) => addedToolNames ?? []),
121
+ )].sort();
122
+ const usage = sumUsage(innerResults);
123
+ return {
124
+ ...result(details),
125
+ ...(addedToolNames.length > 0 ? { addedToolNames } : {}),
126
+ ...(usage ? { usage } : {}),
127
+ ...(innerResults.length > 0 && innerResults.every(({ terminate }) => terminate === true)
128
+ ? { terminate: true }
129
+ : {}),
130
+ };
63
131
  }
64
132
 
65
133
  // 上游把 Code Mode 类工具交付为 AI SDK tool;本仓只取 description 和 execute 两件,
@@ -67,6 +135,7 @@ function codeExecutionTools(tools: ToolRegistry): ToolSet {
67
135
  function toCodeExecutionPort(
68
136
  tool: ToolSet[string],
69
137
  name: string,
138
+ project: (details: unknown) => AgentToolResult<unknown> = result,
70
139
  ): RuntimeCodeExecutionPort {
71
140
  const { description, execute } = tool;
72
141
  if (typeof description !== "string" || description.length === 0) {
@@ -77,7 +146,7 @@ function toCodeExecutionPort(
77
146
  }
78
147
  return {
79
148
  description,
80
- execute: (input) => Promise.resolve(execute(input, {
149
+ execute: async (input) => project(await execute(input, {
81
150
  toolCallId: name,
82
151
  messages: [],
83
152
  context: undefined,
@@ -179,26 +248,33 @@ export function createWorkspaceCodeExecutionFactory(options: {
179
248
  readonly workspace: WorkspacePort;
180
249
  }): RuntimeCodeExecutionFactory {
181
250
  return {
182
- create(tools) {
183
- const { tool } = createExecuteRuntime({
184
- ctx: options.ctx,
185
- loader: options.loader,
186
- globalOutbound: options.outbound,
187
- tools: codeExecutionTools(tools),
188
- // 先于外层 60s 截止结束,给 Runtime RPC 结算和 Worker 释放留出时间。
189
- timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
190
- state: createWorkspaceStateBackend(
191
- options.workspace as unknown as WorkspaceFsLike,
192
- ),
193
- name: "execute",
194
- });
195
- const port = toCodeExecutionPort(tool, "execute");
251
+ create(candidates) {
252
+ const description = codeExecutionDescription(candidates);
196
253
  return {
197
- ...port,
198
- description: `${CODEMODE_TOOL_SURFACE_GUIDANCE}\n\n${port.description.replace(
199
- "- Do not use `fetch` — use connector SDKs.",
200
- "- Raw `fetch` is available inside `codemode.step(...)`; prefer connector SDKs when one owns the target.",
201
- )}`,
254
+ description,
255
+ async execute(input) {
256
+ const innerResults: AgentToolResult<unknown>[] = [];
257
+ const { tool } = createExecuteRuntime({
258
+ ctx: options.ctx,
259
+ loader: options.loader,
260
+ globalOutbound: options.outbound,
261
+ tools: piCandidatesToAiTools(candidates, {
262
+ onResult: (value) => innerResults.push(value),
263
+ }),
264
+ description,
265
+ // 先于外层 60s 截止结束,给 Runtime RPC 结算和 Worker 释放留出时间。
266
+ timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
267
+ state: createWorkspaceStateBackend(
268
+ options.workspace as unknown as WorkspaceFsLike,
269
+ ),
270
+ name: "execute",
271
+ });
272
+ return toCodeExecutionPort(
273
+ tool,
274
+ "execute",
275
+ (details) => codeExecutionResult(details, innerResults),
276
+ ).execute(input);
277
+ },
202
278
  };
203
279
  },
204
280
  };
@@ -219,7 +219,8 @@ const CODEMODE_EXECUTE_TIMEOUT_MS = 60_000;
219
219
  function runCodemode(
220
220
  runtime: RuntimeCodeExecutionPort,
221
221
  label: string,
222
- project: (details: unknown) => AgentToolResult<unknown> = result,
222
+ project: (result: AgentToolResult<unknown>) => AgentToolResult<unknown> =
223
+ (result) => result,
223
224
  ): AgentTool<typeof executeParameters>["execute"] {
224
225
  return async (_toolCallId, input, signal) => {
225
226
  signal?.throwIfAborted();
@@ -245,7 +246,7 @@ function runCodemode(
245
246
  try {
246
247
  return project(await Promise.race([
247
248
  runtime.execute(input),
248
- deadline,
249
+ deadline.then(result),
249
250
  ]));
250
251
  } finally {
251
252
  if (timeout !== undefined) clearTimeout(timeout);
@@ -303,7 +304,11 @@ export function browserExecutionPiToolCandidate(
303
304
  label: "Drive a browser",
304
305
  description: runtime.description,
305
306
  parameters: executeParameters,
306
- execute: runCodemode(runtime, "Browser Code Mode execute", browserResult),
307
+ execute: runCodemode(
308
+ runtime,
309
+ "Browser Code Mode execute",
310
+ ({ details }) => browserResult(details),
311
+ ),
307
312
  };
308
313
  return {
309
314
  owner: "core:browser",
@@ -0,0 +1,42 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import { validateToolArguments } from "@earendil-works/pi-ai";
3
+ import { jsonSchema, tool as aiTool, type ToolSet } from "ai";
4
+ import type { PiToolCandidate } from "./compiler";
5
+
6
+ interface NestedToolOptions {
7
+ readonly fallbackToolCallId?: (name: string) => string;
8
+ readonly onResult?: (result: AgentToolResult<unknown>) => void;
9
+ }
10
+
11
+ /** The one Pi candidate to AI SDK ToolSet adapter used by nested runtimes. */
12
+ export function piCandidatesToAiTools(
13
+ candidates: readonly PiToolCandidate[],
14
+ options: NestedToolOptions,
15
+ ): ToolSet {
16
+ return Object.fromEntries(candidates.map((candidate) => [
17
+ candidate.tool.name,
18
+ aiTool({
19
+ description: candidate.tool.description,
20
+ inputSchema: jsonSchema(candidate.tool.parameters as never),
21
+ execute: async (input, call) => {
22
+ const toolCallId = call?.toolCallId ??
23
+ options.fallbackToolCallId?.(candidate.tool.name) ??
24
+ candidate.tool.name;
25
+ const prepared = candidate.tool.prepareArguments?.(input) ?? input;
26
+ const args = validateToolArguments(candidate.tool, {
27
+ type: "toolCall",
28
+ id: toolCallId,
29
+ name: candidate.tool.name,
30
+ arguments: prepared as Record<string, unknown>,
31
+ });
32
+ const result = await candidate.tool.execute(
33
+ toolCallId,
34
+ args,
35
+ call?.abortSignal ?? new AbortController().signal,
36
+ );
37
+ options.onResult?.(result);
38
+ return result;
39
+ },
40
+ }),
41
+ ]));
42
+ }
@@ -10,10 +10,6 @@ import type {
10
10
  import type { ScheduleSpec } from "../../kernel/receipts";
11
11
  import { serializeOutput } from "../../lib/artifacts";
12
12
  import type { PiToolCandidate } from "./compiler";
13
- import {
14
- toolRegistryFromPiCandidates,
15
- type ToolRegistry,
16
- } from "../../tool-registry";
17
13
 
18
14
  const scheduleTriggerParameters = Type.Union([
19
15
  Type.Object({
@@ -259,8 +255,3 @@ export function schedulePiToolCandidates(
259
255
  }
260
256
 
261
257
  /** 从 {@link RuntimeSchedulePort} 生成定时任务 Tool 集。 */
262
- export function createScheduleTools(
263
- schedule: RuntimeSchedulePort,
264
- ): ToolRegistry {
265
- return toolRegistryFromPiCandidates(schedulePiToolCandidates(schedule));
266
- }
@@ -6,7 +6,7 @@ import {
6
6
  type SkillScriptRunner,
7
7
  type SkillSource,
8
8
  } from "agents/skills";
9
- import { jsonSchema, tool, type ToolSet } from "ai";
9
+ import { tool } from "ai";
10
10
  import { z } from "zod";
11
11
  import type {
12
12
  RuntimeSkillScriptPolicy,
@@ -14,6 +14,7 @@ import type {
14
14
  } from "../../kernel/bindings";
15
15
  import { aiToolToPi } from "./ai-adapter";
16
16
  import type { PiToolCandidate } from "./compiler";
17
+ import { piCandidatesToAiTools } from "./nested-tools";
17
18
  import { serializeWorkspaceMutation } from "./workspace-sandbox";
18
19
 
19
20
  /** A configured Skill source and its script capabilities. */
@@ -31,25 +32,6 @@ export interface SkillPiToolOptions {
31
32
  readonly tools?: readonly PiToolCandidate[];
32
33
  }
33
34
 
34
- function scriptTools(
35
- candidates: readonly PiToolCandidate[],
36
- names: readonly string[],
37
- ): ToolSet {
38
- const byName = new Map(
39
- candidates.map((candidate) => [candidate.tool.name, candidate.tool]),
40
- );
41
- return Object.fromEntries(names.flatMap((name) => {
42
- const candidate = byName.get(name);
43
- if (!candidate) return [];
44
- return [[name, {
45
- description: candidate.description,
46
- inputSchema: jsonSchema(candidate.parameters as never),
47
- execute: async (input: unknown) =>
48
- (await candidate.execute(`skill-script:${name}`, input)).details,
49
- }]];
50
- })) as ToolSet;
51
- }
52
-
53
35
  /**
54
36
  * `run_skill_script` 一次挂载的 Skill 资源总量上限。
55
37
  *
@@ -166,6 +148,10 @@ function scriptRunner(
166
148
  const workspace = policy.workspace !== "none" && options.workspace
167
149
  ? policy.workspace
168
150
  : "none";
151
+ let toolCallOrdinal = 0;
152
+ const selected = (options.tools ?? []).filter(({ tool }) =>
153
+ policy.tools.includes(tool.name)
154
+ );
169
155
  return createSkillScriptRunner({
170
156
  loader: options.loader!,
171
157
  network: policy.network === "full",
@@ -173,7 +159,10 @@ function scriptRunner(
173
159
  ...(workspace !== "none" && options.workspace
174
160
  ? { workspaceInstance: options.workspace }
175
161
  : {}),
176
- tools: scriptTools(options.tools ?? [], policy.tools),
162
+ tools: piCandidatesToAiTools(selected, {
163
+ fallbackToolCallId: (name) =>
164
+ `skill-script:${++toolCallOrdinal}:${name}`,
165
+ }),
177
166
  }).run(request);
178
167
  },
179
168
  };
@@ -13,10 +13,6 @@ import {
13
13
  subagentTerminalStatus,
14
14
  } from "../../kernel/subagent-runtime";
15
15
  import type { PiToolCandidate } from "./compiler";
16
- import {
17
- toolRegistryFromPiCandidates,
18
- type ToolRegistry,
19
- } from "../../tool-registry";
20
16
 
21
17
  const BACKGROUND_MAX_BUDGET_MS = 10 * 60 * 1_000;
22
18
 
@@ -275,12 +271,3 @@ export function subagentPiToolCandidates(
275
271
  }
276
272
 
277
273
  /** 从 {@link RuntimeSubagentPort} 生成已启用 SubAgent Tool 集。 */
278
- export function createSubagentTools(
279
- subagents: RuntimeSubagentPort | undefined,
280
- enabledSubagents: readonly string[],
281
- lifecycle?: RuntimeSubagentLifecyclePort,
282
- ): ToolRegistry {
283
- return toolRegistryFromPiCandidates(
284
- subagentPiToolCandidates(subagents, enabledSubagents, lifecycle),
285
- );
286
- }
@@ -2,10 +2,6 @@ import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
2
2
  import { Type } from "@earendil-works/pi-ai";
3
3
  import type { WorkspaceRevisionRestorePort } from "../../workspace-versioning";
4
4
  import { serializeOutput } from "../../lib/artifacts";
5
- import {
6
- toolRegistryFromPiCandidates,
7
- type ToolRegistry,
8
- } from "../../tool-registry";
9
5
  import type { PiToolCandidate } from "./compiler";
10
6
 
11
7
  const parameters = Type.Object({
@@ -54,11 +50,3 @@ export function workspaceRevisionPiToolCandidate(
54
50
  summary: "Restore the Workspace tree",
55
51
  };
56
52
  }
57
-
58
- export function createWorkspaceRevisionTools(
59
- versions: WorkspaceRevisionRestorePort,
60
- ): ToolRegistry {
61
- return toolRegistryFromPiCandidates([
62
- workspaceRevisionPiToolCandidate(versions),
63
- ]);
64
- }
@@ -21,10 +21,6 @@ import type {
21
21
  import { serializeOutput } from "../../lib/artifacts";
22
22
  import { aiToolToPi } from "./ai-adapter";
23
23
  import type { PiToolCandidate } from "./compiler";
24
- import {
25
- toolRegistryFromPiCandidates,
26
- type ToolRegistry,
27
- } from "../../tool-registry";
28
24
 
29
25
  // #region Shared Pi result helpers
30
26
 
@@ -428,9 +424,6 @@ export function workspacePiToolCandidates(
428
424
  }
429
425
 
430
426
  /** 从 {@link WorkspacePort} 生成标准 workspace 文件 Tool 集(read / write / edit …)。 */
431
- export function createWorkspaceTools(workspace: WorkspacePort): ToolRegistry {
432
- return toolRegistryFromPiCandidates(workspacePiToolCandidates(workspace));
433
- }
434
427
 
435
428
  // #endregion
436
429
 
@@ -625,8 +618,5 @@ export function sandboxPiToolCandidates(
625
618
  }
626
619
 
627
620
  /** 从 {@link RuntimeSandboxPort} 生成 Sandbox Tool 集。 */
628
- export function createSandboxTools(sandbox: RuntimeSandboxPort): ToolRegistry {
629
- return toolRegistryFromPiCandidates(sandboxPiToolCandidates(sandbox));
630
- }
631
621
 
632
622
  // #endregion
@@ -36,12 +36,8 @@ import {
36
36
  import type {
37
37
  ResolvedResources,
38
38
  RuntimeAgentHooks,
39
+ ToolAssemblyResult,
39
40
  } from "./runtime-definition";
40
- import {
41
- normalizeToolAssembly,
42
- type ToolAssemblyResult,
43
- type ToolRegistry,
44
- } from "./tool-registry";
45
41
  import type { RuntimeAssemblyView } from "./kernel/runtime-assembly-view";
46
42
  import type { RuntimeConfigUpdateResult } from "./kernel/runtime-config";
47
43
  import {
@@ -108,7 +104,7 @@ interface RuntimeAgentDefinitionBase<
108
104
  readonly tools: (
109
105
  context: RuntimeAgentPlanningContext<Env, Command, Change>,
110
106
  config: Config,
111
- ) => ToolRegistry | ToolAssemblyResult | Promise<ToolRegistry | ToolAssemblyResult>;
107
+ ) => ToolAssemblyResult | Promise<ToolAssemblyResult>;
112
108
 
113
109
  readonly hooks?:
114
110
  | RuntimeAgentHooks<Env, Config, Command, Change>
@@ -628,16 +624,16 @@ export function defineRuntimeAgent<
628
624
  const context = this.createDefinitionContext({
629
625
  value: loaded.config,
630
626
  });
631
- let tools = normalizeToolAssembly(await withRuntimeLoadTimeout(
627
+ let tools = await withRuntimeLoadTimeout(
632
628
  "definition.tools",
633
629
  () => (
634
630
  definition.tools as (
635
631
  context: RuntimeAgentPlanningContext<Env, Command, Change>,
636
632
  config: Config,
637
- ) => ToolRegistry | ToolAssemblyResult | Promise<ToolRegistry | ToolAssemblyResult>
633
+ ) => ToolAssemblyResult | Promise<ToolAssemblyResult>
638
634
  )(context, loaded.config),
639
635
  { timeoutMs: RUNTIME_LOAD_TIMEOUT_MS },
640
- ));
636
+ );
641
637
  let hooks = resolveDefinitionHooks(context);
642
638
  if (!this.telemetryResolved) {
643
639
  this.resolvedTelemetry = resolveDefinitionTelemetry(context);