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

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/agent-runtime",
3
- "version": "0.2.0-alpha.41",
3
+ "version": "0.2.0-alpha.42",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -0,0 +1,215 @@
1
+ import {
2
+ parseSkillMarkdown,
3
+ type SkillContent,
4
+ type SkillDescriptor,
5
+ type SkillResource,
6
+ type SkillResourceDescriptor,
7
+ type SkillSource,
8
+ } from "agents/skills";
9
+
10
+ type ListedObject = Pick<R2Object, "key" | "size">;
11
+
12
+ interface IndexedSkill {
13
+ readonly descriptor: SkillDescriptor;
14
+ readonly content: SkillContent;
15
+ readonly directory: string;
16
+ readonly resources: ReadonlyMap<string, SkillResourceDescriptor>;
17
+ }
18
+
19
+ const TEXT_EXTENSIONS = new Set([
20
+ ".bash",
21
+ ".css",
22
+ ".csv",
23
+ ".html",
24
+ ".js",
25
+ ".json",
26
+ ".jsx",
27
+ ".md",
28
+ ".mjs",
29
+ ".py",
30
+ ".sh",
31
+ ".svg",
32
+ ".ts",
33
+ ".tsx",
34
+ ".txt",
35
+ ".xml",
36
+ ".yaml",
37
+ ".yml",
38
+ ]);
39
+
40
+ function trimSlashes(value: string): string {
41
+ return value.replace(/^\/+/, "").replace(/\/+$/, "");
42
+ }
43
+
44
+ function resourceKind(path: string): SkillResourceDescriptor["kind"] {
45
+ if (path.startsWith("references/")) return "reference";
46
+ if (path.startsWith("scripts/")) return "script";
47
+ if (path.startsWith("assets/")) return "asset";
48
+ return "file";
49
+ }
50
+
51
+ function resourceEncoding(path: string): "text" | "base64" {
52
+ const file = path.split("/").at(-1) ?? path;
53
+ const dot = file.lastIndexOf(".");
54
+ return TEXT_EXTENSIONS.has(dot < 0 ? "" : file.slice(dot).toLowerCase())
55
+ ? "text"
56
+ : "base64";
57
+ }
58
+
59
+ function normalizedResourcePath(path: string): boolean {
60
+ return !path.startsWith("/") &&
61
+ !path.includes("\0") &&
62
+ path.split("/").every((part) => part !== "" && part !== "." && part !== "..");
63
+ }
64
+
65
+ function base64Encode(buffer: ArrayBuffer): string {
66
+ let binary = "";
67
+ for (const byte of new Uint8Array(buffer)) {
68
+ binary += String.fromCharCode(byte);
69
+ }
70
+ return btoa(binary);
71
+ }
72
+
73
+ async function readR2<T>(operation: () => Promise<T>): Promise<T> {
74
+ try {
75
+ return await operation();
76
+ } catch {
77
+ return operation();
78
+ }
79
+ }
80
+
81
+ async function listAllObjects(
82
+ bucket: R2Bucket,
83
+ prefix: string,
84
+ ): Promise<ListedObject[]> {
85
+ const objects: ListedObject[] = [];
86
+ let cursor: string | undefined;
87
+ do {
88
+ const page = await readR2(() => bucket.list({
89
+ prefix,
90
+ ...(cursor ? { cursor } : {}),
91
+ }));
92
+ objects.push(...page.objects);
93
+ cursor = page.truncated ? page.cursor : undefined;
94
+ } while (cursor);
95
+ return objects;
96
+ }
97
+
98
+ async function readText(bucket: R2Bucket, key: string): Promise<string> {
99
+ return readR2(async () => {
100
+ const object = await bucket.get(key);
101
+ if (!object) throw new Error(`Skill content not found: ${key}`);
102
+ return object.text();
103
+ });
104
+ }
105
+
106
+ async function readResource(
107
+ bucket: R2Bucket,
108
+ key: string,
109
+ descriptor: SkillResourceDescriptor,
110
+ ): Promise<SkillResource> {
111
+ return readR2(async () => {
112
+ const object = await bucket.get(key);
113
+ if (!object) throw new Error(`Skill resource not found: ${key}`);
114
+ const encoding = descriptor.encoding ?? resourceEncoding(descriptor.path);
115
+ return {
116
+ ...descriptor,
117
+ encoding,
118
+ ...(object.httpMetadata?.contentType
119
+ ? { mimeType: object.httpMetadata.contentType }
120
+ : {}),
121
+ content: encoding === "text"
122
+ ? await object.text()
123
+ : base64Encode(await object.arrayBuffer()),
124
+ };
125
+ });
126
+ }
127
+
128
+ /** Create the immutable, single-Skill R2 Source used by SpringBrand runtimes. */
129
+ export function createR2SkillSource(
130
+ bucket: R2Bucket,
131
+ resourceId: string,
132
+ contentHash: string,
133
+ contentRef: string,
134
+ ): SkillSource {
135
+ const prefix = `${trimSlashes(contentRef)}/`;
136
+ const id = `resource-${resourceId}-${contentHash}`;
137
+ let indexPromise: Promise<IndexedSkill> | undefined;
138
+
139
+ const loadIndex = async (): Promise<IndexedSkill> => {
140
+ const objects = await listAllObjects(bucket, prefix);
141
+ const skillObjects = objects.filter(({ key }) => key.endsWith("/SKILL.md"));
142
+ if (skillObjects.length !== 1) {
143
+ throw new Error(`Skill content requires exactly one SKILL.md: ${contentRef}`);
144
+ }
145
+ const skillKey = skillObjects[0]!.key;
146
+ const directory = skillKey.slice(prefix.length, -"/SKILL.md".length);
147
+ if (!directory || directory.includes("/")) {
148
+ throw new Error(`Skill content has an invalid directory: ${contentRef}`);
149
+ }
150
+ const parsed = parseSkillMarkdown(await readText(bucket, skillKey));
151
+ if (!parsed) throw new Error(`Skill content has invalid frontmatter: ${contentRef}`);
152
+
153
+ const resourcePrefix = `${prefix}${directory}/`;
154
+ const resources = objects.flatMap(({ key, size }) => {
155
+ if (key === skillKey || !key.startsWith(resourcePrefix)) return [];
156
+ const path = key.slice(resourcePrefix.length);
157
+ return normalizedResourcePath(path)
158
+ ? [{ path, kind: resourceKind(path), encoding: resourceEncoding(path), size }]
159
+ : [];
160
+ });
161
+ const descriptor: SkillDescriptor = {
162
+ name: parsed.name,
163
+ description: parsed.description,
164
+ compatibility: parsed.compatibility,
165
+ license: parsed.license,
166
+ allowedTools: parsed.allowedTools,
167
+ metadata: parsed.metadata,
168
+ sourceId: id,
169
+ };
170
+ return {
171
+ descriptor,
172
+ directory,
173
+ resources: new Map(resources.map((resource) => [resource.path, resource])),
174
+ content: {
175
+ ...descriptor,
176
+ body: parsed.body,
177
+ resources,
178
+ },
179
+ };
180
+ };
181
+
182
+ const index = (): Promise<IndexedSkill> => {
183
+ if (!indexPromise) {
184
+ indexPromise = loadIndex().catch((error) => {
185
+ indexPromise = undefined;
186
+ throw error;
187
+ });
188
+ }
189
+ return indexPromise;
190
+ };
191
+
192
+ return {
193
+ id,
194
+ fingerprint: id,
195
+ async list() {
196
+ return [{ ...(await index()).descriptor }];
197
+ },
198
+ async load(name) {
199
+ const indexed = await index();
200
+ return indexed.descriptor.name === name ? { ...indexed.content } : null;
201
+ },
202
+ async readResource(name, path) {
203
+ if (!normalizedResourcePath(path)) return null;
204
+ const indexed = await index();
205
+ if (indexed.descriptor.name !== name) return null;
206
+ const descriptor = indexed.resources.get(path);
207
+ if (!descriptor) return null;
208
+ return readResource(
209
+ bucket,
210
+ `${prefix}${indexed.directory}/${path}`,
211
+ descriptor,
212
+ );
213
+ },
214
+ };
215
+ }
@@ -1,8 +1,8 @@
1
- import { r2, type SkillSource } from "agents/skills";
2
1
  import type { RuntimeDegradation } from "../../../kernel/degradation";
