@open-mercato/web-research 0.7.1-develop.7194.1.ab4fc81f82

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.
Files changed (121) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/AGENTS.md +67 -0
  3. package/build.mjs +10 -0
  4. package/dist/contract/adapter.js +13 -0
  5. package/dist/contract/adapter.js.map +7 -0
  6. package/dist/contract/errors.js +28 -0
  7. package/dist/contract/errors.js.map +7 -0
  8. package/dist/contract/http.js +7 -0
  9. package/dist/contract/http.js.map +7 -0
  10. package/dist/contract/index.js +9 -0
  11. package/dist/contract/index.js.map +7 -0
  12. package/dist/contract/outcomes.js +58 -0
  13. package/dist/contract/outcomes.js.map +7 -0
  14. package/dist/contract/policy.js +91 -0
  15. package/dist/contract/policy.js.map +7 -0
  16. package/dist/contract/results.js +13 -0
  17. package/dist/contract/results.js.map +7 -0
  18. package/dist/contract/steps.js +1 -0
  19. package/dist/contract/steps.js.map +7 -0
  20. package/dist/contract/version.js +11 -0
  21. package/dist/contract/version.js.map +7 -0
  22. package/dist/engine/cache.js +29 -0
  23. package/dist/engine/cache.js.map +7 -0
  24. package/dist/engine/deadline.js +54 -0
  25. package/dist/engine/deadline.js.map +7 -0
  26. package/dist/engine/engine.js +481 -0
  27. package/dist/engine/engine.js.map +7 -0
  28. package/dist/engine/index.js +20 -0
  29. package/dist/engine/index.js.map +7 -0
  30. package/dist/engine/runAdapter.js +57 -0
  31. package/dist/engine/runAdapter.js.map +7 -0
  32. package/dist/engine/types.js +1 -0
  33. package/dist/engine/types.js.map +7 -0
  34. package/dist/extract/classify.js +51 -0
  35. package/dist/extract/classify.js.map +7 -0
  36. package/dist/extract/content.js +104 -0
  37. package/dist/extract/content.js.map +7 -0
  38. package/dist/extract/entities.js +95 -0
  39. package/dist/extract/entities.js.map +7 -0
  40. package/dist/extract/index.js +24 -0
  41. package/dist/extract/index.js.map +7 -0
  42. package/dist/extract/text.js +114 -0
  43. package/dist/extract/text.js.map +7 -0
  44. package/dist/extract/tokenizer.js +141 -0
  45. package/dist/extract/tokenizer.js.map +7 -0
  46. package/dist/fusion/canonicalize.js +88 -0
  47. package/dist/fusion/canonicalize.js.map +7 -0
  48. package/dist/fusion/fuse.js +88 -0
  49. package/dist/fusion/fuse.js.map +7 -0
  50. package/dist/fusion/index.js +11 -0
  51. package/dist/fusion/index.js.map +7 -0
  52. package/dist/index.js +21 -0
  53. package/dist/index.js.map +7 -0
  54. package/dist/loader/describe.js +92 -0
  55. package/dist/loader/describe.js.map +7 -0
  56. package/dist/loader/index.js +31 -0
  57. package/dist/loader/index.js.map +7 -0
  58. package/dist/loader/manifest.js +32 -0
  59. package/dist/loader/manifest.js.map +7 -0
  60. package/dist/loader/registry.js +98 -0
  61. package/dist/loader/registry.js.map +7 -0
  62. package/dist/net/client.js +298 -0
  63. package/dist/net/client.js.map +7 -0
  64. package/dist/net/ssrf.js +165 -0
  65. package/dist/net/ssrf.js.map +7 -0
  66. package/dist/net/url.js +9 -0
  67. package/dist/net/url.js.map +7 -0
  68. package/dist/testing/contract.js +118 -0
  69. package/dist/testing/contract.js.map +7 -0
  70. package/dist/testing/fakeAdapter.js +44 -0
  71. package/dist/testing/fakeAdapter.js.map +7 -0
  72. package/dist/testing/index.js +15 -0
  73. package/dist/testing/index.js.map +7 -0
  74. package/jest.config.cjs +24 -0
  75. package/package.json +65 -0
  76. package/src/contract/adapter.ts +100 -0
  77. package/src/contract/errors.ts +48 -0
  78. package/src/contract/http.ts +46 -0
  79. package/src/contract/index.ts +8 -0
  80. package/src/contract/outcomes.ts +91 -0
  81. package/src/contract/policy.ts +157 -0
  82. package/src/contract/results.ts +79 -0
  83. package/src/contract/steps.ts +45 -0
  84. package/src/contract/version.ts +25 -0
  85. package/src/engine/__tests__/engine.test.ts +597 -0
  86. package/src/engine/cache.ts +46 -0
  87. package/src/engine/deadline.ts +65 -0
  88. package/src/engine/engine.ts +633 -0
  89. package/src/engine/index.ts +22 -0
  90. package/src/engine/runAdapter.ts +89 -0
  91. package/src/engine/types.ts +137 -0
  92. package/src/extract/__tests__/content.test.ts +81 -0
  93. package/src/extract/__tests__/text.test.ts +61 -0
  94. package/src/extract/__tests__/tokenizer.test.ts +85 -0
  95. package/src/extract/classify.ts +81 -0
  96. package/src/extract/content.ts +136 -0
  97. package/src/extract/entities.ts +98 -0
  98. package/src/extract/index.ts +11 -0
  99. package/src/extract/text.ts +124 -0
  100. package/src/extract/tokenizer.ts +197 -0
  101. package/src/fusion/__tests__/fusion.test.ts +198 -0
  102. package/src/fusion/canonicalize.ts +119 -0
  103. package/src/fusion/fuse.ts +145 -0
  104. package/src/fusion/index.ts +8 -0
  105. package/src/index.ts +15 -0
  106. package/src/loader/__tests__/describe.test.ts +119 -0
  107. package/src/loader/__tests__/registry.test.ts +134 -0
  108. package/src/loader/describe.ts +145 -0
  109. package/src/loader/index.ts +25 -0
  110. package/src/loader/manifest.ts +44 -0
  111. package/src/loader/registry.ts +156 -0
  112. package/src/net/__tests__/client.test.ts +273 -0
  113. package/src/net/__tests__/ssrf.test.ts +165 -0
  114. package/src/net/client.ts +434 -0
  115. package/src/net/ssrf.ts +235 -0
  116. package/src/net/url.ts +13 -0
  117. package/src/testing/contract.ts +172 -0
  118. package/src/testing/fakeAdapter.ts +69 -0
  119. package/src/testing/index.ts +9 -0
  120. package/tsconfig.json +10 -0
  121. package/watch.mjs +7 -0
