@springbrand/agent-runtime 0.2.0-alpha.19 → 0.2.0-alpha.21

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.
@@ -64,14 +64,137 @@ const workspaceReadParameters = Type.Object({
64
64
  maxLength: 4_096,
65
65
  description: "Absolute Workspace path",
66
66
  }),
67
- offset: Type.Optional(Type.Integer({ minimum: 1 })),
68
- limit: Type.Optional(Type.Integer({ minimum: 1 })),
67
+ offset: Type.Optional(Type.Integer({
68
+ minimum: 1,
69
+ description: "1-indexed line to start from; pass the nextOffset of the previous page",
70
+ })),
71
+ limit: Type.Optional(Type.Integer({
72
+ minimum: 1,
73
+ description: "Maximum lines to return; a page is capped by size regardless",
74
+ })),
69
75
  });
70
76
  const workspaceEditParameters = Type.Object({
71
77
  path: Type.String({ minLength: 1, maxLength: 4_096 }),
72
78
  old_string: Type.String(),
73
79
  new_string: Type.String(),
74
80
  });
81
+
82
+ /**
83
+ * 单页 read 返回的字符上限。
84
+ *
85
+ * 上游 think 的 read 只卡行数(2000)和行宽(2000 字符),二者相乘意味着单次调用
86
+ * 理论上能返回 4 MB。这里补上缺失的总量闸,让一页的大小可预测。
87
+ *
88
+ * MUST ≤ `budget/gate.ts` 的 `STORAGE_LEAF_MAX_CHARS`,否则持久记录会比模型当轮
89
+ * 看到的内容还少;`gate.test.ts` 有断言守着这条关系。
90
+ */
91
+ export const READ_PAGE_MAX_CHARS = 64 * 1024;
92
+
93
+ /** think 给每一行加的 `${lineNo}\t` 前缀。 */
94
+ const NUMBERED_LINE = /^(\d+)\t/;
95
+
96
+ /** 页脚占用的字符预留量;页脚和正文同属一个字符串叶子,必须一起受预算约束。 */
97
+ const MARKER_RESERVE_CHARS = 256;
98
+
99
+ interface ReadPage {
100
+ readonly content?: unknown;
101
+ readonly totalLines?: unknown;
102
+ readonly fromLine?: unknown;
103
+ }
104
+
105
+ /**
106
+ * 把一次 read 收敛成一页,并明确给出下一页的位置。
107
+ *
108
+ * @remarks
109
+ * read 的包装层在拿到上游结果后调用;图片、PDF 和二进制结果原样透传,因为它们
110
+ * 没有行的概念。
111
+ *
112
+ * 行号从内容里回读而不是复用上游的 `toLine`:上游那个字段是按**请求区间**算的,
113
+ * 一旦它自己触发 2000 行截断就会偏大,照抄会让 `nextOffset` 跳过没读到的行。
114
+ *
115
+ * 返回值同时重建模型可见文本和 details,二者必须来自同一份分页结果,否则模型读到
116
+ * 的内容会比 details 记录的多。
117
+ */
118
+ function pageReadResult(
119
+ output: AgentToolResult<unknown>,
120
+ ): AgentToolResult<unknown> {
121
+ const details = output.details;
122
+ if (details === null || typeof details !== "object") return output;
123
+ const page = details as ReadPage;
124
+ if (typeof page.content !== "string") return output;
125
+
126
+ const totalLines = typeof page.totalLines === "number"
127
+ ? page.totalLines
128
+ : undefined;
129
+ const rows = page.content.split("\n").filter((row) => NUMBERED_LINE.test(row));
130
+ if (rows.length === 0) {
131
+ // offset 越过文件末尾时上游返回空内容。空白结果没有任何可操作信息,而模型现在
132
+ // 是被要求自己推进 offset 的,不说清楚它只会换个数再试一次。
133
+ const text = `[no lines at that offset` +
134
+ `${totalLines === undefined ? "" : `; the file has ${totalLines} lines`}]`;
135
+ return {
136
+ ...output,
137
+ content: [{ type: "text", text }],
138
+ details: { ...details, eof: true, ...(totalLines === undefined ? {} : { totalLines }) },
139
+ };
140
+ }
141
+
142
+ // 页脚也要算进预算:它和正文一起构成模型看到的那个字符串叶子,漏算就会正好顶穿
143
+ // STORAGE_LEAF_MAX_CHARS,让持久化再从中间挖掉几十个字符。
144
+ const budget = READ_PAGE_MAX_CHARS - MARKER_RESERVE_CHARS;
145
+ let used = 0;
146
+ let kept = 0;
147
+ for (const row of rows) {
148
+ const next = used + row.length + (kept === 0 ? 0 : 1);
149
+ // 至少留一行,否则单行超限的文件会返回空页,模型无从推进。
150
+ if (kept > 0 && next > budget) break;
151
+ used = next;
152
+ kept += 1;
153
+ }
154
+
155
+ const lastRow = rows[kept - 1] as string;
156
+ const toLine = Number(NUMBERED_LINE.exec(lastRow)?.[1]);
157
+ const eof = totalLines === undefined
158
+ ? kept === rows.length
159
+ : toLine >= totalLines;
160
+ // 上游按 2000 字符截断超长行并就地打标。JSON 溢出产物这类“整块内容挤在一行”的
161
+ // 文件会命中它,此时按行分页永远追不回被砍掉的部分 —— 必须说出来,否则模型会
162
+ // 拿着一个 eof 以为自己读全了。
163
+ const lossy = rows.slice(0, kept).some((row) => row.endsWith("... (truncated)"));
164
+
165
+ const content = rows.slice(0, kept).join("\n");
166
+ const fromLine = typeof page.fromLine === "number"
167
+ ? page.fromLine
168
+ : Number(NUMBERED_LINE.exec(rows[0] as string)?.[1]);
169
+
170
+ // 分页信息 MUST 进 content,不能只放 details:Provider 把 Tool 结果转成 tool_result
171
+ // 时只带 content,details 是给应用和 UI 的(见 pi-ai 的 `convertToolResult`)。
172
+ // 放错地方,模型就看不见下一页在哪,只能把半个文件当成整个文件用。
173
+ const span = `lines ${fromLine}-${toLine} of ${totalLines ?? toLine}`;
174
+ const lossyNote = lossy
175
+ ? " Some lines were longer than 2000 chars and are cut short; " +
176
+ "paging cannot recover them — use grep or bash on this path instead."
177
+ : "";
178
+ const marker = fromLine === 1 && eof && !lossy
179
+ ? ""
180
+ : eof
181
+ ? `\n\n[${span} — end of file.${lossyNote}]`
182
+ : `\n\n[${span} — more remains. ` +
183
+ `Continue with read(path, offset: ${toLine + 1}).${lossyNote}]`;
184
+
185
+ return {
186
+ ...output,
187
+ content: [{ type: "text", text: `${content}${marker}` }],
188
+ details: {
189
+ ...details,
190
+ content,
191
+ fromLine,
192
+ toLine,
193
+ eof,
194
+ ...(eof ? {} : { nextOffset: toLine + 1 }),
195
+ },
196
+ };
197
+ }
75
198
  /**
76
199
  * 把模型给的路径收敛成稳定的闸键。
77
200
  *
@@ -174,7 +297,7 @@ export function workspacePiToolCandidates(
174
297
  );
175
298
  const error = (output.details as { error?: unknown })?.error;
176
299
  if (typeof error === "string") throw new Error(error);
177
- return output;
300
+ return pageReadResult(output);
178
301
  },
179
302
  };
180
303
 
@@ -286,7 +409,16 @@ export function workspacePiToolCandidates(
286
409
  );
287
410
 
288
411
  return [
289
- ...[read, write, edit, list, find, grep, remove].map((tool) => ({
412
+ {
413
+ owner: "workspace",
414
+ requiredExecutionLevel: "safe" as const,
415
+ // read 是大输出外置协议的**出口**:溢出结果的提示语指向它。出口自己再外置,
416
+ // 就会变成 read → 新 artifact → read 的死循环,模型只能靠猜逃出去。
417
+ // 分页上限由 `pageReadResult` 自己保证,不需要外置也不会撑爆一轮。
418
+ outputBudget: { kind: "structure" } as const,
419
+ tool: read,
420
+ },
421
+ ...[write, edit, list, find, grep, remove].map((tool) => ({
290
422
  owner: "workspace",
291
423
  requiredExecutionLevel: "safe" as const,
292
424
  tool,
@@ -54,7 +54,8 @@ import {
54
54
  memoryPiToolCandidate,
55
55
  } from "./pi/tool/base";
56
56
  import {
57
- browserQuickActionPiToolCandidates,
57
+ BROWSER_EXECUTE_TOOL_NAME,
58
+ browserExecutionPiToolCandidate,
58
59
  codeExecutionPiToolCandidate,
59
60
  } from "./pi/tool/core";
60
61
  import { skillPiToolCandidates } from "./pi/tool/skill";
@@ -65,6 +66,7 @@ import {
65
66
  normalizeAgentTelemetryBinding,
66
67
  type AgentTelemetryBinding,
67
68
  } from "./telemetry/contract";
69
+ import { BUILT_IN_RUNTIME_SKILLS } from "./skills";
68
70
 
69
71
  /** Runtime Assembler:校验一份扁平输入并生成可原子提交的 Snapshot。 */
