@dbx-tools/appkit-web-search 0.6.223 → 0.6.225

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/plugin.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * AppKit plugin (registered name: `web-search`) that owns the resolved
3
3
  * web-search runtime - the URL policy, result / length caps, timeout, and
4
- * default approval gate the {@link webSearchTool} / {@link webFetchTool}
5
- * read. Registering it resolves and logs the effective config (which URL
6
- * policy is in force, the caps) so a misconfiguration is visible in the boot
7
- * logs rather than on the first search, and installs the plugin's
8
- * `execute()` as the runtime's executor so every outbound call picks up
9
- * AppKit's cache / retry / timeout / telemetry chain.
4
+ * default approval gate its tools read. Registering it resolves and logs the
5
+ * effective config (which URL policy is in force, the caps) so a
6
+ * misconfiguration is visible in the boot logs rather than on the first
7
+ * search. The plugin's `execute()` is captured by its own runtime so every
8
+ * outbound call picks up AppKit's cache / retry / timeout / telemetry chain.
10
9
  *
11
10
  * The plugin also implements AppKit's `ToolProvider`, so an AppKit agent gets
12
- * `web_search` / `web_fetch` without going through Mastra; the Mastra tools in
13
- * `tool.ts` are the other half and share the same runtime.
11
+ * `web_search` / `web_fetch` without going through Mastra. App-integrated
12
+ * Mastra agents consume this provider through `plugins["web-search"].toolkit()`;
13
+ * standalone factories in `tool.ts` use their own explicit runtime or config.
14
14
  *
15
15
  * @module
16
16
  */
@@ -38,7 +38,7 @@ import {
38
38
  type WebSearchPluginConfig,
39
39
  } from "./config.ts";
40
40
  import { runWebFetch } from "./fetch.ts";
41
- import { getWebSearchRuntime, resetWebSearchRuntime, setWebSearchExecutor } from "./runtime.ts";
41
+ import { createWebSearchRuntime, type WebSearchRuntime } from "./runtime.ts";
42
42
  import {
43
43
  webFetchRequestSchema,
44
44
  webSearchRequestSchema,
@@ -120,6 +120,11 @@ export class WebSearchPlugin extends Plugin<WebSearchPluginConfig> implements To
120
120
  config: { schema: WEB_SEARCH_CONFIG_SCHEMA },
121
121
  } satisfies PluginManifest<"web-search">;
122
122
 
123
+ /** Runtime owned exclusively by this plugin instance. */
124
+ private readonly runtime: WebSearchRuntime = createWebSearchRuntime(this.config, (fn, settings) =>
125
+ this.execute(fn, settings),
126
+ );
127
+
123
128
  /**
124
129
  * Promote the serving endpoint to a required resource once a deployment
125
130
  * pins one, through plugin config or either environment name. Left optional
@@ -166,13 +171,12 @@ export class WebSearchPlugin extends Plugin<WebSearchPluginConfig> implements To
166
171
  };
167
172
 
168
173
  /**
169
- * Prime the shared runtime from this plugin's config (over env), route the
170
- * tools' outbound calls through this plugin's interceptor chain, and log the
171
- * effective policy so an active allow-list / caps are obvious at boot.
174
+ * Log the effective policy so an active allow-list and caps are obvious at
175
+ * boot. Runtime construction already resolved config and captured this
176
+ * plugin's interceptor chain.
172
177
  */