@@ -0,0 +1,481 @@
1
+ import { silentLogger } from "../contract/http.js";
2
+ import { timedOut, toOutcomeFailure } from "../contract/outcomes.js";
3
+ import {
4
+ searchRequestSchema
5
+ } from "../contract/results.js";
6
+ import { classifyPage } from "../extract/classify.js";
7
+ import { extractMainContent } from "../extract/content.js";
8
+ import { extractTitle, htmlToText } from "../extract/text.js";
9
+ import { fuseResults } from "../fusion/fuse.js";
10
+ import { buildCacheKey, createSingleFlight } from "./cache.js";
11
+ import { DEADLINE_REACHED, deadlineRace, linkSignals } from "./deadline.js";
12
+ import { outcomeReason, outcomeToStatus, runAdapter } from "./runAdapter.js";
13
+ const TEXTUAL_CONTENT = ["text/", "application/xhtml+xml", "application/json", "application/xml"];
14
+ const HEALTH_TIMEOUT_MS = 5e3;
15
+ const PROBE_COST_RANK = { free: 0, heavy: 1, billable: 2 };
16
+ function resolveProbeCost(adapter) {
17
+ return adapter.probeCost ?? (adapter.capabilities.cost === "metered" ? "billable" : "free");
18
+ }
19
+ function statusToEvent(status) {
20
+ return status === "skipped" ? "unavailable" : status;
21
+ }
22
+ function normalizeRequest(input, policy) {
23
+ const parsed = searchRequestSchema.parse(input);
24
+ return {
25
+ request: {
26
+ query: parsed.query,
27
+ limit: parsed.limit ?? 10,
28
+ ...parsed.locale ? { locale: parsed.locale } : {},
29
+ ...parsed.freshness ? { freshness: parsed.freshness } : {},
30
+ ...parsed.site ? { site: parsed.site } : {}
31
+ },
32
+ includeContent: parsed.includeContent ?? policy.content.enabledByDefault
33
+ };
34
+ }
35
+ function createSearchEngine(options) {
36
+ const { policy, http } = options;
37
+ const logger = options.logger ?? silentLogger;
38
+ const now = options.now ?? Date.now;
39
+ const singleFlight = createSingleFlight();
40
+ const ordered = [...options.adapters].sort((left, right) => left.order - right.order);
41
+ const browserEntries = ordered.filter((entry) => entry.adapter.kind === "browser");
42
+ function createReporter(sinks) {
43
+ const active = sinks.filter((sink) => sink !== void 0);
44
+ let seq = 0;
45
+ return (phase, event, adapterId, detail, metrics) => {
46
+ if (active.length === 0) return;
47
+ const step = {
48
+ seq: seq++,
49
+ at: new Date(now()).toISOString(),
50
+ phase,
51
+ event,
52
+ ...adapterId ? { adapterId } : {},
53
+ ...detail ? { detail } : {},
54
+ ...metrics ? { metrics } : {}
55
+ };
56
+ for (const sink of active) {
57
+ try {
58
+ sink(step);
59
+ } catch (error) {
60
+ logger.warn("[web-research] step sink threw", { error: String(error) });
61
+ }
62
+ }
63
+ };
64
+ }
65
+ async function enrichContent(results, signal, deadlineAt, report) {
66
+ const targets = results.filter((result) => result.content === null).slice(0, policy.content.maxPages);
67
+ if (targets.length === 0) return { results, pagesRead: 0 };
68
+ report("started", `reading ${targets.length} page(s) for inline content`);
69
+ const contentByUrl = /* @__PURE__ */ new Map();
70
+ let pagesRead = 0;
71
+ const queue = [...targets];
72
+ const workers = Array.from({ length: Math.min(policy.concurrency, queue.length) }, async () => {
73
+ for (let next = queue.shift(); next !== void 0; next = queue.shift()) {
74
+ if (signal.aborted || now() >= deadlineAt) return;
75
+ pagesRead += 1;
76
+ const outcome = await fetchPage(
77
+ { url: next.url, maxBytes: policy.content.maxBytesPerPage },
78
+ { signal }
79
+ );
80
+ if (outcome.status === "ok" && outcome.page.text.trim().length > 0) {
81
+ contentByUrl.set(next.url, outcome.page.text);
82
+ }
83
+ }
84
+ });
85
+ await Promise.all(workers);
86
+ report("ok", `read ${contentByUrl.size} of ${targets.length} page(s)`, {
87
+ resultCount: contentByUrl.size
88
+ });
89
+ return {
90
+ results: results.map((result) => {
91
+ const content = contentByUrl.get(result.url);
92
+ return content === void 0 ? result : { ...result, content };
93
+ }),
94
+ pagesRead
95
+ };
96
+ }
97
+ async function fetchPage(request, runOptions = {}) {
98
+ const maxBytes = request.maxBytes ?? policy.content.maxBytesPerPage;
99
+ const render = request.render ?? "auto";
100
+ if (render !== "always") {
101
+ try {
102
+ const response = await http.request(request.url, {
103
+ maxBytes,
104
+ accept: TEXTUAL_CONTENT,
105
+ ...runOptions.signal ? { signal: runOptions.signal } : {}
106
+ });
107
+ const isHtml = response.contentType === null || response.contentType.includes("html");
108
+ const text = isHtml ? extractMainContent(response.body) || htmlToText(response.body) : response.body;
109
+ const classification = classifyPage({
110
+ status: response.status,
111
+ contentType: response.contentType,
112
+ html: response.body,
113
+ text
114
+ });
115
+ const page = {
116
+ url: response.url,
117
+ title: isHtml ? extractTitle(response.body) : null,
118
+ text,
119
+ contentType: response.contentType,
120
+ status: response.status,
121
+ truncated: response.truncated,
122
+ renderedWith: "http"
123
+ };
124
+ if (classification.verdict === "ok" || render === "never") return { status: "ok", page };
125
+ const escalated = await escalateFetch(request, classification.verdict, maxBytes, runOptions);
126
+ if (escalated?.status === "ok") return escalated;
127
+ return { status: "ok", page: { ...page, escalatedBecause: classification.verdict } };
128
+ } catch (error) {
129
+ const failure = toOutcomeFailure(error);
130
+ if (render !== "never" && failure.status === "blocked") {
131
+ const escalated = await escalateFetch(request, "blocked", maxBytes, runOptions);
132
+ if (escalated?.status === "ok") return escalated;
133
+ }
134
+ return failure;
135
+ }
136
+ }
137
+ const rendered = await escalateFetch(request, "ok", maxBytes, runOptions);
138
+ return rendered ?? { status: "unavailable", reason: "no browser adapter is configured for rendering" };
139
+ }
140
+ async function withFetchDeadline(budgetMs, parentSignal, run) {
141
+ const budget = Math.max(0, budgetMs);
142
+ const controller = new AbortController();
143
+ const linked = linkSignals(parentSignal ? [parentSignal, controller.signal] : [controller.signal]);
144
+ let timer;
145
+ const timeout = new Promise((resolve) => {
146
+ timer = setTimeout(() => {
147
+ controller.abort();
148
+ resolve({ kind: "deadline" });
149
+ }, budget);
150
+ timer.unref?.();
151
+ });
152
+ try {
153
+ const raced = await Promise.race([
154
+ run(linked.signal).then((outcome) => ({ kind: "outcome", outcome })).catch((error) => ({ kind: "outcome", outcome: toOutcomeFailure(error) })),
155
+ timeout
156
+ ]);
157
+ return raced.kind === "deadline" ? timedOut(`fetch exceeded ${budget}ms`) : raced.outcome;
158
+ } finally {
159
+ if (timer) clearTimeout(timer);
160
+ linked.dispose();
161
+ }
162
+ }
163
+ async function escalateFetch(request, because, maxBytes, runOptions) {
164
+ if (!policy.escalateToBrowser) return null;
165
+ const entry = browserEntries.find(
166
+ (candidate) => candidate.enabled && candidate.adapter.fetch !== void 0 && candidate.adapter.readiness().ready
167
+ );
168
+ if (!entry?.adapter.fetch) return null;
169
+ const browserFetch = entry.adapter.fetch;
170
+ return withFetchDeadline(policy.hardDeadlineMs, runOptions.signal, async (signal) => {
171
+ const outcome = await browserFetch(
172
+ { url: request.url, maxBytes, render: "always" },
173
+ { signal, http, report: () => {
174
+ }, deadlineAt: now() + policy.hardDeadlineMs, logger }
175
+ );
176
+ if (outcome.status !== "ok") return outcome;
177
+ return { status: "ok", page: { ...outcome.page, renderedWith: "browser", escalatedBecause: because } };
178
+ });
179
+ }
180
+ async function runSearch(request, includeContent, runOptions, report) {
181
+ const startedAt = now();
182
+ const deadlineAt = startedAt + policy.hardDeadlineMs;
183
+ const softDeadlineAt = startedAt + policy.softDeadlineMs;
184
+ const diagnostics = [];
185
+ const collected = [];
186
+ const answersById = /* @__PURE__ */ new Map();
187
+ const attempted = /* @__PURE__ */ new Set();
188
+ const blockedAdapters = [];
189
+ let successCount = 0;
190
+ let totalResults = 0;
191
+ let answer = null;
192
+ const searchable = [];
193
+ for (const entry of ordered) {
194
+ if (entry.adapter.kind === "browser") continue;
195
+ if (!entry.enabled) continue;
196
+ if (!entry.adapter.capabilities.search) continue;
197
+ const readiness = entry.adapter.readiness();
198
+ if (!readiness.ready) {
199
+ diagnostics.push({
200
+ id: entry.adapter.id,
201
+ status: "skipped",
202
+ latencyMs: 0,
203
+ resultCount: 0,
204
+ reason: readiness.reason
205
+ });
206
+ report("plan", "unavailable", entry.adapter.id, readiness.reason);
207
+ continue;
208
+ }
209
+ searchable.push(entry);
210
+ report("plan", "queued", entry.adapter.id);
211
+ }
212
+ const waveController = new AbortController();
213
+ const wave = linkSignals([runOptions.signal, waveController.signal]);
214
+ const adapterDeps = {
215
+ http,
216
+ logger,
217
+ now,
218
+ report: (adapterId, event, detail, metrics) => report("adapter", event, adapterId, detail, metrics),
219
+ parentSignal: wave.signal,
220
+ deadlineAt,
221
+ adapterTimeoutMs: policy.adapterTimeoutMs
222
+ };
223
+ const queue = [...searchable];
224
+ const inflight = /* @__PURE__ */ new Map();
225
+ const absorb = (settled) => {
226
+ const id = settled.entry.adapter.id;
227
+ inflight.delete(id);
228
+ const status = outcomeToStatus(settled.outcome);
229
+ const resultCount = settled.outcome.status === "ok" ? settled.outcome.results.length : 0;
230
+ const reason = outcomeReason(settled.outcome);
231
+ diagnostics.push({
232
+ id,
233
+ status,
234
+ latencyMs: settled.latencyMs,
235
+ resultCount,
236
+ ...reason ? { reason } : {}
237
+ });
238
+ report("adapter", statusToEvent(status), id, reason, {
239
+ latencyMs: settled.latencyMs,
240
+ resultCount
241
+ });
242
+ if (settled.outcome.status === "ok") {
243
+ successCount += 1;
244
+ totalResults += resultCount;
245
+ collected.push({
246
+ adapterId: id,
247
+ weight: settled.entry.weight,
248
+ results: settled.outcome.results
249
+ });
250
+ if (settled.outcome.answer) answersById.set(id, settled.outcome.answer);
251
+ if (answer === null && settled.outcome.answer) answer = settled.outcome.answer;
252
+ }
253
+ if (status === "blocked") blockedAdapters.push(id);
254
+ };
255
+ const launch = () => {
256
+ while (inflight.size < policy.concurrency && queue.length > 0) {
257
+ const entry = queue.shift();
258
+ if (!entry) break;
259
+ attempted.add(entry.adapter.id);
260
+ inflight.set(entry.adapter.id, runAdapter(entry, request, adapterDeps));
261
+ }
262
+ };
263
+ const satisfied = () => {
264
+ if (policy.settleMode === "exhaustive") return false;
265
+ if (totalResults < policy.minResults) return false;
266
+ if (policy.settleMode === "race") return true;
267
+ return successCount >= policy.minAdapters;
268
+ };
269
+ launch();
270
+ while (inflight.size > 0) {
271
+ const barrier = totalResults > 0 && policy.settleMode !== "exhaustive" ? softDeadlineAt : deadlineAt;
272
+ const remaining = barrier - now();
273
+ if (remaining <= 0) break;
274
+ const timer = deadlineRace(remaining);
275
+ let winner;
276
+ try {
277
+ winner = await Promise.race([...inflight.values(), timer.promise]);
278
+ } finally {
279
+ timer.cancel();
280
+ }
281
+ if (winner === DEADLINE_REACHED) break;
282
+ absorb(winner);
283
+ if (satisfied()) break;
284
+ launch();
285
+ }
286
+ if (inflight.size > 0 || queue.length > 0) {
287
+ waveController.abort();
288
+ const elapsed = now() - startedAt;
289
+ for (const id of inflight.keys()) {
290
+ diagnostics.push({ id, status: "cancelled", latencyMs: elapsed, resultCount: 0 });
291
+ report("adapter", "cancelled", id);
292
+ }
293
+ for (const promise of inflight.values()) promise.catch(() => void 0);
294
+ inflight.clear();
295
+ }
296
+ wave.dispose();
297
+ let escalated = false;
298
+ if (policy.escalateToBrowser && totalResults < policy.minResults && blockedAdapters.length > 0 && now() < deadlineAt) {
299
+ const entry = browserEntries.find(
300
+ (candidate) => candidate.enabled && candidate.adapter.capabilities.search && candidate.adapter.readiness().ready && !attempted.has(candidate.adapter.id)
301
+ );
302
+ if (entry) {
303
+ attempted.add(entry.adapter.id);
304
+ escalated = true;
305
+ report(
306
+ "adapter",
307
+ "escalated",
308
+ entry.adapter.id,
309
+ `retrying with a browser after ${blockedAdapters.join(", ")} were blocked`
310
+ );
311
+ const escalation = linkSignals([runOptions.signal]);
312
+ try {
313
+ absorb(await runAdapter(entry, request, { ...adapterDeps, parentSignal: escalation.signal }));
314
+ } finally {
315
+ escalation.dispose();
316
+ }
317
+ }
318
+ }
319
+ if (totalResults < policy.minResults && policy.lastResort !== null && now() < deadlineAt) {
320
+ const entry = ordered.find(
321
+ (candidate) => candidate.adapter.id === policy.lastResort && candidate.adapter.capabilities.search && !attempted.has(candidate.adapter.id)
322
+ );
323
+ const readiness = entry?.adapter.readiness();
324
+ if (entry && readiness?.ready) {
325
+ attempted.add(entry.adapter.id);
326
+ report("adapter", "started", entry.adapter.id, "last-resort adapter");
327
+ const fallback = linkSignals([runOptions.signal]);
328
+ try {
329
+ absorb(await runAdapter(entry, request, { ...adapterDeps, parentSignal: fallback.signal }));
330
+ } finally {
331
+ fallback.dispose();
332
+ }
333
+ }
334
+ }
335
+ const fused = fuseResults(collected, {
336
+ limit: request.limit,
337
+ minResults: policy.minResults,
338
+ minConfidence: policy.minConfidence,
339
+ maxPerDomain: policy.maxPerDomain
340
+ });
341
+ report("fusion", "fused", void 0, fused.degraded ? "confidence threshold relaxed" : void 0, {
342
+ resultCount: fused.results.length
343
+ });
344
+ let results = fused.results;
345
+ let pagesRead = 0;
346
+ if (includeContent && results.length > 0) {
347
+ const expiry = new AbortController();
348
+ const timer = setTimeout(() => expiry.abort(), Math.max(0, deadlineAt - now()));
349
+ timer.unref?.();
350
+ const enrichment = linkSignals([runOptions.signal, expiry.signal]);
351
+ try {
352
+ const enriched = await enrichContent(
353
+ results,
354
+ enrichment.signal,
355
+ deadlineAt,
356
+ (event, detail, metrics) => report("fetch", event, void 0, detail, metrics)
357
+ );
358
+ results = enriched.results;
359
+ pagesRead = enriched.pagesRead;
360
+ } finally {
361
+ clearTimeout(timer);
362
+ enrichment.dispose();
363
+ }
364
+ }
365
+ const elapsedMs = now() - startedAt;
366
+ report("done", "completed", void 0, void 0, { latencyMs: elapsedMs, resultCount: results.length });
367
+ const lostSource = diagnostics.some(
368
+ (entry) => entry.status === "blocked" || entry.status === "timeout" || entry.status === "error"
369
+ );
370
+ return {
371
+ results,
372
+ answer,
373
+ diagnostics: {
374
+ adapters: diagnostics,
375
+ degraded: fused.degraded || results.length === 0 || lostSource,
376
+ cached: false,
377
+ escalated,
378
+ elapsedMs,
379
+ pagesRead
380
+ },
381
+ ...runOptions.includeAdapterResults ? {
382
+ byAdapter: collected.map((input) => ({
383
+ adapterId: input.adapterId,
384
+ weight: input.weight,
385
+ answer: answersById.get(input.adapterId) ?? null,
386
+ results: input.results
387
+ }))
388
+ } : {}
389
+ };
390
+ }
391
+ async function search(input, runOptions = {}) {
392
+ const report = createReporter([options.onStep, runOptions.onStep]);
393
+ const { request, includeContent } = normalizeRequest(input, policy);
394
+ const adapterIds = ordered.filter((entry) => entry.enabled).map((entry) => entry.adapter.id);
395
+ const compare = runOptions.includeAdapterResults === true;
396
+ const cacheKey = `${buildCacheKey(request, adapterIds)}:${includeContent ? "full" : "lite"}${compare ? ":compare" : ""}`;
397
+ const useCache = options.cache !== void 0 && policy.cacheTtlMs > 0 && !compare;
398
+ if (useCache) {
399
+ const cached = await options.cache.get(cacheKey).catch(() => null);
400
+ if (cached) {
401
+ report("done", "cached", void 0, "served from cache", { resultCount: cached.results.length });
402
+ return { ...cached, diagnostics: { ...cached.diagnostics, cached: true, pagesRead: 0 } };
403
+ }
404
+ }
405
+ const result = await singleFlight(cacheKey, () => runSearch(request, includeContent, runOptions, report));
406
+ if (useCache && result.results.length > 0) {
407
+ await options.cache.set(cacheKey, result, policy.cacheTtlMs).catch(() => void 0);
408
+ }
409
+ return result;
410
+ }
411
+ async function health(runOptions = {}) {
412
+ const probe = runOptions.probe ?? true;
413
+ const budget = PROBE_COST_RANK[runOptions.maxProbeCost ?? "billable"];
414
+ const only = runOptions.only ? new Set(runOptions.only) : null;
415
+ return Promise.all(
416
+ ordered.map(async (entry) => {
417
+ const probeCost = resolveProbeCost(entry.adapter);
418
+ const readiness = entry.adapter.readiness();
419
+ if (!readiness.ready) {
420
+ return { id: entry.adapter.id, ready: false, ok: false, detail: readiness.reason, probeCost, probed: false };
421
+ }
422
+ const withinBudget = PROBE_COST_RANK[probeCost] <= budget && (!only || only.has(entry.adapter.id));
423
+ if (!probe || !withinBudget || !entry.adapter.healthCheck) {
424
+ return { id: entry.adapter.id, ready: true, ok: true, probeCost, probed: false };
425
+ }
426
+ const startedAt = now();
427
+ const controller = new AbortController();
428
+ const timer = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
429
+ timer.unref?.();
430
+ const linked = linkSignals([runOptions.signal, controller.signal]);
431
+ try {
432
+ const result = await entry.adapter.healthCheck({
433
+ signal: linked.signal,
434
+ http,
435
+ report: () => {
436
+ },
437
+ deadlineAt: startedAt + HEALTH_TIMEOUT_MS,
438
+ logger
439
+ });
440
+ return { id: entry.adapter.id, ready: true, latencyMs: now() - startedAt, probeCost, probed: true, ...result };
441
+ } catch (error) {
442
+ return {
443
+ id: entry.adapter.id,
444
+ ready: true,
445
+ ok: false,
446
+ detail: error instanceof Error ? error.message : String(error),
447
+ latencyMs: now() - startedAt,
448
+ probeCost,
449
+ probed: true
450
+ };
451
+ } finally {
452
+ clearTimeout(timer);
453
+ linked.dispose();
454
+ }
455
+ })
456
+ );
457
+ }
458
+ async function dispose() {
459
+ await Promise.all(
460
+ ordered.map(async (entry) => {
461
+ try {
462
+ await entry.adapter.dispose?.();
463
+ } catch (error) {
464
+ logger.warn(`[web-research] ${entry.adapter.id} failed to dispose`, { error: String(error) });
465
+ }
466
+ })
467
+ );
468
+ }
469
+ async function boundedFetch(request, runOptions = {}) {
470
+ return withFetchDeadline(
471
+ policy.hardDeadlineMs,
472
+ runOptions.signal,
473
+ (signal) => fetchPage(request, { ...runOptions, signal })
474
+ );
475
+ }
476
+ return { search, fetch: boundedFetch, health, dispose };
477
+ }
478
+ export {
479
+ createSearchEngine
480
+ };
481
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/engine/engine.ts"],
4
+ "sourcesContent": ["import type { ProbeCost, SearchAdapter } from '../contract/adapter'\nimport { silentLogger } from '../contract/http'\nimport { timedOut, toOutcomeFailure, type FetchOutcome } from '../contract/outcomes'\nimport type { SearchPolicy } from '../contract/policy'\nimport {\n searchRequestSchema,\n type FetchRequest,\n type FetchedPage,\n type PageVerdict,\n type SearchRequest,\n type SearchRequestInput,\n type SearchResult,\n} from '../contract/results'\nimport type { SearchStep, SearchStepEvent, SearchStepMetrics, StepSink } from '../contract/steps'\nimport { classifyPage } from '../extract/classify'\nimport { extractMainContent } from '../extract/content'\nimport { extractTitle, htmlToText } from '../extract/text'\nimport { fuseResults, type FusionInput } from '../fusion/fuse'\nimport { buildCacheKey, createSingleFlight } from './cache'\nimport { DEADLINE_REACHED, deadlineRace, linkSignals } from './deadline'\nimport { outcomeReason, outcomeToStatus, runAdapter, type AdapterRunResult } from './runAdapter'\nimport type {\n AdapterDiagnostic,\n AdapterDiagnosticStatus,\n AdapterHealthReport,\n EngineAdapterEntry,\n HealthOptions,\n RunOptions,\n SearchEngine,\n SearchEngineOptions,\n SearchEngineResult,\n} from './types'\n\nconst TEXTUAL_CONTENT = ['text/', 'application/xhtml+xml', 'application/json', 'application/xml']\nconst HEALTH_TIMEOUT_MS = 5_000\n\nconst PROBE_COST_RANK: Record<ProbeCost, number> = { free: 0, heavy: 1, billable: 2 }\n\n/**\n * An adapter that does not declare `probeCost` is assumed to bill when it is a\n * metered source. Pessimistic on purpose: the cost of guessing wrong downwards\n * is a surprise invoice, and the cost of guessing wrong upwards is one row that\n * says \"not verified\" until somebody clicks.\n */\nfunction resolveProbeCost(adapter: SearchAdapter): ProbeCost {\n return adapter.probeCost ?? (adapter.capabilities.cost === 'metered' ? 'billable' : 'free')\n}\n\nfunction statusToEvent(status: AdapterDiagnosticStatus): SearchStepEvent {\n return status === 'skipped' ? 'unavailable' : status\n}\n\nfunction normalizeRequest(\n input: SearchRequestInput,\n policy: SearchPolicy,\n): { request: SearchRequest; includeContent: boolean } {\n const parsed = searchRequestSchema.parse(input)\n return {\n request: {\n query: parsed.query,\n limit: parsed.limit ?? 10,\n ...(parsed.locale ? { locale: parsed.locale } : {}),\n ...(parsed.freshness ? { freshness: parsed.freshness } : {}),\n ...(parsed.site ? { site: parsed.site } : {}),\n },\n includeContent: parsed.includeContent ?? policy.content.enabledByDefault,\n }\n}\n\nexport function createSearchEngine(options: SearchEngineOptions): SearchEngine {\n const { policy, http } = options\n const logger = options.logger ?? silentLogger\n const now = options.now ?? Date.now\n const singleFlight = createSingleFlight<SearchEngineResult>()\n\n const ordered = [...options.adapters].sort((left, right) => left.order - right.order)\n const browserEntries = ordered.filter((entry) => entry.adapter.kind === 'browser')\n\n function createReporter(sinks: ReadonlyArray<StepSink | undefined>) {\n const active = sinks.filter((sink): sink is StepSink => sink !== undefined)\n let seq = 0\n return (\n phase: SearchStep['phase'],\n event: SearchStepEvent,\n adapterId?: string,\n detail?: string,\n metrics?: SearchStepMetrics,\n ): void => {\n if (active.length === 0) return\n const step: SearchStep = {\n seq: seq++,\n at: new Date(now()).toISOString(),\n phase,\n event,\n ...(adapterId ? { adapterId } : {}),\n ...(detail ? { detail } : {}),\n ...(metrics ? { metrics } : {}),\n }\n for (const sink of active) {\n try {\n sink(step)\n } catch (error) {\n logger.warn('[web-research] step sink threw', { error: String(error) })\n }\n }\n }\n }\n\n async function enrichContent(\n results: readonly SearchResult[],\n signal: AbortSignal,\n deadlineAt: number,\n report: (event: SearchStepEvent, detail?: string, metrics?: SearchStepMetrics) => void,\n ): Promise<{ results: readonly SearchResult[]; pagesRead: number }> {\n const targets = results.filter((result) => result.content === null).slice(0, policy.content.maxPages)\n if (targets.length === 0) return { results, pagesRead: 0 }\n report('started', `reading ${targets.length} page(s) for inline content`)\n\n const contentByUrl = new Map<string, string>()\n // Counts pages we actually went to the network for, not pages that yielded\n // usable text: the budget this feeds is about egress, and a fetch that came\n // back empty still cost a request to somebody else's server.\n let pagesRead = 0\n const queue = [...targets]\n // Bounded by the same knob as the search wave: reading N pages at once is the\n // same egress burst as running N adapters, and it lands on third-party hosts.\n const workers = Array.from({ length: Math.min(policy.concurrency, queue.length) }, async () => {\n for (let next = queue.shift(); next !== undefined; next = queue.shift()) {\n // The run deadline binds here too \u2014 otherwise a search bounded to\n // `hardDeadlineMs` could spend minutes reading pages after fusion.\n if (signal.aborted || now() >= deadlineAt) return\n pagesRead += 1\n const outcome = await fetchPage(\n { url: next.url, maxBytes: policy.content.maxBytesPerPage },\n { signal },\n )\n if (outcome.status === 'ok' && outcome.page.text.trim().length > 0) {\n contentByUrl.set(next.url, outcome.page.text)\n }\n }\n })\n await Promise.all(workers)\n\n report('ok', `read ${contentByUrl.size} of ${targets.length} page(s)`, {\n resultCount: contentByUrl.size,\n })\n\n return {\n results: results.map((result) => {\n const content = contentByUrl.get(result.url)\n return content === undefined ? result : { ...result, content }\n }),\n pagesRead,\n }\n }\n\n async function fetchPage(request: FetchRequest, runOptions: RunOptions = {}): Promise<FetchOutcome> {\n const maxBytes = request.maxBytes ?? policy.content.maxBytesPerPage\n const render = request.render ?? 'auto'\n\n if (render !== 'always') {\n try {\n const response = await http.request(request.url, {\n maxBytes,\n accept: TEXTUAL_CONTENT,\n ...(runOptions.signal ? { signal: runOptions.signal } : {}),\n })\n const isHtml = response.contentType === null || response.contentType.includes('html')\n const text = isHtml ? extractMainContent(response.body) || htmlToText(response.body) : response.body\n const classification = classifyPage({\n status: response.status,\n contentType: response.contentType,\n html: response.body,\n text,\n })\n const page: FetchedPage = {\n url: response.url,\n title: isHtml ? extractTitle(response.body) : null,\n text,\n contentType: response.contentType,\n status: response.status,\n truncated: response.truncated,\n renderedWith: 'http',\n }\n if (classification.verdict === 'ok' || render === 'never') return { status: 'ok', page }\n\n // Escalation is best-effort: a browser failure must not lose the text we\n // already have, so an unusable render falls back to the annotated HTTP page.\n const escalated = await escalateFetch(request, classification.verdict, maxBytes, runOptions)\n if (escalated?.status === 'ok') return escalated\n return { status: 'ok', page: { ...page, escalatedBecause: classification.verdict } }\n } catch (error) {\n const failure = toOutcomeFailure(error)\n if (render !== 'never' && failure.status === 'blocked') {\n const escalated = await escalateFetch(request, 'blocked', maxBytes, runOptions)\n if (escalated?.status === 'ok') return escalated\n }\n return failure\n }\n }\n\n const rendered = await escalateFetch(request, 'ok', maxBytes, runOptions)\n return rendered ?? { status: 'unavailable', reason: 'no browser adapter is configured for rendering' }\n }\n\n /**\n * Races a fetch against a hard wall-clock budget so an adapter that ignores its\n * abort signal \u2014 a wedged headless-browser navigation is the classic case \u2014\n * can never hang the caller. When the timer fires it both aborts the signal (so\n * a cooperative adapter cancels) and wins the race, returning a `timeout`\n * outcome; the leaked adapter promise keeps running in the background without\n * blocking anyone. This is the abort+deadline the search scheduler already\n * gives the adapter wave (`runAdapter` + `deadlineRace`); the fetch/escalation\n * path previously passed only a cooperative signal and awaited directly, so a\n * misbehaving browser render could pin the run for the full parent deadline.\n */\n async function withFetchDeadline(\n budgetMs: number,\n parentSignal: AbortSignal | undefined,\n run: (signal: AbortSignal) => Promise<FetchOutcome>,\n ): Promise<FetchOutcome> {\n const budget = Math.max(0, budgetMs)\n const controller = new AbortController()\n const linked = linkSignals(parentSignal ? [parentSignal, controller.signal] : [controller.signal])\n let timer: ReturnType<typeof setTimeout> | undefined\n const timeout = new Promise<{ readonly kind: 'deadline' }>((resolve) => {\n timer = setTimeout(() => {\n controller.abort()\n resolve({ kind: 'deadline' })\n }, budget)\n timer.unref?.()\n })\n try {\n const raced = await Promise.race([\n run(linked.signal)\n .then((outcome) => ({ kind: 'outcome' as const, outcome }))\n .catch((error) => ({ kind: 'outcome' as const, outcome: toOutcomeFailure(error) })),\n timeout,\n ])\n return raced.kind === 'deadline' ? timedOut(`fetch exceeded ${budget}ms`) : raced.outcome\n } finally {\n if (timer) clearTimeout(timer)\n linked.dispose()\n }\n }\n\n /** Re-reads a page through a browser adapter when the HTTP read was insufficient. */\n async function escalateFetch(\n request: FetchRequest,\n because: PageVerdict,\n maxBytes: number,\n runOptions: RunOptions,\n ): Promise<FetchOutcome | null> {\n if (!policy.escalateToBrowser) return null\n const entry = browserEntries.find(\n (candidate) =>\n candidate.enabled && candidate.adapter.fetch !== undefined && candidate.adapter.readiness().ready,\n )\n if (!entry?.adapter.fetch) return null\n const browserFetch = entry.adapter.fetch\n\n return withFetchDeadline(policy.hardDeadlineMs, runOptions.signal, async (signal) => {\n const outcome = await browserFetch(\n { url: request.url, maxBytes, render: 'always' },\n { signal, http, report: () => {}, deadlineAt: now() + policy.hardDeadlineMs, logger },\n )\n if (outcome.status !== 'ok') return outcome\n return { status: 'ok', page: { ...outcome.page, renderedWith: 'browser', escalatedBecause: because } }\n })\n }\n\n async function runSearch(\n request: SearchRequest,\n includeContent: boolean,\n runOptions: RunOptions,\n report: ReturnType<typeof createReporter>,\n ): Promise<SearchEngineResult> {\n const startedAt = now()\n const deadlineAt = startedAt + policy.hardDeadlineMs\n const softDeadlineAt = startedAt + policy.softDeadlineMs\n\n const diagnostics: AdapterDiagnostic[] = []\n const collected: FusionInput[] = []\n const answersById = new Map<string, string>()\n const attempted = new Set<string>()\n const blockedAdapters: string[] = []\n let successCount = 0\n let totalResults = 0\n let answer: string | null = null\n\n const searchable: EngineAdapterEntry[] = []\n for (const entry of ordered) {\n if (entry.adapter.kind === 'browser') continue\n if (!entry.enabled) continue\n if (!entry.adapter.capabilities.search) continue\n const readiness = entry.adapter.readiness()\n if (!readiness.ready) {\n diagnostics.push({\n id: entry.adapter.id,\n status: 'skipped',\n latencyMs: 0,\n resultCount: 0,\n reason: readiness.reason,\n })\n report('plan', 'unavailable', entry.adapter.id, readiness.reason)\n continue\n }\n searchable.push(entry)\n report('plan', 'queued', entry.adapter.id)\n }\n\n const waveController = new AbortController()\n const wave = linkSignals([runOptions.signal, waveController.signal])\n const adapterDeps = {\n http,\n logger,\n now,\n report: (adapterId: string, event: SearchStepEvent, detail?: string, metrics?: SearchStepMetrics) =>\n report('adapter', event, adapterId, detail, metrics),\n parentSignal: wave.signal,\n deadlineAt,\n adapterTimeoutMs: policy.adapterTimeoutMs,\n }\n\n const queue = [...searchable]\n const inflight = new Map<string, Promise<AdapterRunResult>>()\n\n const absorb = (settled: AdapterRunResult): void => {\n const id = settled.entry.adapter.id\n inflight.delete(id)\n const status = outcomeToStatus(settled.outcome)\n const resultCount = settled.outcome.status === 'ok' ? settled.outcome.results.length : 0\n const reason = outcomeReason(settled.outcome)\n diagnostics.push({\n id,\n status,\n latencyMs: settled.latencyMs,\n resultCount,\n ...(reason ? { reason } : {}),\n })\n report('adapter', statusToEvent(status), id, reason, {\n latencyMs: settled.latencyMs,\n resultCount,\n })\n if (settled.outcome.status === 'ok') {\n successCount += 1\n totalResults += resultCount\n collected.push({\n adapterId: id,\n weight: settled.entry.weight,\n results: settled.outcome.results,\n })\n // The top-level answer stays first-wins, but every adapter's prose is\n // kept so a comparison run can show them side by side.\n if (settled.outcome.answer) answersById.set(id, settled.outcome.answer)\n if (answer === null && settled.outcome.answer) answer = settled.outcome.answer\n }\n if (status === 'blocked') blockedAdapters.push(id)\n }\n\n const launch = (): void => {\n while (inflight.size < policy.concurrency && queue.length > 0) {\n const entry = queue.shift()\n if (!entry) break\n attempted.add(entry.adapter.id)\n inflight.set(entry.adapter.id, runAdapter(entry, request, adapterDeps))\n }\n }\n\n const satisfied = (): boolean => {\n if (policy.settleMode === 'exhaustive') return false\n if (totalResults < policy.minResults) return false\n if (policy.settleMode === 'race') return true\n return successCount >= policy.minAdapters\n }\n\n launch()\n while (inflight.size > 0) {\n // Past the soft deadline we only keep waiting while we have nothing at all;\n // holding an answer back for a straggler is worse than answering now.\n const barrier =\n totalResults > 0 && policy.settleMode !== 'exhaustive' ? softDeadlineAt : deadlineAt\n const remaining = barrier - now()\n if (remaining <= 0) break\n const timer = deadlineRace(remaining)\n let winner: AdapterRunResult | typeof DEADLINE_REACHED\n try {\n winner = await Promise.race([...inflight.values(), timer.promise])\n } finally {\n timer.cancel()\n }\n if (winner === DEADLINE_REACHED) break\n absorb(winner)\n if (satisfied()) break\n launch()\n }\n\n if (inflight.size > 0 || queue.length > 0) {\n waveController.abort()\n const elapsed = now() - startedAt\n for (const id of inflight.keys()) {\n diagnostics.push({ id, status: 'cancelled', latencyMs: elapsed, resultCount: 0 })\n report('adapter', 'cancelled', id)\n }\n for (const promise of inflight.values()) promise.catch(() => undefined)\n inflight.clear()\n }\n wave.dispose()\n\n let escalated = false\n if (\n policy.escalateToBrowser &&\n totalResults < policy.minResults &&\n blockedAdapters.length > 0 &&\n now() < deadlineAt\n ) {\n const entry = browserEntries.find(\n (candidate) =>\n candidate.enabled &&\n candidate.adapter.capabilities.search &&\n candidate.adapter.readiness().ready &&\n !attempted.has(candidate.adapter.id),\n )\n if (entry) {\n attempted.add(entry.adapter.id)\n escalated = true\n report(\n 'adapter',\n 'escalated',\n entry.adapter.id,\n `retrying with a browser after ${blockedAdapters.join(', ')} were blocked`,\n )\n const escalation = linkSignals([runOptions.signal])\n try {\n absorb(await runAdapter(entry, request, { ...adapterDeps, parentSignal: escalation.signal }))\n } finally {\n escalation.dispose()\n }\n }\n }\n\n if (totalResults < policy.minResults && policy.lastResort !== null && now() < deadlineAt) {\n const entry = ordered.find(\n (candidate) =>\n candidate.adapter.id === policy.lastResort &&\n candidate.adapter.capabilities.search &&\n !attempted.has(candidate.adapter.id),\n )\n const readiness = entry?.adapter.readiness()\n if (entry && readiness?.ready) {\n attempted.add(entry.adapter.id)\n report('adapter', 'started', entry.adapter.id, 'last-resort adapter')\n const fallback = linkSignals([runOptions.signal])\n try {\n absorb(await runAdapter(entry, request, { ...adapterDeps, parentSignal: fallback.signal }))\n } finally {\n fallback.dispose()\n }\n }\n }\n\n const fused = fuseResults(collected, {\n limit: request.limit,\n minResults: policy.minResults,\n minConfidence: policy.minConfidence,\n maxPerDomain: policy.maxPerDomain,\n })\n report('fusion', 'fused', undefined, fused.degraded ? 'confidence threshold relaxed' : undefined, {\n resultCount: fused.results.length,\n })\n\n let results = fused.results\n let pagesRead = 0\n if (includeContent && results.length > 0) {\n const expiry = new AbortController()\n const timer = setTimeout(() => expiry.abort(), Math.max(0, deadlineAt - now()))\n timer.unref?.()\n const enrichment = linkSignals([runOptions.signal, expiry.signal])\n try {\n const enriched = await enrichContent(results, enrichment.signal, deadlineAt, (event, detail, metrics) =>\n report('fetch', event, undefined, detail, metrics),\n )\n results = enriched.results\n pagesRead = enriched.pagesRead\n } finally {\n clearTimeout(timer)\n enrichment.dispose()\n }\n }\n\n const elapsedMs = now() - startedAt\n report('done', 'completed', undefined, undefined, { latencyMs: elapsedMs, resultCount: results.length })\n\n // `degraded` means the engine could not do its job properly, not that the web\n // held few answers. Flagging every run that returned fewer than `minResults`\n // marked a citation-style adapter's normal 1-3 hits as degraded on every\n // call, and an agent reading that reports its own findings as unreliable.\n // `cancelled` is excluded on purpose: race and quorum cancel by design.\n const lostSource = diagnostics.some(\n (entry) => entry.status === 'blocked' || entry.status === 'timeout' || entry.status === 'error',\n )\n\n return {\n results,\n answer,\n diagnostics: {\n adapters: diagnostics,\n degraded: fused.degraded || results.length === 0 || lostSource,\n cached: false,\n escalated,\n elapsedMs,\n pagesRead,\n },\n ...(runOptions.includeAdapterResults\n ? {\n byAdapter: collected.map((input) => ({\n adapterId: input.adapterId,\n weight: input.weight,\n answer: answersById.get(input.adapterId) ?? null,\n results: input.results,\n })),\n }\n : {}),\n }\n }\n\n async function search(input: SearchRequestInput, runOptions: RunOptions = {}): Promise<SearchEngineResult> {\n const report = createReporter([options.onStep, runOptions.onStep])\n const { request, includeContent } = normalizeRequest(input, policy)\n const adapterIds = ordered.filter((entry) => entry.enabled).map((entry) => entry.adapter.id)\n const compare = runOptions.includeAdapterResults === true\n // The suffix keeps a comparison run off the normal run's single-flight slot,\n // which would otherwise hand one caller a result built for the other shape.\n const cacheKey = `${buildCacheKey(request, adapterIds)}:${includeContent ? 'full' : 'lite'}${compare ? ':compare' : ''}`\n // A comparison is a diagnostic: serving it from cache would show an operator\n // yesterday's adapter behaviour while they change settings, and storing it\n // would put every adapter's full result list in the cache for one debug run.\n const useCache = options.cache !== undefined && policy.cacheTtlMs > 0 && !compare\n\n if (useCache) {\n const cached = await options.cache!.get(cacheKey).catch(() => null)\n if (cached) {\n report('done', 'cached', undefined, 'served from cache', { resultCount: cached.results.length })\n // A cache hit read nothing, so it owes nothing to the fetch budget.\n return { ...cached, diagnostics: { ...cached.diagnostics, cached: true, pagesRead: 0 } }\n }\n }\n\n const result = await singleFlight(cacheKey, () => runSearch(request, includeContent, runOptions, report))\n\n if (useCache && result.results.length > 0) {\n await options.cache!.set(cacheKey, result, policy.cacheTtlMs).catch(() => undefined)\n }\n return result\n }\n\n async function health(runOptions: HealthOptions = {}): Promise<readonly AdapterHealthReport[]> {\n const probe = runOptions.probe ?? true\n const budget = PROBE_COST_RANK[runOptions.maxProbeCost ?? 'billable']\n const only = runOptions.only ? new Set(runOptions.only) : null\n return Promise.all(\n ordered.map(async (entry): Promise<AdapterHealthReport> => {\n const probeCost = resolveProbeCost(entry.adapter)\n const readiness = entry.adapter.readiness()\n if (!readiness.ready) {\n return { id: entry.adapter.id, ready: false, ok: false, detail: readiness.reason, probeCost, probed: false }\n }\n // Readiness alone already answers \"is this configured\", which is what a\n // status display needs. Calling the adapter costs money on a metered\n // source, so it stays opt-in \u2014 and the budget keeps an unattended caller\n // to the adapters whose probe is free.\n const withinBudget = PROBE_COST_RANK[probeCost] <= budget && (!only || only.has(entry.adapter.id))\n if (!probe || !withinBudget || !entry.adapter.healthCheck) {\n return { id: entry.adapter.id, ready: true, ok: true, probeCost, probed: false }\n }\n const startedAt = now()\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS)\n timer.unref?.()\n const linked = linkSignals([runOptions.signal, controller.signal])\n try {\n const result = await entry.adapter.healthCheck({\n signal: linked.signal,\n http,\n report: () => {},\n deadlineAt: startedAt + HEALTH_TIMEOUT_MS,\n logger,\n })\n return { id: entry.adapter.id, ready: true, latencyMs: now() - startedAt, probeCost, probed: true, ...result }\n } catch (error) {\n return {\n id: entry.adapter.id,\n ready: true,\n ok: false,\n detail: error instanceof Error ? error.message : String(error),\n latencyMs: now() - startedAt,\n probeCost,\n probed: true,\n }\n } finally {\n clearTimeout(timer)\n linked.dispose()\n }\n }),\n )\n }\n\n async function dispose(): Promise<void> {\n await Promise.all(\n ordered.map(async (entry) => {\n try {\n await entry.adapter.dispose?.()\n } catch (error) {\n logger.warn(`[web-research] ${entry.adapter.id} failed to dispose`, { error: String(error) })\n }\n }),\n )\n }\n\n /**\n * Public single-URL fetch. Wraps `fetchPage` in the same hard deadline so a\n * direct `web_fetch` \u2014 which carries no caller signal \u2014 is bounded end-to-end\n * (HTTP read AND any browser escalation) at `hardDeadlineMs`, symmetric with the\n * scheduler-bounded search path.\n */\n async function boundedFetch(request: FetchRequest, runOptions: RunOptions = {}): Promise<FetchOutcome> {\n return withFetchDeadline(policy.hardDeadlineMs, runOptions.signal, (signal) =>\n fetchPage(request, { ...runOptions, signal }),\n )\n }\n\n return { search, fetch: boundedFetch, health, dispose }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,20 @@
1
+ import {
2
+ buildCacheKey,
3
+ createSingleFlight
4
+ } from "./cache.js";
5
+ import { DEADLINE_REACHED, deadlineRace, linkSignals, sleep } from "./deadline.js";
6
+ import { createSearchEngine } from "./engine.js";
7
+ import { outcomeReason, outcomeToStatus, runAdapter } from "./runAdapter.js";
8
+ export {
9
+ DEADLINE_REACHED,
10
+ buildCacheKey,
11
+ createSearchEngine,
12
+ createSingleFlight,
13
+ deadlineRace,
14
+ linkSignals,
15
+ outcomeReason,
16
+ outcomeToStatus,
17
+ runAdapter,
18
+ sleep
19
+ };
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/engine/index.ts"],
4
+ "sourcesContent": ["export {\n buildCacheKey,\n createSingleFlight,\n type CacheEntry,\n type ResultCache,\n} from './cache'\nexport { DEADLINE_REACHED, deadlineRace, linkSignals, sleep, type Cancellable } from './deadline'\nexport { createSearchEngine } from './engine'\nexport { outcomeReason, outcomeToStatus, runAdapter, type AdapterRunResult } from './runAdapter'\nexport type {\n AdapterDiagnostic,\n AdapterDiagnosticStatus,\n AdapterHealthReport,\n AdapterResultSet,\n EngineAdapterEntry,\n HealthOptions,\n RunOptions,\n SearchDiagnostics,\n SearchEngine,\n SearchEngineOptions,\n SearchEngineResult,\n} from './types'\n"],
5
+ "mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AACP,SAAS,kBAAkB,cAAc,aAAa,aAA+B;AACrF,SAAS,0BAA0B;AACnC,SAAS,eAAe,iBAAiB,kBAAyC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,57 @@
1
+ import { errorMessage } from "../contract/errors.js";
2
+ import { failed } from "../contract/outcomes.js";
3
+ import { linkSignals } from "./deadline.js";
4
+ function outcomeToStatus(outcome) {
5
+ switch (outcome.status) {
6
+ case "ok":
7
+ return "ok";
8
+ case "empty":
9
+ return "empty";
10
+ case "unavailable":
11
+ return "unavailable";
12
+ case "blocked":
13
+ return "blocked";
14
+ case "timeout":
15
+ return "timeout";
16
+ case "error":
17
+ return "error";
18
+ }
19
+ }
20
+ function outcomeReason(outcome) {
21
+ if (outcome.status === "ok" || outcome.status === "empty") return void 0;
22
+ return outcome.reason;
23
+ }
24
+ async function runAdapter(entry, request, deps) {
25
+ const startedAt = deps.now();
26
+ const allowance = entry.timeoutMs ?? deps.adapterTimeoutMs;
27
+ const budget = Math.max(0, Math.min(allowance, deps.deadlineAt - startedAt));
28
+ const controller = new AbortController();
29
+ const timer = setTimeout(() => controller.abort(), budget);
30
+ timer.unref?.();
31
+ const linked = linkSignals([deps.parentSignal, controller.signal]);
32
+ const context = {
33
+ signal: linked.signal,
34
+ http: deps.http,
35
+ report: (event, detail, metrics) => deps.report(entry.adapter.id, event, detail, metrics),
36
+ deadlineAt: startedAt + budget,
37
+ logger: deps.logger
38
+ };
39
+ deps.report(entry.adapter.id, "started");
40
+ try {
41
+ const outcome = await entry.adapter.search(request, context);
42
+ return { entry, outcome, latencyMs: deps.now() - startedAt };
43
+ } catch (error) {
44
+ const detail = `adapter threw instead of returning an outcome: ${errorMessage(error)}`;
45
+ deps.logger.warn(`[web-research] ${entry.adapter.id} ${detail}`);
46
+ return { entry, outcome: failed(detail, false), latencyMs: deps.now() - startedAt };
47
+ } finally {
48
+ clearTimeout(timer);
49
+ linked.dispose();
50
+ }
51
+ }
52
+ export {
53
+ outcomeReason,
54
+ outcomeToStatus,
55
+ runAdapter
56
+ };
57
+ //# sourceMappingURL=runAdapter.js.map