@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
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.43",
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,
@@ -29,9 +29,6 @@ const READONLY_WORKSPACE_TOOLS = new Set([
29
29
  "grep",
30
30
  ]);
31
31
  const DENIED_TOOLS = [
32
- // SubAgents have no approval channel; keep the restored high-risk Workspace
33
- // shell on the main Agent surface, matching the pre-Pi behavior.
34
- "bash",
35
32
  "run_agent",
36
33
  "subagents",
37
34
  "fanout",
@@ -46,11 +43,11 @@ const executeParameters = Type.Object({
46
43
  description:
47
44
  "JavaScript to run. Return the final value. The sandbox provides fetch and state.*.",
48
45
  }),
49
- });
46
+ }, { additionalProperties: false });
50
47
 
51
48
  const fetchParameters = Type.Object({
52
49
  url: Type.String({ minLength: 1, maxLength: 8_192 }),
53
- });
50
+ }, { additionalProperties: false });
54
51
 
55
52
  function result(details: unknown): AgentToolResult<unknown> {
56
53
  let text: 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 {
@@ -54,14 +55,7 @@ const DEFAULT_MEMORY: RuntimeMemoryProfile = Object.freeze({
54
55
  });
55
56
 
56
57
  const WORKSPACE_TOOL_NAMES = [
57
- "read",
58
- "write",
59
- "edit",
60
- "list",
61
- "find",
62
- "grep",
63
- "delete",
64
- "bash",
58
+ "execute",
65
59
  ] as const;
66
60
 
67
61
  export function workspaceRequired(input: WorkspaceRequirement): boolean {
@@ -113,6 +107,11 @@ export function createPlatformLoader<
113
107
  ): PlatformLoader {
114
108
  let cached: ReturnType<PlatformLoader> | undefined;
115
109
  return () => cached ??= Promise.resolve().then(() => {
110
+ const exports = (context.ctx as unknown as {
111
+ exports: {
112
+ HttpGateway(options: Record<string, never>): Fetcher;
113
+ };
114
+ }).exports;
116
115
  return {
117
116
  loader: context.env.LOADER,
118
117
  // 绑定缺失时整条浏览器能力不进 Platform Port,Tool Surface 因此注册空集。
@@ -128,17 +127,30 @@ export function createPlatformLoader<
128
127
  }),
129
128
  }
130
129
  : {}),
130
+ outbound: () => exports.HttpGateway({}),
131
131
  };
132
132
  });
133
133
  }
134
134
 
135
135
  export async function prepareWorkspace<Env extends Cloudflare.Env>(
136
+ context: RuntimeAgentConfigContext<Env>,
136
137
  workspaceLoader: WorkspaceLoader,
138
+ platformLoader: PlatformLoader,
137
139
  ) {
138
- const workspace = await workspaceLoader();
140
+ const [workspace, platform] = await Promise.all([
141
+ workspaceLoader(),
142
+ platformLoader(),
143
+ ]);
139
144
  if (!workspace.value) return { degradations: workspace.degradations };
140
145
  return {
141
146
  workspace: workspace.value,
147
+ codeExecution: createWorkspaceCodeExecutionFactory({
148
+ ctx: context.ctx,
149
+ loader: platform.loader,
150
+ outbound: platform.outbound(),
151
+ workspace: workspace.value,
152
+ workspaceAccessMode: "write",
153
+ }),
142
154
  degradations: workspace.degradations,
143
155
  };
144
156
  }
@@ -12,8 +12,8 @@ import { schedulePiToolCandidates } from "../../../pi/tool/schedule";
12
12
  import { workspaceRevisionPiToolCandidate } from "../../../pi/tool/workspace-revision";
13
13
  import {
14
14
  sandboxPiToolCandidates,
15
- workspacePiToolCandidates,
16
15
  } from "../../../pi/tool/workspace-sandbox";
16
+ import type { RuntimeCodeExecutionFactory } from "../../../kernel/bindings";
17
17
  import type { ToolAssemblyResult } from "../../../runtime-definition";
18
18
  import type { PiToolCandidate } from "../../../pi/tool/compiler";
19
19
 