3
2
  import type { RuntimeExtensionConfig } from "../../../kernel/extensions";
4
3
  import { withRuntimeLoadTimeout } from "../../../kernel/runtime-load";
5
4
  import type { RuntimeExtensionContribution } from "../../../runtime-definition";
5
+ export { createR2SkillSource } from "./r2-skill-source";
6
6
 
7
7
  export interface CloudflareRuntimeExtensionSource {
8
8
  readonly name: string;
@@ -11,29 +11,12 @@ export interface CloudflareRuntimeExtensionSource {
11
11
  readonly sourceHash: string;
12
12
  }
13
13
 
14
- function trimSlashes(value: string): string {
15
- return value.replace(/^\/+/, "").replace(/\/+$/, "");
16
- }
17
-
18
14
  function hex(bytes: ArrayBuffer): string {
19
15
  return [...new Uint8Array(bytes)]
20
16
  .map((byte) => byte.toString(16).padStart(2, "0"))
21
17
  .join("");
22
18
  }
23
19
 
24
- export function createR2SkillSource(
25
- bucket: R2Bucket,
26
- resourceId: string,
27
- contentHash: string,
28
- contentRef: string,
29
- ): SkillSource {
30
- return r2(bucket, {
31
- prefix: `${trimSlashes(contentRef)}/`,
32
- id: `resource-${resourceId}-${contentHash}`,
33
- fingerprint: "metadata",
34
- });
35
- }
36
-
37
20
  export async function readVerifiedR2Text(
38
21
  bucket: R2Bucket,
39
22
  sourceRef: string,
@@ -16,6 +16,7 @@ import { AGENT_TYPES } from "../../../layers/orchestration/subagents/agent-types
16
16
  import type { RuntimeAgentConfigContext } from "../../../runtime-agent-context";
17
17
  import {
18
18
  createBrowserExecutionFactory,
19
+ createWorkspaceCodeExecutionFactory,
19
20
  type RuntimeBrowserBinding,
20
21
  } from "../../../pi/tool/core-host";
21
22
  import {
@@ -62,6 +63,7 @@ const WORKSPACE_TOOL_NAMES = [
62
63
  "grep",
63
64
  "delete",
64
65
  "bash",
66
+ "execute",
65
67
  ] as const;
66
68
 
67
69
  export function workspaceRequired(input: WorkspaceRequirement): boolean {
@@ -113,6 +115,11 @@ export function createPlatformLoader<
113
115
  ): PlatformLoader {
114
116
  let cached: ReturnType<PlatformLoader> | undefined;
115
117
  return () => cached ??= Promise.resolve().then(() => {
118
+ const exports = (context.ctx as unknown as {
119
+ exports: {
120
+ HttpGateway(options: Record<string, never>): Fetcher;
121
+ };
122
+ }).exports;
116
123
  return {
117
124
  loader: context.env.LOADER,
118
125
  // 绑定缺失时整条浏览器能力不进 Platform Port,Tool Surface 因此注册空集。
@@ -128,17 +135,29 @@ export function createPlatformLoader<
128
135
  }),
129
136
  }
130
137
  : {}),
138
+ outbound: () => exports.HttpGateway({}),
131
139
  };
132
140
  });