70
72
 
@@ -192,15 +194,23 @@ async function createToolSurface(
192
194
  const visible = (candidate: PiToolCandidate) =>
193
195
  !deny.has(candidate.tool.name) &&
194
196
  allowsTool?.(candidate.tool.name) !== false;
195
- const browserCandidates = input.platform.browser
196
- ? browserQuickActionPiToolCandidates(input.platform.browser)
197
+ // 平台没有浏览器能力时注册空集:宁可没有这个 Tool,也不注册一个必然失败的 Tool 误导模型。
198
+ // `create()` 推迟到确认这个名字真的可见之后,被 deny 的装配不白建一个浏览器连接器。
199
+ // `direct` 使它只走 Direct 调用,不再被并进 `execute` 的工具集——两个 Code Mode 类工具互相嵌套
200
+ // 会让「哪次执行被记录、被重放」变得无法解释,而它们的分工本就由 System Prompt 划清。
201
+ const browserVisible = !deny.has(BROWSER_EXECUTE_TOOL_NAME) &&
202
+ allowsTool?.(BROWSER_EXECUTE_TOOL_NAME) !== false;
203
+ const browserCandidates = input.platform.browser && browserVisible
204
+ ? [{
205
+ ...browserExecutionPiToolCandidate(input.platform.browser.create()),
206
+ direct: true as const,
207
+ }]
197
208
  : [];
198
209
  const baseCandidates = basePiToolCandidates(input.webSearch);
199
210
  const memoryCandidates = input.memory
200
211
  ? [memoryPiToolCandidate(input.memory.port, input.memory.profile)]
201
212
  : [];
202
213
  const scriptCandidates = [
203
- ...browserCandidates,
204
214
  ...input.hostTools,
205
215
  ...baseCandidates,
206
216
  ...memoryCandidates,
@@ -238,6 +248,9 @@ async function createToolSurface(
238
248
  }
239
249
 
240
250
  const finalized = [...tools.values()];
251
+ const directVisible = finalized.filter(
252
+ (candidate) => !candidate.codeExecutionOnly,
253
+ );
241
254
  if (tools.has("execute")) {
242
255
  throw new Error("SpringBrand reserved Runtime Tool name: execute");
243
256
  }
@@ -246,7 +259,7 @@ async function createToolSurface(
246
259
  deny.has("execute") ||
247
260
  allowsTool?.("execute") === false
248
261
  ) {
249
- return Object.freeze(finalized);
262
+ return Object.freeze(directVisible);
250
263
  }
251
264
  const mergeable = finalized.filter(
252
265
  (candidate) =>
@@ -264,14 +277,22 @@ async function createToolSurface(
264
277
  ),
265
278
  codeExecutionTools,
266
279
  }),
267
- ...finalized,
280
+ ...directVisible,
268
281
  ]);