173
178
  override async setup(): Promise<void> {
174
- const { config } = getWebSearchRuntime(this.config);
175
- setWebSearchExecutor((fn, settings) => this.execute(fn, settings));
179
+ const { config } = this.runtime;
176
180
  logger.info("ready", {
177
181
  model: config.model ?? `fallbacks:[${config.modelFallbacks.join(", ")}]`,
178
182
  modelSource: config.modelSource,
@@ -186,24 +190,16 @@ export class WebSearchPlugin extends Plugin<WebSearchPluginConfig> implements To
186
190
  }
187
191
 
188
192
  /**
189
- * Drop the shared runtime so a restarted app re-resolves config and does not
190
- * keep calling through a torn-down plugin's `execute()`. Bounded and
191
- * idempotent: there is no connection to drain, only the memo to clear.
193
+ * No runtime resources require draining. The instance-owned config and
194
+ * executor become unreachable with this plugin.
192
195
  */
193
- async shutdown(): Promise<void> {
194
- resetWebSearchRuntime();
196
+ shutdown(): void {
197
+ return;
195
198
  }
196
199
 
197
- /**
198
- * Abort in-flight work. AppKit's graceful shutdown only invokes this hook -
199
- * it never calls {@link shutdown} - so the runtime memo is dropped from here
200
- * to keep a restarted app from calling through a torn-down `execute()`. The
201
- * teardown is synchronous and idempotent, so the un-awaited call costs
202
- * nothing.
203
- */
200
+ /** Abort in-flight work owned by this plugin instance. */
204
201
  override abortActiveOperations(): void {
205
202
  super.abortActiveOperations();
206
- void this.shutdown();
207
203
  }
208
204
 
209
205
  /** AppKit `ToolProvider`: the tool definitions offered to an agent. */
@@ -224,14 +220,14 @@ export class WebSearchPlugin extends Plugin<WebSearchPluginConfig> implements To
224
220
  return {
225
221
  /**
226
222
  * Run a web search directly (bypassing the agent tool). Resolves the
227
- * OBO client from the active execution context and reads the shared
228
- * runtime config primed at setup.
223
+ * OBO client from the active execution context and reads this plugin's
224
+ * runtime config.
229
225
  */
230
226
  search: (request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult> =>
231
227
  this.search(request, signal),
232
228
  /**
233
229
  * Fetch one URL directly (bypassing the agent tool). Enforces the
234
- * configured URL policy. Reads the shared runtime config.
230
+ * configured URL policy.
235
231
  */
236
232
  fetch: (request: WebFetchRequest, signal?: AbortSignal): Promise<WebFetchResult> =>
237
233
  this.fetch(request, signal),
@@ -239,16 +235,11 @@ export class WebSearchPlugin extends Plugin<WebSearchPluginConfig> implements To
239
235
  }
240
236
 
241
237
  private async search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult> {
242
- return runWebSearch(
243
- request,
244
- getWebSearchRuntime().config,
245
- await resolveWebSearchContext(),
246
- signal,
247
- );
238
+ return runWebSearch(request, this.runtime, await resolveWebSearchContext(), signal);
248
239
  }
249
240
 
250
241
  private async fetch(request: WebFetchRequest, signal?: AbortSignal): Promise<WebFetchResult> {
251
- return runWebFetch(request, getWebSearchRuntime().config, signal);
242
+ return runWebFetch(request, this.runtime, signal);
252
243
  }
253
244
  }
254
245
 
@@ -258,12 +249,14 @@ export class WebSearchPlugin extends Plugin<WebSearchPluginConfig> implements To
258
249
  * @example
259
250
  * ```ts
260
251
  * import { createApp, server } from "@databricks/appkit";
261
- * import { plugin as webSearchPlugin, tool as webTool } from "@dbx-tools/appkit-web-search";
252
+ * import { plugin as webSearchPlugin } from "@dbx-tools/appkit-web-search";
262
253
  * import { agents, plugin as mastraPlugin } from "@dbx-tools/appkit-mastra";
263
254
  *
264
255
  * const researcher = agents.createAgent({
265
256
  * instructions: "Research questions with web_search, then read sources with web_fetch.",
266
- * tools: () => ({ web_search: webTool.webSearchTool(), web_fetch: webTool.webFetchTool() }),
257
+ * async tools(plugins) {
258
+ * return { ...(await plugins["web-search"].toolkit()) };
259
+ * },
267
260
  * });
268
261
  *
269
262
  * await createApp({
package/src/runtime.ts CHANGED
@@ -1,20 +1,9 @@
1
1
  /**
2
- * The web-search runtime: a lazily-resolved, process-wide config shared by
3
- * the plugin and the `web_search` / `web_fetch` tools, so both read one
4
- * resolved allow-list / cap / timeout set. The first caller (normally the
5
- * plugin at setup) primes it from the plugin's config; later callers (the
6
- * tools' `execute`) reuse it.
2
+ * Web-search runtime construction and execution helpers.
7
3
  *
8
- * The runtime also carries the {@link WebSearchExecutor} every outbound call
9
- * runs through. The plugin installs its own `execute()` there at setup, which
10
- * is how the tools - plain functions with no plugin instance in scope - still
11
- * get AppKit's cache / retry / timeout / telemetry chain. Without a
12
- * registered plugin (a direct call from a script or a test) the calls still
13
- * run, just without interceptors.
14
- *
15
- * Unlike the email runtime there is no connection to pool - the backend is
16
- * stateless HTTP per call - so the runtime holds only the resolved config and
17
- * that executor.
4
+ * Each plugin instance owns one runtime. Standalone tool factories create their
5
+ * own runtime, so separate apps and direct consumers cannot overwrite each
6
+ * other's policy or executor.
18
7
  *
19
8
  * @module
20
9
  */
@@ -40,51 +29,84 @@ export type WebSearchExecutor = <T>(
40
29
  settings: WebSearchExecutionSettings,
41
30
  ) => Promise<ExecutionResult<T>>;
42
31
 
43
- /** The shared resolved config plus the executor outbound calls run through. */
32
+ /** The resolved config plus the executor outbound calls run through. */
44
33
  export interface WebSearchRuntime {
45
34
  config: ResolvedWebSearchConfig;
46
35
  execute: WebSearchExecutor;
47
36
  }
48
37
 
38
+ /** Runtime-aware operation input, retaining resolved-config compatibility. */
39
+ export type WebSearchRuntimeInput = WebSearchRuntime | ResolvedWebSearchConfig;
40
+
49
41
  /**
50
- * Executor used until (or unless) the plugin installs its own: run the call
51
- * directly, mapping a throw onto the same {@link ExecutionResult} shape so
52
- * call sites branch on `ok` either way.
42
+ * Executor used by standalone runtimes: run the call directly, mapping a throw
43
+ * onto the same {@link ExecutionResult} shape so call sites branch on `ok`
44
+ * either way.
53
45
  */
54
46
  const directExecute = execution.directExecutor<WebSearchExecutionSettings>();
55
47
 
56
- let runtime: WebSearchRuntime | undefined;
48
+ /** Build an isolated runtime for one plugin instance or standalone tool set. */
49
+ export function createWebSearchRuntime(
50
+ overrides?: WebSearchPluginConfig,
51
+ execute: WebSearchExecutor = directExecute,
52
+ ): WebSearchRuntime {
53
+ return { config: resolveWebSearchConfig(overrides), execute };
54
+ }
55
+
56
+ /** Build an isolated runtime from config that has already been resolved. */
57
+ export function createResolvedWebSearchRuntime(
58
+ config: ResolvedWebSearchConfig,
59
+ execute: WebSearchExecutor = directExecute,
60
+ ): WebSearchRuntime {
61
+ return { config, execute };
62
+ }
63
+
64
+ /** Normalize an explicit runtime or legacy resolved config for one operation. */
65
+ export function toWebSearchRuntime(input: WebSearchRuntimeInput): WebSearchRuntime {
66
+ return "execute" in input ? input : createResolvedWebSearchRuntime(input);
67
+ }
57
68
 
58
69
  /**
59
- * Return the shared runtime, building it on first use from the supplied
60
- * config layered over environment defaults. Overrides are only read when the
61
- * runtime is first created, so prime it from the plugin's config at setup;
62
- * subsequent calls (the tools' `execute`) pass nothing and get the same
63
- * instance.
70
+ * Build an isolated standalone runtime.
71
+ *
72
+ * @deprecated Use {@link createWebSearchRuntime}. This compatibility helper
73
+ * returns a new runtime on every call and never reads or updates plugin state.
64
74
  */
65
75
  export function getWebSearchRuntime(overrides?: WebSearchPluginConfig): WebSearchRuntime {
66
- if (!runtime) {
67
- runtime = { config: resolveWebSearchConfig(overrides), execute: directExecute };
68
- }
69
- return runtime;
76
+ return createWebSearchRuntime(overrides);
70
77
  }
71
78
 
79
+ /** Install an executor on an explicit runtime. */
80
+ export function setWebSearchExecutor(runtime: WebSearchRuntime, execute: WebSearchExecutor): void;
72
81
  /**
73
- * Install the executor outbound calls run through. The plugin calls this at
74
- * setup with its own `execute()`; a second call replaces the previous one, so
75
- * a re-registered plugin does not leave the tools bound to a dead instance.
82
+ * @deprecated Process-global executor registration is no longer supported.
83
+ * Pass a runtime as the first argument or provide the executor to
84
+ * {@link createWebSearchRuntime}.
76
85
  */
77
- export function setWebSearchExecutor(execute: WebSearchExecutor): void {
78
- getWebSearchRuntime().execute = execute;
86
+ export function setWebSearchExecutor(execute: WebSearchExecutor): never;
87
+ export function setWebSearchExecutor(
88
+ runtimeOrExecute: WebSearchRuntime | WebSearchExecutor,
89
+ execute?: WebSearchExecutor,
90
+ ): void {
91
+ if (typeof runtimeOrExecute === "function") {
92
+ throw new TypeError(
93
+ "setWebSearchExecutor requires an explicit runtime; use createWebSearchRuntime(config, executor)",
94
+ );
95
+ }
96
+ if (!execute) throw new TypeError("setWebSearchExecutor requires an executor");
97
+ runtimeOrExecute.execute = execute;
79
98
  }
80
99
 
81
- /** Drop the memoized runtime so the next {@link getWebSearchRuntime} rebuilds it. */
100
+ /**
101
+ * @deprecated Plugin runtimes are instance-owned and need no global reset.
102
+ * This compatibility helper is intentionally a no-op.
103
+ */
82
104
  export function resetWebSearchRuntime(): void {
83
- runtime = undefined;
105
+ return;
84
106
  }
85
107
 
86
108
  /**
87
- * Run one idempotent read through the shared executor and unwrap it.
109
+ * Run one idempotent read through the runtime executor and unwrap it.
88
110
  *
89
111
  * `execute()` never throws, so a failed call arrives as `{ ok: false }` with
90
112
  * a status the interceptors already sanitized; it is logged here and re-raised
@@ -94,16 +116,16 @@ export function resetWebSearchRuntime(): void {
94
116
  * interceptor supplies so either one unwinds the I/O.
95
117
  */
96
118
  export async function executeRead<T>(
119
+ runtime: WebSearchRuntime,
97
120
  operation: string,
98
121
  settings: WebSearchExecutionSettings,
99
122
  fn: (signal?: AbortSignal) => Promise<T>,
100
123
  signal?: AbortSignal,
101
124
  ): Promise<T> {
102
- const { execute } = getWebSearchRuntime();
103
125
  return execution.run({
104
126
  operation,
105
127
  settings,
106
- execute,
128
+ execute: runtime.execute,
107
129
  fn,
108
130
  signal,
109
131
  canceled: ExecutionError.canceled,
package/src/scrape.ts CHANGED
@@ -19,11 +19,11 @@
19
19
  */
20
20
 
21
21
  import { log } from "@dbx-tools/shared-core";
22
+ import { load } from "cheerio";
22
23
  import { gotScraping } from "got-scraping";
23
- import type { ResolvedWebSearchConfig } from "./config.ts";
24
24
  import { scrapeSearchExecuteDefaults, toCallSettings } from "./defaults.ts";
25
25
  import { htmlFragmentToText } from "./html-text.ts";
26
- import { executeRead } from "./runtime.ts";
26
+ import { executeRead, toWebSearchRuntime, type WebSearchRuntimeInput } from "./runtime.ts";
27
27
  import type { WebSearchCitation, WebSearchRequest, WebSearchResult } from "./schema.ts";
28
28
 
29
29
  const logger = log.logger("web-search/scrape");
@@ -47,25 +47,18 @@ function unwrapDdgUrl(href: string): string {
47
47
  }
48
48
 
49
49
  /** Parse DDG HTML result blocks into citations (title, url, snippet). */
50
- function parseDdgHtml(html: string): WebSearchCitation[] {
50
+ export function parseDdgHtml(html: string): WebSearchCitation[] {
51
+ const $ = load(html);
51
52
  const citations: WebSearchCitation[] = [];
52
- // Each result: an anchor with class result__a (title + href) and a snippet
53
- // with class result__snippet. Match anchors, then the following snippet.
54
- const anchorRe = /<a[^>]*class="[^"]*result__a[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g;
55
- const snippetRe = /<a[^>]*class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>/g;
56
- const snippets: string[] = [];
57
- let sm: RegExpExecArray | null;
58
- while ((sm = snippetRe.exec(html)) !== null) snippets.push(htmlFragmentToText(sm[1] ?? ""));
59
- let am: RegExpExecArray | null;
60
- let i = 0;
61
- while ((am = anchorRe.exec(html)) !== null) {
62
- const url = unwrapDdgUrl(am[1] ?? "");
63
- const title = htmlFragmentToText(am[2] ?? "");
64
- if (!url || !title) continue;
65
- const snippet = snippets[i] ?? "";
53
+ $("a.result__a").each((_index, element) => {
54
+ const anchor = $(element);
55
+ const url = unwrapDdgUrl(anchor.attr("href") ?? "");
56
+ const title = htmlFragmentToText(anchor.html() ?? "");
57
+ if (!url || !title) return;
58
+ const snippetElement = anchor.closest(".result").find(".result__snippet").first();
59
+ const snippet = htmlFragmentToText(snippetElement.html() ?? "");
66
60
  citations.push({ url, title, ...(snippet ? { snippet } : {}) });
67
- i += 1;
68
- }
61
+ });
69
62
  return citations;
70
63
  }
71
64
 
@@ -78,10 +71,13 @@ function parseDdgHtml(html: string): WebSearchCitation[] {
78
71
  */
79
72
  export async function runScrapeSearch(
80
73
  request: WebSearchRequest,
81
- config: ResolvedWebSearchConfig,
74
+ runtimeOrConfig: WebSearchRuntimeInput,
82
75
  signal?: AbortSignal,
83
76
  ): Promise<WebSearchResult> {
77
+ const runtime = toWebSearchRuntime(runtimeOrConfig);
78
+ const { config } = runtime;
84
79
  const page = await executeRead(
80
+ runtime,
85
81
  "scrape-search",
86
82
  toCallSettings(scrapeSearchExecuteDefaults, config.timeoutMs, [
87
83
  "web-search",
package/src/search.ts CHANGED
@@ -34,7 +34,12 @@ import { openaiChat, openaiResponses } from "@dbx-tools/shared-model";
34
34
  import { MODEL_ENV, SERVING_ENDPOINT_ENV, type ResolvedWebSearchConfig } from "./config.ts";
35
35
  import { toCallSettings, webSearchExecuteDefaults } from "./defaults.ts";
36
36
  import { detectWebSearchProvider, supportsWebSearch, webSearchToolSpec } from "./provider.ts";
37
- import { executeRead } from "./runtime.ts";
37
+ import {
38
+ executeRead,
39
+ toWebSearchRuntime,
40
+ type WebSearchRuntime,
41
+ type WebSearchRuntimeInput,
42
+ } from "./runtime.ts";
38
43
  import type { WebSearchCitation, WebSearchRequest, WebSearchResult } from "./schema.ts";
39
44
  import { runScrapeSearch } from "./scrape.ts";
40
45
 
@@ -168,11 +173,13 @@ async function postServing(
168
173
  ctx: WebSearchContext,
169
174
  url: string,
170
175
  body: unknown,
171
- config: ResolvedWebSearchConfig,
176
+ runtime: WebSearchRuntime,
172
177
  cacheKey: readonly (string | number)[],
173
178
  signal?: AbortSignal,
174
179
  ): Promise<Record<string, unknown>> {
180
+ const { config } = runtime;
175
181
  const payload = await executeRead(
182
+ runtime,
176
183
  "serving-request",
177
184
  toCallSettings(webSearchExecuteDefaults, config.timeoutMs, cacheKey),
178
185
  async (executeSignal): Promise<unknown> => {
@@ -262,17 +269,19 @@ function fromChatPayload(payload: Record<string, unknown>): {
262
269
  */
263
270
  export async function runWebSearch(
264
271
  request: WebSearchRequest,
265
- config: ResolvedWebSearchConfig,
272
+ runtimeOrConfig: WebSearchRuntimeInput,
266
273
  ctx: WebSearchContext,
267
274
  signal?: AbortSignal,
268
275
  ): Promise<WebSearchResult> {
276
+ const runtime = toWebSearchRuntime(runtimeOrConfig);
277
+ const { config } = runtime;
269
278
  const modelId = await resolveWebSearchModel(ctx, config, request.model);
270
279
 
271
280
  if (modelId === null) {
272
281
  // No native web-search model deployed in this workspace.
273
282
  if (config.scrapeFallback) {
274
283
  logger.info("no-native-model:scrape-fallback", { query: request.query });
275
- return runScrapeSearch(request, config, signal);
284
+ return runScrapeSearch(request, runtime, signal);
276
285
  }
277
286
  throw ConfigurationError.resourceNotFound(
278
287
  "Web-search-capable serving endpoint",
@@ -304,7 +313,7 @@ export async function runWebSearch(
304
313
  ctx,
305
314
  url,
306
315
  body,
307
- config,
316
+ runtime,
308
317
  ["web-search", "serving", spec.api, modelId, request.query],
309
318
  signal,
310
319
  );
package/src/tool.ts CHANGED
@@ -13,10 +13,11 @@
13
13
  * list) gates only calls whose URL matches - for `web_fetch` that is evaluated
14
14
  * against the target URL, while `web_search` (whose result URLs aren't known
15
15
  * before the call) treats a pattern gate as "always gate". `approval` falls
16
- * back to the plugin's `approval` config when a tool omits its own.
16
+ * back to the standalone runtime's `approval` config when a tool omits its own.
17
17
  *
18
- * The same two tools are exposed to AppKit's own agents through the plugin's
19
- * `ToolProvider` (see `plugin.ts`); this module is the Mastra half.
18
+ * App-integrated agents should consume the plugin's native toolkit so calls
19
+ * retain that plugin's policy and executor. These factories are the standalone
20
+ * Mastra surface and capture an explicit runtime or isolated config.
20
21
  *
21
22
  * @module
22
23
  */
@@ -29,9 +30,10 @@ import {
29
30
  toApprovalPolicy,
30
31
  type ApprovalGate,
31
32
  type ApprovalPolicy,
33
+ type WebSearchPluginConfig,
32
34
  } from "./config.ts";
33
35
  import { runWebFetch } from "./fetch.ts";
34
- import { getWebSearchRuntime } from "./runtime.ts";
36
+ import { createWebSearchRuntime, type WebSearchRuntime } from "./runtime.ts";
35
37
  import {
36
38
  webFetchRequestSchema,
37
39
  webFetchResultSchema,
@@ -60,19 +62,29 @@ function parseToolInput<S extends z.ZodType>(schema: S, input: unknown): z.infer
60
62
  export interface WebSearchToolOptions {
61
63
  /** Override the tool id. */
62
64
  id?: string;
65
+ /** Isolated runtime used by this standalone tool. Mutually exclusive with `config`. */
66
+ runtime?: WebSearchRuntime;
67
+ /** Config used to create an isolated direct-execution runtime. */
68
+ config?: WebSearchPluginConfig;
63
69
  /**
64
- * Approval gate for this tool, overriding the plugin's `approval`. `true`
70
+ * Approval gate for this tool, overriding the runtime's `approval`. `true`
65
71
  * gates every call; a URL-pattern (or list) gates only matching calls;
66
72
  * omit / `false` for no approval. See {@link ApprovalGate}.
67
73
  */
68
74
  approval?: ApprovalGate | ApprovalPolicy;
69
75
  }
70
76
 
71
- /** Resolve the effective gate: explicit tool option, else the plugin default. */
72
- function effectiveGate(opts: WebSearchToolOptions): ApprovalPolicy {
73
- return opts.approval === undefined
74
- ? getWebSearchRuntime().config.approval
75
- : toApprovalPolicy(opts.approval);
77
+ /** Resolve the isolated runtime captured by one standalone tool factory. */
78
+ function toolRuntime(opts: WebSearchToolOptions): WebSearchRuntime {
79
+ if (opts.runtime && opts.config) {
80
+ throw new TypeError("web-search tool options accept either runtime or config, not both");
81
+ }
82
+ return opts.runtime ?? createWebSearchRuntime(opts.config);
83
+ }
84
+
85
+ /** Resolve the effective gate: explicit tool option, else the runtime default. */
86
+ function effectiveGate(opts: WebSearchToolOptions, runtime: WebSearchRuntime): ApprovalPolicy {
87
+ return opts.approval === undefined ? runtime.config.approval : toApprovalPolicy(opts.approval);
76
88
  }
77
89
 
78
90
  /**
@@ -91,7 +103,8 @@ function effectiveGate(opts: WebSearchToolOptions): ApprovalPolicy {
91
103
  * ```
92
104
  */
93
105
  export function webSearchTool(opts: WebSearchToolOptions = {}) {
94
- const gate = effectiveGate(opts);
106
+ const runtime = toolRuntime(opts);
107
+ const gate = effectiveGate(opts, runtime);
95
108
  return createTool({
96
109
  id: opts.id ?? "web_search",
97
110
  description: WEB_SEARCH_TOOL_DESCRIPTION,
@@ -101,9 +114,8 @@ export function webSearchTool(opts: WebSearchToolOptions = {}) {
101
114
  // is treated as "always gate".
102
115
  ...(gate.mode === "none" ? {} : { requireApproval: () => true }),
103
116
  execute: async (input) => {
104
- const { config } = getWebSearchRuntime();
105
117
  const request = parseToolInput(webSearchRequestSchema, input);
106
- return runWebSearch(request, config, await resolveWebSearchContext());
118
+ return runWebSearch(request, runtime, await resolveWebSearchContext());
107
119
  },
108
120
  });
109
121
  }
@@ -120,7 +132,8 @@ export function webSearchTool(opts: WebSearchToolOptions = {}) {
120
132
  * ```
121
133
  */
122
134
  export function webFetchTool(opts: WebSearchToolOptions = {}) {
123
- const gate = effectiveGate(opts);
135
+ const runtime = toolRuntime(opts);
136
+ const gate = effectiveGate(opts, runtime);
124
137
  return createTool({
125
138
  id: opts.id ?? "web_fetch",
126
139
  description: WEB_FETCH_TOOL_DESCRIPTION,
@@ -136,8 +149,7 @@ export function webFetchTool(opts: WebSearchToolOptions = {}) {
136
149
  approvalMatches(gate, [parseToolInput(webFetchRequestSchema, input).url]),
137
150
  }),
138
151
  execute: async (input) => {
139
- const { config } = getWebSearchRuntime();
140
- return runWebFetch(parseToolInput(webFetchRequestSchema, input), config);
152
+ return runWebFetch(parseToolInput(webFetchRequestSchema, input), runtime);
141
153
  },
142
154
  });
143
155
  }