@omnicross/contracts 0.2.0 → 0.2.1

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.
@@ -0,0 +1,420 @@
1
+ /**
2
+ * Provider-neutral search contracts — the Phase-1 target surface.
3
+ *
4
+ * This module is the vocabulary every later search stage implements against:
5
+ * HTTP providers, API providers, the registry/orchestrator, and the protocol
6
+ * frontends. It is deliberately pure — no Node builtins, no Electron, no HTTP
7
+ * client, no upstream SDK types — so it can be imported from a browser bundle,
8
+ * a daemon, or a host application alike. `AbortSignal` is used only as an
9
+ * ambient web-standard type.
10
+ *
11
+ * Two rules this module exists to enforce:
12
+ *
13
+ * 1. **Nothing is inferred from an identifier's spelling.** A provider's
14
+ * transport and trust boundary are declared explicitly on
15
+ * {@link SearchProviderContribution} (`kind` / `source`). No exported
16
+ * function here classifies a provider from its id, and none ever should —
17
+ * that is what the deprecated `isApiProvider`/`isLocalProvider` helpers in
18
+ * `./websearch-types` did.
19
+ * 2. **The provider id space is open.** {@link KnownSearchProviderId} lists
20
+ * only the providers Omnicross itself ships; host contributions and future
21
+ * providers arrive as plain strings without a contract change.
22
+ *
23
+ * Legacy `WebSearch*` shapes in `./websearch-types` stay where they are;
24
+ * `./search-compat` converts between the two while consumers migrate.
25
+ */
26
+ /** A single search result item. Field names are the legacy ones, on purpose. */
27
+ interface SearchResult {
28
+ /** Result title. Non-empty for a usable result. */
29
+ title: string;
30
+ /** Direct result URL (not a SERP redirect). */
31
+ url: string;
32
+ /** Result snippet or page content. May be `''` when the source has none. */
33
+ content: string;
34
+ }
35
+ /**
36
+ * A completed search from ONE provider.
37
+ *
38
+ * There is no `success` flag and no error string: a response only exists for a
39
+ * search that produced results. Failures travel as {@link SearchProviderError}
40
+ * (thrown) or {@link SearchErrorShape} (serialized).
41
+ */
42
+ interface SearchResponse {
43
+ /** The query that produced these results. */
44
+ query: string;
45
+ /** The provider that produced them. */
46
+ providerId: SearchProviderId;
47
+ /** Results in provider order, already deduplicated by the producer. */
48
+ results: SearchResult[];
49
+ }
50
+ /** Recency window for providers whose `supportsTimeRange` capability is true. */
51
+ type SearchTimeRange = 'day' | 'week' | 'month' | 'year';
52
+ /**
53
+ * Per-request options. The first four fields are the legacy
54
+ * `WebSearchOptions` set, unchanged, so legacy call sites assign without a
55
+ * mapping; the rest match the capability flags a provider declares.
56
+ *
57
+ * A provider MUST ignore an option it does not declare support for rather than
58
+ * failing the request.
59
+ */
60
+ interface SearchOptions {
61
+ /** Maximum number of results to return. */
62
+ maxResults?: number;
63
+ /** Request timeout in milliseconds. */
64
+ timeout?: number;
65
+ /** Cancellation signal — the only cancellation channel in this contract. */
66
+ signal?: AbortSignal;
67
+ /** Fetch full page content instead of snippets, where the provider can. */
68
+ fetchPageContent?: boolean;
69
+ /** Region/market hint (provider-specific spelling, e.g. `us`, `zh-CN`). */
70
+ region?: string;
71
+ /** Result language hint. */
72
+ language?: string;
73
+ /** Recency window. */
74
+ timeRange?: SearchTimeRange;
75
+ }
76
+ /** A search to execute, optionally pinned to one provider. */
77
+ interface SearchRequest {
78
+ /** The user or tool supplied query. Untrusted input. */
79
+ query: string;
80
+ /** Pin the search to one provider; omit to let the runtime choose. */
81
+ provider?: SearchProviderId;
82
+ /** Per-request options. */
83
+ options?: SearchOptions;
84
+ }
85
+ /**
86
+ * The provider ids Omnicross itself ships in Phase 1.
87
+ *
88
+ * This list is NOT the set of valid ids — see {@link SearchProviderId}. Ids
89
+ * absent from it are absent by decision, not by oversight: `local-*` providers
90
+ * remain host (Elftia) contributions because they need a browser runtime, and
91
+ * `grok` / `claude` / `exa` / `bocha` are deliberately not registered in
92
+ * Phase 1.
93
+ */
94
+ type KnownSearchProviderId = 'http-bing' | 'http-duckduckgo' | 'tavily' | 'jina' | 'searxng' | 'zhipu' | 'z.ai';
95
+ /**
96
+ * A provider identifier — open by construction.
97
+ *
98
+ * `(string & {})` keeps autocomplete for {@link KnownSearchProviderId} while
99
+ * accepting any other string, so host contributions and namespaced custom ids
100
+ * (`acme:internal-search`) are first-class without growing a union. Whether an
101
+ * id is *registered* is a registry question, not a type-level one.
102
+ */
103
+ type SearchProviderId = KnownSearchProviderId | (string & {});
104
+ /**
105
+ * Narrow an id to {@link KnownSearchProviderId}.
106
+ *
107
+ * This is a membership test against the shipped list — it does NOT classify.
108
+ * A `false` result says "not one of the ids Omnicross ships", never "invalid",
109
+ * "untrusted", or "local".
110
+ */
111
+ declare function isKnownSearchProviderId(id: SearchProviderId): id is KnownSearchProviderId;
112
+ /**
113
+ * What a provider can do, declared explicitly.
114
+ *
115
+ * Flat booleans plus one limit: the shape serializes as-is into capability
116
+ * discovery and doctor output, and extends additively.
117
+ */
118
+ interface SearchProviderCapabilities {
119
+ /** Requires a configured API key to run at all. */
120
+ requiresApiKey: boolean;
121
+ /** Honors {@link SearchOptions.region}. */
122
+ supportsRegion: boolean;
123
+ /** Honors {@link SearchOptions.language}. */
124
+ supportsLanguage: boolean;
125
+ /** Honors {@link SearchOptions.timeRange}. */
126
+ supportsTimeRange: boolean;
127
+ /** Implements {@link SearchProvider.readUrl}. */
128
+ supportsUrlRead: boolean;
129
+ /** Honors {@link SearchOptions.signal}. */
130
+ supportsCancellation: boolean;
131
+ /** Upper bound on results per request, when the provider imposes one. */
132
+ maxResults?: number;
133
+ }
134
+ /** The result of reading one URL through a provider that supports it. */
135
+ interface SearchUrlReadResult {
136
+ /** The URL that was read (post-redirect, when the provider reports it). */
137
+ url: string;
138
+ /** Page title, when available. */
139
+ title?: string;
140
+ /** Extracted page content, when available. */
141
+ content?: string;
142
+ }
143
+ /**
144
+ * A search provider.
145
+ *
146
+ * `search` returns bare results; assembling a {@link SearchResponse} is the
147
+ * runtime's job. Failures are thrown as {@link SearchProviderError} — there is
148
+ * no in-band failure channel, because a code-less error string cannot drive a
149
+ * fallback policy.
150
+ */
151
+ interface SearchProvider {
152
+ /** Stable provider identifier, matching its contribution's `id`. */
153
+ readonly id: SearchProviderId;
154
+ /** Run a search. Throws {@link SearchProviderError} on failure. */
155
+ search(query: string, options?: SearchOptions): Promise<SearchResult[]>;
156
+ /** Read one URL, when `capabilities.supportsUrlRead` is true. */
157
+ readUrl?(url: string, options?: SearchOptions): Promise<SearchUrlReadResult>;
158
+ /** Report health without performing a real search. */
159
+ healthCheck?(): Promise<SearchProviderDiagnostic>;
160
+ }
161
+ /** Who supplied a provider: the Omnicross runtime, or the embedding host. */
162
+ type SearchProviderSource = 'builtin' | 'host';
163
+ /**
164
+ * How a provider reaches the network. This is the baseline's observed
165
+ * transport vocabulary, kept verbatim so behavior comparisons across the
166
+ * legacy and new runtimes speak one language.
167
+ */
168
+ type SearchTransportKind = 'api' | 'http' | 'local-browser' | 'native';
169
+ /**
170
+ * A provider offered to the registry, with everything the runtime must never
171
+ * infer stated up front.
172
+ *
173
+ * `source` and `kind` together are the structural replacement for
174
+ * `id.startsWith('local-')`: eligibility, secret handling, and egress policy
175
+ * read these declared fields, never the id's spelling.
176
+ */
177
+ interface SearchProviderContribution {
178
+ /** Stable id the provider registers under. */
179
+ id: SearchProviderId;
180
+ /** Explicit origin declaration. */
181
+ source: SearchProviderSource;
182
+ /** Explicit transport declaration. */
183
+ kind: SearchTransportKind;
184
+ /** The provider implementation. */
185
+ provider: SearchProvider;
186
+ /** Explicit capability declaration. */
187
+ capabilities: SearchProviderCapabilities;
188
+ /** Ordering hint for the registry; lower runs earlier. Ties keep registration order. */
189
+ priorityHint?: number;
190
+ }
191
+ /**
192
+ * The stable search error taxonomy — exactly eight codes.
193
+ *
194
+ * These are the vocabulary a fallback policy decides on (`cancelled` and
195
+ * `policy_denied` must not be retried) and the vocabulary behavior comparisons
196
+ * are written in. `blocked` is deliberately absent: it is a *diagnostic
197
+ * status* ({@link SearchProviderHealthStatus}), not a failure code.
198
+ */
199
+ type SearchErrorCode = 'config_missing' | 'auth_failed' | 'rate_limited' | 'timeout' | 'upstream_unavailable' | 'parse_failed' | 'cancelled' | 'policy_denied';
200
+ /**
201
+ * A search failure in serializable form — what crosses a wire, lands in a
202
+ * diagnostic, or gets logged.
203
+ *
204
+ * `message` and `details` values MUST be pre-sanitized by the producer: no API
205
+ * keys, no cookies, no raw headers, no proxy URLs. `details` is deliberately
206
+ * `Record<string, string>` so nothing nested can smuggle a credential object.
207
+ */
208
+ interface SearchErrorShape {
209
+ /** Stable taxonomy code. */
210
+ code: SearchErrorCode;
211
+ /** Human-readable, pre-sanitized message. */
212
+ message: string;
213
+ /** The provider that failed, when known. */
214
+ providerId?: SearchProviderId;
215
+ /** Whether retrying the same provider could plausibly succeed. */
216
+ retryable?: boolean;
217
+ /** Pre-sanitized string-valued context. */
218
+ details?: Record<string, string>;
219
+ }
220
+ /** Optional fields for {@link SearchProviderError}. */
221
+ interface SearchProviderErrorInit {
222
+ /** The provider that failed. */
223
+ providerId?: SearchProviderId;
224
+ /** Whether retrying the same provider could plausibly succeed. */
225
+ retryable?: boolean;
226
+ /** Pre-sanitized string-valued context. */
227
+ details?: Record<string, string>;
228
+ /** Underlying error, for local diagnosis. Never serialized into the shape. */
229
+ cause?: unknown;
230
+ }
231
+ /**
232
+ * The error providers throw.
233
+ *
234
+ * Carries the same fields as {@link SearchErrorShape} plus an optional
235
+ * `cause`, which stays local — {@link toSearchErrorShape} never serializes it.
236
+ */
237
+ declare class SearchProviderError extends Error {
238
+ /** Stable taxonomy code. */
239
+ readonly code: SearchErrorCode;
240
+ /** The provider that failed, when known. */
241
+ readonly providerId?: SearchProviderId;
242
+ /** Whether retrying the same provider could plausibly succeed. */
243
+ readonly retryable?: boolean;
244
+ /** Pre-sanitized string-valued context. */
245
+ readonly details?: Record<string, string>;
246
+ constructor(code: SearchErrorCode, message: string, init?: SearchProviderErrorInit);
247
+ /** This error as its serializable {@link SearchErrorShape}. */
248
+ toShape(): SearchErrorShape;
249
+ }
250
+ /**
251
+ * Whether a value is a search error carrying a taxonomy code.
252
+ *
253
+ * Structural as well as `instanceof`, because `@omnicross/contracts` can be
254
+ * present twice in one process (ESM and CJS builds, or `src` under test beside
255
+ * `dist` at runtime) and a bare `instanceof` silently fails across those
256
+ * copies.
257
+ */
258
+ declare function isSearchProviderError(value: unknown): value is SearchProviderError;
259
+ /**
260
+ * Convert any thrown value into a {@link SearchErrorShape}.
261
+ *
262
+ * A {@link SearchProviderError} keeps its code and fields. Anything else gets
263
+ * the documented default code `upstream_unavailable` — this function does NOT
264
+ * sniff messages; string-to-code translation of legacy error text lives in
265
+ * `./search-compat` so exactly one mapping exists.
266
+ */
267
+ declare function toSearchErrorShape(value: unknown): SearchErrorShape;
268
+ /**
269
+ * The knobs a runtime uses to choose and order providers.
270
+ *
271
+ * These are plan §11.3's query-egress controls: a query reaches a provider only
272
+ * because policy allowed it, so "send this query to exactly one provider" is
273
+ * expressible (`fallbackEnabled: false`, or a one-id `allowed`) rather than
274
+ * implied. Every field is optional; the documented defaults are fallback on,
275
+ * every registered provider allowed, and an unbounded number of attempts.
276
+ */
277
+ interface SearchPolicy {
278
+ /** Try this provider first when the request pins none. Unknown ids are skipped, not errors. */
279
+ preferred?: SearchProviderId;
280
+ /** Restrict candidates to these ids. Omit to allow every registered provider. */
281
+ allowed?: SearchProviderId[];
282
+ /** Whether a failed candidate may be followed by another. Omit for `true`. */
283
+ fallbackEnabled?: boolean;
284
+ /** Upper bound on candidates attempted for one search. Omit for unbounded. */
285
+ maxAttempts?: number;
286
+ }
287
+ /** How one provider attempt ended. Empty results are a `success`. */
288
+ type SearchAttemptOutcome = 'success' | 'failed';
289
+ /**
290
+ * One provider attempt, as recorded by the orchestrator.
291
+ *
292
+ * This is the structured record; it carries no query text and no result
293
+ * content, so it is safe to log or return to a caller verbatim.
294
+ */
295
+ interface SearchAttempt {
296
+ /** The provider that was attempted. */
297
+ providerId: SearchProviderId;
298
+ /** How the attempt ended. */
299
+ outcome: SearchAttemptOutcome;
300
+ /** Taxonomy code for a `failed` attempt. */
301
+ errorCode?: SearchErrorCode;
302
+ /** Results the attempt produced; `0` is a legitimate success. */
303
+ resultCount?: number;
304
+ /** Wall-clock duration of the attempt in milliseconds. */
305
+ durationMs: number;
306
+ }
307
+ /**
308
+ * A {@link SearchResponse} plus what the orchestrator did to produce it.
309
+ *
310
+ * `attempts` records provider EXECUTIONS in order; candidates that were never
311
+ * attempted (skipped by policy, cut off by `maxAttempts`) do not appear.
312
+ */
313
+ interface OrchestratedSearchResponse extends SearchResponse {
314
+ /** Every provider attempt, in the order they ran. */
315
+ attempts: SearchAttempt[];
316
+ /** Attempts beyond the first — `attempts.length - 1`, never negative. */
317
+ fallbackCount: number;
318
+ }
319
+ /**
320
+ * A registered provider as capability discovery sees it.
321
+ *
322
+ * Serializable by construction: the provider instance is deliberately absent,
323
+ * so a descriptor can cross a wire, land in doctor output, or reach a renderer
324
+ * without dragging an implementation (or its configuration) along.
325
+ */
326
+ interface SearchProviderDescriptor {
327
+ /** The id the provider is registered under. */
328
+ id: SearchProviderId;
329
+ /** Declared origin. */
330
+ source: SearchProviderSource;
331
+ /** Declared transport. */
332
+ kind: SearchTransportKind;
333
+ /** Declared capabilities. */
334
+ capabilities: SearchProviderCapabilities;
335
+ }
336
+ /**
337
+ * Who is registering a contribution.
338
+ *
339
+ * Minimal on purpose: the parameter exists today so a Phase-2 host registering
340
+ * its `local-*` providers does not force a signature change, and so registry
341
+ * policy has somewhere to read a caller identity from when it needs one.
342
+ */
343
+ interface SearchContributionContext {
344
+ /** Identifier of the embedding host making the registration. */
345
+ hostId?: string;
346
+ }
347
+ /**
348
+ * Fields every runtime event carries.
349
+ *
350
+ * `queryHash` — never the query — is the correlation key (plan §11.3). No
351
+ * variant of {@link SearchRuntimeEvent} declares a field for query text, a URL,
352
+ * or result content, and none ever should: these events are built to be logged.
353
+ */
354
+ interface SearchRuntimeEventBase {
355
+ /** Correlates every event of one search. Random per search. */
356
+ requestId: string;
357
+ /** Short one-way hash of the query, computed by the runtime. */
358
+ queryHash: string;
359
+ /** Wall-clock duration in milliseconds. */
360
+ durationMs: number;
361
+ }
362
+ /** One provider attempt, as observed. */
363
+ interface SearchAttemptEvent extends SearchRuntimeEventBase {
364
+ type: 'search_attempt';
365
+ /** The provider attempted, or the one a policy skipped. */
366
+ providerId: SearchProviderId;
367
+ /** How the attempt ended. */
368
+ outcome: SearchAttemptOutcome;
369
+ /** Taxonomy code for a `failed` attempt. */
370
+ errorCode?: SearchErrorCode;
371
+ /** Results the attempt produced. */
372
+ resultCount?: number;
373
+ }
374
+ /**
375
+ * The single terminal event of a search, emitted for success and failure alike.
376
+ *
377
+ * `providerId` is present exactly when a provider produced the response — its
378
+ * absence means no provider succeeded, which is what separates a failed search
379
+ * from a provider that authoritatively found nothing (present id, zero results).
380
+ */
381
+ interface SearchCompleteEvent extends SearchRuntimeEventBase {
382
+ type: 'search_complete';
383
+ /** The provider that produced the response, when one did. */
384
+ providerId?: SearchProviderId;
385
+ /** Results returned to the caller; `0` for a failed search. */
386
+ resultCount: number;
387
+ /** Attempts beyond the first. */
388
+ fallbackCount: number;
389
+ }
390
+ /** Everything a runtime observability listener can receive. */
391
+ type SearchRuntimeEvent = SearchAttemptEvent | SearchCompleteEvent;
392
+ /**
393
+ * Provider health as reported by doctor/diagnostics surfaces.
394
+ *
395
+ * `blocked` means an egress or policy decision stopped the provider — the
396
+ * status that keeps `blocked` out of {@link SearchErrorCode}.
397
+ */
398
+ type SearchProviderHealthStatus = 'healthy' | 'degraded' | 'unconfigured' | 'blocked' | 'failed';
399
+ /**
400
+ * One provider's health.
401
+ *
402
+ * This type has no field for an API key, cookie, token, or raw request or
403
+ * response header, and must never gain one. `reason` and any
404
+ * `error.details` values MUST be pre-sanitized by the producer before they
405
+ * reach this shape — diagnostics are displayed and logged.
406
+ */
407
+ interface SearchProviderDiagnostic {
408
+ /** The provider this diagnostic describes. */
409
+ providerId: SearchProviderId;
410
+ /** Current status. */
411
+ status: SearchProviderHealthStatus;
412
+ /** ISO-8601 timestamp of the check. */
413
+ checkedAt?: string;
414
+ /** Short, pre-sanitized explanation of a non-healthy status. */
415
+ reason?: string;
416
+ /** The failure behind a `failed`/`degraded` status. */
417
+ error?: SearchErrorShape;
418
+ }
419
+
420
+ export { type KnownSearchProviderId, type OrchestratedSearchResponse, type SearchAttempt, type SearchAttemptEvent, type SearchAttemptOutcome, type SearchCompleteEvent, type SearchContributionContext, type SearchErrorCode, type SearchErrorShape, type SearchOptions, type SearchPolicy, type SearchProvider, type SearchProviderCapabilities, type SearchProviderContribution, type SearchProviderDescriptor, type SearchProviderDiagnostic, SearchProviderError, type SearchProviderErrorInit, type SearchProviderHealthStatus, type SearchProviderId, type SearchProviderSource, type SearchRequest, type SearchResponse, type SearchResult, type SearchRuntimeEvent, type SearchRuntimeEventBase, type SearchTimeRange, type SearchTransportKind, type SearchUrlReadResult, isKnownSearchProviderId, isSearchProviderError, toSearchErrorShape };