269
282
  },
270
283
  }),
271
284
  extensions: input.extensions.filter(
272
285
  (extension) => allowsExtension?.(extension) !== false,
273
286
  ),
274
- degradations: [],
287
+ // 浏览器是可选能力:平台没给就只记诊断,不让整个装配失败。
288
+ // 被 policy deny 不算降级——那是有人主动关的,不是平台缺件。
289
+ degradations: input.platform.browser
290
+ ? []
291
+ : [{
292
+ capability: "browser" as const,
293
+ reason: "unavailable" as const,
294
+ detail: "browser_binding_missing",
295
+ }],
275
296
  };
276
297
  }
277
298
 
@@ -827,7 +848,10 @@ export async function assembleRuntimeSnapshot<
827
848
  ? { subagents: toolAssembly.bindings.subagents }
828
849
  : {}),
829
850
  hostTools,
830
- skills: resources.skills,
851
+ skills:
852
+ input.ctx.role === "primary" && toolAssembly.bindings?.workspace
853
+ ? [...BUILT_IN_RUNTIME_SKILLS, ...resources.skills]
854
+ : resources.skills,
831
855
  connectors: resources.connectors.servers,
832
856
  ...(resources.connectors.gateway
833
857
  ? { gateway: resources.connectors.gateway }
package/src/runtime.ts CHANGED
@@ -1076,11 +1076,7 @@ export abstract class AgentRuntimeKernel<
1076
1076
  const running = this.db.submissions.findRunning() as StoredSubmission | null;
1077
1077
  if (running) {
1078
1078
  const effect = this.decidePiRecovery(running).effect;
1079
- if (
1080
- effect.kind === "wait" &&
1081
- (effect.reason === "approval" ||
1082
- effect.reason === "uncertain-tool")
1083
- ) {
1079
+ if (effect.kind === "wait" && effect.reason === "approval") {
1084
1080
  return;
1085
1081
  }
1086
1082
  }
@@ -1894,7 +1890,7 @@ export abstract class AgentRuntimeKernel<
1894
1890
  const submitted = await this.submissions.submit(
1895
1891
  this.submissionInput(userMessage, options),
1896
1892
  );
1897
- await this.drainRuntimeEvents();
1893
+ this.ctx.waitUntil(this.drainRuntimeEvents());
1898
1894
  await this.broadcastApprovals();
1899
1895
  return submitted;
1900
1896
  }
@@ -2496,7 +2492,7 @@ export abstract class AgentRuntimeKernel<
2496
2492
  // - undefined:续跑里程碑已暂存待派发,`dispatchPendingContinuations`
2497
2493
  // 会来接手 —— 判它失败等于杀掉一轮本来有人管的 Turn。
2498
2494
  // - uncertain-tool:**没有任何人会来解开**。非幂等工具结果不确定,恢复
2499
- // 拒绝重放,`onStart` 也显式跳过 —— 停在这里就是永久挂死。
2495
+ // 拒绝重放;停在这里就是永久挂死。
2500
2496
  return decision.effect.reason === "uncertain-tool"
2501
2497
  ? {
2502
2498
  kind: "unresumable",
@@ -2772,9 +2768,11 @@ export abstract class AgentRuntimeKernel<
2772
2768
  submission = await this.activateQueuedSubmission(submission);
2773
2769
  }
2774
2770
  const recoveryAdapter = this.createSubmissionExecutionAdapter(submission);
2771
+ let awaitingHumanInput = false;
2775
2772
  // 让步续跑跳过整套里程碑重放:这一片是主动让出的,transcript 完好、没有半路的
2776
2773
  // Tool 要重建。走恢复通道不只是白跑,重放成本还随让步次数增长 —— 第 N 片要
2777
- // 重放前 N-1 片积累的全部里程碑。
2774
+ // 重放前 N-1 片积累的全部里程碑。但调度回调可能跨过等人的停靠期,所以下面仍需检查
2775
+ // 恢复决策,不能把尾部 assistant Tool Use 直接交给 Pi 续跑。
2778
2776
  if (mode === "recovery") {
2779
2777
  const deactivatePreparation = this.submissions.activate(submission, {
2780
2778
  submissionId,
@@ -2817,7 +2815,11 @@ export abstract class AgentRuntimeKernel<
2817
2815
  );
2818
2816
  }
2819
2817
  } else {
2820
- await this.materializeRecoveredToolResults(submission);
2818
+ const decision = await this.materializeRecoveredToolResults(submission);
2819
+ awaitingHumanInput = mode === "continuation" &&
2820
+ decision.effect.kind === "wait" &&
2821
+ (decision.effect.reason === "approval" ||
2822
+ decision.effect.reason === "interaction");
2821
2823
  }
2822
2824
 
2823
2825
  const ready = this.readSubmission(submissionId);
@@ -2832,6 +2834,7 @@ export abstract class AgentRuntimeKernel<
2832
2834
  ready.abortReason,
2833
2835
  );
2834
2836
  }
2837
+ if (awaitingHumanInput) return ready;
2835
2838
  if (ready.status === "pending") {
2836
2839
  if (this.db.submissions.transition(submissionId, "running", ["pending"])) {
2837
2840
  this.telemetry.capture("turnStarted", {
@@ -0,0 +1,14 @@
1
+ import { fromManifest } from "agents/skills";
2
+ import { SPRINGBRAND_WORKER_WEBSITE_SKILL } from "./springbrand-worker-website";
3
+
4
+ const source = fromManifest({
5
+ id: "springbrand-runtime-built-ins",
6
+ fingerprint: JSON.stringify(SPRINGBRAND_WORKER_WEBSITE_SKILL),
7
+ skills: [SPRINGBRAND_WORKER_WEBSITE_SKILL],
8
+ });
9
+
10
+ export const BUILT_IN_RUNTIME_SKILLS = Object.freeze([Object.freeze({
11
+ name: SPRINGBRAND_WORKER_WEBSITE_SKILL.name,
12
+ description: SPRINGBRAND_WORKER_WEBSITE_SKILL.description,
13
+ source,
14
+ })]);
@@ -0,0 +1,73 @@
1
+ export const SPRINGBRAND_WORKER_WEBSITE_SKILL = {
2
+ name: "springbrand-worker-website",
3
+ description:
4
+ "Use when an Agent creates, restructures, previews, verifies, or deploys a Website in a SpringBrand Workspace; provide the complete Cloudflare Worker project layout, asset conventions, and browser validation workflow.",
5
+ body: [
6
+ "# SpringBrand Worker Website",
7
+ "",
8
+ "Keep the Website in one stable project directory under `/creations/<website-name>/`. Treat that directory as the complete deployable Website project, not as a directory containing only one HTML file.",
9
+ "",
10
+ "## Recommended layout",
11
+ "",
12
+ "When creating a new Website and no existing project structure needs to be preserved, prefer:",
13
+ "",
14
+ "```text",
15
+ "/creations/<website-name>/",
16
+ "├── wrangler.json",
17
+ "├── package.json",
18
+ "├── src/",
19
+ "│ ├── worker.ts",
20
+ "│ ├── routes/",
21
+ "│ ├── lib/",
22
+ "│ └── client/",
23
+ "└── public/",
24
+ " ├── index.html",
25
+ " ├── about/",
26
+ " │ └── index.html",
27
+ " ├── products/",
28
+ " │ └── index.html",
29
+ " ├── assets/",
30
+ " │ ├── css/",
31
+ " │ ├── js/",
32
+ " │ ├── images/",
33
+ " │ ├── fonts/",
34
+ " │ └── icons/",
35
+ " ├── data/",
36
+ " ├── favicon.ico",
37
+ " ├── robots.txt",
38
+ " └── manifest.webmanifest",
39
+ "```",
40
+ "",
41
+ "This is a recommended default, not a required layout. Preserve an existing valid framework or project convention such as `dist/`, `build/`, another Worker entry path, or another static asset directory, and configure Wrangler accordingly.",
42
+ "",
43
+ "## Project rules",
44
+ "",
45
+ "- Keep Worker configuration, package metadata, and build configuration at the Website project root.",
46
+ "- Keep Worker code, routes, application logic, and editable source files under `src/`.",
47
+ "- Keep browser-accessible, deployment-ready static files under `public/`.",
48
+ "- Use `public/index.html` as the recommended home page. Put additional pages under clear routes such as `public/about/index.html` and `public/products/index.html`.",
49
+ "- Put stylesheets, browser scripts, images, fonts, icons, and other static resources under the corresponding `public/assets/` subdirectory.",
50
+ "- Put publicly accessible static data under `public/data/` when needed.",
51
+ "- Keep every file required to build and deploy the Website inside its project directory.",
52
+ "- Do not duplicate the same entry page at both the project root and `public/`.",
53
+ "- Do not place secrets, temporary files, logs, raw tool output, or unprocessed attachments inside the Website project.",
54
+ "",
55
+ "## Worker and browser requirements",
56
+ "",
57
+ "- Configure Wrangler `main` to point to the actual Worker source file.",
58
+ "- Export `class App extends DurableObject` from that module when the Website uses the SpringBrand App Facet.",
59
+ "- Configure `assets.directory` to point to the actual browser asset directory, preferably `public/`.",
60
+ "- Use `./` or `../` for local Website files, never a root-relative Workspace path, so preview and deployed URL prefixes both work.",
61
+ "- Load CSS from HTML instead of importing it from browser JavaScript.",
62
+ "- Without a build step, load browser-native third-party ESM through a full pinned HTTPS URL or an import map to a pinned CORS-enabled CDN.",
63
+ "- With a build step, produce deployment-ready assets before preview instead of depending on a development server.",
64
+ "",
65
+ "## Workflow",
66
+ "",
67
+ "1. Inspect the existing Website directory before changing it and preserve valid conventions.",
68
+ "2. Create or update the complete project rather than only an entry HTML file.",
69
+ "3. Build when the project requires a build step.",
70
+ "4. Preview the Website through the available Workspace or Space capability.",
71
+ "5. When `browser_execute` is available, check runtime errors, failed resources, CSP violations, responsive layout, clipping, overlap, and visual hierarchy. Capture a screenshot when visual appearance matters, fix discovered problems, and verify once more.",
72
+ ].join("\n"),
73
+ };
@@ -1,51 +1,34 @@
1
- import type { ExecutionLevel } from "./lib/execution-level";
2
1
  import type {
3
- RuntimeCodeExecutionPort,
2
+ RuntimeCodeExecutionFactory,
4
3
  RuntimeMemoryPort,
5
4
  RuntimeSubagentPort,
6
5
  WorkspacePort,
7
6
  } from "./kernel/bindings";
8
7
  import type { RuntimeExtensionConfig } from "./kernel/extensions";
9
8
  import type { RuntimeDegradation } from "./kernel/degradation";
9
+ import type {
10
+ ToolContext,
11
+ ToolRegistry,
12
+ ToolSpec,
13
+ } from "./kernel/tool-surface";
10
14
  import type { PiToolCandidate } from "./pi/tool/compiler";
11
15
  import { validateToolArguments } from "@earendil-works/pi-ai";
12
16
 
17
+ // Tool Surface 的形状类型住在 `kernel/tool-surface.ts`:bindings 也要用它们,放在这里
18
+ // 会让 `bindings → tool-registry → bindings` 成环。这里继续导出是为了保持调用方不变。
19
+ export type {
20
+ RuntimeCodeExecutionFactory,
21
+ ToolContext,
22
+ ToolRegistry,
23
+ ToolSpec,
24
+ };
25
+
13
26
  /** Tool Surface 筛选策略(不含 Manifest deny 列表)。 */
14
27
  export interface ToolSurfaceSelectionPolicy {
15
28
  readonly allowsTool?: (name: string) => boolean;
16
29
  readonly allowsExtension?: (extension: RuntimeExtensionConfig) => boolean;
17
30
  }
18
31
 
19
- /** 模型调用 Tool 时传给 `execute` 的上下文。 */
20
- export interface ToolContext {
21
- readonly toolCallId: string;
22
- readonly signal: AbortSignal;
23
- }
24
-
25
- /** Definition 作者声明的一个 Tool。 */
26
- export interface ToolSpec extends Partial<
27
- Omit<PiToolCandidate, "tool" | "requiredExecutionLevel">
28
- > {
29
- readonly label: string;
30
- readonly description: string;
31
- readonly parameters: unknown;
32
- readonly requiredExecutionLevel: ExecutionLevel;
33
- /** Require a fresh human decision for every call, regardless of execution level. */
34
- readonly alwaysRequiresApproval?: boolean;
35
- readonly execute: (
36
- input: unknown,
37
- ctx: ToolContext,
38
- ) => Promise<unknown>;
39
- }
40
-
41
- /** 一次装配声明的全部 Tool,键即模型可见的工具名。 */
42
- export type ToolRegistry = Record<string, ToolSpec>;
43
-
44
- /** 延迟到最终 Tool Surface 完成后再创建 Code Mode 执行能力。 */
45
- export interface RuntimeCodeExecutionFactory {
46
- create(tools: ToolRegistry): RuntimeCodeExecutionPort;
47
- }
48
-
49
32
  export function emptyToolRegistry(): ToolRegistry {
50
33
  return {};
51
34
  }