@springbrand/agent-runtime 0.1.3-alpha.1 → 0.1.3-alpha.3

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.
@@ -1,12 +1,12 @@
1
1
  import {
2
- createWorkspaceStateBackend,
3
- type FileSystemStateBackend,
4
- type WorkspaceFsLike,
5
- } from "@cloudflare/shell";
6
- import {
2
+ createBashTool,
7
3
  createDeleteTool,
4
+ createEditTool,
8
5
  createFindTool,
6
+ createGrepTool,
9
7
  createListTool,
8
+ createReadTool,
9
+ createWriteTool,
10
10
  } from "@cloudflare/think/tools/workspace";
11
11
  import type {
12
12
  AgentTool,
@@ -54,15 +54,6 @@ function running(
54
54
 
55
55
  // #region Workspace tools
56
56
 
57
- const workspaceWriteParameters = Type.Object({
58
- path: Type.String({
59
- minLength: 1,
60
- maxLength: 4_096,
61
- description: "Absolute Workspace path",
62
- }),
63
- content: Type.String({ description: "Content to write" }),
64
- });
65
-
66
57
  const workspaceReadParameters = Type.Object({
67
58
  path: Type.String({
68
59
  minLength: 1,
@@ -77,32 +68,6 @@ const workspaceEditParameters = Type.Object({
77
68
  old_string: Type.String(),
78
69
  new_string: Type.String(),
79
70
  });
80
- const workspaceGrepParameters = Type.Object({
81
- query: Type.String(),
82
- include: Type.Optional(Type.String({ minLength: 1, maxLength: 4_096 })),
83
- fixedString: Type.Optional(Type.Boolean()),
84
- caseSensitive: Type.Optional(Type.Boolean()),
85
- contextLines: Type.Optional(Type.Integer({ minimum: 0, maximum: 10 })),
86
- });
87
-
88
- const MAX_READ_LINES = 2_000;
89
- const MAX_READ_LINE_CHARS = 2_000;
90
- const MAX_GREP_MATCHES = 200;
91
-
92
- // 作用:计算一个非空字符串在文本中出现了多少次。
93
- // 调用:edit 在精确替换前调用,空 old_string 已由创建文件分支提前处理。
94
- // 原因:只允许唯一命中可避免模型给出短片段时误改多个位置。
95
- function countOccurrences(text: string, search: string): number {
96
- let count = 0;
97
- let position = 0;
98
- while (true) {
99
- const index = text.indexOf(search, position);
100
- if (index === -1) return count;
101
- count += 1;
102
- position = index + 1;
103
- }
104
- }
105
-
106
71
  /**
107
72
  * 把模型给的路径收敛成稳定的闸键。
108
73
  *
@@ -176,164 +141,97 @@ function serializeByPath<T>(
176
141
  *
177
142
  * 本文件中 “Workspace” 指会话作用域的持久文件视图,“Port” 指 Runtime 只依赖
178
143
  * 的窄文件接口,“候选工具”指进入 Pi 编译和统一治理前的描述。read、write、
179
- * edit、grep 在这里保留现有结果契约;list、find、delete 复用 @cloudflare/think
180
- * 的上游工厂,避免维护第二套同义文件操作。
144
+ * edit、list、find、grep、delete 复用 @cloudflare/think 的上游工厂。
181
145
  *
182
146
  * `Pi`、`Port` 与“候选工具”等项目核心术语见 `../../index.ts`。
183
147
  */
184
148
  export function workspacePiToolCandidates(
185
149
  workspace: WorkspacePort,
186
150
  ): PiToolCandidate[] {
187
- let backend: FileSystemStateBackend | undefined;
188
- // 作用:按需创建并复用 @cloudflare/shell 的 Workspace 搜索后端。
189
- // 调用:grep execute 第一次搜索及后续搜索时调用。
190
- // 原因:只有 grep 需要该适配器,
191
- // 延迟创建可避免普通读写 Turn 支付无用初始化成本。
192
- const searchBackend = (): FileSystemStateBackend => {
193
- backend ??= createWorkspaceStateBackend(
194
- workspace as unknown as WorkspaceFsLike,
195
- );
196
- return backend;
197
- };
151
+ const readTool = aiToolToPi(
152
+ "read",
153
+ createReadTool({ ops: workspace }),
154
+ { label: "Read file" },
155
+ );
198
156
  const read: AgentTool<typeof workspaceReadParameters> = {
199
- name: "read",
200
- label: "Read file",
201
- description:
202
- "Read a Workspace text file with line numbers. Use offset and limit for large files.",
157
+ name: readTool.name,
158
+ label: readTool.label,
159
+ description: readTool.description,
203
160
  parameters: workspaceReadParameters,
204
- // 作用:分段读取一个 Workspace 文本文件,并返回带行号的内容。
205
- // 调用:Pi 需要查看文件或为后续精确编辑取上下文时调用。
206
- // 原因:先核对文件类型,再限制行数和单行长度,
207
- // 避免一次结果挤满模型上下文。
208
- async execute(_toolCallId, { path, offset, limit }, signal, onUpdate) {
161
+ async execute(toolCallId, params, signal, onUpdate) {
209
162
  signal?.throwIfAborted();
210
163
  running(onUpdate, "Reading the Workspace file.");
211
- const info = await workspace.stat(path);
212
- if (!info) throw new Error(`File not found: ${path}`);
213
- if (info.type !== "file") {
214
- throw new Error(`${path} is not a file`);
215
- }
216
- const content = await workspace.readFile(path);
217
- if (content === null) {
218
- throw new Error(`Could not read file: ${path}`);
219
- }
220
- const lines = content.split("\n");
221
- const start = (offset ?? 1) - 1;
222
- const requestedEnd = limit === undefined ? lines.length : start + limit;
223
- const end = Math.min(requestedEnd, start + MAX_READ_LINES);
224
- const numbered = lines.slice(start, end).map((line, index) => {
225
- const visible =
226
- line.length > MAX_READ_LINE_CHARS
227
- ? `${line.slice(0, MAX_READ_LINE_CHARS)}... (truncated)`
228
- : line;
229
- return `${start + index + 1}\t${visible}`;
230
- });
231
- return result({
232
- path,
233
- content: numbered.join("\n"),
234
- totalLines: lines.length,
235
- fromLine: start + 1,
236
- toLine: Math.min(end, lines.length),
237
- ...(requestedEnd > end ? { truncated: true } : {}),
238
- });
164
+ const output = await readTool.execute(
165
+ toolCallId,
166
+ params,
167
+ signal,
168
+ onUpdate,
169
+ );
170
+ const error = (output.details as { error?: unknown })?.error;
171
+ if (typeof error === "string") throw new Error(error);
172
+ return output;
239
173
  },
240
174
  };
241
175
 
242
- const write: AgentTool<typeof workspaceWriteParameters> = {
243
- name: "write",
244
- label: "Write file",
245
- description:
246
- "Write content to a file in this Chat's scoped Workspace. Parent directories are created automatically.",
247
- parameters: workspaceWriteParameters,
248
- // 作用:把完整文本写入 Workspace 文件,并按需创建父目录。
249
- // 调用:Pi 明确要创建或覆盖文件时调用。
250
- // 原因:目录创建和写入都经同一个 WorkspacePort,
251
- // 不能绕过会话路径与配额策略。
252
- async execute(_toolCallId, { path, content }, signal, onUpdate) {
176
+ const writeTool = aiToolToPi(
177
+ "write",
178
+ createWriteTool({ ops: workspace }),
179
+ { label: "Write file" },
180
+ );
181
+ const write: AgentTool<any, unknown> = {
182
+ ...writeTool,
183
+ async execute(toolCallId, params, signal, onUpdate) {
184
+ const path = (params as { path?: unknown }).path;
185
+ if (typeof path !== "string") {
186
+ return writeTool.execute(toolCallId, params, signal, onUpdate);
187
+ }
253
188
  signal?.throwIfAborted();
254
189
  running(onUpdate, "Writing the Workspace file.");
255
190
 
256
191
  return serializeByPath(workspace, path, async () => {
257
192
  // 排队期间可能已经被取消,拿到闸之后必须重新确认,否则会写一个已放弃的结果。
258
193
  signal?.throwIfAborted();
259
- const separator = path.lastIndexOf("/");
260
- const parent = separator > 0 ? path.slice(0, separator) : "";
261
- if (parent && parent !== "/") {
262
- await workspace.mkdir(parent, { recursive: true });
263
- }
264
- await workspace.writeFile(path, content);
265
-
266
- return result({
267
- path,
268
- bytesWritten: new TextEncoder().encode(content).byteLength,
269
- lines: content.split("\n").length,
270
- });
194
+ return writeTool.execute(
195
+ toolCallId,
196
+ params,
197
+ signal,
198
+ onUpdate,
199
+ );
271
200
  });
272
201
  },
273
202
  };
274
203
 
204
+ const editTool = aiToolToPi(
205
+ "edit",
206
+ createEditTool({ ops: workspace }),
207
+ { label: "Edit file" },
208
+ );
275
209
  const edit: AgentTool<typeof workspaceEditParameters> = {
276
- name: "edit",
277
- label: "Edit file",
278
- description:
279
- "Replace one exact string in a Workspace file. An empty old_string creates a new file.",
210
+ name: editTool.name,
211
+ label: editTool.label,
212
+ description: editTool.description,
280
213
  parameters: workspaceEditParameters,
281
- // 作用:唯一命中时精确替换文本,或用空 old_string 创建新文件。
282
- // 调用:Pi 已读取文件并能提供足够上下文时调用。
283
- // 原因:零命中和多命中都拒绝写入,
284
- // 防止过短片段静默改错位置。
285
- async execute(
286
- _toolCallId,
287
- { path, old_string, new_string },
288
- signal,
289
- onUpdate,
290
- ) {
214
+ async execute(toolCallId, params, signal, onUpdate) {
291
215
  signal?.throwIfAborted();
292
216
  running(onUpdate, "Editing the Workspace file.");
293
217
  // 读-比对-写整段都在闸内:闸只包住最后那次 writeFile 的话,基准内容仍然可能在
294
218
  // 比对之后被别的调用换掉,丢写照旧发生。
295
- return serializeByPath(workspace, path, async () => {
219
+ return serializeByPath(workspace, params.path, async () => {
296
220
  signal?.throwIfAborted();
297
- const content = await workspace.readFile(path);
298
- if (old_string === "") {
299
- if (content !== null) {
300
- throw new Error(
301
- "File already exists. Provide old_string to edit, or use write to overwrite.",
302
- );
303
- }
304
- await workspace.writeFile(path, new_string);
305
- return result({
306
- path,
307
- created: true,
308
- lines: new_string.split("\n").length,
309
- });
310
- }
311
- if (content === null) {
312
- throw new Error(`File not found: ${path}`);
313
- }
314
- const occurrences = countOccurrences(content, old_string);
315
- if (occurrences === 0) {
316
- throw new Error(
317
- "old_string not found in file. Read the file first and match whitespace exactly.",
318
- );
319
- }
320
- if (occurrences > 1) {
321
- throw new Error(
322
- `old_string appears ${occurrences} times. Include more surrounding context.`,
323
- );
324
- }
325
- const next = content.replace(old_string, new_string);
326
- await workspace.writeFile(path, next);
327
- return result({
328
- path,
329
- replaced: true,
330
- lines: next.split("\n").length,
331
- });
221
+ const output = await editTool.execute(
222
+ toolCallId,
223
+ params,
224
+ signal,
225
+ onUpdate,
226
+ );
227
+ const error = (output.details as { error?: unknown })?.error;
228
+ if (typeof error === "string") throw new Error(error);
229
+ return output;
332
230
  });
333
231
  },
334
232
  };
335
233
 
336
- // list / find / delete come from the upstream think factories: WorkspacePort
234
+ // list / find / grep / delete come from the upstream think factories: WorkspacePort
337
235
  // already satisfies their ops interfaces structurally, and their output shapes
338
236
  // match what this file used to build by hand.
339
237
  const list = aiToolToPi("list", createListTool({ ops: workspace }), {
@@ -344,95 +242,9 @@ export function workspacePiToolCandidates(
344
242
  label: "Find files",
345
243
  });
346
244
 
347
- const grep: AgentTool<typeof workspaceGrepParameters> = {
348
- name: "grep",
245
+ const grep = aiToolToPi("grep", createGrepTool({ ops: workspace }), {
349
246
  label: "Search files",
350
- description:
351
- "Search bounded Workspace file contents with a regex or fixed string.",
352
- parameters: workspaceGrepParameters,
353
- // 作用:在匹配 glob 的 Workspace 文件中做有上限的文本搜索。
354
- // 调用:Pi 需要定位定义、调用方或内容片段时调用。
355
- // 原因:搜索交给 @cloudflare/shell 后端并限制命中数;
356
- // 正则先在本地检查以保留既有错误契约。
357
- async execute(
358
- _toolCallId,
359
- {
360
- query,
361
- include = "**/*",
362
- fixedString = false,
363
- caseSensitive = false,
364
- contextLines = 0,
365
- },
366
- signal,
367
- onUpdate,
368
- ) {
369
- signal?.throwIfAborted();
370
- running(onUpdate, "Searching Workspace files.");
371
- // Reject bad patterns before reaching the backend so the error text stays
372
- // identical to the previous in-isolate implementation.
373
- if (!fixedString) {
374
- try {
375
- new RegExp(query);
376
- } catch {
377
- throw new Error(`Invalid regex: ${query}`);
378
- }
379
- }
380
-
381
- // TODO(待确认):@cloudflare/shell@0.4.3 的 searchFiles 会先 glob,再逐个
382
- // 调 readFile;WorkspacePort 跨 DO 时,内容可能仍会经 RPC 回到当前 isolate。
383
- const hits = await searchBackend().searchFiles(include, query, {
384
- regex: !fixedString,
385
- caseSensitive,
386
- maxMatches: MAX_GREP_MATCHES,
387
- ...(contextLines > 0
388
- ? { contextBefore: contextLines, contextAfter: contextLines }
389
- : {}),
390
- });
391
- signal?.throwIfAborted();
392
-
393
- const matches: Array<string | {
394
- file: string;
395
- line: number;
396
- context: string;
397
- }> = [];
398
- let filesWithMatches = 0;
399
-
400
- for (const hit of hits) {
401
- if (matches.length >= MAX_GREP_MATCHES) break;
402
- if (hit.matches.length > 0) filesWithMatches += 1;
403
- for (const match of hit.matches) {
404
- if (matches.length >= MAX_GREP_MATCHES) break;
405
- if (contextLines === 0) {
406
- matches.push(`${hit.path}:${match.line}: ${match.lineText}`);
407
- continue;
408
- }
409
- const before = match.beforeLines ?? [];
410
- const after = match.afterLines ?? [];
411
- const firstLine = match.line - before.length;
412
- const context = [...before, match.lineText, ...after]
413
- .map(
414
- (line, offset) =>
415
- `${firstLine + offset === match.line ? ">" : " "} ${
416
- firstLine + offset
417
- }\t${line}`,
418
- )
419
- .join("\n");
420
- matches.push({ file: hit.path, line: match.line, context });
421
- }
422
- }
423
-
424
- return result({
425
- query,
426
- filesSearched: hits.length,
427
- filesWithMatches,
428
- totalMatches: matches.length,
429
- matches,
430
- ...(matches.length >= MAX_GREP_MATCHES
431
- ? { truncated: true }
432
- : {}),
433
- });
434
- },
435
- };
247
+ });
436
248
 
437
249
  // delete 来自上游 think 工厂,只能在外面包一层闸。它自己没有读-改-写窗口,但必须与
438
250
  // edit 的窗口互斥:否则 edit 读到内容 → delete 删掉文件 → edit 落盘,已删的文件会被
@@ -456,12 +268,34 @@ export function workspacePiToolCandidates(
456
268
  },
457
269
  };
458
270
 
459
- return [read, write, edit, list, find, grep, remove].map((tool) => ({
460
- owner: "workspace",
461
- authorized: true,
462
- requiredExecutionLevel: "safe" as const,
463
- tool,
464
- }));
271
+ const bash = aiToolToPi(
272
+ "bash",
273
+ createBashTool({ ops: workspace }),
274
+ {
275
+ label: "Workspace Bash",
276
+ description:
277
+ "Run a Bash script over Workspace files for shell-style workflows that combine multiple file operations. " +
278
+ "The Workspace is mounted at / and changes are written back. This is a virtual filesystem, not a machine: " +
279
+ "there is no network or system utilities; use execute for fetch and sandbox_exec for system commands.",
280
+ },
281
+ );
282
+
283
+ return [
284
+ ...[read, write, edit, list, find, grep, remove].map((tool) => ({
285
+ owner: "workspace",
286
+ authorized: true,
287
+ requiredExecutionLevel: "safe" as const,
288
+ tool,
289
+ })),
290
+ {
291
+ owner: "workspace",
292
+ authorized: true,
293
+ requiredExecutionLevel: "high" as const,
294
+ summary: "Run a Bash script over Workspace files",
295
+ source: "action" as const,
296
+ tool: bash,
297
+ },
298
+ ];
465
299
  }
466
300
 
467
301
  // #endregion
package/src/plugins.ts CHANGED
@@ -33,7 +33,10 @@ import {
33
33
  type PiRuntimeAssembly,
34
34
  } from "./pi/assembly/snapshot";
35
35
  import type { PiToolCandidate } from "./pi/tool/compiler";
36
- import { basePiToolCandidates } from "./pi/tool/base";
36
+ import {
37
+ basePiToolCandidates,
38
+ memoryPiToolCandidate,
39
+ } from "./pi/tool/base";
37
40
  import {
38
41
  browserQuickActionPiToolCandidates,
39
42
  codeExecutionPiToolCandidate,
@@ -315,6 +318,10 @@ function assertMemoryProfile(profile: RuntimeMemoryProfile): void {
315
318
  interface ToolSurfaceInput {
316
319
  readonly platform: RuntimePlatformPort;
317
320
  readonly workspace?: WorkspacePort;
321
+ readonly memory?: {
322
+ readonly port: RuntimeMemoryPort;
323
+ readonly profile: RuntimeMemoryProfile;
324
+ };
318
325
  readonly codeExecution?: RuntimeCodeExecutionPort;
319
326
  readonly sandbox?: RuntimeSandboxPort;
320
327
  readonly hostTools: readonly PiToolCandidate[];
@@ -322,7 +329,7 @@ interface ToolSurfaceInput {
322
329
  readonly schedule?: RuntimeSchedulePort;
323
330
  readonly subagents?: RuntimeSubagentPort;
324
331
  readonly enabledSubagents: readonly string[];
325
- readonly webSearch?: Parameters<typeof basePiToolCandidates>[0];
332
+ readonly webSearch: Parameters<typeof basePiToolCandidates>[0];
326
333
  readonly extensions: readonly RuntimeExtensionConfig[];
327
334
  readonly policy?: RuntimeToolSurfacePolicy;
328
335
  }
@@ -335,27 +342,66 @@ interface ToolSurface {
335
342
 
336
343
  // 唯一 Tool Surface seam:只读取已授权 Port、Resource 和 Agent policy,
337
344
  // 统一生成可见 Tool,并在一处做名称、执行档位与冲突校验。
338
- function createToolSurface(input: ToolSurfaceInput): ToolSurface {
345
+ async function createToolSurface(
346
+ input: ToolSurfaceInput,
347
+ ): Promise<ToolSurface> {
348
+ const deny = new Set(input.policy?.denyPolicy?.deny ?? []);
349
+ const allowsTool = input.policy?.allowsTool;
350
+ const allowsExtension = input.policy?.allowsExtension;
351
+ const visible = (candidate: PiToolCandidate) =>
352
+ candidate.authorized &&
353
+ !deny.has(candidate.tool.name) &&
354
+ allowsTool?.(candidate.tool.name) !== false;
355
+ const browserCandidates = input.platform.browser
356
+ ? browserQuickActionPiToolCandidates(input.platform.browser)
357
+ : [];
358
+ const workspaceCandidates = input.workspace
359
+ ? workspacePiToolCandidates(input.workspace)
360
+ : [];
361
+ const executionCandidates = input.workspace && input.codeExecution
362
+ ? [codeExecutionPiToolCandidate(input.codeExecution)]
363
+ : [];
364
+ const sandboxCandidates = input.sandbox
365
+ ? sandboxPiToolCandidates(input.sandbox)
366
+ : [];
367
+ const subagentCandidates = subagentPiToolCandidates(
368
+ input.subagents,
369
+ input.enabledSubagents,
370
+ );
371
+ const scheduleCandidates = input.schedule
372
+ ? schedulePiToolCandidates(input.schedule)
373
+ : [];
374
+ const baseCandidates = basePiToolCandidates(input.webSearch);
375
+ const memoryCandidates = input.memory
376
+ ? [memoryPiToolCandidate(input.memory.port, input.memory.profile)]
377
+ : [];
378
+ const scriptCandidates = [
379
+ ...browserCandidates,
380
+ ...workspaceCandidates,
381
+ ...executionCandidates,
382
+ ...sandboxCandidates,
383
+ ...input.hostTools,
384
+ ...scheduleCandidates,
385
+ ...subagentCandidates,
386
+ ...baseCandidates,
387
+ ...memoryCandidates,
388
+ ].filter(visible);
339
389
  const staticCandidates = [
340
- ...(input.platform.browser
341
- ? browserQuickActionPiToolCandidates(input.platform.browser)
342
- : []),
343
- ...(input.workspace ? workspacePiToolCandidates(input.workspace) : []),
344
- ...(input.workspace && input.codeExecution
345
- ? [codeExecutionPiToolCandidate(input.codeExecution)]
346
- : []),
347
- ...(input.sandbox ? sandboxPiToolCandidates(input.sandbox) : []),
390
+ ...browserCandidates,
391
+ ...workspaceCandidates,
392
+ ...executionCandidates,
393
+ ...sandboxCandidates,
348
394
  ...input.hostTools,
349
- ...skillPiToolCandidates(input.skills, {
395
+ ...(await skillPiToolCandidates(input.skills, {
350
396
  loader: input.platform.loader,
351
- }),
352
- ...(input.schedule ? schedulePiToolCandidates(input.schedule) : []),
353
- ...subagentPiToolCandidates(input.subagents, input.enabledSubagents),
354
- ...basePiToolCandidates(input.webSearch),
397
+ ...(input.workspace ? { workspace: input.workspace } : {}),
398
+ tools: scriptCandidates,
399
+ })),
400
+ ...scheduleCandidates,
401
+ ...subagentCandidates,
402
+ ...baseCandidates,
403
+ ...memoryCandidates,
355
404
  ];
356
- const deny = new Set(input.policy?.denyPolicy?.deny ?? []);
357
- const allowsTool = input.policy?.allowsTool;
358
- const allowsExtension = input.policy?.allowsExtension;
359
405
 
360
406
  return {
361
407
  surface: Object.freeze({
@@ -367,11 +413,7 @@ function createToolSurface(input: ToolSurfaceInput): ToolSurface {
367
413
  ...authorizedCandidates,
368
414
  ]) {
369
415
  const name = requiredName(candidate.tool.name, "Pi Tool");
370
- if (
371
- !candidate.authorized ||
372
- deny.has(name) ||
373
- allowsTool?.(name) === false
374
- ) continue;
416
+ if (!visible(candidate)) continue;
375
417
  if (!EXECUTION_LEVELS.includes(candidate.requiredExecutionLevel)) {
376
418
  throw new Error(`Pi Tool ${name} execution level is invalid`);
377
419
  }
@@ -387,13 +429,7 @@ function createToolSurface(input: ToolSurfaceInput): ToolSurface {
387
429
  extensions: input.extensions.filter(
388
430
  (extension) => allowsExtension?.(extension) !== false,
389
431
  ),
390
- degradations: input.webSearch
391
- ? []
392
- : [{
393
- capability: "web_search",
394
- reason: "unavailable",
395
- detail: "Native web search is unavailable for openrouter-chat",
396
- }],
432
+ degradations: [],
397
433
  };
398
434
  }
399
435
 
@@ -580,7 +616,7 @@ class RuntimeBuilder {
580
616
  // 作用:校验全部声明,并生成一个不可变候选 Runtime。
581
617
  // 调用:所有 Plugin 按固定顺序完成合并后调用一次。
582
618
  // 原因:所有交叉约束都在这里通过后才返回候选,旧 Snapshot 不会半更新。
583
- build(): RuntimeCandidate {
619
+ async build(): Promise<RuntimeCandidate> {
584
620
  if (!this.profile) {
585
621
  throw new Error("Runtime profile Plugin did not configure a profile");
586
622
  }
@@ -664,18 +700,19 @@ class RuntimeBuilder {
664
700
  }
665
701
  const resolvedModel = resolvePiModel(provider, profile.model);
666
702
  const endpoint = endpoints[0]!;
667
- const webSearch = endpoint.protocol === "openrouter-chat"
668
- ? undefined
669
- : createWebSearch({
670
- endpoint,
671
- model: profile.model,
672
- maxTokens: resolvedModel.maxTokens,
673
- reasoning: resolvedModel.reasoning,
674
- });
703
+ const webSearch = createWebSearch({
704
+ endpoint,
705
+ model: profile.model,
706
+ maxTokens: resolvedModel.maxTokens,
707
+ reasoning: resolvedModel.reasoning,
708
+ });
675
709
  const skillSources = [...this.skillSources.values()];
676
- const toolSurface = createToolSurface({
710
+ const toolSurface = await createToolSurface({
677
711
  platform: this.platform,
678
712
  ...(this.workspace ? { workspace: this.workspace } : {}),
713
+ ...(this.memoryProfile.enabled && this.memory
714
+ ? { memory: { port: this.memory, profile: this.memoryProfile } }
715
+ : {}),
679
716
  ...(this.codeExecution ? { codeExecution: this.codeExecution } : {}),
680
717
  ...(this.sandbox ? { sandbox: this.sandbox } : {}),
681
718
  hostTools: this.hostTools,
@@ -683,7 +720,7 @@ class RuntimeBuilder {
683
720
  ...(this.schedule ? { schedule: this.schedule } : {}),
684
721
  ...(this.subagents ? { subagents: this.subagents } : {}),
685
722
  enabledSubagents: [...this.enabledSubagents],
686
- ...(webSearch ? { webSearch } : {}),
723
+ webSearch,
687
724
  extensions: [...this.extensions.values()],
688
725
  ...(this.toolPolicy ? { policy: this.toolPolicy } : {}),
689
726
  });