@omnicross/core 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BuiltinToolExecutor-CS2WpXhM.d.cts → BuiltinToolExecutor-BHgH-EaZ.d.cts} +25 -2
- package/dist/{BuiltinToolExecutor-BluWyeob.d.ts → BuiltinToolExecutor-Bi1LmiTP.d.ts} +25 -2
- package/dist/{CompletionService-BiJqftc-.d.cts → CompletionService-BniP7j88.d.cts} +2 -2
- package/dist/{CompletionService-NXN_Av5W.d.ts → CompletionService-BqrKPwfA.d.ts} +2 -2
- package/dist/{ProviderProxy-CgIcCVwk.d.cts → ProviderProxy-Bz_nkOwq.d.cts} +1 -1
- package/dist/{ProviderProxy-DYoY5XoT.d.ts → ProviderProxy-Ckr6oTrE.d.ts} +1 -1
- package/dist/auth/GeminiCodeAssistProjectResolver.cjs +2 -2
- package/dist/auth/GeminiCodeAssistProjectResolver.js +1 -1
- package/dist/{chunk-FJNKXOWE.js → chunk-2C3TTBS7.js} +4 -4
- package/dist/{chunk-NZYN7C3P.cjs → chunk-2RCCLQYV.cjs} +53 -53
- package/dist/{chunk-CDHS2QDC.js → chunk-4PFWFODB.js} +53 -53
- package/dist/{chunk-OST5RJOJ.cjs → chunk-4Z3ZLLC2.cjs} +13 -7
- package/dist/chunk-7RJI3NDI.cjs +236 -0
- package/dist/chunk-BTBEJRGL.cjs +781 -0
- package/dist/chunk-DBBR6RHF.js +236 -0
- package/dist/chunk-DXWFBBSK.js +781 -0
- package/dist/chunk-E4QHVXV5.cjs +118 -0
- package/dist/chunk-L7O2YQ5O.js +386 -0
- package/dist/{chunk-WSJE42SG.js → chunk-LKDAN4D7.js} +3684 -2462
- package/dist/{chunk-DEIUR2TH.js → chunk-LQYS2N47.js} +12 -8
- package/dist/chunk-MZEGQSKQ.js +118 -0
- package/dist/chunk-PTU7SXM3.cjs +386 -0
- package/dist/{chunk-CTKM773J.cjs → chunk-UPQIWJIE.cjs} +12 -8
- package/dist/{chunk-CFSGYFOZ.js → chunk-VL5TUDTM.js} +7 -1
- package/dist/{chunk-7B56AJBB.cjs → chunk-VSYQ6XYN.cjs} +2233 -1011
- package/dist/{chunk-TVYA5JSQ.cjs → chunk-ZAPN7XA6.cjs} +5 -5
- package/dist/completion/BuiltinToolExecutor.cjs +2 -2
- package/dist/completion/BuiltinToolExecutor.d.cts +3 -1
- package/dist/completion/BuiltinToolExecutor.d.ts +3 -1
- package/dist/completion/BuiltinToolExecutor.js +1 -1
- package/dist/completion/CompletionService.cjs +11 -10
- package/dist/completion/CompletionService.d.cts +6 -3
- package/dist/completion/CompletionService.d.ts +6 -3
- package/dist/completion/CompletionService.js +10 -9
- package/dist/completion.cjs +11 -10
- package/dist/completion.d.cts +6 -3
- package/dist/completion.d.ts +6 -3
- package/dist/completion.js +10 -9
- package/dist/egress-I46O_p5L.d.cts +126 -0
- package/dist/egress-I46O_p5L.d.ts +126 -0
- package/dist/frontends-Doz0RaWu.d.cts +97 -0
- package/dist/frontends-Doz0RaWu.d.ts +97 -0
- package/dist/image-generation.cjs +2 -2
- package/dist/image-generation.d.cts +7 -4
- package/dist/image-generation.d.ts +7 -4
- package/dist/image-generation.js +1 -1
- package/dist/index.cjs +25 -10
- package/dist/index.d.cts +12 -8
- package/dist/index.d.ts +12 -8
- package/dist/index.js +24 -9
- package/dist/openai-operation.d.cts +4 -1
- package/dist/openai-operation.d.ts +4 -1
- package/dist/outbound-api/routeResolver.d.cts +9 -5
- package/dist/outbound-api/routeResolver.d.ts +9 -5
- package/dist/outbound-api/subscriptionRegistryPort.d.cts +4 -1
- package/dist/outbound-api/subscriptionRegistryPort.d.ts +4 -1
- package/dist/outbound-api/types.d.cts +8 -4
- package/dist/outbound-api/types.d.ts +8 -4
- package/dist/outbound-api.cjs +25 -10
- package/dist/outbound-api.d.cts +117 -8
- package/dist/outbound-api.d.ts +117 -8
- package/dist/outbound-api.js +24 -9
- package/dist/pipeline/AccountAllowanceScheduling.d.cts +8 -4
- package/dist/pipeline/AccountAllowanceScheduling.d.ts +8 -4
- package/dist/pipeline/upstreamFetch.cjs +2 -2
- package/dist/pipeline/upstreamFetch.js +1 -1
- package/dist/ports.d.cts +8 -4
- package/dist/ports.d.ts +8 -4
- package/dist/provider-proxy/ProviderProxy.cjs +11 -10
- package/dist/provider-proxy/ProviderProxy.d.cts +5 -2
- package/dist/provider-proxy/ProviderProxy.d.ts +5 -2
- package/dist/provider-proxy/ProviderProxy.js +10 -9
- package/dist/provider-proxy/ingress/providerProxyShared.cjs +13 -10
- package/dist/provider-proxy/ingress/providerProxyShared.d.cts +22 -3
- package/dist/provider-proxy/ingress/providerProxyShared.d.ts +22 -3
- package/dist/provider-proxy/ingress/providerProxyShared.js +12 -9
- package/dist/provider-proxy/types.d.cts +4 -1
- package/dist/provider-proxy/types.d.ts +4 -1
- package/dist/provider-proxy.cjs +11 -10
- package/dist/provider-proxy.d.cts +10 -6
- package/dist/provider-proxy.d.ts +10 -6
- package/dist/provider-proxy.js +10 -9
- package/dist/proxy-UZTtjPZx.d.cts +18 -0
- package/dist/proxy-UZTtjPZx.d.ts +18 -0
- package/dist/{routeResolver-rpKQLyDO.d.cts → routeResolver-Da0yvkL1.d.cts} +2 -2
- package/dist/{routeResolver-CTCcSHWu.d.ts → routeResolver-STwMz_G4.d.ts} +2 -2
- package/dist/runtime-5NCT3J3T.cjs +8 -0
- package/dist/runtime-JTHUWCBM.js +8 -0
- package/dist/runtime-t8AZacFF.d.cts +233 -0
- package/dist/runtime-t8AZacFF.d.ts +233 -0
- package/dist/search/api.cjs +766 -0
- package/dist/search/api.d.cts +385 -0
- package/dist/search/api.d.ts +385 -0
- package/dist/search/api.js +766 -0
- package/dist/search/http.cjs +37 -0
- package/dist/search/http.d.cts +323 -0
- package/dist/search/http.d.ts +323 -0
- package/dist/search/http.js +37 -0
- package/dist/search.cjs +57 -0
- package/dist/search.d.cts +113 -0
- package/dist/search.d.ts +113 -0
- package/dist/search.js +57 -0
- package/dist/transformer/transformers/AnthropicTransformer.cjs +3 -3
- package/dist/transformer/transformers/AnthropicTransformer.js +2 -2
- package/dist/transformer/transformers.cjs +7 -7
- package/dist/transformer/transformers.js +6 -6
- package/dist/transformer.cjs +6 -6
- package/dist/transformer.js +5 -5
- package/dist/{types-DsHBBk3h.d.cts → types-BSIJdsyl.d.cts} +15 -0
- package/dist/{types-CbglB73l.d.cts → types-Be-yt_6T.d.cts} +1 -1
- package/dist/{types-CVRge76p.d.ts → types-By9Tg6-I.d.ts} +15 -0
- package/dist/types-CbCZqLGg.d.cts +127 -0
- package/dist/types-CbCZqLGg.d.ts +127 -0
- package/dist/{types-jjU24eYG.d.cts → types-D-oHYusb.d.cts} +73 -4
- package/dist/{types-Bd72H4SB.d.ts → types-Dm4_36V4.d.ts} +1 -1
- package/dist/{types-5T223WYQ.d.ts → types-Dvlpjn0d.d.ts} +73 -4
- package/package.json +6 -3
- package/dist/{chunk-6OC3ORW6.js → chunk-EUZRH544.js} +3 -3
- package/dist/{chunk-SQMAEDNE.cjs → chunk-POUTD5AM.cjs} +2 -2
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }// src/search/frontends.ts
|
|
2
|
+
var SEARCH_FRONTEND_NAMES = Object.freeze([
|
|
3
|
+
"codex",
|
|
4
|
+
"responses",
|
|
5
|
+
"anthropic"
|
|
6
|
+
]);
|
|
7
|
+
var SEARCH_FRONTEND_MODES = Object.freeze([
|
|
8
|
+
"native",
|
|
9
|
+
"managed",
|
|
10
|
+
"off"
|
|
11
|
+
]);
|
|
12
|
+
var DEFAULT_SEARCH_FRONTEND_MODES = Object.freeze({
|
|
13
|
+
codex: "off",
|
|
14
|
+
responses: "native",
|
|
15
|
+
anthropic: "native"
|
|
16
|
+
});
|
|
17
|
+
var SEARCH_UNSUPPORTED_CAPABILITY_CODE = "unsupported_capability";
|
|
18
|
+
function searchErrorHttpStatus(code) {
|
|
19
|
+
switch (code) {
|
|
20
|
+
case "rate_limited":
|
|
21
|
+
return 429;
|
|
22
|
+
case "timeout":
|
|
23
|
+
return 504;
|
|
24
|
+
case "cancelled":
|
|
25
|
+
return 499;
|
|
26
|
+
case "config_missing":
|
|
27
|
+
return 503;
|
|
28
|
+
case "auth_failed":
|
|
29
|
+
case "upstream_unavailable":
|
|
30
|
+
case "parse_failed":
|
|
31
|
+
case "policy_denied":
|
|
32
|
+
return 502;
|
|
33
|
+
default:
|
|
34
|
+
return 502;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function isSearchFrontendMode(value) {
|
|
38
|
+
return typeof value === "string" && SEARCH_FRONTEND_MODES.includes(value);
|
|
39
|
+
}
|
|
40
|
+
function record(value) {
|
|
41
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
42
|
+
}
|
|
43
|
+
function normalizeSearchFrontendModes(value) {
|
|
44
|
+
const raw = _nullishCoalesce(record(value), () => ( {}));
|
|
45
|
+
const modes = {
|
|
46
|
+
...DEFAULT_SEARCH_FRONTEND_MODES
|
|
47
|
+
};
|
|
48
|
+
for (const name of SEARCH_FRONTEND_NAMES) {
|
|
49
|
+
const candidate = raw[name];
|
|
50
|
+
if (isSearchFrontendMode(candidate)) modes[name] = candidate;
|
|
51
|
+
}
|
|
52
|
+
return Object.freeze(modes);
|
|
53
|
+
}
|
|
54
|
+
function validateSearchFrontendModes(value, path = "$.search.modes") {
|
|
55
|
+
if (value === void 0) return [];
|
|
56
|
+
const raw = record(value);
|
|
57
|
+
if (!raw) return [`${path}: expected an object`];
|
|
58
|
+
const errors = [];
|
|
59
|
+
for (const [key, candidate] of Object.entries(raw)) {
|
|
60
|
+
if (!SEARCH_FRONTEND_NAMES.includes(key)) {
|
|
61
|
+
errors.push(`${path}.${key}: unknown frontend (expected ${SEARCH_FRONTEND_NAMES.join(", ")})`);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (isSearchFrontendMode(candidate)) continue;
|
|
65
|
+
errors.push(
|
|
66
|
+
`${path}.${key}: not a mode (got ${typeof candidate}; expected ${SEARCH_FRONTEND_MODES.join(", ")})`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return errors;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/search/webSearchBackendCompat.ts
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
var _searchcompat = require('@omnicross/contracts/search-compat');
|
|
77
|
+
var _searchtypes = require('@omnicross/contracts/search-types');
|
|
78
|
+
function searchRuntimeAsWebSearchBackend(runtime, options = {}) {
|
|
79
|
+
return {
|
|
80
|
+
async search(query, providerId, searchOptions) {
|
|
81
|
+
try {
|
|
82
|
+
const response = await runtime.search({
|
|
83
|
+
query,
|
|
84
|
+
provider: providerId,
|
|
85
|
+
// `WebSearchOptions` is a strict subset of `SearchOptions` with
|
|
86
|
+
// identical field names — asserted in the contracts tests.
|
|
87
|
+
...searchOptions === void 0 ? {} : { options: searchOptions }
|
|
88
|
+
});
|
|
89
|
+
return _searchcompat.toLegacyWebSearchResponse.call(void 0, response);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return _searchcompat.searchErrorToLegacyWebSearchResponse.call(void 0, query, _searchtypes.toSearchErrorShape.call(void 0, error));
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
isProviderEnabled(id) {
|
|
95
|
+
return runtime.listProviders().some((descriptor) => descriptor.id === id);
|
|
96
|
+
},
|
|
97
|
+
async readUrl(url, readOptions) {
|
|
98
|
+
if (options.readUrl !== void 0) return options.readUrl(url, readOptions);
|
|
99
|
+
return {
|
|
100
|
+
success: false,
|
|
101
|
+
url,
|
|
102
|
+
error: "URL reading is not available through the search runtime adapter"
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
exports.SEARCH_FRONTEND_NAMES = SEARCH_FRONTEND_NAMES; exports.SEARCH_FRONTEND_MODES = SEARCH_FRONTEND_MODES; exports.DEFAULT_SEARCH_FRONTEND_MODES = DEFAULT_SEARCH_FRONTEND_MODES; exports.SEARCH_UNSUPPORTED_CAPABILITY_CODE = SEARCH_UNSUPPORTED_CAPABILITY_CODE; exports.searchErrorHttpStatus = searchErrorHttpStatus; exports.isSearchFrontendMode = isSearchFrontendMode; exports.normalizeSearchFrontendModes = normalizeSearchFrontendModes; exports.validateSearchFrontendModes = validateSearchFrontendModes; exports.searchRuntimeAsWebSearchBackend = searchRuntimeAsWebSearchBackend;
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import {
|
|
2
|
+
builtinHttpSearchContributions
|
|
3
|
+
} from "./chunk-DXWFBBSK.js";
|
|
4
|
+
|
|
5
|
+
// src/search/orchestrator.ts
|
|
6
|
+
import { SearchProviderError, toSearchErrorShape } from "@omnicross/contracts/search-types";
|
|
7
|
+
import { createHash, randomUUID } from "crypto";
|
|
8
|
+
|
|
9
|
+
// src/search/normalize.ts
|
|
10
|
+
var DEFAULT_NORMALIZED_MAX_RESULTS = 5;
|
|
11
|
+
var SERP_REDIRECT_RULES = [
|
|
12
|
+
{ domain: "google.com", paths: ["/search", "/goto"] },
|
|
13
|
+
{ domain: "bing.com", paths: ["/search"] },
|
|
14
|
+
{ domain: "duckduckgo.com", paths: ["/html/", "/l/"] },
|
|
15
|
+
{ domain: "baidu.com", paths: ["/s"] }
|
|
16
|
+
];
|
|
17
|
+
function isDirectResultUrl(rawUrl) {
|
|
18
|
+
let url;
|
|
19
|
+
try {
|
|
20
|
+
url = new URL(rawUrl);
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
25
|
+
const host = url.hostname.toLowerCase();
|
|
26
|
+
for (const rule of SERP_REDIRECT_RULES) {
|
|
27
|
+
if (host !== rule.domain && !host.endsWith(`.${rule.domain}`)) continue;
|
|
28
|
+
if (rule.paths.includes(url.pathname)) return false;
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
function normalizedResultLimit(maxResults) {
|
|
33
|
+
const requested = maxResults === void 0 || !Number.isFinite(maxResults) ? DEFAULT_NORMALIZED_MAX_RESULTS : Math.floor(maxResults);
|
|
34
|
+
return Math.max(1, requested);
|
|
35
|
+
}
|
|
36
|
+
function normalizeSearchResults(results, maxResults) {
|
|
37
|
+
const limit = normalizedResultLimit(maxResults);
|
|
38
|
+
const seenUrls = /* @__PURE__ */ new Set();
|
|
39
|
+
const normalized = [];
|
|
40
|
+
for (const result of results) {
|
|
41
|
+
if (result === null || typeof result !== "object") continue;
|
|
42
|
+
const title = typeof result.title === "string" ? result.title.trim() : "";
|
|
43
|
+
const url = typeof result.url === "string" ? result.url.trim() : "";
|
|
44
|
+
if (title.length === 0 || !isDirectResultUrl(url)) continue;
|
|
45
|
+
if (seenUrls.has(url)) continue;
|
|
46
|
+
seenUrls.add(url);
|
|
47
|
+
normalized.push({
|
|
48
|
+
title,
|
|
49
|
+
url,
|
|
50
|
+
content: typeof result.content === "string" ? result.content.trim() : ""
|
|
51
|
+
});
|
|
52
|
+
if (normalized.length >= limit) break;
|
|
53
|
+
}
|
|
54
|
+
return normalized;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/search/orchestrator.ts
|
|
58
|
+
var QUERY_HASH_LENGTH = 12;
|
|
59
|
+
function hashSearchQuery(query) {
|
|
60
|
+
return createHash("sha256").update(query).digest("hex").slice(0, QUERY_HASH_LENGTH);
|
|
61
|
+
}
|
|
62
|
+
var SearchOrchestrator = class {
|
|
63
|
+
registry;
|
|
64
|
+
defaultPolicy;
|
|
65
|
+
onEvent;
|
|
66
|
+
constructor(registry, options = {}) {
|
|
67
|
+
this.registry = registry;
|
|
68
|
+
this.defaultPolicy = options.policy ?? {};
|
|
69
|
+
this.onEvent = options.onEvent;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Run one search.
|
|
73
|
+
*
|
|
74
|
+
* @param request - the query, an optional pinned provider, and per-request
|
|
75
|
+
* options. `options` is passed to the provider untouched: providers own
|
|
76
|
+
* their own timeouts and the orchestrator adds no second timeout layer.
|
|
77
|
+
* @param policyOverride - per-request policy, merged field-by-field over the
|
|
78
|
+
* orchestrator's default.
|
|
79
|
+
* @throws {SearchProviderError} when no provider produced a response.
|
|
80
|
+
*/
|
|
81
|
+
async search(request, policyOverride) {
|
|
82
|
+
const policy = { ...this.defaultPolicy, ...policyOverride };
|
|
83
|
+
const requestId = randomUUID();
|
|
84
|
+
const queryHash = hashSearchQuery(request.query);
|
|
85
|
+
const startedAt = Date.now();
|
|
86
|
+
const attempts = [];
|
|
87
|
+
const emitComplete = (providerId, resultCount) => {
|
|
88
|
+
this.emit({
|
|
89
|
+
type: "search_complete",
|
|
90
|
+
requestId,
|
|
91
|
+
queryHash,
|
|
92
|
+
...providerId === void 0 ? {} : { providerId },
|
|
93
|
+
resultCount,
|
|
94
|
+
fallbackCount: fallbackCountOf(attempts),
|
|
95
|
+
durationMs: Date.now() - startedAt
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
let plan;
|
|
99
|
+
try {
|
|
100
|
+
plan = this.planCandidates(request, policy);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
emitComplete(void 0, 0);
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
if (plan.skippedPreferred !== void 0) {
|
|
106
|
+
this.emit({
|
|
107
|
+
type: "search_attempt",
|
|
108
|
+
requestId,
|
|
109
|
+
queryHash,
|
|
110
|
+
providerId: plan.skippedPreferred.providerId,
|
|
111
|
+
outcome: "failed",
|
|
112
|
+
errorCode: plan.skippedPreferred.errorCode,
|
|
113
|
+
durationMs: 0
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
let lastError;
|
|
117
|
+
for (const candidate of plan.candidates) {
|
|
118
|
+
const attemptStartedAt = Date.now();
|
|
119
|
+
try {
|
|
120
|
+
const produced = await candidate.provider.search(request.query, request.options);
|
|
121
|
+
const durationMs = Date.now() - attemptStartedAt;
|
|
122
|
+
const results = normalizeSearchResults(produced, request.options?.maxResults);
|
|
123
|
+
attempts.push({
|
|
124
|
+
providerId: candidate.id,
|
|
125
|
+
outcome: "success",
|
|
126
|
+
resultCount: results.length,
|
|
127
|
+
durationMs
|
|
128
|
+
});
|
|
129
|
+
this.emit({
|
|
130
|
+
type: "search_attempt",
|
|
131
|
+
requestId,
|
|
132
|
+
queryHash,
|
|
133
|
+
providerId: candidate.id,
|
|
134
|
+
outcome: "success",
|
|
135
|
+
resultCount: results.length,
|
|
136
|
+
durationMs
|
|
137
|
+
});
|
|
138
|
+
emitComplete(candidate.id, results.length);
|
|
139
|
+
return {
|
|
140
|
+
query: request.query,
|
|
141
|
+
providerId: candidate.id,
|
|
142
|
+
results,
|
|
143
|
+
attempts,
|
|
144
|
+
fallbackCount: fallbackCountOf(attempts)
|
|
145
|
+
};
|
|
146
|
+
} catch (error) {
|
|
147
|
+
const durationMs = Date.now() - attemptStartedAt;
|
|
148
|
+
const shape = toSearchErrorShape(error);
|
|
149
|
+
const isCancelled = shape.code === "cancelled" || request.options?.signal?.aborted === true;
|
|
150
|
+
const errorCode = isCancelled ? "cancelled" : shape.code;
|
|
151
|
+
attempts.push({
|
|
152
|
+
providerId: candidate.id,
|
|
153
|
+
outcome: "failed",
|
|
154
|
+
errorCode,
|
|
155
|
+
durationMs
|
|
156
|
+
});
|
|
157
|
+
this.emit({
|
|
158
|
+
type: "search_attempt",
|
|
159
|
+
requestId,
|
|
160
|
+
queryHash,
|
|
161
|
+
providerId: candidate.id,
|
|
162
|
+
outcome: "failed",
|
|
163
|
+
errorCode,
|
|
164
|
+
durationMs
|
|
165
|
+
});
|
|
166
|
+
if (isCancelled) {
|
|
167
|
+
emitComplete(void 0, 0);
|
|
168
|
+
throw shape.code === "cancelled" ? error : new SearchProviderError("cancelled", "search was cancelled by the caller", {
|
|
169
|
+
providerId: candidate.id,
|
|
170
|
+
retryable: false,
|
|
171
|
+
cause: error
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
lastError = asSearchProviderError(error, shape.code, shape.message, candidate.id);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
emitComplete(void 0, 0);
|
|
178
|
+
throw exhaustionError(attempts, lastError);
|
|
179
|
+
}
|
|
180
|
+
/** Resolve the candidate list for one request. */
|
|
181
|
+
planCandidates(request, policy) {
|
|
182
|
+
if (request.provider !== void 0) {
|
|
183
|
+
const pinned = this.registry.get(request.provider);
|
|
184
|
+
if (pinned === void 0) {
|
|
185
|
+
throw new SearchProviderError(
|
|
186
|
+
"upstream_unavailable",
|
|
187
|
+
`search provider '${request.provider}' is not registered in this runtime`,
|
|
188
|
+
{ providerId: request.provider, retryable: false }
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
if (!isAllowed(pinned.id, policy)) {
|
|
192
|
+
throw new SearchProviderError(
|
|
193
|
+
"policy_denied",
|
|
194
|
+
`search provider '${request.provider}' is not allowed by the active search policy`,
|
|
195
|
+
{ providerId: request.provider, retryable: false }
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
return { candidates: [pinned] };
|
|
199
|
+
}
|
|
200
|
+
const allowed = this.registry.list().filter((entry) => isAllowed(entry.id, policy));
|
|
201
|
+
let ordered = allowed;
|
|
202
|
+
let skippedPreferred;
|
|
203
|
+
if (policy.preferred !== void 0) {
|
|
204
|
+
const preferred = allowed.find((entry) => entry.id === policy.preferred);
|
|
205
|
+
if (preferred === void 0) {
|
|
206
|
+
skippedPreferred = {
|
|
207
|
+
providerId: policy.preferred,
|
|
208
|
+
errorCode: this.registry.has(policy.preferred) ? "policy_denied" : "upstream_unavailable"
|
|
209
|
+
};
|
|
210
|
+
} else {
|
|
211
|
+
ordered = [preferred, ...allowed.filter((entry) => entry.id !== preferred.id)];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (policy.fallbackEnabled === false) ordered = ordered.slice(0, 1);
|
|
215
|
+
if (policy.maxAttempts !== void 0) {
|
|
216
|
+
ordered = ordered.slice(0, Math.max(0, Math.floor(policy.maxAttempts)));
|
|
217
|
+
}
|
|
218
|
+
return { candidates: ordered, ...skippedPreferred === void 0 ? {} : { skippedPreferred } };
|
|
219
|
+
}
|
|
220
|
+
/** Deliver one event. A listener that throws must never fail a search. */
|
|
221
|
+
emit(event) {
|
|
222
|
+
if (this.onEvent === void 0) return;
|
|
223
|
+
try {
|
|
224
|
+
this.onEvent(event);
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
function fallbackCountOf(attempts) {
|
|
230
|
+
return Math.max(0, attempts.length - 1);
|
|
231
|
+
}
|
|
232
|
+
function isAllowed(id, policy) {
|
|
233
|
+
return policy.allowed === void 0 || policy.allowed.includes(id);
|
|
234
|
+
}
|
|
235
|
+
function asSearchProviderError(error, code, message, providerId) {
|
|
236
|
+
if (error instanceof SearchProviderError) return error;
|
|
237
|
+
return new SearchProviderError(code, message, { providerId, cause: error });
|
|
238
|
+
}
|
|
239
|
+
function exhaustionError(attempts, lastError) {
|
|
240
|
+
if (attempts.length === 1 && lastError !== void 0) return lastError;
|
|
241
|
+
if (attempts.length === 0) {
|
|
242
|
+
return new SearchProviderError(
|
|
243
|
+
"upstream_unavailable",
|
|
244
|
+
"no eligible search provider was available (0 attempts)",
|
|
245
|
+
{ retryable: false }
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
const trail = attempts.map((attempt) => `${attempt.providerId}:${attempt.errorCode ?? "unknown"}`).join(",");
|
|
249
|
+
return new SearchProviderError(
|
|
250
|
+
lastError?.code ?? "upstream_unavailable",
|
|
251
|
+
`search failed after ${attempts.length} attempt(s)`,
|
|
252
|
+
{ retryable: lastError?.retryable, details: { attempts: trail }, cause: lastError }
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/search/registry.ts
|
|
257
|
+
var SearchRegistryError = class _SearchRegistryError extends Error {
|
|
258
|
+
constructor(message) {
|
|
259
|
+
super(message);
|
|
260
|
+
this.name = "SearchRegistryError";
|
|
261
|
+
Object.setPrototypeOf(this, _SearchRegistryError.prototype);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
var SearchProviderRegistry = class {
|
|
265
|
+
entries = /* @__PURE__ */ new Map();
|
|
266
|
+
allowBuiltinOverride;
|
|
267
|
+
nextSequence = 0;
|
|
268
|
+
constructor(options = {}) {
|
|
269
|
+
this.allowBuiltinOverride = options.allowBuiltinOverride === true;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Register one provider.
|
|
273
|
+
*
|
|
274
|
+
* @param contribution - the provider with its origin, transport, and
|
|
275
|
+
* capabilities declared.
|
|
276
|
+
* @param context - who is registering, for a Phase-2 host. Retained as-is;
|
|
277
|
+
* no rule reads it today, and the parameter exists so adding one later is
|
|
278
|
+
* not a signature change for every caller.
|
|
279
|
+
* @throws {SearchRegistryError} on a blank id, a duplicate id, or a host
|
|
280
|
+
* contribution colliding with a builtin without `allowBuiltinOverride`.
|
|
281
|
+
*/
|
|
282
|
+
register(contribution, context) {
|
|
283
|
+
const id = contribution.id;
|
|
284
|
+
if (typeof id !== "string" || id.trim().length === 0) {
|
|
285
|
+
throw new SearchRegistryError("a search provider contribution needs a non-blank id");
|
|
286
|
+
}
|
|
287
|
+
const existing = this.entries.get(id);
|
|
288
|
+
let sequence = this.nextSequence;
|
|
289
|
+
if (existing !== void 0) {
|
|
290
|
+
const isPermittedOverride = this.allowBuiltinOverride && contribution.source === "host" && existing.contribution.source === "builtin";
|
|
291
|
+
if (!isPermittedOverride) {
|
|
292
|
+
throw new SearchRegistryError(
|
|
293
|
+
existing.contribution.source === "builtin" && contribution.source === "host" ? `search provider '${id}' is a builtin; overriding it requires allowBuiltinOverride` : `search provider '${id}' is already registered`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
sequence = existing.sequence;
|
|
297
|
+
} else {
|
|
298
|
+
this.nextSequence += 1;
|
|
299
|
+
}
|
|
300
|
+
this.entries.set(id, { contribution, context, sequence });
|
|
301
|
+
}
|
|
302
|
+
/** Whether an id is currently registered. */
|
|
303
|
+
has(id) {
|
|
304
|
+
return this.entries.has(id);
|
|
305
|
+
}
|
|
306
|
+
/** The contribution registered under `id`, if any. */
|
|
307
|
+
get(id) {
|
|
308
|
+
return this.entries.get(id)?.contribution;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Remove a provider.
|
|
312
|
+
*
|
|
313
|
+
* The Phase-2 host-disconnect hook: when a host goes away its contributions
|
|
314
|
+
* go with it, and the id becomes free to register again.
|
|
315
|
+
*
|
|
316
|
+
* @returns whether something was removed.
|
|
317
|
+
*/
|
|
318
|
+
unregister(id) {
|
|
319
|
+
return this.entries.delete(id);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Every registered contribution, in candidate order.
|
|
323
|
+
*
|
|
324
|
+
* `priorityHint` ascending; contributions without a hint come AFTER every
|
|
325
|
+
* hinted one (an absent hint is "no opinion", not "highest priority"); ties
|
|
326
|
+
* keep registration order. The builtin HTTP contributions carry no hints, so
|
|
327
|
+
* today this is exactly registration order.
|
|
328
|
+
*/
|
|
329
|
+
list() {
|
|
330
|
+
return [...this.entries.values()].sort((a, b) => {
|
|
331
|
+
const aHint = a.contribution.priorityHint;
|
|
332
|
+
const bHint = b.contribution.priorityHint;
|
|
333
|
+
if (aHint !== bHint) {
|
|
334
|
+
if (aHint === void 0) return 1;
|
|
335
|
+
if (bHint === void 0) return -1;
|
|
336
|
+
return aHint - bHint;
|
|
337
|
+
}
|
|
338
|
+
return a.sequence - b.sequence;
|
|
339
|
+
}).map((entry) => entry.contribution);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// src/search/runtime.ts
|
|
344
|
+
function createSearchRuntime(options = {}) {
|
|
345
|
+
const registry = new SearchProviderRegistry({
|
|
346
|
+
allowBuiltinOverride: options.allowBuiltinOverride === true
|
|
347
|
+
});
|
|
348
|
+
for (const contribution of options.contributions ?? builtinHttpSearchContributions()) {
|
|
349
|
+
registry.register(contribution);
|
|
350
|
+
}
|
|
351
|
+
const orchestrator = new SearchOrchestrator(registry, {
|
|
352
|
+
...options.policy === void 0 ? {} : { policy: options.policy },
|
|
353
|
+
...options.onEvent === void 0 ? {} : { onEvent: options.onEvent }
|
|
354
|
+
});
|
|
355
|
+
return {
|
|
356
|
+
search(request) {
|
|
357
|
+
return orchestrator.search(request);
|
|
358
|
+
},
|
|
359
|
+
registerContribution(contribution, context) {
|
|
360
|
+
registry.register(contribution, context);
|
|
361
|
+
},
|
|
362
|
+
unregisterContribution(id) {
|
|
363
|
+
return registry.unregister(id);
|
|
364
|
+
},
|
|
365
|
+
listProviders() {
|
|
366
|
+
return registry.list().map((contribution) => ({
|
|
367
|
+
id: contribution.id,
|
|
368
|
+
source: contribution.source,
|
|
369
|
+
kind: contribution.kind,
|
|
370
|
+
capabilities: { ...contribution.capabilities }
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export {
|
|
377
|
+
DEFAULT_NORMALIZED_MAX_RESULTS,
|
|
378
|
+
isDirectResultUrl,
|
|
379
|
+
normalizedResultLimit,
|
|
380
|
+
normalizeSearchResults,
|
|
381
|
+
hashSearchQuery,
|
|
382
|
+
SearchOrchestrator,
|
|
383
|
+
SearchRegistryError,
|
|
384
|
+
SearchProviderRegistry,
|
|
385
|
+
createSearchRuntime
|
|
386
|
+
};
|