@@ -21,6 +21,7 @@ export function assembleUniversalAgentTools(options: {
21
21
  hostTools?: readonly PiToolCandidate[];
22
22
  workspace?: WorkspacePort;
23
23
  workspaceRevisions?: WorkspaceRevisionRestorePort;
24
+ codeExecution?: RuntimeCodeExecutionFactory;
24
25
  sandbox?: RuntimeSandboxPort;
25
26
  schedule?: RuntimeSchedulePort;
26
27
  subagents?: RuntimeSubagentPort;
@@ -32,7 +33,6 @@ export function assembleUniversalAgentTools(options: {
32
33
  }): ToolAssemblyResult {
33
34
  const tools = [
34
35
  ...(options.hostTools ?? []),
35
- ...(options.workspace ? workspacePiToolCandidates(options.workspace) : []),
36
36
  ...(options.workspaceRevisions
37
37
  ? [workspaceRevisionPiToolCandidate(options.workspaceRevisions)]
38
38
  : []),
@@ -44,6 +44,7 @@ export function assembleUniversalAgentTools(options: {
44
44
  tools,
45
45
  bindings: {
46
46
  ...(options.workspace ? { workspace: options.workspace } : {}),
47
+ ...(options.codeExecution ? { codeExecution: options.codeExecution } : {}),
47
48
  ...(options.memory ? { memory: options.memory } : {}),
48
49
  ...(options.subagents ? { subagents: options.subagents } : {}),
49
50
  },
@@ -2,6 +2,7 @@ import type {
2
2
  WorkspaceAdminPort,
3
3
  WorkspaceFileInfo,
4
4
  WorkspacePort,
5
+ WorkspaceStreamWriteOptions,
5
6
  WorkspaceQuota,
6
7
  WorkspaceUsage,
7
8
  } from "../../../kernel/bindings";
@@ -216,6 +217,18 @@ export class ScopedWorkspace implements WorkspacePort, WorkspaceAdminPort {
216
217
  );
217
218
  }
218
219
 
220
+ async writeFileStream(
221
+ path: string,
222
+ content: ReadableStream<Uint8Array>,
223
+ options?: WorkspaceStreamWriteOptions,
224
+ ) {
225
+ return this.parent.writeFileStream(
226
+ await this.guardedPhysical(path, true),
227
+ content,
228
+ options,
229
+ );
230
+ }
231
+
219
232
  async writeFileBytesIfUnchanged(
220
233
  path: string,
221
234
  data: Uint8Array,
@@ -363,6 +376,8 @@ export function createWorkspacePortFacade(
363
376
  workspace.writeFile(path, content, mimeType),
364
377
  writeFileBytes: (path, data, mimeType) =>
365
378
  workspace.writeFileBytes(path, data, mimeType),
379
+ writeFileStream: (path, content, options) =>
380
+ workspace.writeFileStream(path, content, options),
366
381
  appendFile: (path, content, mimeType) =>
367
382
  workspace.appendFile(path, content, mimeType),
368
383
  exists: (path) => workspace.exists(path),
package/src/index.ts CHANGED
@@ -93,6 +93,7 @@ export type {
93
93
  CompilePiToolsOptions,
94
94
  PiToolCandidate,
95
95
  SettledPiToolCall,
96
+ ToolExposureMode,
96
97
  } from "./pi/tool";
97
98
  export { basePiToolCandidates } from "./pi/tool";
98
99
  export { createPiDeclaredToolCandidate } from "./pi/tool";
@@ -115,9 +116,11 @@ export type { PiSkillBinding } from "./pi/tool";
115
116
  export {
116
117
  BROWSER_EXECUTE_TOOL_NAME,
117
118
  browserExecutionPiToolCandidate,
119
+ codeExecutionPiToolCandidate,
118
120
  } from "./pi/tool";
119
121
  export {
120
122
  createBrowserExecutionFactory,
123
+ createWorkspaceCodeExecutionFactory,
121
124
  } from "./pi/tool";
122
125
  export {
123
126
  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,
@@ -91,6 +92,11 @@ export interface WorkspacePort {
91
92
  data: Uint8Array | ArrayBuffer,
92
93
  mimeType?: string,
93
94
  ): Promise<void>;
95
+ writeFileStream(
96
+ path: string,
97
+ content: ReadableStream<Uint8Array>,
98
+ options?: WorkspaceStreamWriteOptions,
99
+ ): Promise<{ path: string; bytes: number }>;
94
100
  /**
95
101
  * 把文本追加到文件末尾。
96
102
  *
@@ -204,12 +210,17 @@ export interface WorkspacePort {
204
210
  glob(pattern: string): Promise<WorkspaceFileInfo[]>;
205
211
  }
206
212
 
213
+ export interface WorkspaceStreamWriteOptions {
214
+ contentLength?: number;
215
+ mediaType?: string;
216
+ }
217
+
207
218
  /**
208
- * 向 Runtime 提供已组装的浏览器 Code Mode 执行能力。
219
+ * 向 Runtime 提供已组装的 Workspace Code Mode 执行能力。
209
220
  *
210
221
  * @remarks
211
- * Browser Host Adapter 负责绑定 Durable Object、Worker Loader 和 Browser Run;
212
- * Tool Surface 只把这个已授权 Port 转成 `browser_execute` Tool。
222
+ * Host Adapter 负责绑定 Durable Object、Worker Loader、网络出口和
223
+ * Workspace;Tool Surface 只把这个已授权 Port 转成 `execute` Tool。
213
224
  */
214
225
  export interface RuntimeCodeExecutionPort {
215
226
  readonly description: string;
@@ -932,15 +943,24 @@ export interface RuntimeProviderPort {
932
943
  * 向 Runtime 提供 Cloudflare 执行平台上的可授权能力。
933
944
  *
934
945
  * @remarks
935
- * Platform Plugin 准备它,Browser 工具、遥测和工具门卫按需使用。
946
+ * Platform Plugin 准备它,Workspace Codemode、Browser 工具、遥测和工具门卫按需使用。
936
947
  *
937
- * Worker Loader Host 选择,使 Dynamic Worker 只获得已授权绑定;术语见 `../index.ts`。
948
+ * Worker Loader 与网络出口由 Host 选择,使 Dynamic Worker 只获得已授权绑定;术语见 `../index.ts`。
938
949
  */
939
950
  export interface RuntimePlatformPort {
940
- /** Browser Code Mode 在创建 Dynamic Worker 执行器时使用的 Worker Loader。 */
951
+ /** Workspace Codemode 在创建 Dynamic Worker 执行器时使用的 Worker Loader。 */
941
952
  loader: WorkerLoader;
942
953
  /** Platform Plugin 存在 Browser Run 绑定时用它生成 `browser_execute`;缺失即整条浏览器 Tool 面不注册。 */
943
954
  browser?: RuntimeBrowserPort;
955
+ /**
956
+ * 为一次 Dynamic Worker 组装取得已限定的网络出口。
957
+ *
958
+ * @remarks
959
+ * Workspace Plugin 创建 Codemode 工具时调用,并把返回的 `Fetcher` 交给 `DynamicWorkerExecutor`。
960
+ *
961
+ * 出口由 Host 通过 Service Binding 等平台边界控制,不使用无约束的 Runtime 全局网络能力。
962
+ */
963
+ outbound: () => Fetcher;
944
964
  /**
945
965
  * 在 Pi 工具真正执行前请 Host 审查本次调用。
946
966
  *
@@ -1009,9 +1029,21 @@ export interface RuntimeSkillSourceBinding {
1009
1029
  *
1010
1030
  * 它不携带业务 ID、Repository、数据库 Key、任意能力注册表或凭据配置;术语见 `../index.ts`。
1011
1031
  */
1032
+ /**
1033
+ * 延迟到最终 Tool Surface 完成后再创建 Code Mode 执行能力。
1034
+ *
1035
+ * @remarks
1036
+ * 工厂与返回 Port 同住 Host 绑定边界;输入直接使用最终 Pi Tool candidates,
1037
+ * 不再经过 Registry 或另一份 Tool 元数据协议。
1038
+ */
1039
+ export interface RuntimeCodeExecutionFactory {
1040
+ create(candidates: readonly PiToolCandidate[]): RuntimeCodeExecutionPort;
1041
+ }
1042
+
1012
1043
  export interface RuntimeBindings {
1013
1044
  provider: RuntimeProviderPort;
1014
1045
  platform: RuntimePlatformPort;
1046
+ codeExecution?: RuntimeCodeExecutionFactory;
1015
1047
  /** Secret-capability binding; implementations must keep Runtime Grant in closure state. */
1016
1048
  gateway?: RuntimeGatewayPort;
1017
1049
  workspace?: WorkspacePort;
@@ -173,13 +173,13 @@ export async function spillDurableToolOutput(
173
173
  // 出口指令必须是可执行的,而且必须只承诺模型真的拿得到的东西:Provider 只把
174
174
  // Tool 结果的 content 发给模型,所以这里不能引用只存在于 details 的字段。
175
175
  // 溢出产物是 JSON,一个大字符串叶子会整块挤在一行上,而按行分页追不回被行宽
176
- // 截断的内容 —— 那种情况要走 grep / bash,不能让模型以为 read 一定够用。
176
+ // 截断的内容 —— 那种情况要走 state.searchText,不能让模型以为分页读取一定够用。
177
177
  note:
178
178
  "Output was large and has been saved to the Workspace file above. " +
179
- "Read it in pages with read(path, offset, limit) — do not read it whole; " +
179
+ "Read it in pages inside execute with state.readFile — do not read it whole; " +
180
180
  "each page ends with a footer telling you the line range and the next offset. " +
181
181
  "This file is JSON, so a single large value can sit on one very long line: " +
182
- "if a page reports that lines were cut short, use grep or bash on the path instead.",
182
+ "if a page reports that lines were cut short, use state.searchText on the path instead.",
183
183
  };
184
184
  } catch {
185
185
  return null;
@@ -83,6 +83,8 @@ export function createTemporaryAgentWorkspace(
83
83
  // 原因:二进制写入必须与文本写入使用同一 Memory 禁止,不能留下第二条写入路径。
84
84
  writeFileBytes: async (path, data, mimeType) =>
85
85
  workspace.writeFileBytes(allowed(path), data, mimeType),
86
+ writeFileStream: async (path, content, options) =>
87
+ workspace.writeFileStream(allowed(path), content, options),
86
88
  // 作用:在一个允许的文件末尾追加文本。
87
89
  // 调用:临时 Agent 需要增量写入时通过 Workspace Tool 调用。
88
90
  // 原因:追加也是写操作,必须在委托前拦截 Memory 路径。
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,31 @@ 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, and sequential calls for repeated or durable operations. " +
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. " +
51
- "bash is a shell over the workspace filesystem only " +
52
- "(no network, no system utilities) and is approval-gated don't reach for it to read files or fetch. " +
53
- "Use read for one existing Workspace file, write to create or replace one file, and edit for one localized change. " +
54
- "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. " +
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. " +
68
+ "Every Workspace filesystem operation goes through execute and state.*; no Workspace file methods exist under tools.*. " +
55
69
  "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
70
  "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 " +
71
+ "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; " +
72
+ "only independent top-level-only Direct Tools should be issued together in one turn. When you reference " +
58
73
  "code, cite it as file_path:line_number.";
59
74
 
60
75
  // A real browser is the only way to observe what a page actually does, and the
@@ -67,9 +82,10 @@ export const TOOLS =
67
82
  // browser tool belongs to that tool's own description, which the model sees if
68
83
  // and only if the tool is registered.
69
84
  export const BROWSER =
70
- "Browser: a real browser observes what a page actually does: " +
85
+ "Browser: a real browser is for the one thing execute cannot do — observe what a page actually does: " +
71
86
  "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. " +
87
+ "interaction. Everything else stays with execute: computation, files, and ordinary HTTP requests (fetching " +
88
+ "HTML is execute's job, not the browser's). " +
73
89
  "After you write or change a web page in the workspace, open it once with the browser tools you have before " +
74
90
  "you call the work done, and say in your reply what you checked and what you saw. " +
75
91
  "Drive each browser tool the way its own description tells you to; the failure mode they share is coming back " +
@@ -84,8 +100,8 @@ export const BROWSER =
84
100
  // Uploaded-file routing prevents lossy markdown conversions from becoming data sources.
85
101
  export const FILES =
86
102
  "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 " +
103
+ "'<file>.md' (structured markdown). Policy: to summarize, quote, or search files, use state.* inside execute. " +
104
+ "To compute/aggregate/transform (especially csv/xlsx), write code in execute Code Mode or the Linux Sandbox that reads " +
89
105
  "the ORIGINAL file — converted markdown tables are not for computation, and large spreadsheets may " +
90
106
  "have no .md at all. For formats without a companion .md (e.g. pptx: unzip and read ppt/slides/*.xml; " +
91
107
  "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. */