@dbx-tools/appkit-web-search 0.9.132 → 0.9.136

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
@@ -31,7 +31,14 @@ import {
31
31
  WEB_SEARCH_CONFIG_SCHEMA,
32
32
  type WebSearchPluginConfig,
33
33
  } from "./config.ts";
34
- import { runWebFetch } from "./fetch.ts";
34
+ import {
35
+ runWebAssetPageFetch,
36
+ runWebFetch,
37
+ runWebImageFetch,
38
+ type WebAssetPageFetchRequest,
39
+ type WebImageFetchRequest,
40
+ type WebImageFetchResult,
41
+ } from "./fetch.ts";
35
42
  import { createWebSearchRuntime, type WebSearchRuntime } from "./runtime.ts";
36
43
  import {
37
44
  webFetchRequestSchema,
@@ -230,6 +237,16 @@ export class WebSearchPlugin extends ToolRegistryPlugin<WebSearchPluginConfig> {
230
237
  */
231
238
  fetch: (request: WebFetchRequest, signal?: AbortSignal): Promise<WebFetchResult> =>
232
239
  this.fetch(request, signal),
240
+ /** Fetch expanded HTML for in-process asset discovery. */
241
+ fetchAssetPage: (
242
+ request: WebAssetPageFetchRequest,
243
+ signal?: AbortSignal,
244
+ ): Promise<WebFetchResult> => runWebAssetPageFetch(request, this.runtime, signal),
245
+ /** Fetch one image for in-process visual research. */
246
+ fetchImage: (
247
+ request: WebImageFetchRequest,
248
+ signal?: AbortSignal,
249
+ ): Promise<WebImageFetchResult> => runWebImageFetch(request, this.runtime, signal),
233
250
  };
234
251
  }
235
252
 
package/src/runtime.ts CHANGED
@@ -37,6 +37,10 @@ export interface WebSearchRuntime {
37
37
  execute: WebSearchExecutor;
38
38
  /** Provider-family cooldown deadlines learned from workspace failures. */
39
39
  familyCooldowns: Map<policy.ModelFamily, number>;
40
+ /** Provider families that completed a native search successfully. */
41
+ availableFamilies: Set<policy.ModelFamily>;
42
+ /** First native request currently establishing one family's availability. */
43
+ familyProbes: Map<policy.ModelFamily, Promise<void>>;
40
44
  }
41
45
 
42
46
  /** Runtime-aware operation input, retaining resolved-config compatibility. */
@@ -54,7 +58,13 @@ export function createWebSearchRuntime(
54
58
  overrides?: WebSearchPluginConfig,
55
59
  execute: WebSearchExecutor = directExecute,
56
60
  ): WebSearchRuntime {
57
- return { config: resolveWebSearchConfig(overrides), execute, familyCooldowns: new Map() };
61
+ return {
62
+ config: resolveWebSearchConfig(overrides),
63
+ execute,
64
+ familyCooldowns: new Map(),
65
+ availableFamilies: new Set(),
66
+ familyProbes: new Map(),
67
+ };
58
68
  }
59
69
 
60
70
  /** Build an isolated runtime from config that has already been resolved. */
@@ -62,7 +72,13 @@ export function createResolvedWebSearchRuntime(
62
72
  config: ResolvedWebSearchConfig,
63
73
  execute: WebSearchExecutor = directExecute,
64
74
  ): WebSearchRuntime {
65
- return { config, execute, familyCooldowns: new Map() };
75
+ return {
76
+ config,
77
+ execute,
78
+ familyCooldowns: new Map(),
79
+ availableFamilies: new Set(),
80
+ familyProbes: new Map(),
81
+ };
66
82
  }
67
83
 
68
84
  /** Normalize an explicit runtime or legacy resolved config for one operation. */