133
141
  }
134
142
 
135
143
  export async function prepareWorkspace<Env extends Cloudflare.Env>(
144
+ context: RuntimeAgentConfigContext<Env>,
136
145
  workspaceLoader: WorkspaceLoader,
146
+ platformLoader: PlatformLoader,
137
147
  ) {
138
- const workspace = await workspaceLoader();
148
+ const [workspace, platform] = await Promise.all([
149
+ workspaceLoader(),
150
+ platformLoader(),
151
+ ]);
139
152
  if (!workspace.value) return { degradations: workspace.degradations };
140
153
  return {
141
154
  workspace: workspace.value,
155
+ codeExecution: createWorkspaceCodeExecutionFactory({
156
+ ctx: context.ctx,
157
+ loader: platform.loader,
158
+ outbound: platform.outbound(),
159
+ workspace: workspace.value,
160
+ }),
142
161
  degradations: workspace.degradations,
143
162
  };
144
163
  }
@@ -14,6 +14,7 @@ import {
14
14
  sandboxPiToolCandidates,
15
15
  workspacePiToolCandidates,
16
16
  } from "../../../pi/tool/workspace-sandbox";
17
+ import type { RuntimeCodeExecutionFactory } from "../../../kernel/bindings";
17
18
  import type { ToolAssemblyResult } from "../../../runtime-definition";
