@springbrand/agent-runtime 0.2.0-alpha.43 → 0.2.0-alpha.45

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.
@@ -2,13 +2,13 @@ import {
2
2
  PreparedPiTurnAdapter,
3
3
  type CreatePreparedPiTurnOptions,
4
4
  } from "./execution";
5
- export { RetryableModelError } from "./execution";
5
+ export { ContextOverflowError, RetryableModelError } from "./execution";
6
6
  import {
7
7
  uiUserMessageToPi,
8
8
  } from "../message";
9
9
  import type { UIMessage } from "ai";
10
10
  import { createModels, type MutableModels } from "@earendil-works/pi-ai";
11
- import { configurePiModels, createPiModels, resolvePiApiKey } from "./models";
11
+ import { configurePiModels, createPiModels } from "./models";
12
12
  export {
13
13
  MODEL_STREAM_STALL_TIMEOUT_MS,
14
14
  modelRequestUrl,
@@ -120,12 +120,6 @@ export class PiRuntimeAdapter {
120
120
  return uiUserMessageToPi(input, privateContext);
121
121
  }
122
122
 
123
- resolveApiKey(
124
- ...args: Parameters<typeof resolvePiApiKey>
125
- ): ReturnType<typeof resolvePiApiKey> {
126
- return resolvePiApiKey(...args);
127
- }
128
-
129
123
  /**
130
124
  * 加载并预检一份可供后续准入使用的 Prepared Runtime。
131
125
  *
@@ -111,6 +111,38 @@ export type PiGenerationLifecycleObserver = (
111
111
  event: PiGenerationLifecycleEvent,
112
112
  ) => void;
113
113
 
114
+ export function structuredOutputStream(
115
+ streamFn: StreamFn,
116
+ model: Model<Api>,
117
+ schema: Record<string, unknown>,
118
+ ): StreamFn | undefined {
119
+ if (
120
+ model.api !== "anthropic-messages" ||
121
+ !model.provider.startsWith("model-api-openrouter-messages-")
122
+ ) return undefined;
123
+
124
+ return (activeModel, context, options) =>
125
+ streamFn(activeModel, context, {
126
+ ...options,
127
+ onPayload: async (payload, payloadModel) => {
128
+ const value = await options?.onPayload?.(payload, payloadModel) ?? payload;
129
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
130
+ throw new Error("Structured model payload must be an object");
131
+ }
132
+ const current = (value as Record<string, unknown>).output_config;
133
+ return {
134
+ ...value,
135
+ output_config: {
136
+ ...(current && typeof current === "object" && !Array.isArray(current)
137
+ ? current
138
+ : {}),
139
+ format: { type: "json_schema", schema },
140
+ },
141
+ };
142
+ },
143
+ });
144
+ }
145
+
114
146
  export function isModelStreamStallMessage(message?: string): message is string {
115
147
  return message === MODEL_STREAM_STALL_MESSAGE ||
116
148
  message?.startsWith(MODEL_STREAM_STALL_DETAILS_PREFIX) === true;
@@ -534,8 +566,8 @@ export function withProviderRetry(
534
566
  }
535
567
 
536
568
  // 找出部署把这个模型放在哪个端点,并保留该端点在配置中的位置。
537
- // resolvePiModel resolvePiApiKey 在组装运行快照、发起模型请求前调用它,调用方只需传部署配置和 modelId。
538
- // 这里复用同一条查找路径,避免模型与密钥落到不同端点;同一 modelId 若配置多次会取第一个,是否应改为拒绝重复配置仍待确认。
569
+ // resolvePiModel 在组装运行快照前调用它,调用方只需传部署配置和 modelId。
570
+ // 同一 modelId 若配置多次会取第一个,是否应改为拒绝重复配置仍待确认。
539
571
  function endpointFor(
540
572
  provider: RuntimeProviderPort,
541
573
  modelId: string,
@@ -585,6 +617,20 @@ function piApiFor(protocol: RuntimeModelProtocol) {
585
617
  }
586
618
  }
587
619
 
620
+ function anthropicMessagesModelForOpenRouter(
621
+ modelId: string,
622
+ ): Model<"anthropic-messages"> | undefined {
623
+ if (!modelId.startsWith("anthropic/")) return;
624
+ const anthropicModelId = modelId
625
+ .slice("anthropic/".length)
626
+ .replace(/:batch$/, "")
627
+ .replace(/-fast$/, "")
628
+ .replaceAll(".", "-");
629
+ return CATALOGS["anthropic-messages"].find(
630
+ (model) => model.id === anthropicModelId,
631
+ );
632
+ }
633
+
588
634
  // 把部署声明的模型改造成可由对应自定义端点执行的 Pi 模型。
589
635
  // resolvePiModel 为单个运行快照调用它,configurePiModels 则在注册端点时为端点中的每个 modelId 调用它。
590
636
  // Pi 的内置 OpenAI、Anthropic provider 提供上下文窗口、费用和推理能力等元数据;这里保留这些元数据,但以部署协议、provider 标识和 baseURL 覆盖内置路由,未知模型会在初始化阶段直接报错,而不是拖到请求阶段。
@@ -602,10 +648,10 @@ function configuredModel(
602
648
  );
603
649
  }
604
650
  const {
605
- api: _api,
651
+ api: catalogApi,
606
652
  provider: _provider,
607
653
  baseUrl: _baseUrl,
608
- compat,
654
+ compat: catalogCompat,
609
655
  headers: catalogHeaders,
610
656
  ...metadata
611
657
  } = catalogModel;
@@ -614,17 +660,24 @@ function configuredModel(
614
660
  ...endpoint.headers,
615
661
  };
616
662
  const openRouterProviderPin = endpoint.openRouterProviderPins?.[modelId];
617
- const supportsStrictTools = modelId.startsWith("anthropic/claude-");
663
+ const configuredApi = apiFor(endpoint.protocol);
664
+ // OpenRouter uses dotted versions and optional transport suffixes while the
665
+ // Anthropic catalog uses dashed base model ids.
666
+ const anthropicMessagesCompat = endpoint.protocol === "openrouter-messages"
667
+ ? anthropicMessagesModelForOpenRouter(modelId)?.compat
668
+ : undefined;
618
669
  return {
619
670
  ...metadata,
620
- api: apiFor(endpoint.protocol),
671
+ api: configuredApi,
621
672
  provider: providerId(endpoint, index),
622
673
  baseUrl: endpoint.baseURL,
623
674
  ...(endpoint.protocol === "openrouter-messages"
624
675
  ? {
625
676
  compat: {
677
+ ...anthropicMessagesCompat,
626
678
  supportsEagerToolInputStreaming: false,
627
- supportsStrictTools,
679
+ supportsStrictTools:
680
+ anthropicMessagesCompat?.supportsStrictTools ?? false,
628
681
  supportsToolReferences: true,
629
682
  openRouterRouting: {
630
683
  require_parameters: true,
@@ -637,7 +690,9 @@ function configuredModel(
637
690
  },
638
691
  },
639
692
  }
640
- : {}),
693
+ : catalogApi === configuredApi && catalogCompat
694
+ ? { compat: catalogCompat }
695
+ : {}),
641
696
  ...(Object.keys(headers).length > 0 ? { headers } : {}),
642
697
  };
643
698
  }
@@ -661,24 +716,6 @@ export function resolvePiModel(
661
716
  return configuredModel(endpoint, index, modelId);
662
717
  }
663
718
 
664
- /**
665
- * 取出负责指定模型的部署端点密钥。
666
- *
667
- * Runtime 在创建一次模型执行或压缩上下文前调用它;辅助的标题生成器也在请求 Pi 时使用它。
668
- * 调用方应传入与 {@link resolvePiModel} 相同的 provider 和 modelId,并只把返回值用于当前请求。
669
- *
670
- * 它与模型解析共用 endpointFor,确保密钥和模型总是选择同一个端点,也让未配置的模型在密钥离开部署边界前失败。
671
- * 不要另写按协议选密钥的分支,否则同协议的多个端点会拿错凭据。
672
- *
673
- * @throws 当部署未配置该 modelId 时抛出错误。
674
- */
675
- export function resolvePiApiKey(
676
- provider: RuntimeProviderPort,
677
- modelId: string,
678
- ): string {
679
- return endpointFor(provider, modelId).endpoint.apiKey;
680
- }
681
-
682
719
  /**
683
720
  * 用当前部署端点完整重建一个已有的 Pi 模型集合。
684
721
  *
@@ -440,7 +440,7 @@ export class PiRuntimeTranscript {
440
440
  /**
441
441
  * 在 Pi 请求模型前按 Runtime 阈值压缩当前上下文,并把压缩 entry 写回 Session。
442
442
  *
443
- * `AgentRuntimeKernel.transformPiContext` 作为 Pi 的 `transformContext` 调用它,并提供本次固定的模型、密钥和取消信号。
443
+ * `AgentRuntimeKernel.transformPiContext` 作为 Pi 的 `transformContext` 调用它,并提供本次固定的模型和取消信号。
444
444
  *
445
445
  * 方法先逐条核对调用参数与耐久分支;不一致或压缩不可用时保留原消息,避免把漂移的内存上下文提交成新的规范分支。
446
446
  */
@@ -448,8 +448,8 @@ export class PiRuntimeTranscript {
448
448
  messages: AgentMessage[],
449
449
  options: {
450
450
  compactAfterTokens: number;
451
+ force?: boolean;
451
452
  model: Model<Api>;
452
- apiKey: string;
453
453
  signal?: AbortSignal;
454
454
  submissionId?: string;
455
455
  onCompactionPersisted?: (event: RuntimeModelUsageEvent) => void;
@@ -474,11 +474,14 @@ export class PiRuntimeTranscript {
474
474
  const result = await compactPiContext({
475
475
  branch,
476
476
  compactAfterTokens: options.compactAfterTokens,
477
+ force: options.force,
477
478
  models: this.models,
478
479
  model: options.model,
479
- apiKey: options.apiKey,
480
480
  signal: options.signal,
481
481
  });
482
+ if (options.force && !result.didCompact) {
483
+ throw new Error("Forced context compaction did not reduce the model input");
484
+ }
482
485
  if (result.compactionEntry) {
483
486
  // 整对象写入:assembly 侧已经造好了完整的 compaction 条目,这里不再拆成位置
484
487
  // 参数。旧写法拆 5 个参数喂 pi 的 Session.appendCompaction,pi 0.81 把它加到
@@ -2,7 +2,12 @@ import type {
2
2
  AgentTool,
3
3
  AgentToolResult,
4
4
  } from "@earendil-works/pi-agent-core";
5
- import { Type, type Static, type TSchema } from "@earendil-works/pi-ai";
5
+ import {
6
+ StringEnum,
7
+ Type,
8
+ type Static,
9
+ type TSchema,
10
+ } from "@earendil-works/pi-ai";
6
11
  import { estimateStringTokens } from "agents/experimental/memory/utils";
7
12
  import type { RuntimeMemoryPort } from "../../kernel/bindings";
8
13
  import type { RuntimeMemoryProfile } from "../../kernel/profile";
@@ -145,11 +150,7 @@ const updatePlanParameters = Type.Object({
145
150
  text: Type.String({
146
151
  description: "The step, phrased as a short imperative.",
147
152
  }),
148
- status: Type.Union([
149
- Type.Literal("pending"),
150
- Type.Literal("in_progress"),
151
- Type.Literal("done"),
152
- ], {
153
+ status: StringEnum(["pending", "in_progress", "done"] as const, {
153
154
  description:
154
155
  'Current status. Use "done" for a finished step; never use "completed".',
155
156
  }),
@@ -214,18 +215,12 @@ export function normalizeUpdatePlanArguments(
214
215
  }
215
216
 
216
217
  const setContextParameters = Type.Object({
217
- label: Type.Union([
218
- Type.Literal("memory"),
219
- Type.Literal("preferences"),
220
- ], {
218
+ label: StringEnum(["memory", "preferences"] as const, {
221
219
  description:
222
220
  "Context block to update: memory for durable facts and active context, preferences for tone and workflow choices.",
223
221
  }),
224
222
  content: Type.String({ description: "Text to store in the selected context block." }),
225
- action: Type.Optional(Type.Union([
226
- Type.Literal("replace"),
227
- Type.Literal("append"),
228
- ], {
223
+ action: Type.Optional(StringEnum(["replace", "append"] as const, {
229
224
  description: 'Whether to replace the block or append to it. Defaults to "replace".',
230
225
  })),
231
226
  }, { additionalProperties: false });
@@ -318,7 +313,7 @@ export function basePiToolCandidates(
318
313
  name: "update_plan",
319
314
  label: "Update plan",
320
315
  description:
321
- "Maintain the user-visible plan for the current task. Call this whenever a task involves 2 or more steps, and again every time the plan or a step's status changes. Always pass the FULL plan — it replaces the previous plan entirely (idempotent overwrite), so omitted steps disappear.",
316
+ "Maintain the user-visible plan for the current task. Required input: steps. Call this whenever a task involves 2 or more steps, and again every time the plan or a step's status changes. Always pass the FULL plan — it replaces the previous plan entirely (idempotent overwrite), so omitted steps disappear.",
322
317
  parameters: updatePlanParameters,
323
318
  prepareArguments: normalizeUpdatePlanArguments,
324
319
  async execute(_toolCallId, input) {
@@ -66,7 +66,7 @@ export interface PiToolCandidate {
66
66
  * 那是失效不是降级。`deny` 仍然优先,被 deny 的工具根本不会注入。
67
67
  */
68
68
  readonly alwaysRequiresApproval?: boolean;
69
- /** Recovery may replay this Tool Call only when the candidate guarantees convergence. */
69
+ /** Recovery may replay this Tool Call only when the candidate guarantees convergence. Omitted means non-idempotent. */
70
70
  readonly retry?: "idempotent" | "non-idempotent";
71
71
  readonly summary?: string;
72
72
  readonly source?: ApprovalReceipt["source"];
@@ -196,6 +196,7 @@ export function listExtensionsPiToolCandidate(
196
196
  return {
197
197
  owner: "core:extensions",
198
198
  requiredExecutionLevel: "low",
199
+ retry: "idempotent",
199
200
  tool,
200
201
  };
201
202
  }
@@ -278,6 +279,7 @@ export function codeExecutionPiToolCandidate(
278
279
  return {
279
280
  owner: "core:codemode",
280
281
  requiredExecutionLevel: "safe",
282
+ retry: "idempotent",
281
283
  outputBudget: { kind: "structure" },
282
284
  source: "codemode",
283
285
  summary: "Run JavaScript with network and configured connector access",
@@ -92,7 +92,7 @@ function candidate<T extends TSchema>(
92
92
  options: Partial<
93
93
  Pick<
94
94
  PiToolCandidate,
95
- "alwaysRequiresApproval" | "exposureMode" | "owner" | "requiredExecutionLevel" | "summary"
95
+ "alwaysRequiresApproval" | "exposureMode" | "owner" | "requiredExecutionLevel" | "retry" | "summary"
96
96
  >
97
97
  > = {},
98
98
  ): PiToolCandidate {
@@ -106,6 +106,7 @@ function candidate<T extends TSchema>(
106
106
  ? { alwaysRequiresApproval: true }
107
107
  : {}),
108
108
  ...(options.summary ? { summary: options.summary } : {}),
109
+ ...(options.retry ? { retry: options.retry } : {}),
109
110
  };
110
111
  }
111
112
 
@@ -138,17 +139,20 @@ export function schedulePiToolCandidates(
138
139
  summary: "Create a scheduled task",
139
140
  },
140
141
  ),
141
- candidate({
142
- name: "list_schedules",
143
- label: "List schedules",
144
- description:
145
- "List the user's existing scheduled tasks. Call this when the user asks what they have scheduled, or before cancelling one — you need the id, and the user will refer to a task by what it does, not by its id.",
146
- parameters: noParameters,
147
- async execute(_toolCallId, _input, signal) {
148
- signal?.throwIfAborted();
149
- return result({ schedules: await schedule.list() });
142
+ candidate(
143
+ {
144
+ name: "list_schedules",
145
+ label: "List schedules",
146
+ description:
147
+ "List the user's existing scheduled tasks. Call this when the user asks what they have scheduled, or before cancelling one — you need the id, and the user will refer to a task by what it does, not by its id.",
148
+ parameters: noParameters,
149
+ async execute(_toolCallId, _input, signal) {
150
+ signal?.throwIfAborted();
151
+ return result({ schedules: await schedule.list() });
152
+ },
150
153
  },
151
- }),
154
+ { retry: "idempotent" },
155
+ ),
152
156
  candidate({
153
157
  name: "update_schedule",
154
158
  label: "Update schedule",
@@ -534,6 +534,9 @@ export async function skillPiToolCandidates(
534
534
  ...(name === "activate_skill"
535
535
  ? { exposureMode: "direct" as const }
536
536
  : { exposureMode: "codemode" as const }),
537
+ ...(name === "activate_skill" || name === "read_skill_resource"
538
+ ? { retry: "idempotent" as const }
539
+ : {}),
537
540
  tool: adapted,
538
541
  };
539
542
  });
@@ -182,6 +182,7 @@ export function subagentPiToolCandidates(
182
182
  owner: `subagent:${type.name}`,
183
183
  exposureMode: "direct",
184
184
  requiredExecutionLevel: "safe",
185
+ retry: "idempotent",
185
186
  tool,
186
187
  };
187
188
  });
@@ -265,6 +266,7 @@ export function subagentPiToolCandidates(
265
266
  owner: "subagent:background",
266
267
  exposureMode: "direct",
267
268
  requiredExecutionLevel: "low",
269
+ retry: "idempotent",
268
270
  summary: "Dispatch a background sub-agent",
269
271
  source: "action",
270
272
  tool: background,
@@ -11,6 +11,7 @@ export function getTimePiToolCandidate(owner = "utilities"): PiToolCandidate {
11
11
  return {
12
12
  owner,
13
13
  requiredExecutionLevel: "safe",
14
+ retry: "idempotent",
14
15
  source: "action",
15
16
  summary: "Get the current server time as an ISO string.",
16
17
  tool: {
@@ -707,6 +707,12 @@ async function callOpenAIStream(
707
707
  tools: isOpenRouter
708
708
  ? [{ type: "openrouter:web_search", parameters: { max_uses: 10 } }]
709
709
  : [{ type: "web_search" }],
710
+ ...(isOpenRouter
711
+ ? {
712
+ max_output_tokens: Math.min(model.maxTokens, OPENROUTER_MAX_OUTPUT_TOKENS),
713
+ reasoning: { effort: "none" },
714
+ }
715
+ : {}),
710
716
  ...(!isOpenRouter
711
717
  ? {
712
718
  include: isCodex
@@ -1108,6 +1114,8 @@ export function applyCitations(text: string, groundingMetadata: any): { text: st
1108
1114
 
1109
1115
  const PROVIDER_ERROR_MAX_LENGTH = 2_048;
1110
1116
  const NATIVE_TIMEOUT_MS = 60_000;
1117
+ const OPENROUTER_MAX_OUTPUT_TOKENS = 4_096;
1118
+ export const WEB_SEARCH_MODEL = "deepseek/deepseek-v4-flash";
1111
1119
 
1112
1120
  export interface WebSearchOptions {
1113
1121
  endpoint: RuntimeModelEndpoint;
@@ -121,6 +121,7 @@ export function webSearchPiToolCandidate(
121
121
  owner: "runtime-base",
122
122
  exposureMode: "direct",
123
123
  requiredExecutionLevel: "safe",
124
+ retry: "idempotent",
124
125
  source: "action",
125
126
  tool,
126
127
  };
@@ -402,6 +402,7 @@ export function workspacePiToolCandidates(
402
402
  {
403
403
  owner: "workspace",
404
404
  requiredExecutionLevel: "safe" as const,
405
+ retry: "idempotent" as const,
405
406
  // read 是大输出外置协议的**出口**:溢出结果的提示语指向它。出口自己再外置,
406
407
  // 就会变成 read → 新 artifact → read 的死循环,模型只能靠猜逃出去。
407
408
  // 分页上限由 `pageReadResult` 自己保证,不需要外置也不会撑爆一轮。
@@ -411,6 +412,7 @@ export function workspacePiToolCandidates(
411
412
  ...[write, edit, list, find, grep, remove].map((tool) => ({
412
413
  owner: "workspace",
413
414
  requiredExecutionLevel: "safe" as const,
415
+ retry: "idempotent" as const,
414
416
  tool,
415
417
  })),
416
418
  ];
@@ -621,6 +623,7 @@ export function sandboxPiToolCandidates(
621
623
  {
622
624
  owner: "sandbox",
623
625
  requiredExecutionLevel: "safe",
626
+ retry: "idempotent",
624
627
  tool: processLogs,
625
628
  },
626
629
  {
@@ -301,6 +301,7 @@ export interface RuntimeAgentControls {
301
301
  requestId?: string,
302
302
  reason?: string,
303
303
  ): Promise<{ ok: boolean }>;
304
+ stopAllSubmissions(reason?: string): Promise<{ ok: boolean }>;
304
305
  }
305
306
 
306
307
  export interface RuntimeAgentConfigControls<Command, Change> {
@@ -525,6 +526,10 @@ export function defineRuntimeAgent<
525
526
  return super.stopTurn(requestId, reason);
526
527
  }
527
528
 
529
+ stopAllSubmissions(reason?: string): Promise<{ ok: boolean }> {
530
+ return super.stopAllSubmissions(reason);
531
+ }
532
+
528
533
  protected override async prepareAgentToolRun(
529
534
  input: unknown,
530
535
  ): Promise<string> {
@@ -922,6 +927,10 @@ export function defineRuntimeAgent<
922
927
  callableContext,
923
928
  );
924
929
  callable()(GeneratedRuntimeAgent.prototype.stopTurn, callableContext);
930
+ callable()(
931
+ GeneratedRuntimeAgent.prototype.stopAllSubmissions,
932
+ callableContext,
933
+ );
925
934
 
926
935
  return GeneratedRuntimeAgent as unknown as RuntimeAgentClass<
927
936
  Env,
@@ -58,7 +58,7 @@ import {
58
58
  codeExecutionPiToolCandidate,
59
59
  } from "./pi/tool/core";
60
60
  import { skillPiToolCandidates } from "./pi/tool/skill";
61
- import { createWebSearch } from "./pi/tool/web-search";
61
+ import { createWebSearch, WEB_SEARCH_MODEL } from "./pi/tool/web-search";
62
62
  import { subagentPiToolCandidates } from "./pi/tool/subagent";
63
63
  import { resolvePiModel } from "./pi/runtime-adapter/models";
64
64
  import {
@@ -170,7 +170,7 @@ interface ToolSurfaceInput {
170
170
  readonly hostTools: readonly PiToolCandidate[];
171
171
  readonly skills: readonly RuntimeSkillSourceBinding[];
172
172
  readonly enabledSubagents: readonly string[];
173
- readonly webSearch: Parameters<typeof basePiToolCandidates>[0];
173
+ readonly webSearch?: Parameters<typeof basePiToolCandidates>[0];
174
174
  readonly extensions: readonly RuntimeExtensionConfig[];
175
175
  readonly policy?: RuntimeToolSurfacePolicy;
176
176
  }
@@ -581,14 +581,20 @@ class RuntimeBuilder {
581
581
  : `Model is configured by multiple endpoints: ${profile.model}`,
582
582
  );
583
583
  }
584
- const resolvedModel = resolvePiModel(provider, profile.model);
585
- const endpoint = endpoints[0]!;
586
- const webSearch = createWebSearch({
587
- endpoint,
588
- model: profile.model,
589
- maxTokens: resolvedModel.maxTokens,
590
- reasoning: resolvedModel.reasoning,
591
- });
584
+ const webSearchEndpoint = provider.endpoints.find((endpoint) =>
585
+ endpoint.models.includes(WEB_SEARCH_MODEL)
586
+ );
587
+ const webSearch = webSearchEndpoint
588
+ ? (() => {
589
+ const webSearchModel = resolvePiModel(provider, WEB_SEARCH_MODEL);
590
+ return createWebSearch({
591
+ endpoint: webSearchEndpoint,
592
+ model: WEB_SEARCH_MODEL,
593
+ maxTokens: webSearchModel.maxTokens,
594
+ reasoning: webSearchModel.reasoning,
595
+ });
596
+ })()
597
+ : undefined;
592
598
  const skillSources = [...this.skillSources.values()];
593
599
  const toolSurface = await createToolSurface({
594
600
  ...(this.codeExecution ? { codeExecution: this.codeExecution } : {}),
@@ -606,7 +612,7 @@ class RuntimeBuilder {
606
612
  ],
607
613
  skills: skillSources,
608
614
  enabledSubagents: [...this.enabledSubagents],
609
- webSearch,
615
+ ...(webSearch ? { webSearch } : {}),
610
616
  extensions: [...this.extensions.values()],
611
617
  ...(this.toolPolicy ? { policy: this.toolPolicy } : {}),
612
618
  });