@@ -91,10 +107,11 @@ export async function executeRead<T>(
91
107
  settings: WebSearchExecutionSettings,
92
108
  fn: (signal?: AbortSignal) => Promise<T>,
93
109
  signal?: AbortSignal,
110
+ logFailure = true,
94
111
  ): Promise<T> {
95
112
  return pluginExecution.runPluginExecution({
96
113
  plugin: "web-search",
97
- logger,
114
+ logger: logFailure ? logger : { warn() {} },
98
115
  operation,
99
116
  settings,
100
117
  execute: runtime.execute,
package/src/search.ts CHANGED
@@ -231,25 +231,22 @@ async function postServing(
231
231
  const responseBody = await response.text();
232
232
  const model =
233
233
  object.isRecord(body) && typeof body.model === "string" ? body.model : undefined;
234
- logger.error("serving-request-rejected", {
235
- model,
236
- api: url.endsWith("/responses") ? "responses" : "chat",
237
- url,
238
- status: response.status,
239
- responseBody,
240
- });
241
- if (
234
+ const crossRegionDisabled =
242
235
  response.status === 400 &&
243
236
  model &&
244
237
  policy.modelFamily(model) === policy.ModelFamily.Gemini &&
245
- GEMINI_CROSS_REGION_DISABLED.test(responseBody)
246
- ) {
238
+ GEMINI_CROSS_REGION_DISABLED.test(responseBody);
239
+ if (crossRegionDisabled) {
247
240
  const expiresAt = Date.now() + GEMINI_CROSS_REGION_COOLDOWN_MS;
248
241
  runtime.familyCooldowns.set(policy.ModelFamily.Gemini, expiresAt);
249
- logger.warn("provider-family-suppressed", {
250
- family: policy.ModelFamily.Gemini,
251
- reason: "cross-region-disabled",
252
- expiresAt: new Date(expiresAt).toISOString(),
242
+ runtime.availableFamilies.delete(policy.ModelFamily.Gemini);
243
+ } else {
244
+ logger.error("serving-request-rejected", {
245
+ model,
246
+ api: url.endsWith("/responses") ? "responses" : "chat",
247
+ url,
248
+ status: response.status,
249
+ responseBody,
253
250
  });
254
251
  }
255
252
  // Load-shedding and server faults are worth another attempt; a 4xx is
@@ -268,6 +265,7 @@ async function postServing(
268
265
  : response.json();
269
266
  },
270
267
  signal,
268
+ false,
271
269
  );
272
270
  return object.isRecord(payload) ? payload : {};
273
271
  }
@@ -456,29 +454,95 @@ export async function runWebSearch(
456
454
  );
457
455
  }
458
456
 
457
+ return runSelectedWebSearch(request, runtime, ctx, endpoint, signal, writeProgress);
458
+ }
459
+
460
+ /** Run one selected family after its first request establishes availability. */
461
+ async function runSelectedWebSearch(
462
+ request: WebSearchRequest,
463
+ runtime: WebSearchRuntime,
464
+ ctx: WebSearchContext,
465
+ endpoint: ServingEndpointSummary,
466
+ signal?: AbortSignal,
467
+ writeProgress?: ProgressWriter,
468
+ ): Promise<WebSearchResult> {
469
+ const family = endpointFamily(endpoint);
470
+ if (!family || runtime.availableFamilies.has(family)) {
471
+ try {
472
+ return await runNativeWebSearch(request, runtime, ctx, endpoint, signal, writeProgress);
473
+ } catch (error) {
474
+ return runSearchFallback(request, runtime, ctx, endpoint, error, signal, writeProgress);
475
+ }
476
+ }
477
+
478
+ const activeProbe = runtime.familyProbes.get(family);
479
+ if (activeProbe) {
480
+ signal?.throwIfAborted();
481
+ await activeProbe;
482
+ signal?.throwIfAborted();
483
+ return runWebSearch(request, runtime, ctx, signal, writeProgress);
484
+ }
485
+
486
+ let releaseProbe!: () => void;
487
+ const probe = new Promise<void>((resolve) => {
488
+ releaseProbe = resolve;
489
+ });
490
+ runtime.familyProbes.set(family, probe);
491
+
492
+ if (runtime.availableFamilies.has(family) || activeFamilyCooldowns(runtime).has(family)) {
493
+ runtime.familyProbes.delete(family);
494
+ releaseProbe();
495
+ return runWebSearch(request, runtime, ctx, signal, writeProgress);
496
+ }
497
+
498
+ let failure: unknown;
459
499
  try {
460
- return await runNativeWebSearch(request, runtime, ctx, endpoint, signal, writeProgress);
500
+ const result = await runNativeWebSearch(request, runtime, ctx, endpoint, signal, writeProgress);
501
+ runtime.availableFamilies.add(family);
502
+ return result;
461
503
  } catch (error) {
462
- if (
463
- endpointFamily(endpoint) !== policy.ModelFamily.Gemini ||
464
- !activeFamilyCooldowns(runtime).has(policy.ModelFamily.Gemini)
465
- ) {
466
- throw error;
467
- }
468
- const fallback = await resolveWebSearchModel(ctx, runtime, request.model);
469
- if (!fallback) {
470
- if (config.scrapeFallback) {
471
- logger.info("gemini-unavailable:scrape-fallback", { query: request.query });
472
- return runScrapeSearch(request, runtime, signal);
473
- }
474
- throw error;
504
+ failure = error;
505
+ } finally {
506
+ if (runtime.familyProbes.get(family) === probe) runtime.familyProbes.delete(family);
507
+ releaseProbe();
508
+ }
509
+ return runSearchFallback(request, runtime, ctx, endpoint, failure, signal, writeProgress);
510
+ }
511
+
512
+ /** Resolve a cached family failure to another model or the scrape fallback. */
513
+ async function runSearchFallback(
514
+ request: WebSearchRequest,
515
+ runtime: WebSearchRuntime,
516
+ ctx: WebSearchContext,
517
+ endpoint: ServingEndpointSummary,
518
+ error: unknown,
519
+ signal?: AbortSignal,
520
+ writeProgress?: ProgressWriter,
521
+ ): Promise<WebSearchResult> {
522
+ if (
523
+ endpointFamily(endpoint) !== policy.ModelFamily.Gemini ||
524
+ !activeFamilyCooldowns(runtime).has(policy.ModelFamily.Gemini)
525
+ ) {
526
+ throw error;
527
+ }
528
+ const fallback = await resolveWebSearchModel(ctx, runtime, request.model);
529
+ if (!fallback) {
530
+ if (runtime.config.scrapeFallback) {
531
+ logger.warn("web-search-model-fallback", {
532
+ failedModel: endpoint.name,
533
+ fallback: "scrape",
534
+ reason: "cross-region-disabled",
535
+ });
536
+ return runScrapeSearch(request, runtime, signal);
475
537
  }
476
- logger.info("gemini-unavailable:model-fallback", {
477
- failedModel: endpoint.name,
478
- fallbackModel: fallback.name,
479
- });
480
- return runNativeWebSearch(request, runtime, ctx, fallback, signal, writeProgress);
538
+ throw error;
481
539
  }
540
+ logger.warn("web-search-model-fallback", {
541
+ failedModel: endpoint.name,
542
+ fallbackModel: fallback.name,
543
+ reason: "cross-region-disabled",
544
+ });
545
+ return runSelectedWebSearch(request, runtime, ctx, fallback, signal, writeProgress);
482
546
  }
483
547
 
484
548
  /** Execute one native provider request against an already-selected endpoint. */