18
19
  import type { PiToolCandidate } from "../../../pi/tool/compiler";
19
20
 
@@ -21,6 +22,7 @@ export function assembleUniversalAgentTools(options: {
21
22
  hostTools?: readonly PiToolCandidate[];
22
23
  workspace?: WorkspacePort;
23
24
  workspaceRevisions?: WorkspaceRevisionRestorePort;
25
+ codeExecution?: RuntimeCodeExecutionFactory;
24
26
  sandbox?: RuntimeSandboxPort;
25
27
  schedule?: RuntimeSchedulePort;
26
28
  subagents?: RuntimeSubagentPort;
@@ -44,6 +46,7 @@ export function assembleUniversalAgentTools(options: {
44
46
  tools,
45
47
  bindings: {
46
48
  ...(options.workspace ? { workspace: options.workspace } : {}),
49
+ ...(options.codeExecution ? { codeExecution: options.codeExecution } : {}),
47
50
  ...(options.memory ? { memory: options.memory } : {}),
48
51
  ...(options.subagents ? { subagents: options.subagents } : {}),
49
52
  },
package/src/index.ts CHANGED
@@ -115,9 +115,11 @@ export type { PiSkillBinding } from "./pi/tool";
115
115
  export {
116
116
  BROWSER_EXECUTE_TOOL_NAME,
117
117
  browserExecutionPiToolCandidate,
118
+ codeExecutionPiToolCandidate,
118
119
  } from "./pi/tool";
119
120
  export {
120
121
  createBrowserExecutionFactory,
122
+ createWorkspaceCodeExecutionFactory,
121
123
  } from "./pi/tool";
122
124
  export {
123
125
  assemblePiExtensions,
@@ -3,6 +3,7 @@ import type { ScheduleSpec } from "./receipts";
3
3
  import type { RuntimeActivityProjection } from "./state";
4
4
  import type { ExecutionLevel } from "../lib/execution-level";
5
5
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
6
+ import type { PiToolCandidate } from "../pi/tool/compiler";
6
7
  import type {
7
8
  RuntimeEventConfirmation,
8
9
  RuntimeLifecycleFact,
@@ -205,11 +206,11 @@ export interface WorkspacePort {
205
206
  }
206
207
 
207
208
  /**
208
- * 向 Runtime 提供已组装的浏览器 Code Mode 执行能力。
209
+ * 向 Runtime 提供已组装的 Workspace Code Mode 执行能力。
209
210
  *
210
211
  * @remarks
211
- * Browser Host Adapter 负责绑定 Durable Object、Worker Loader 和 Browser Run;
212
- * Tool Surface 只把这个已授权 Port 转成 `browser_execute` Tool。
212
+ * Host Adapter 负责绑定 Durable Object、Worker Loader、网络出口和
213
+ * Workspace;Tool Surface 只把这个已授权 Port 转成 `execute` Tool。
213
214
  */
214
215
  export interface RuntimeCodeExecutionPort {
215
216
  readonly description: string;
@@ -932,15 +933,24 @@ export interface RuntimeProviderPort {
932
933
  * 向 Runtime 提供 Cloudflare 执行平台上的可授权能力。
933
934
  *
934
935
  * @remarks
935
- * Platform Plugin 准备它,Browser 工具、遥测和工具门卫按需使用。
936
+ * Platform Plugin 准备它,Workspace Codemode、Browser 工具、遥测和工具门卫按需使用。
936
937
  *
937
- * Worker Loader Host 选择,使 Dynamic Worker 只获得已授权绑定;术语见 `../index.ts`。
938
+ * Worker Loader 与网络出口由 Host 选择,使 Dynamic Worker 只获得已授权绑定;术语见 `../index.ts`。
938
939
  */
939
940
  export interface RuntimePlatformPort {
940
- /** Browser Code Mode 在创建 Dynamic Worker 执行器时使用的 Worker Loader。 */
941
+ /** Workspace Codemode 在创建 Dynamic Worker 执行器时使用的 Worker Loader。 */
941
942
  loader: WorkerLoader;
942
943
  /** Platform Plugin 存在 Browser Run 绑定时用它生成 `browser_execute`;缺失即整条浏览器 Tool 面不注册。 */
943
944
  browser?: RuntimeBrowserPort;
945
+ /**
946
+ * 为一次 Dynamic Worker 组装取得已限定的网络出口。
947
+ *
948
+ * @remarks
949
+ * Workspace Plugin 创建 Codemode 工具时调用,并把返回的 `Fetcher` 交给 `DynamicWorkerExecutor`。
950
+ *
951
+ * 出口由 Host 通过 Service Binding 等平台边界控制,不使用无约束的 Runtime 全局网络能力。
952
+ */
953
+ outbound: () => Fetcher;
944
954
  /**
945
955
  * 在 Pi 工具真正执行前请 Host 审查本次调用。
946
956
  *
@@ -1009,9 +1019,21 @@ export interface RuntimeSkillSourceBinding {
1009
1019
  *
1010
1020
  * 它不携带业务 ID、Repository、数据库 Key、任意能力注册表或凭据配置;术语见 `../index.ts`。
1011
1021
  */
1022
+ /**
1023
+ * 延迟到最终 Tool Surface 完成后再创建 Code Mode 执行能力。
1024
+ *
1025
+ * @remarks
1026
+ * 工厂与返回 Port 同住 Host 绑定边界;输入直接使用最终 Pi Tool candidates,
1027
+ * 不再经过 Registry 或另一份 Tool 元数据协议。
1028
+ */
1029
+ export interface RuntimeCodeExecutionFactory {
1030
+ create(candidates: readonly PiToolCandidate[]): RuntimeCodeExecutionPort;
1031
+ }
1032
+
1012
1033
  export interface RuntimeBindings {
1013
1034
  provider: RuntimeProviderPort;
1014
1035
  platform: RuntimePlatformPort;
1036
+ codeExecution?: RuntimeCodeExecutionFactory;
1015
1037
  /** Secret-capability binding; implementations must keep Runtime Grant in closure state. */
1016
1038
  gateway?: RuntimeGatewayPort;
1017
1039
  workspace?: WorkspacePort;
package/src/lib/prompt.ts CHANGED
@@ -12,11 +12,18 @@
12
12
  export const PERSONA =
13
13
  "You are a personal assistant agent running on the universal-agent runtime. " +
14
14
  "You can recall user-managed cold memory across sessions and keep working memory within the current Session, " +
15
- "manage files in your workspace, and call the declared Tools available to the current Runtime.";
15
+ "manage files in your workspace, and use execute Code Mode for network requests and tool composition.";
16
16
 
17
17
  // Names the stable runtime environment.
18
18
  export const RUNTIME =
19
- "Runtime: this agent runs on Cloudflare Workers.";
19
+ "Runtime: this agent runs on Cloudflare Workers. When execute is present, its Code Mode Dynamic Worker is your " +
20
+ "instrument — code there runs with outbound network access (fetch), your " +
21
+ "workspace filesystem (state.*), and the tools.* methods listed in its own description. Use it for raw or customized HTTP " +
22
+ "requests, parsing a payload, hitting several known endpoints, or computing over a file. Write plain " +
23
+ "JavaScript — the sandbox evaluates " +
24
+ "it directly, so TypeScript type annotations (`: number`, `as Type`) are a syntax error, and there " +
25
+ "is no Python interpreter or package manager to invoke. Use the JS/Workers equivalent of what you " +
26
+ "would reach for in another ecosystem.";
20
27
 
21
28
  // Shared behavioral contract for the main Agent and bounded sub-agents.
22
29
  export const BEHAVIOR =
@@ -38,23 +45,35 @@ export const PLANNING =
38
45
  "step or a purely conversational reply — just do it.";
39
46
 
40
47
  const TOOL_ROUTING =
41
- "Tools: Call only declared Tools. If a required Tool Schema is not visible, use Provider Tool Search with that exact name. Never invent a Tool name or input field. ";
48
+ "Tools: execute can call only the tools.* methods explicitly listed in its description; that list is exhaustive. Never guess a tools.* method. " +
49
+ "If a required top-level Tool is not visible, do not use execute. Call top-level Tool Search with that exact name, then call the discovered Tool directly. " +
50
+ "codemode.search searches only methods already installed inside execute; it cannot discover deferred top-level Tools. " +
51
+ "Never invent a Tool name or input field.";
42
52
 
43
53
  // Tool-selection guidance mirrors the actual approval and network boundaries.
44
54
  export const TOOLS =
45
55
  TOOL_ROUTING +
56
+ " Relatedness, repetition, or multiple calls never overrides this availability rule. " +
57
+ "When every required Tool is available inside execute, use one execute for repeated or related calls, branching, or repetition. Inside execute, " +
58
+ "use state.* for workspace file operations, tools.* for other host capabilities, a loop for repeated calls, and Promise.all for independent calls. " +
59
+ "Top-level-only Tools remain Direct even when called repeatedly. " +
46
60
  "For web tasks, use web_search for web discovery, current facts, cited research, " +
47
- "and public URL analysis. When sandbox_* tools are present, use that isolated Linux environment for Python/Node, " +
61
+ "and public URL analysis. For web access specifically, use execute Code Mode only for raw or customized network requests, structured " +
62
+ "API calls, or when web_search cannot retrieve the required content; do not use execute for ordinary web " +
63
+ "searches. When execute is absent, use the actually exposed Direct Tools. When sandbox_* tools are present, use that isolated Linux environment for Python/Node, " +
48
64
  "package managers, system commands, builds, tests and background processes; its filesystem is temporary, " +
49
65
  "persistent inputs are copied in automatically on first use, /userspace is read-only, and only explicitly " +
50
- "published /workspace outputs survive. Use run_skill_script only for scripts supplied by an activated Skill and allowed by its policy. " +
66
+ "published /workspace outputs survive. Use Code Mode for computation, multi-step data work, and the " +
67
+ "raw or customized network cases described above; every response and failure it sees is visible to you. " +
51
68
  "bash is a shell over the workspace filesystem only " +
52
69
  "(no network, no system utilities) and is approval-gated — don't reach for it to read files or fetch. " +
70
+ "When execute is present, make related file changes in one execute with state.*. " +
53
71
  "Use read for one existing Workspace file, write to create or replace one file, and edit for one localized change. " +
54
72
  "Use bash only when a single shell workflow must coordinate multiple Workspace files; do not use it for a single-file read, write, or edit. " +
55
73
  "Do not re-plan or explain between consecutive tool calls. When a run is within the last five model turns, stop expanding scope and " +
56
74
  "prioritize verification, saving durable results, and the final response. " +
57
- "When Tool calls are independent, issue them together in one model step. When you reference " +
75
+ "When related Tool calls can run independently, all are available inside execute, and execute is present, run them inside that execute rather than as parallel top-level calls; " +
76
+ "only independent top-level-only Direct Tools should be issued together in one turn. When you reference " +
58
77
  "code, cite it as file_path:line_number.";
59
78
 
60
79
  // A real browser is the only way to observe what a page actually does, and the
@@ -67,9 +86,10 @@ export const TOOLS =
67
86
  // browser tool belongs to that tool's own description, which the model sees if
68
87
  // and only if the tool is registered.
69
88
  export const BROWSER =
70
- "Browser: a real browser observes what a page actually does: " +
89
+ "Browser: a real browser is for the one thing execute cannot do — observe what a page actually does: " +
71
90
  "rendered result, console output, runtime exceptions, failed resource loads, CSP blocks, and state after an " +
72
- "interaction. Use web_search for public discovery and Workspace or Sandbox Tools for files and computation. " +
91
+ "interaction. Everything else stays with execute: computation, files, and ordinary HTTP requests (fetching " +
92
+ "HTML is execute's job, not the browser's). " +
73
93
  "After you write or change a web page in the workspace, open it once with the browser tools you have before " +
74
94
  "you call the work done, and say in your reply what you checked and what you saw. " +
75
95
  "Drive each browser tool the way its own description tells you to; the failure mode they share is coming back " +
@@ -84,8 +104,9 @@ export const BROWSER =
84
104
  // Uploaded-file routing prevents lossy markdown conversions from becoming data sources.
85
105
  export const FILES =
86
106
  "Uploaded files live under /uploads/ in your workspace; convertible formats have a companion " +
87
- "'<file>.md' (structured markdown). Policy: to summarize/quote/search one file, read or grep the .md. " +
88
- "To compute/aggregate/transform (especially csv/xlsx), use the Linux Sandbox to read " +
107
+ "'<file>.md' (structured markdown). Policy: to summarize/quote/search one file, read or grep the .md; when execute is present, " +
108
+ "read or search multiple files through state.* in one execute instead of repeated top-level file Tool calls. " +
109
+ "To compute/aggregate/transform (especially csv/xlsx), write code in execute Code Mode or the Linux Sandbox that reads " +
89
110
  "the ORIGINAL file — converted markdown tables are not for computation, and large spreadsheets may " +
90
111
  "have no .md at all. For formats without a companion .md (e.g. pptx: unzip and read ppt/slides/*.xml; " +
91
112
  "zip archives; unknown types), parse the original in the sandbox with JS.";
@@ -23,7 +23,13 @@ import type { PiToolCandidate } from "../tool/compiler";
23
23
  export interface PiToolSurface {
24
24
  finalize(
25
25
  candidates: readonly PiToolCandidate[],
26
- ): readonly PiToolCandidate[];
26
+ ): FinalizedPiToolSurface;
27
+ }
28
+
29
+ /** Internal finalization output; not a second Tool protocol. */
30
+ export interface FinalizedPiToolSurface {
31
+ readonly candidates: readonly PiToolCandidate[];
32
+ readonly codeExecutionCandidates: readonly PiToolCandidate[];
27
33
  }
28
34
 
29
35
  /** Immutable inputs consumed directly by Pi Agent Core. */
@@ -10,7 +10,7 @@ import type {
10
10
  import type { RuntimeProfile } from "../../kernel/profile";
11
11
  import type { ExecutionLevel } from "../../lib/execution-level";
12
12
  import type { RuntimeSnapshot } from "../../runtime-assembler";
13
- import type { PiRuntimeAssembly } from "../assembly";
13
+ import type { FinalizedPiToolSurface, PiRuntimeAssembly } from "../assembly";
14
14
  import {
15
15
  assemblePiSystemContext,
16
16
  assemblePiExtensions,
@@ -77,7 +77,7 @@ interface PreparedPiAssembly {
77
77
  readonly loaded: readonly PiLoadedExtension[];
78
78
  readonly context: readonly PiExtensionContextContribution[];
79
79
  readonly degradations: PiExtensionAssembly["degradations"];
80
- readonly toolSurface: readonly PiToolCandidate[];
80
+ readonly toolSurface: FinalizedPiToolSurface;
81
81
  }
82
82
 
83
83
  /**
@@ -193,6 +193,7 @@ export interface PreparedPiRuntimeState {
193
193
  readonly snapshot: RuntimeSnapshot;
194
194
  readonly assembly: PreparedPiAssembly;
195
195
  readonly candidates: readonly PiToolCandidate[];
196
+ readonly codeExecutionCandidates: readonly PiToolCandidate[];
196
197
  }
197
198
 
198
199
  // #endregion
@@ -208,6 +209,7 @@ const IDEMPOTENT_TOOL_NAMES = new Set([
208
209
  "bind_resource",
209
210
  "delete",
210
211
  "edit",
212
+ "execute",
211
213
  "find",
212
214
  "get_time",
213
215
  "grep",
@@ -446,7 +448,7 @@ export async function preparePiRuntime(
446
448
  owner: object,
447
449
  ): Promise<PreparedPiRuntime> {
448
450
  const assembly = await preparePiAssembly(options);
449
- const candidates = assembly.toolSurface;
451
+ const candidates = assembly.toolSurface.candidates;
450
452
  return Object.freeze(new PreparedRuntime(
451
453
  describeRuntime(options.snapshot, candidates, assembly.loaded),
452
454
  Object.freeze([
@@ -460,6 +462,9 @@ export async function preparePiRuntime(
460
462
  snapshot: options.snapshot,
461
463
  assembly,
462
464
  candidates: Object.freeze([...candidates]),
465
+ codeExecutionCandidates: Object.freeze([
466
+ ...assembly.toolSurface.codeExecutionCandidates,
467
+ ]),
463
468
  }),
464
469
  ));
465
470
  }