@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,2 @@
1
+ [build:web-research] found 35 entry points
2
+ [build:web-research] built successfully
package/AGENTS.md ADDED
@@ -0,0 +1,67 @@
1
+ # Web Research Engine
2
+
3
+ `@open-mercato/web-research` is the adapter-based web search/read engine. It is pure library code:
4
+ no DI, no tenants, no ORM, no vendor. Governance (ACL, rate limits, traces, per-tenant policy) lives
5
+ in `agent_orchestrator`; individual sources live in their own `@open-mercato/web-research-*` packages.
6
+
7
+ ## Always
8
+
9
+ - Keep this package **zero-dependency** apart from `zod`. Node builtins only. Adapter packages carry
10
+ their own SDKs; the core must stay installable with no transitive weight.
11
+ - Route every byte of egress through `HttpClient` (`src/net/client.ts`). Never call `fetch`,
12
+ `node:https`, or an SDK's own transport from an adapter — that bypasses the SSRF guard, DNS pinning,
13
+ redirect re-validation, byte caps and politeness in one step.
14
+ - Return **outcomes, never throw**, from anything implementing `SearchAdapter`. `unavailable`,
15
+ `empty`, `blocked`, `timeout` and `error` drive different scheduler behaviour and must stay distinct.
16
+ - Keep `readiness()` synchronous and I/O-free — the scheduler calls it while planning.
17
+ - **Dispose every engine you create.** `createSearchEngine` is called per request by the host, and
18
+ `dispose()` is the only thing that releases adapter-owned OS resources (the browser tier holds a
19
+ sidecar process and a Chromium). A caller that skips it leaks one of each per request.
20
+ - Gate any adapter you reach for outside the normal wave on `entry.enabled`. `policy.lastResort` is
21
+ the single documented exception — browser escalation is not one, or an operator who turned the
22
+ most expensive tier off still pays for it.
23
+ - Parse HTML with `tokenizeHtml` (`src/extract/tokenizer.ts`). No new regex parsers.
24
+ - Any new public type on the adapter contract is a **contract change**: bump `CONTRACT_VERSION`
25
+ (`src/contract/version.ts`) and follow `BACKWARD_COMPATIBILITY.md`, because third-party adapter
26
+ packages compile against it.
27
+
28
+ ## Never
29
+
30
+ - Never let a confidence threshold or domain cap produce an empty result set — relax and flag
31
+ `degraded` instead (`src/fusion/fuse.ts`).
32
+ - Never trust an adapter to honour the contract. `runAdapter` converts a throw into an `error`
33
+ outcome on purpose; do not remove that guard.
34
+ - Never put a vendor name in this package.
35
+
36
+ ## Layout
37
+
38
+ | Path | Role |
39
+ |---|---|
40
+ | `src/contract/` | Adapter interface, outcomes, steps, policy, errors, `CONTRACT_VERSION` |
41
+ | `src/net/` | SSRF guard + the single hardened, DNS-pinned HTTP client |
42
+ | `src/extract/` | Linear HTML tokenizer, text, title, main-content, page classifier |
43
+ | `src/fusion/` | URL canonicalization, dedup, reciprocal-rank fusion, domain diversity |
44
+ | `src/engine/` | Wave scheduler, deadlines, browser escalation, single-flight, cache seam |
45
+ | `src/testing/` | `describeAdapterContract()` and fakes — published for third-party adapters |
46
+
47
+ ## Design notes worth knowing before changing things
48
+
49
+ - **DNS pinning** (`createPinnedLookup`) is why the client is built on `node:https` rather than
50
+ `fetch`: the connection must resolve to exactly the addresses the SSRF guard vetted, or the
51
+ rebinding window between check and connect stays open.
52
+ - **`assertPublicUrl` fails closed on DNS failure by default**, unlike reference implementations that
53
+ fail open. We pin, so no addresses means no pinning, and proceeding would silently drop a control.
54
+ - **RRF, not score blending** — an LLM citation list, a SERP and a paid API have no comparable score
55
+ scale. Rank is the only signal they all agree on.
56
+ - **The soft deadline only binds once results exist.** With nothing in hand the scheduler keeps
57
+ waiting to the hard deadline; holding back a usable answer for a straggler is the worse failure.
58
+ - **Browser escalation is decided here, in code**, from `classifyPage` — not by prompting the model,
59
+ which costs a round-trip and answers differently each time.
60
+
61
+ ## Validation
62
+
63
+ ```bash
64
+ yarn workspace @open-mercato/web-research test
65
+ yarn workspace @open-mercato/web-research typecheck
66
+ yarn workspace @open-mercato/web-research build
67
+ ```
package/build.mjs ADDED
@@ -0,0 +1,10 @@
1
+ import { dirname } from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { buildPackage } from '../../scripts/build-package.mjs'
4
+
5
+ const packageDir = dirname(fileURLToPath(import.meta.url))
6
+
7
+ await buildPackage(packageDir, {
8
+ name: 'web-research',
9
+ entryPoints: 'src/**/*.ts',
10
+ })
@@ -0,0 +1,13 @@
1
+ function defineAdapterModule(module) {
2
+ return module;
3
+ }
4
+ const READY = { ready: true };
5
+ function notReady(reason) {
6
+ return { ready: false, reason };
7
+ }
8
+ export {
9
+ READY,
10
+ defineAdapterModule,
11
+ notReady
12
+ };
13
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/contract/adapter.ts"],
4
+ "sourcesContent": ["import type { z } from 'zod'\nimport type { HttpClient, Logger } from './http'\nimport type { FetchOutcome, SearchOutcome } from './outcomes'\nimport type { FetchRequest, SearchRequest } from './results'\nimport type { StepReporter } from './steps'\n\nexport type AdapterKind = 'serp' | 'api' | 'model' | 'browser'\n\nexport type AdapterCost = 'free' | 'metered' | 'llm-tokens'\n\n/**\n * What calling `healthCheck()` costs \u2014 which is NOT what searching costs.\n *\n * Exa's probe reads a local key and Model-native's resolves configuration, both\n * free, though both are priced adapters to search with. Firecrawl's and Tavily's\n * probes issue a real query and bill for it. The browser tier's spawns an OS\n * process: no money, but not something to run on every page view either.\n *\n * A status surface may run `free` probes unattended; `heavy` and `billable` need\n * a human to ask for them.\n */\nexport type ProbeCost = 'free' | 'heavy' | 'billable'\n\nexport type AdapterCapabilities = {\n readonly search: boolean\n readonly fetch: boolean\n /** Returns real snippets rather than bare URLs. */\n readonly snippets: boolean\n /** Returns full page text inline, sparing a follow-up fetch. */\n readonly content: boolean\n readonly freshness: boolean\n readonly siteFilter: boolean\n readonly cost: AdapterCost\n}\n\nexport type AdapterReadiness =\n | { readonly ready: true }\n | { readonly ready: false; readonly reason: string }\n\nexport type AdapterHealth = {\n readonly ok: boolean\n readonly detail?: string\n readonly latencyMs?: number\n}\n\nexport type AdapterContext = {\n readonly signal: AbortSignal\n readonly http: HttpClient\n /** Narrates progress; the engine supplies the sequence, timestamp and adapter id. */\n readonly report: StepReporter\n /** Epoch ms after which the engine stops caring about this adapter's answer. */\n readonly deadlineAt: number\n readonly logger: Logger\n}\n\nexport interface SearchAdapter {\n readonly id: string\n readonly kind: AdapterKind\n readonly capabilities: AdapterCapabilities\n /**\n * Synchronous and I/O-free by contract, so planning a wave never blocks and an\n * unconfigured adapter costs nothing to skip.\n */\n readiness(): AdapterReadiness\n /**\n * What `healthCheck()` costs to call. Omitting it is safe but pessimistic: the\n * engine then derives `billable` from `capabilities.cost === 'metered'` and\n * `free` otherwise, so an adapter that never declares it is simply left out of\n * unattended probing. Declare it whenever the probe costs money.\n */\n readonly probeCost?: ProbeCost\n search(request: SearchRequest, context: AdapterContext): Promise<SearchOutcome>\n fetch?(request: FetchRequest, context: AdapterContext): Promise<FetchOutcome>\n healthCheck?(context: AdapterContext): Promise<AdapterHealth>\n dispose?(): Promise<void>\n}\n\n/**\n * What an adapter package default-exports. A descriptor rather than an instance,\n * because the engine builds one instance per tenant from that tenant's options.\n */\nexport type AdapterModule<TOptions = unknown> = {\n readonly id: string\n readonly kind: AdapterKind\n readonly contractVersion: number\n readonly optionsSchema: z.ZodType<TOptions>\n createAdapter(options: TOptions): SearchAdapter\n}\n\nexport function defineAdapterModule<TOptions>(\n module: AdapterModule<TOptions>,\n): AdapterModule<TOptions> {\n return module\n}\n\nexport const READY: AdapterReadiness = { ready: true }\n\nexport function notReady(reason: string): AdapterReadiness {\n return { ready: false, reason }\n}\n"],
5
+ "mappings": "AAyFO,SAAS,oBACd,QACyB;AACzB,SAAO;AACT;AAEO,MAAM,QAA0B,EAAE,OAAO,KAAK;AAE9C,SAAS,SAAS,QAAkC;AACzD,SAAO,EAAE,OAAO,OAAO,OAAO;AAChC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,28 @@
1
+ class WebResearchError extends Error {
2
+ constructor(code, message, init = {}) {
3
+ super(message, init.cause === void 0 ? void 0 : { cause: init.cause });
4
+ /**
5
+ * Brand checked by {@link isWebResearchError} instead of `instanceof`. The
6
+ * package resolves as raw `src` for typecheck and as built `dist` at runtime,
7
+ * so a value can cross a realm where the two class identities differ.
8
+ */
9
+ this.__webResearchError = true;
10
+ this.name = "WebResearchError";
11
+ this.code = code;
12
+ if (init.retryAfterMs !== void 0) this.retryAfterMs = init.retryAfterMs;
13
+ if (init.status !== void 0) this.status = init.status;
14
+ }
15
+ }
16
+ function isWebResearchError(value) {
17
+ return typeof value === "object" && value !== null && value.__webResearchError === true;
18
+ }
19
+ function errorMessage(value) {
20
+ if (value instanceof Error) return value.message;
21
+ return String(value);
22
+ }
23
+ export {
24
+ WebResearchError,
25
+ errorMessage,
26
+ isWebResearchError
27
+ };
28
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/contract/errors.ts"],
4
+ "sourcesContent": ["export type WebResearchErrorCode =\n | 'ssrf_blocked'\n | 'timeout'\n | 'aborted'\n | 'blocked'\n | 'bad_response'\n | 'too_many_redirects'\n | 'unsupported_content_type'\n | 'network'\n\nexport type WebResearchErrorInit = {\n readonly retryAfterMs?: number\n readonly status?: number\n readonly cause?: unknown\n}\n\nexport class WebResearchError extends Error {\n /**\n * Brand checked by {@link isWebResearchError} instead of `instanceof`. The\n * package resolves as raw `src` for typecheck and as built `dist` at runtime,\n * so a value can cross a realm where the two class identities differ.\n */\n readonly __webResearchError = true as const\n readonly code: WebResearchErrorCode\n readonly retryAfterMs?: number\n readonly status?: number\n\n constructor(code: WebResearchErrorCode, message: string, init: WebResearchErrorInit = {}) {\n super(message, init.cause === undefined ? undefined : { cause: init.cause })\n this.name = 'WebResearchError'\n this.code = code\n if (init.retryAfterMs !== undefined) this.retryAfterMs = init.retryAfterMs\n if (init.status !== undefined) this.status = init.status\n }\n}\n\nexport function isWebResearchError(value: unknown): value is WebResearchError {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { __webResearchError?: unknown }).__webResearchError === true\n )\n}\n\nexport function errorMessage(value: unknown): string {\n if (value instanceof Error) return value.message\n return String(value)\n}\n"],
5
+ "mappings": "AAgBO,MAAM,yBAAyB,MAAM;AAAA,EAW1C,YAAY,MAA4B,SAAiB,OAA6B,CAAC,GAAG;AACxF,UAAM,SAAS,KAAK,UAAU,SAAY,SAAY,EAAE,OAAO,KAAK,MAAM,CAAC;AAN7E;AAAA;AAAA;AAAA;AAAA;AAAA,SAAS,qBAAqB;AAO5B,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,QAAI,KAAK,iBAAiB,OAAW,MAAK,eAAe,KAAK;AAC9D,QAAI,KAAK,WAAW,OAAW,MAAK,SAAS,KAAK;AAAA,EACpD;AACF;AAEO,SAAS,mBAAmB,OAA2C;AAC5E,SACE,OAAO,UAAU,YACjB,UAAU,QACT,MAA2C,uBAAuB;AAEvE;AAEO,SAAS,aAAa,OAAwB;AACnD,MAAI,iBAAiB,MAAO,QAAO,MAAM;AACzC,SAAO,OAAO,KAAK;AACrB;",
6
+ "names": []
7
+ }
@@ -0,0 +1,7 @@
1
+ const noop = () => {
2
+ };
3
+ const silentLogger = { debug: noop, info: noop, warn: noop, error: noop };
4
+ export {
5
+ silentLogger
6
+ };
7
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/contract/http.ts"],
4
+ "sourcesContent": ["export type HttpMethod = 'GET' | 'POST'\n\nexport type HttpRequestInit = {\n readonly method?: HttpMethod\n readonly headers?: Readonly<Record<string, string>>\n readonly body?: string\n readonly maxBytes?: number\n readonly timeoutMs?: number\n readonly signal?: AbortSignal\n /**\n * Content-type prefixes the caller can handle. A response outside the list is\n * rejected with `unsupported_content_type` instead of being decoded as text \u2014\n * without this a PDF or image comes back as mojibake the model then tries to read.\n */\n readonly accept?: readonly string[]\n}\n\nexport type HttpResponse = {\n /** Final URL after redirects, each of which was re-vetted. */\n readonly url: string\n readonly status: number\n readonly headers: ReadonlyMap<string, string>\n readonly contentType: string | null\n readonly body: string\n readonly truncated: boolean\n}\n\n/**\n * The single egress path. Adapters receive one of these and must never reach for\n * `fetch` themselves \u2014 the SSRF guard, DNS pinning, redirect re-validation, byte\n * caps and politeness live here, and a bypass silently loses all of them.\n */\nexport interface HttpClient {\n request(url: string, init?: HttpRequestInit): Promise<HttpResponse>\n}\n\nexport type Logger = {\n debug(message: string, meta?: Record<string, unknown>): void\n info(message: string, meta?: Record<string, unknown>): void\n warn(message: string, meta?: Record<string, unknown>): void\n error(message: string, meta?: Record<string, unknown>): void\n}\n\nconst noop = (): void => {}\n\nexport const silentLogger: Logger = { debug: noop, info: noop, warn: noop, error: noop }\n"],
5
+ "mappings": "AA2CA,MAAM,OAAO,MAAY;AAAC;AAEnB,MAAM,eAAuB,EAAE,OAAO,MAAM,MAAM,MAAM,MAAM,MAAM,OAAO,KAAK;",
6
+ "names": []
7
+ }
@@ -0,0 +1,9 @@
1
+ export * from "./adapter.js";
2
+ export * from "./errors.js";
3
+ export * from "./http.js";
4
+ export * from "./outcomes.js";
5
+ export * from "./policy.js";
6
+ export * from "./results.js";
7
+ export * from "./steps.js";
8
+ export * from "./version.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/contract/index.ts"],
4
+ "sourcesContent": ["export * from './adapter'\nexport * from './errors'\nexport * from './http'\nexport * from './outcomes'\nexport * from './policy'\nexport * from './results'\nexport * from './steps'\nexport * from './version'\n"],
5
+ "mappings": "AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;",
6
+ "names": []
7
+ }
@@ -0,0 +1,58 @@
1
+ import { isWebResearchError, errorMessage } from "./errors.js";
2
+ function searchOk(results, answer) {
3
+ const hasAnswer = answer !== void 0 && answer.trim().length > 0;
4
+ if (results.length === 0 && !hasAnswer) return { status: "empty" };
5
+ return hasAnswer ? { status: "ok", results, answer } : { status: "ok", results };
6
+ }
7
+ function searchEmpty() {
8
+ return { status: "empty" };
9
+ }
10
+ function unavailable(reason) {
11
+ return { status: "unavailable", reason };
12
+ }
13
+ function blocked(reason, retryAfterMs) {
14
+ return retryAfterMs === void 0 ? { status: "blocked", reason } : { status: "blocked", reason, retryAfterMs };
15
+ }
16
+ function timedOut(reason) {
17
+ return reason === void 0 ? { status: "timeout" } : { status: "timeout", reason };
18
+ }
19
+ function failed(reason, retriable = false) {
20
+ return { status: "error", reason, retriable };
21
+ }
22
+ function isFailure(outcome) {
23
+ return outcome.status !== "ok" && outcome.status !== "empty";
24
+ }
25
+ function toOutcomeFailure(error) {
26
+ if (isWebResearchError(error)) {
27
+ switch (error.code) {
28
+ case "timeout":
29
+ return timedOut(error.message);
30
+ case "aborted":
31
+ return timedOut(error.message);
32
+ case "blocked":
33
+ return blocked(error.message, error.retryAfterMs);
34
+ case "ssrf_blocked":
35
+ case "unsupported_content_type":
36
+ return failed(error.message, false);
37
+ case "network":
38
+ case "bad_response":
39
+ case "too_many_redirects":
40
+ return failed(error.message, true);
41
+ }
42
+ }
43
+ if (error instanceof Error && error.name === "AbortError") {
44
+ return timedOut(error.message);
45
+ }
46
+ return failed(errorMessage(error), false);
47
+ }
48
+ export {
49
+ blocked,
50
+ failed,
51
+ isFailure,
52
+ searchEmpty,
53
+ searchOk,
54
+ timedOut,
55
+ toOutcomeFailure,
56
+ unavailable
57
+ };
58
+ //# sourceMappingURL=outcomes.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/contract/outcomes.ts"],
4
+ "sourcesContent": ["import { isWebResearchError, errorMessage } from './errors'\nimport type { FetchedPage, RawResult } from './results'\n\n/**\n * Failure shapes shared by search and fetch. Keeping `unavailable`, `blocked`\n * and `error` distinct is the point of the whole contract: the scheduler demotes\n * an `unavailable` adapter for the rest of the run, escalates a `blocked` one to\n * the browser tier, and retries only a retriable `error`. Collapsing them into a\n * thrown exception \u2014 as the previous implementation did \u2014 makes all three\n * indistinguishable and forces the caller to guess.\n */\nexport type OutcomeFailure =\n | { readonly status: 'unavailable'; readonly reason: string }\n | { readonly status: 'blocked'; readonly reason: string; readonly retryAfterMs?: number }\n | { readonly status: 'timeout'; readonly reason?: string }\n | { readonly status: 'error'; readonly reason: string; readonly retriable: boolean }\n\nexport type SearchOutcome =\n | { readonly status: 'ok'; readonly results: readonly RawResult[]; readonly answer?: string }\n | { readonly status: 'empty' }\n | OutcomeFailure\n\nexport type FetchOutcome = { readonly status: 'ok'; readonly page: FetchedPage } | OutcomeFailure\n\nexport type OutcomeStatus = SearchOutcome['status']\n\n/**\n * A prose answer with no citations is still a successful search \u2014 the model\n * knew something, it just did not link anywhere. Only a turn that produced\n * neither links nor prose is `empty`.\n */\nexport function searchOk(results: readonly RawResult[], answer?: string): SearchOutcome {\n const hasAnswer = answer !== undefined && answer.trim().length > 0\n if (results.length === 0 && !hasAnswer) return { status: 'empty' }\n return hasAnswer ? { status: 'ok', results, answer } : { status: 'ok', results }\n}\n\nexport function searchEmpty(): SearchOutcome {\n return { status: 'empty' }\n}\n\nexport function unavailable(reason: string): OutcomeFailure {\n return { status: 'unavailable', reason }\n}\n\nexport function blocked(reason: string, retryAfterMs?: number): OutcomeFailure {\n return retryAfterMs === undefined\n ? { status: 'blocked', reason }\n : { status: 'blocked', reason, retryAfterMs }\n}\n\nexport function timedOut(reason?: string): OutcomeFailure {\n return reason === undefined ? { status: 'timeout' } : { status: 'timeout', reason }\n}\n\nexport function failed(reason: string, retriable = false): OutcomeFailure {\n return { status: 'error', reason, retriable }\n}\n\nexport function isFailure(outcome: SearchOutcome | FetchOutcome): outcome is OutcomeFailure {\n return outcome.status !== 'ok' && outcome.status !== 'empty'\n}\n\n/**\n * Maps anything thrown by the shared HTTP client onto the outcome vocabulary, so\n * an adapter body reduces to `try { \u2026 } catch (err) { return toOutcomeFailure(err) }`\n * and never leaks an exception to the scheduler.\n */\nexport function toOutcomeFailure(error: unknown): OutcomeFailure {\n if (isWebResearchError(error)) {\n switch (error.code) {\n case 'timeout':\n return timedOut(error.message)\n case 'aborted':\n return timedOut(error.message)\n case 'blocked':\n return blocked(error.message, error.retryAfterMs)\n case 'ssrf_blocked':\n case 'unsupported_content_type':\n return failed(error.message, false)\n case 'network':\n case 'bad_response':\n case 'too_many_redirects':\n return failed(error.message, true)\n }\n }\n if (error instanceof Error && error.name === 'AbortError') {\n return timedOut(error.message)\n }\n return failed(errorMessage(error), false)\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB,oBAAoB;AA+B1C,SAAS,SAAS,SAA+B,QAAgC;AACtF,QAAM,YAAY,WAAW,UAAa,OAAO,KAAK,EAAE,SAAS;AACjE,MAAI,QAAQ,WAAW,KAAK,CAAC,UAAW,QAAO,EAAE,QAAQ,QAAQ;AACjE,SAAO,YAAY,EAAE,QAAQ,MAAM,SAAS,OAAO,IAAI,EAAE,QAAQ,MAAM,QAAQ;AACjF;AAEO,SAAS,cAA6B;AAC3C,SAAO,EAAE,QAAQ,QAAQ;AAC3B;AAEO,SAAS,YAAY,QAAgC;AAC1D,SAAO,EAAE,QAAQ,eAAe,OAAO;AACzC;AAEO,SAAS,QAAQ,QAAgB,cAAuC;AAC7E,SAAO,iBAAiB,SACpB,EAAE,QAAQ,WAAW,OAAO,IAC5B,EAAE,QAAQ,WAAW,QAAQ,aAAa;AAChD;AAEO,SAAS,SAAS,QAAiC;AACxD,SAAO,WAAW,SAAY,EAAE,QAAQ,UAAU,IAAI,EAAE,QAAQ,WAAW,OAAO;AACpF;AAEO,SAAS,OAAO,QAAgB,YAAY,OAAuB;AACxE,SAAO,EAAE,QAAQ,SAAS,QAAQ,UAAU;AAC9C;AAEO,SAAS,UAAU,SAAkE;AAC1F,SAAO,QAAQ,WAAW,QAAQ,QAAQ,WAAW;AACvD;AAOO,SAAS,iBAAiB,OAAgC;AAC/D,MAAI,mBAAmB,KAAK,GAAG;AAC7B,YAAQ,MAAM,MAAM;AAAA,MAClB,KAAK;AACH,eAAO,SAAS,MAAM,OAAO;AAAA,MAC/B,KAAK;AACH,eAAO,SAAS,MAAM,OAAO;AAAA,MAC/B,KAAK;AACH,eAAO,QAAQ,MAAM,SAAS,MAAM,YAAY;AAAA,MAClD,KAAK;AAAA,MACL,KAAK;AACH,eAAO,OAAO,MAAM,SAAS,KAAK;AAAA,MACpC,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AACH,eAAO,OAAO,MAAM,SAAS,IAAI;AAAA,IACrC;AAAA,EACF;AACA,MAAI,iBAAiB,SAAS,MAAM,SAAS,cAAc;AACzD,WAAO,SAAS,MAAM,OAAO;AAAA,EAC/B;AACA,SAAO,OAAO,aAAa,KAAK,GAAG,KAAK;AAC1C;",
6
+ "names": []
7
+ }
@@ -0,0 +1,91 @@
1
+ import { z } from "zod";
2
+ const MODEL_ADAPTER_TIMEOUT_MS = 45e3;
3
+ const DEFAULT_POLICY = {
4
+ settleMode: "quorum",
5
+ concurrency: 2,
6
+ minResults: 5,
7
+ minAdapters: 1,
8
+ minConfidence: 0,
9
+ // The soft deadline only binds once results exist, so a fast adapter still
10
+ // settles the run in about a second; this ceiling is what a lone slow adapter
11
+ // is allowed to use, not what a normal search costs.
12
+ softDeadlineMs: 6e3,
13
+ hardDeadlineMs: 6e4,
14
+ adapterTimeoutMs: 8e3,
15
+ maxPerDomain: 3,
16
+ cacheTtlMs: 9e5,
17
+ lastResort: "model-native",
18
+ escalateToBrowser: true,
19
+ content: { enabledByDefault: false, maxPages: 3, maxBytesPerPage: 128 * 1024 },
20
+ adapters: []
21
+ };
22
+ const adapterPolicyEntrySchema = z.object({
23
+ id: z.string().min(1),
24
+ enabled: z.boolean().optional(),
25
+ order: z.number().int().min(0).optional(),
26
+ weight: z.number().min(0).max(10).optional(),
27
+ timeoutMs: z.number().int().min(250).max(12e4).optional(),
28
+ maxCallsPerHour: z.number().int().min(1).max(1e6).optional()
29
+ });
30
+ const searchPolicySchema = z.object({
31
+ settleMode: z.enum(["race", "quorum", "exhaustive"]).optional(),
32
+ concurrency: z.number().int().min(1).max(16).optional(),
33
+ minResults: z.number().int().min(1).max(50).optional(),
34
+ minAdapters: z.number().int().min(1).max(16).optional(),
35
+ minConfidence: z.number().min(0).max(1).optional(),
36
+ softDeadlineMs: z.number().int().min(250).max(12e4).optional(),
37
+ hardDeadlineMs: z.number().int().min(500).max(18e4).optional(),
38
+ adapterTimeoutMs: z.number().int().min(250).max(12e4).optional(),
39
+ maxPerDomain: z.number().int().min(1).max(50).optional(),
40
+ cacheTtlMs: z.number().int().min(0).max(864e5).optional(),
41
+ lastResort: z.string().min(1).nullable().optional(),
42
+ escalateToBrowser: z.boolean().optional(),
43
+ content: z.object({
44
+ enabledByDefault: z.boolean().optional(),
45
+ maxPages: z.number().int().min(0).max(20).optional(),
46
+ maxBytesPerPage: z.number().int().min(1024).max(4 * 1024 * 1024).optional()
47
+ }).optional(),
48
+ adapters: z.array(adapterPolicyEntrySchema).optional()
49
+ });
50
+ function resolvePolicy(input) {
51
+ const patch = input ?? {};
52
+ const hardDeadlineMs = patch.hardDeadlineMs ?? DEFAULT_POLICY.hardDeadlineMs;
53
+ const softDeadlineMs = Math.min(patch.softDeadlineMs ?? DEFAULT_POLICY.softDeadlineMs, hardDeadlineMs);
54
+ return {
55
+ settleMode: patch.settleMode ?? DEFAULT_POLICY.settleMode,
56
+ concurrency: patch.concurrency ?? DEFAULT_POLICY.concurrency,
57
+ minResults: patch.minResults ?? DEFAULT_POLICY.minResults,
58
+ minAdapters: patch.minAdapters ?? DEFAULT_POLICY.minAdapters,
59
+ minConfidence: patch.minConfidence ?? DEFAULT_POLICY.minConfidence,
60
+ softDeadlineMs,
61
+ hardDeadlineMs,
62
+ adapterTimeoutMs: Math.min(patch.adapterTimeoutMs ?? DEFAULT_POLICY.adapterTimeoutMs, hardDeadlineMs),
63
+ maxPerDomain: patch.maxPerDomain ?? DEFAULT_POLICY.maxPerDomain,
64
+ cacheTtlMs: patch.cacheTtlMs ?? DEFAULT_POLICY.cacheTtlMs,
65
+ lastResort: patch.lastResort === void 0 ? DEFAULT_POLICY.lastResort : patch.lastResort,
66
+ escalateToBrowser: patch.escalateToBrowser ?? DEFAULT_POLICY.escalateToBrowser,
67
+ content: {
68
+ enabledByDefault: patch.content?.enabledByDefault ?? DEFAULT_POLICY.content.enabledByDefault,
69
+ maxPages: patch.content?.maxPages ?? DEFAULT_POLICY.content.maxPages,
70
+ maxBytesPerPage: patch.content?.maxBytesPerPage ?? DEFAULT_POLICY.content.maxBytesPerPage
71
+ },
72
+ adapters: (patch.adapters ?? []).map((entry, index) => ({
73
+ id: entry.id,
74
+ enabled: entry.enabled ?? true,
75
+ order: entry.order ?? index,
76
+ weight: entry.weight ?? 1,
77
+ // Clamped to the hard deadline for the same reason the shared budget is:
78
+ // a per-adapter timeout past it can never be reached.
79
+ ...entry.timeoutMs === void 0 ? {} : { timeoutMs: Math.min(entry.timeoutMs, hardDeadlineMs) },
80
+ ...entry.maxCallsPerHour === void 0 ? {} : { maxCallsPerHour: entry.maxCallsPerHour }
81
+ }))
82
+ };
83
+ }
84
+ export {
85
+ DEFAULT_POLICY,
86
+ MODEL_ADAPTER_TIMEOUT_MS,
87
+ adapterPolicyEntrySchema,
88
+ resolvePolicy,
89
+ searchPolicySchema
90
+ };
91
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/contract/policy.ts"],
4
+ "sourcesContent": ["import { z } from 'zod'\n\nexport type SettleMode = 'race' | 'quorum' | 'exhaustive'\n\nexport type AdapterPolicyEntry = {\n readonly id: string\n readonly enabled: boolean\n readonly order: number\n /** Relative pull in the fusion step; higher trusts this adapter's ranking more. */\n readonly weight: number\n /**\n * Overrides `adapterTimeoutMs` for this adapter alone. Latency is not uniform\n * across kinds: a SERP read finishes in about a second, while a model doing its\n * own multi-step web search takes tens of seconds. One shared budget has to be\n * either too tight for the model or too slack for everything else.\n */\n readonly timeoutMs?: number\n /**\n * Ceiling on how often this adapter may be called, per tenant, per hour.\n *\n * The engine does not enforce this \u2014 it is per-request and has no memory. The\n * host counts calls and simply stops enabling the adapter once the ceiling is\n * reached, which is why the knob lives on the policy rather than in the engine.\n * Undefined means no ceiling.\n */\n readonly maxCallsPerHour?: number\n}\n\nexport type ContentPolicy = {\n readonly enabledByDefault: boolean\n readonly maxPages: number\n readonly maxBytesPerPage: number\n}\n\nexport type SearchPolicy = {\n readonly settleMode: SettleMode\n readonly concurrency: number\n readonly minResults: number\n readonly minAdapters: number\n readonly minConfidence: number\n readonly softDeadlineMs: number\n readonly hardDeadlineMs: number\n readonly adapterTimeoutMs: number\n readonly maxPerDomain: number\n readonly cacheTtlMs: number\n /**\n * Adapter run when every wave is exhausted and the fused set is still short,\n * even if it is disabled in the normal order. This is what makes \"the engine\n * always returns something\" true rather than aspirational.\n */\n readonly lastResort: string | null\n readonly escalateToBrowser: boolean\n readonly content: ContentPolicy\n readonly adapters: readonly AdapterPolicyEntry[]\n}\n\n/**\n * Budget for `model-native`, the adapter enabled by default. It runs the model's\n * own multi-step web search, which measures at roughly 30s end to end, so the\n * generic adapter budget would time it out on every single run and the shipped\n * configuration could never return anything.\n */\nexport const MODEL_ADAPTER_TIMEOUT_MS = 45_000\n\nexport const DEFAULT_POLICY: SearchPolicy = {\n settleMode: 'quorum',\n concurrency: 2,\n minResults: 5,\n minAdapters: 1,\n minConfidence: 0,\n // The soft deadline only binds once results exist, so a fast adapter still\n // settles the run in about a second; this ceiling is what a lone slow adapter\n // is allowed to use, not what a normal search costs.\n softDeadlineMs: 6_000,\n hardDeadlineMs: 60_000,\n adapterTimeoutMs: 8_000,\n maxPerDomain: 3,\n cacheTtlMs: 900_000,\n lastResort: 'model-native',\n escalateToBrowser: true,\n content: { enabledByDefault: false, maxPages: 3, maxBytesPerPage: 128 * 1024 },\n adapters: [],\n}\n\nexport const adapterPolicyEntrySchema = z.object({\n id: z.string().min(1),\n enabled: z.boolean().optional(),\n order: z.number().int().min(0).optional(),\n weight: z.number().min(0).max(10).optional(),\n timeoutMs: z.number().int().min(250).max(120_000).optional(),\n maxCallsPerHour: z.number().int().min(1).max(1_000_000).optional(),\n})\n\nexport const searchPolicySchema = z.object({\n settleMode: z.enum(['race', 'quorum', 'exhaustive']).optional(),\n concurrency: z.number().int().min(1).max(16).optional(),\n minResults: z.number().int().min(1).max(50).optional(),\n minAdapters: z.number().int().min(1).max(16).optional(),\n minConfidence: z.number().min(0).max(1).optional(),\n softDeadlineMs: z.number().int().min(250).max(120_000).optional(),\n hardDeadlineMs: z.number().int().min(500).max(180_000).optional(),\n adapterTimeoutMs: z.number().int().min(250).max(120_000).optional(),\n maxPerDomain: z.number().int().min(1).max(50).optional(),\n cacheTtlMs: z.number().int().min(0).max(86_400_000).optional(),\n lastResort: z.string().min(1).nullable().optional(),\n escalateToBrowser: z.boolean().optional(),\n content: z\n .object({\n enabledByDefault: z.boolean().optional(),\n maxPages: z.number().int().min(0).max(20).optional(),\n maxBytesPerPage: z.number().int().min(1024).max(4 * 1024 * 1024).optional(),\n })\n .optional(),\n adapters: z.array(adapterPolicyEntrySchema).optional(),\n})\n\nexport type SearchPolicyInput = z.infer<typeof searchPolicySchema>\n\n/**\n * Merges a partial policy over the defaults. Deadlines are reconciled rather\n * than trusted: a soft deadline past the hard one would make the scheduler wait\n * for a barrier it can never reach.\n */\nexport function resolvePolicy(input: SearchPolicyInput | null | undefined): SearchPolicy {\n const patch = input ?? {}\n const hardDeadlineMs = patch.hardDeadlineMs ?? DEFAULT_POLICY.hardDeadlineMs\n const softDeadlineMs = Math.min(patch.softDeadlineMs ?? DEFAULT_POLICY.softDeadlineMs, hardDeadlineMs)\n return {\n settleMode: patch.settleMode ?? DEFAULT_POLICY.settleMode,\n concurrency: patch.concurrency ?? DEFAULT_POLICY.concurrency,\n minResults: patch.minResults ?? DEFAULT_POLICY.minResults,\n minAdapters: patch.minAdapters ?? DEFAULT_POLICY.minAdapters,\n minConfidence: patch.minConfidence ?? DEFAULT_POLICY.minConfidence,\n softDeadlineMs,\n hardDeadlineMs,\n adapterTimeoutMs: Math.min(patch.adapterTimeoutMs ?? DEFAULT_POLICY.adapterTimeoutMs, hardDeadlineMs),\n maxPerDomain: patch.maxPerDomain ?? DEFAULT_POLICY.maxPerDomain,\n cacheTtlMs: patch.cacheTtlMs ?? DEFAULT_POLICY.cacheTtlMs,\n lastResort: patch.lastResort === undefined ? DEFAULT_POLICY.lastResort : patch.lastResort,\n escalateToBrowser: patch.escalateToBrowser ?? DEFAULT_POLICY.escalateToBrowser,\n content: {\n enabledByDefault: patch.content?.enabledByDefault ?? DEFAULT_POLICY.content.enabledByDefault,\n maxPages: patch.content?.maxPages ?? DEFAULT_POLICY.content.maxPages,\n maxBytesPerPage: patch.content?.maxBytesPerPage ?? DEFAULT_POLICY.content.maxBytesPerPage,\n },\n adapters: (patch.adapters ?? []).map((entry, index) => ({\n id: entry.id,\n enabled: entry.enabled ?? true,\n order: entry.order ?? index,\n weight: entry.weight ?? 1,\n // Clamped to the hard deadline for the same reason the shared budget is:\n // a per-adapter timeout past it can never be reached.\n ...(entry.timeoutMs === undefined ? {} : { timeoutMs: Math.min(entry.timeoutMs, hardDeadlineMs) }),\n ...(entry.maxCallsPerHour === undefined ? {} : { maxCallsPerHour: entry.maxCallsPerHour }),\n })),\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ const searchRequestSchema = z.object({
3
+ query: z.string().trim().min(1).max(500),
4
+ limit: z.number().int().min(1).max(50).optional(),
5
+ locale: z.string().min(2).max(16).optional(),
6
+ freshness: z.enum(["day", "week", "month", "year"]).optional(),
7
+ site: z.string().min(1).max(253).optional(),
8
+ includeContent: z.boolean().optional()
9
+ });
10
+ export {
11
+ searchRequestSchema
12
+ };
13
+ //# sourceMappingURL=results.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/contract/results.ts"],
4
+ "sourcesContent": ["import { z } from 'zod'\n\nexport type Freshness = 'day' | 'week' | 'month' | 'year'\n\n/** Normalized request handed to adapters. The engine resolves defaults first. */\nexport type SearchRequest = {\n readonly query: string\n readonly limit: number\n readonly locale?: string\n readonly freshness?: Freshness\n readonly site?: string\n}\n\n/**\n * What an adapter returns per hit. Everything but `url` is optional because\n * sources differ wildly \u2014 an LLM citation list has no snippet, a SERP has no\n * content, a scraping API has both.\n */\nexport type RawResult = {\n readonly url: string\n readonly title?: string | null\n readonly snippet?: string | null\n readonly content?: string | null\n readonly publishedAt?: string | null\n /** Adapter-native relevance, used only to break ties within one adapter. */\n readonly score?: number | null\n}\n\nexport type ResultProvenance = {\n readonly adapterId: string\n readonly rank: number\n}\n\n/** A hit after canonicalization, dedup and cross-adapter fusion. */\nexport type SearchResult = {\n readonly url: string\n readonly title: string\n readonly snippet: string | null\n readonly content: string | null\n readonly publishedAt: string | null\n readonly confidence: number\n readonly sources: readonly ResultProvenance[]\n}\n\nexport type RenderMode = 'auto' | 'never' | 'always'\n\nexport type FetchRequest = {\n readonly url: string\n readonly maxBytes?: number\n readonly render?: RenderMode\n}\n\n/**\n * Why an HTTP read was judged insufficient. Drives browser escalation, and is\n * reported back so the decision is auditable rather than invisible.\n */\nexport type PageVerdict = 'ok' | 'js-shell' | 'blocked' | 'empty'\n\nexport type FetchedPage = {\n readonly url: string\n readonly title: string | null\n readonly text: string\n readonly contentType: string | null\n readonly status: number\n readonly truncated: boolean\n readonly renderedWith: 'http' | 'browser'\n readonly escalatedBecause?: PageVerdict\n}\n\nexport const searchRequestSchema = z.object({\n query: z.string().trim().min(1).max(500),\n limit: z.number().int().min(1).max(50).optional(),\n locale: z.string().min(2).max(16).optional(),\n freshness: z.enum(['day', 'week', 'month', 'year']).optional(),\n site: z.string().min(1).max(253).optional(),\n includeContent: z.boolean().optional(),\n})\n\nexport type SearchRequestInput = z.infer<typeof searchRequestSchema>\n"],
5
+ "mappings": "AAAA,SAAS,SAAS;AAqEX,MAAM,sBAAsB,EAAE,OAAO;AAAA,EAC1C,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EAChD,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,EAC3C,WAAW,EAAE,KAAK,CAAC,OAAO,QAAQ,SAAS,MAAM,CAAC,EAAE,SAAS;AAAA,EAC7D,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC1C,gBAAgB,EAAE,QAAQ,EAAE,SAAS;AACvC,CAAC;",
6
+ "names": []
7
+ }
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=steps.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
@@ -0,0 +1,11 @@
1
+ const CONTRACT_VERSION = 1;
2
+ const MIN_SUPPORTED_CONTRACT_VERSION = 1;
3
+ function isSupportedContractVersion(version) {
4
+ return typeof version === "number" && Number.isInteger(version) && version >= MIN_SUPPORTED_CONTRACT_VERSION && version <= CONTRACT_VERSION;
5
+ }
6
+ export {
7
+ CONTRACT_VERSION,
8
+ MIN_SUPPORTED_CONTRACT_VERSION,
9
+ isSupportedContractVersion
10
+ };
11
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/contract/version.ts"],
4
+ "sourcesContent": ["/**\n * Adapter contract version. Adapter packages declare the version they were built\n * against; the loader refuses to instantiate a mismatched major rather than let a\n * stale adapter fail in an unpredictable place at request time.\n */\nexport const CONTRACT_VERSION = 1\n\n/**\n * Oldest adapter contract this engine still accepts.\n *\n * An exact-match check makes every bump a flag day: a purely additive change\n * would disable every third-party adapter at once, with nothing wrong with any\n * of them. A range lets an additive bump raise `CONTRACT_VERSION` while leaving\n * this behind, and a genuinely breaking one raise both together.\n */\nexport const MIN_SUPPORTED_CONTRACT_VERSION = 1\n\nexport function isSupportedContractVersion(version: unknown): version is number {\n return (\n typeof version === 'number' &&\n Number.isInteger(version) &&\n version >= MIN_SUPPORTED_CONTRACT_VERSION &&\n version <= CONTRACT_VERSION\n )\n}\n"],
5
+ "mappings": "AAKO,MAAM,mBAAmB;AAUzB,MAAM,iCAAiC;AAEvC,SAAS,2BAA2B,SAAqC;AAC9E,SACE,OAAO,YAAY,YACnB,OAAO,UAAU,OAAO,KACxB,WAAW,kCACX,WAAW;AAEf;",
6
+ "names": []
7
+ }
@@ -0,0 +1,29 @@
1
+ import { createHash } from "node:crypto";
2
+ function buildCacheKey(request, adapterIds) {
3
+ const payload = JSON.stringify({
4
+ query: request.query.trim().toLowerCase(),
5
+ limit: request.limit,
6
+ locale: request.locale ?? null,
7
+ freshness: request.freshness ?? null,
8
+ site: request.site ?? null,
9
+ adapters: [...adapterIds].sort((left, right) => left < right ? -1 : left > right ? 1 : 0)
10
+ });
11
+ return createHash("sha256").update(payload).digest("hex");
12
+ }
13
+ function createSingleFlight() {
14
+ const inflight = /* @__PURE__ */ new Map();
15
+ return (key, run) => {
16
+ const existing = inflight.get(key);
17
+ if (existing) return existing;
18
+ const promise = run().finally(() => {
19
+ inflight.delete(key);
20
+ });
21
+ inflight.set(key, promise);
22
+ return promise;
23
+ };
24
+ }
25
+ export {
26
+ buildCacheKey,
27
+ createSingleFlight
28
+ };
29
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/engine/cache.ts"],
4
+ "sourcesContent": ["import { createHash } from 'node:crypto'\nimport type { SearchRequest } from '../contract/results'\n\nexport type CacheEntry<T> = {\n readonly value: T\n readonly storedAt: number\n}\n\n/**\n * Storage seam. Deliberately narrow so a host can back it with any store, and\n * deliberately failable \u2014 a cache outage must degrade to a live search, never to\n * an error, so implementations should swallow their own faults.\n */\nexport type ResultCache<T> = {\n get(key: string): Promise<T | null>\n set(key: string, value: T, ttlMs: number): Promise<void>\n}\n\nexport function buildCacheKey(request: SearchRequest, adapterIds: readonly string[]): string {\n const payload = JSON.stringify({\n query: request.query.trim().toLowerCase(),\n limit: request.limit,\n locale: request.locale ?? null,\n freshness: request.freshness ?? null,\n site: request.site ?? null,\n adapters: [...adapterIds].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0)),\n })\n return createHash('sha256').update(payload).digest('hex')\n}\n\n/**\n * Collapses concurrent identical calls onto one execution. A fan-out of agents\n * asking the same question hits each adapter once instead of once per agent.\n */\nexport function createSingleFlight<T>(): (key: string, run: () => Promise<T>) => Promise<T> {\n const inflight = new Map<string, Promise<T>>()\n return (key, run) => {\n const existing = inflight.get(key)\n if (existing) return existing\n const promise = run().finally(() => {\n inflight.delete(key)\n })\n inflight.set(key, promise)\n return promise\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,kBAAkB;AAkBpB,SAAS,cAAc,SAAwB,YAAuC;AAC3F,QAAM,UAAU,KAAK,UAAU;AAAA,IAC7B,OAAO,QAAQ,MAAM,KAAK,EAAE,YAAY;AAAA,IACxC,OAAO,QAAQ;AAAA,IACf,QAAQ,QAAQ,UAAU;AAAA,IAC1B,WAAW,QAAQ,aAAa;AAAA,IAChC,MAAM,QAAQ,QAAQ;AAAA,IACtB,UAAU,CAAC,GAAG,UAAU,EAAE,KAAK,CAAC,MAAM,UAAW,OAAO,QAAQ,KAAK,OAAO,QAAQ,IAAI,CAAE;AAAA,EAC5F,CAAC;AACD,SAAO,WAAW,QAAQ,EAAE,OAAO,OAAO,EAAE,OAAO,KAAK;AAC1D;AAMO,SAAS,qBAA4E;AAC1F,QAAM,WAAW,oBAAI,IAAwB;AAC7C,SAAO,CAAC,KAAK,QAAQ;AACnB,UAAM,WAAW,SAAS,IAAI,GAAG;AACjC,QAAI,SAAU,QAAO;AACrB,UAAM,UAAU,IAAI,EAAE,QAAQ,MAAM;AAClC,eAAS,OAAO,GAAG;AAAA,IACrB,CAAC;AACD,aAAS,IAAI,KAAK,OAAO;AACzB,WAAO;AAAA,EACT;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,54 @@
1
+ const DEADLINE_REACHED = /* @__PURE__ */ Symbol("web-research/deadline");
2
+ function deadlineRace(ms) {
3
+ let cancel = () => {
4
+ };
5
+ const promise = new Promise((resolve) => {
6
+ const timer = setTimeout(() => resolve(DEADLINE_REACHED), Math.max(0, ms));
7
+ timer.unref?.();
8
+ cancel = () => {
9
+ clearTimeout(timer);
10
+ };
11
+ });
12
+ return { promise, cancel };
13
+ }
14
+ function sleep(ms, signal) {
15
+ if (ms <= 0) return Promise.resolve();
16
+ return new Promise((resolve) => {
17
+ const timer = setTimeout(finish, ms);
18
+ function finish() {
19
+ clearTimeout(timer);
20
+ signal?.removeEventListener("abort", finish);
21
+ resolve();
22
+ }
23
+ signal?.addEventListener("abort", finish, { once: true });
24
+ });
25
+ }
26
+ function linkSignals(signals) {
27
+ const controller = new AbortController();
28
+ const active = signals.filter((candidate) => candidate !== void 0);
29
+ const abort = () => {
30
+ controller.abort();
31
+ };
32
+ for (const signal of active) {
33
+ if (signal.aborted) {
34
+ controller.abort();
35
+ break;
36
+ }
37
+ }
38
+ if (!controller.signal.aborted) {
39
+ for (const signal of active) signal.addEventListener("abort", abort, { once: true });
40
+ }
41
+ return {
42
+ signal: controller.signal,
43
+ dispose: () => {
44
+ for (const signal of active) signal.removeEventListener("abort", abort);
45
+ }
46
+ };
47
+ }
48
+ export {
49
+ DEADLINE_REACHED,
50
+ deadlineRace,
51
+ linkSignals,
52
+ sleep
53
+ };
54
+ //# sourceMappingURL=deadline.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/engine/deadline.ts"],
4
+ "sourcesContent": ["export const DEADLINE_REACHED = Symbol('web-research/deadline')\n\nexport type DeadlineSignal = typeof DEADLINE_REACHED\n\nexport type Cancellable<T> = {\n readonly promise: Promise<T>\n cancel(): void\n}\n\n/** A race participant that resolves once `ms` elapses and can be torn down. */\nexport function deadlineRace(ms: number): Cancellable<DeadlineSignal> {\n let cancel = (): void => {}\n const promise = new Promise<DeadlineSignal>((resolve) => {\n const timer = setTimeout(() => resolve(DEADLINE_REACHED), Math.max(0, ms))\n timer.unref?.()\n cancel = () => {\n clearTimeout(timer)\n }\n })\n return { promise, cancel }\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0) return Promise.resolve()\n return new Promise((resolve) => {\n const timer = setTimeout(finish, ms)\n function finish(): void {\n clearTimeout(timer)\n signal?.removeEventListener('abort', finish)\n resolve()\n }\n signal?.addEventListener('abort', finish, { once: true })\n })\n}\n\n/**\n * Aborts when any input aborts. Node's `AbortSignal.any` exists on modern\n * runtimes but is wrapped here so teardown is explicit and listeners are always\n * removed \u2014 an engine run creates one of these per adapter per request.\n */\nexport function linkSignals(signals: ReadonlyArray<AbortSignal | undefined>): {\n readonly signal: AbortSignal\n dispose(): void\n} {\n const controller = new AbortController()\n const active = signals.filter((candidate): candidate is AbortSignal => candidate !== undefined)\n const abort = (): void => {\n controller.abort()\n }\n for (const signal of active) {\n if (signal.aborted) {\n controller.abort()\n break\n }\n }\n if (!controller.signal.aborted) {\n for (const signal of active) signal.addEventListener('abort', abort, { once: true })\n }\n return {\n signal: controller.signal,\n dispose: () => {\n for (const signal of active) signal.removeEventListener('abort', abort)\n },\n }\n}\n"],
5
+ "mappings": "AAAO,MAAM,mBAAmB,uBAAO,uBAAuB;AAUvD,SAAS,aAAa,IAAyC;AACpE,MAAI,SAAS,MAAY;AAAA,EAAC;AAC1B,QAAM,UAAU,IAAI,QAAwB,CAAC,YAAY;AACvD,UAAM,QAAQ,WAAW,MAAM,QAAQ,gBAAgB,GAAG,KAAK,IAAI,GAAG,EAAE,CAAC;AACzE,UAAM,QAAQ;AACd,aAAS,MAAM;AACb,mBAAa,KAAK;AAAA,IACpB;AAAA,EACF,CAAC;AACD,SAAO,EAAE,SAAS,OAAO;AAC3B;AAEO,SAAS,MAAM,IAAY,QAAqC;AACrE,MAAI,MAAM,EAAG,QAAO,QAAQ,QAAQ;AACpC,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAM,QAAQ,WAAW,QAAQ,EAAE;AACnC,aAAS,SAAe;AACtB,mBAAa,KAAK;AAClB,cAAQ,oBAAoB,SAAS,MAAM;AAC3C,cAAQ;AAAA,IACV;AACA,YAAQ,iBAAiB,SAAS,QAAQ,EAAE,MAAM,KAAK,CAAC;AAAA,EAC1D,CAAC;AACH;AAOO,SAAS,YAAY,SAG1B;AACA,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,SAAS,QAAQ,OAAO,CAAC,cAAwC,cAAc,MAAS;AAC9F,QAAM,QAAQ,MAAY;AACxB,eAAW,MAAM;AAAA,EACnB;AACA,aAAW,UAAU,QAAQ;AAC3B,QAAI,OAAO,SAAS;AAClB,iBAAW,MAAM;AACjB;AAAA,IACF;AAAA,EACF;AACA,MAAI,CAAC,WAAW,OAAO,SAAS;AAC9B,eAAW,UAAU,OAAQ,QAAO,iBAAiB,SAAS,OAAO,EAAE,MAAM,KAAK,CAAC;AAAA,EACrF;AACA,SAAO;AAAA,IACL,QAAQ,WAAW;AAAA,IACnB,SAAS,MAAM;AACb,iBAAW,UAAU,OAAQ,QAAO,oBAAoB,SAAS,KAAK;AAAA,IACxE;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }