@hydraharness/harness-web 0.1.1-rc.10
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/LICENSE +21 -0
- package/README.md +73 -0
- package/lib/index.js +347 -0
- package/lib/invariant.js +23 -0
- package/lib/types/index.d.ts +131 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/search.d.ts +52 -0
- package/lib/types/settings.d.ts +13 -0
- package/lib/types/types.d.ts +139 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# @hydraharness/harness-web
|
|
2
|
+
|
|
3
|
+
The **`WebRuntime`** (`ctx.web`) defines WHAT web access the harness has — search the web, fetch a URL — over multiple providers, without binding the model contract to one vendor's API shape.
|
|
4
|
+
|
|
5
|
+
This package owns the Service Definition role of the web capability. Unlike shell/fs it spans two operations (search and fetch) on one seam, with potentially multiple providers each:
|
|
6
|
+
|
|
7
|
+
| Package | Role |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `@hydraharness/harness-web` (this) | Service Definition: the service, provider registries, selection policy, request/result vocabulary, the `WebError` taxonomy |
|
|
10
|
+
| `@hydraharness/harness-web-search-exa` | Search provider: Exa |
|
|
11
|
+
| `@hydraharness/harness-web-search-perplexity` | Search provider: Perplexity |
|
|
12
|
+
| `@hydraharness/harness-web-fetch-http` | Fetch provider: anonymous public HTTP(S) |
|
|
13
|
+
| `@hydraharness/harness-tool-web` | Consumer: the model-facing `web_search` / `web_fetch` tool schemas over `ctx.web` |
|
|
14
|
+
|
|
15
|
+
Search and fetch share no request schema and no business logic, but they are deliberately one seam: `ctx.web` is a single web-access middle layer with one provider-selection policy owner, one abort/error vocabulary, and one product-facing "how this harness reaches the web" config surface. The `Search`/`Fetch` method pairs are deliberately parallel.
|
|
16
|
+
|
|
17
|
+
## Service API (`ctx.web`)
|
|
18
|
+
|
|
19
|
+
| Member | Semantics |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `registerSearchProvider(provider)` / `registerFetchProvider(provider)` | Register a backend. Throws `WebError` `WEB_DUPLICATE_PROVIDER` on a duplicate id within that capability kind. Returns a disposer. Disposed with the calling fiber. |
|
|
22
|
+
| `search(request, signal?)` | Resolve the search provider and run one search. Enforces `request.maxResults` on the result (truncates `sources[]`, sets `truncated`). Throws `WebError` when the capability cannot run. |
|
|
23
|
+
| `fetch(request, signal?)` | Resolve the fetch provider and retrieve one URL. A non-2xx response is a result, not a throw. Throws `WebError` for failures to safely retrieve or represent the resource. |
|
|
24
|
+
|
|
25
|
+
Providers register **capabilities**, not tools. `@hydraharness/harness-tool-web` is the only owner of model-facing names, descriptions, prompt guidance, JSON schemas, and presentation.
|
|
26
|
+
|
|
27
|
+
## Product search settings
|
|
28
|
+
|
|
29
|
+
The base bundle sets `requireSearchSelection: true`. The `web-search` namespace stores `enabled` (true), `provider` (empty), `maxQueries` (4), `maxResults` (8), and `timeoutMs` (60000). Empty selection and disabled search fail as `CONFIG_ERROR`. Provider selection is independent of chat and takes effect on the next call. Product mode never runs fallback providers.
|
|
30
|
+
|
|
31
|
+
An explicit legacy DeepSeek search section initializes the selected provider to `deepseek-official`; an existing chat credential alone does not. The initialized empty selection is persisted so configuring a provider later does not trigger migration. An explicit composition `searchProvider` seeds selection. Read-only settings retain the composition choice and cannot persist migration.
|
|
32
|
+
|
|
33
|
+
Providers optionally declare a descriptor with supported search types, a settings namespace, a credential reference, and control metadata. `listSearchProviders()` feeds Settings; `testSearchProvider()` executes a small real query against saved configuration. DeepSeek, Serper, and Other advertise only web search. Exa and Perplexity remain available to explicit compositions, without MVP Settings controls.
|
|
34
|
+
|
|
35
|
+
Product search failures expose `SearchProviderError`: provider id, safe message, retryable flag, optional HTTP status, and one of `AUTH_ERROR`, `RATE_LIMITED`, `TIMEOUT`, `NETWORK_ERROR`, `INVALID_RESPONSE`, `CONFIG_ERROR`, or `UNKNOWN`. Logs contain provider, query count, duration, normalized result count, status, retries, and category. Credentials and upstream exception text are excluded.
|
|
36
|
+
|
|
37
|
+
Normalized sources retain optional provider, position, and score. Duplicate comparison removes fragments and common tracking parameters. URL parsing normalizes host spelling, default ports, and root slashes; non-root trailing slashes remain distinct because servers may serve different resources. The tool merges query results in rank order and applies the total cap.
|
|
38
|
+
|
|
39
|
+
## Standalone selection
|
|
40
|
+
|
|
41
|
+
Selection never depends on registration, config, or HMR order. Search and fetch use their own explicit provider ids (config `searchProvider`/`fetchProvider`, or env `$HYDRA_WEB_SEARCH_PROVIDER`/`$HYDRA_WEB_FETCH_PROVIDER` feeding the same fields), or auto-select when exactly one usable provider is registered. The active LLM provider does not participate in web-provider selection. `search()`/`fetch()` resolve at execution time:
|
|
42
|
+
|
|
43
|
+
| Situation | Execution |
|
|
44
|
+
|---|---|
|
|
45
|
+
| configured id registered and `available()` | runs that provider |
|
|
46
|
+
| configured id not registered | `WEB_PROVIDER_CONFIGURED_MISSING` |
|
|
47
|
+
| configured id registered but unavailable | `WEB_PROVIDER_CONFIGURED_UNAVAILABLE` |
|
|
48
|
+
| no id, exactly one registered usable provider | runs it |
|
|
49
|
+
| no id, no usable provider | `WEB_PROVIDER_UNAVAILABLE` |
|
|
50
|
+
| no id, multiple usable providers | `WEB_PROVIDER_AMBIGUOUS` |
|
|
51
|
+
|
|
52
|
+
The failure branches throw `WebError`, whose structured code (plus message detail — the missing id, the ambiguous candidate set) is the direct callers route on. A provider's own `available()` is a cheap local check (credential presence, parseable config) that feeds this execution-time selection and **must not make network calls**; `@hydraharness/harness-tool-web` never calls it — the tool executes through `ctx.web.search()`/`fetch()` and routes on the thrown codes, so provider selection has one owner.
|
|
53
|
+
|
|
54
|
+
## Vocabulary
|
|
55
|
+
|
|
56
|
+
`WebSearchRequest` (`query`, `maxResults?`) → `WebSearchResult` (`content?`, `sources[]`, `truncated`); each `WebSearchSource` has a required `url` and optional `title`/`snippet`/`publishedAt` (Perplexity citations may be URL-only). `WebFetchRequest` (`url`) → `WebFetchResult` (final `url`, `statusCode`, `body`, `truncated`); cancellation is a direct optional `AbortSignal` argument to `search()`/`fetch()`. `WebFetchBody` is a CLOSED discriminated union (`html` | `text`) owned here — consumers `switch` to exhaustiveness so a new kind breaks their compilation until handled. See `src/types.ts` for the full contracts and the `WebError` code taxonomy.
|
|
57
|
+
|
|
58
|
+
`searchFallbackProviders` optionally lists backups for an explicit `searchProvider`. The complete chain must name registered, distinct providers. Each provider runs at most once under the same caller signal. Provider availability, credentials, and provider failures may advance the chain; cancellation, destination denial, configuration errors, and non-WebError exceptions do not. Empty results are successful. The default chain is empty.
|
|
59
|
+
|
|
60
|
+
## Model Experience
|
|
61
|
+
|
|
62
|
+
Indirectly, through `@hydraharness/harness-tool-web`, which retains bounded normalized provider data or the exact configured-provider, unavailable-provider, no-provider, multiple-provider, and `Error: <message>` failures while this registry contributes no prompt or schema itself.
|
|
63
|
+
|
|
64
|
+
#### KV Cache effect
|
|
65
|
+
|
|
66
|
+
No direct invalidation; the named consumer owns any request-prefix changes.
|
|
67
|
+
|
|
68
|
+
## Known Limitations and Deferred Work
|
|
69
|
+
|
|
70
|
+
- Provider descriptors describe configuration and implemented search types; credential status remains owned by the credentials subsystem.
|
|
71
|
+
- Domain filtering and provider-specific search depth remain outside the common request.
|
|
72
|
+
- **`WebFetchBody` has no `pdf` arm** — text-extractable PDF support is named deferred work; the closed union makes adding it a compile-enforced change across the three web packages.
|
|
73
|
+
- **Provider-backed page extraction is out of scope of `fetch()`** — a Firecrawl/Tavily-style `web_extract` capability is deferred rather than widening the fetch operation.
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { Service } from "@hydraharness/cordis";
|
|
2
|
+
import z from "@hydraharness/schemastery";
|
|
3
|
+
import { installSettingsSection, settingsNamespace } from "@hydraharness/harness-settings";
|
|
4
|
+
import { HarnessError } from "@hydraharness/harness-llm";
|
|
5
|
+
//#region lib/types/settings.js
|
|
6
|
+
/** Product search selection and invocation limits, independent of chat configuration. */
|
|
7
|
+
/** Schema shared by registration and composition default resolution. */
|
|
8
|
+
const SearchSettings = z.object({
|
|
9
|
+
enabled: z.boolean().default(true),
|
|
10
|
+
provider: z.string().default(""),
|
|
11
|
+
maxQueries: z.number().step(1).min(1).max(100).default(4),
|
|
12
|
+
maxResults: z.number().step(1).min(1).max(100).default(8),
|
|
13
|
+
timeoutMs: z.number().step(1).min(1).max(6e5).default(6e4)
|
|
14
|
+
});
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region lib/types/types.js
|
|
17
|
+
/**
|
|
18
|
+
* Vocabulary for the web capability seam (`ctx.web`). Search and fetch deliberately share one
|
|
19
|
+
* seam so provider selection, cancellation, errors, and product configuration have one owner,
|
|
20
|
+
* while retaining separate request and result types.
|
|
21
|
+
* @module @hydraharness/harness-web/types
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Typed web error with a machine-routable, open-string `code` and chained `cause`.
|
|
25
|
+
* Consumers must tolerate provider-specific codes. Shared codes cover unavailable,
|
|
26
|
+
* missing, unusable, ambiguous, or duplicate providers, cancellation, and provider failure;
|
|
27
|
+
* the local fetch provider additionally distinguishes invalid or blocked URLs, redirects,
|
|
28
|
+
* size and timeout limits, and unsupported content types. Tool execution exposes the code in
|
|
29
|
+
* structured error metadata.
|
|
30
|
+
*/
|
|
31
|
+
var WebError = class extends HarnessError {};
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region lib/types/search.js
|
|
34
|
+
/** Shared search errors, provider configuration metadata, and URL normalization. */
|
|
35
|
+
/** Web-only capabilities; endpoints for other search types are not advertised. */
|
|
36
|
+
const WEB_SEARCH_CAPABILITIES = {
|
|
37
|
+
web: true,
|
|
38
|
+
images: false,
|
|
39
|
+
news: false,
|
|
40
|
+
videos: false,
|
|
41
|
+
academic: false
|
|
42
|
+
};
|
|
43
|
+
const messages = {
|
|
44
|
+
AUTH_ERROR: "Authentication failed. Replace the API key in Settings > Web Search.",
|
|
45
|
+
RATE_LIMITED: "Provider quota or rate limit exceeded (HTTP 429).",
|
|
46
|
+
TIMEOUT: "Search connection timed out.",
|
|
47
|
+
NETWORK_ERROR: "Could not connect to the search provider.",
|
|
48
|
+
INVALID_RESPONSE: "Search response format is incompatible with the configured mapping.",
|
|
49
|
+
CONFIG_ERROR: "Configure the provider and its credential in Settings > Web Search.",
|
|
50
|
+
UNKNOWN: "Search provider failed."
|
|
51
|
+
};
|
|
52
|
+
/** Search failure containing only owned diagnostics, never upstream text or causes. */
|
|
53
|
+
var SearchProviderError = class extends WebError {
|
|
54
|
+
provider;
|
|
55
|
+
code;
|
|
56
|
+
statusCode;
|
|
57
|
+
/** Whether a later retry may succeed without reconfiguration. */
|
|
58
|
+
retryable;
|
|
59
|
+
constructor(provider, code, statusCode, message = messages[code]) {
|
|
60
|
+
super(message, code);
|
|
61
|
+
this.provider = provider;
|
|
62
|
+
this.code = code;
|
|
63
|
+
this.statusCode = statusCode;
|
|
64
|
+
this.retryable = [
|
|
65
|
+
"RATE_LIMITED",
|
|
66
|
+
"TIMEOUT",
|
|
67
|
+
"NETWORK_ERROR"
|
|
68
|
+
].includes(code);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Translate an HTTP failure without reading an untrusted error body.
|
|
73
|
+
* @param provider - registered provider id.
|
|
74
|
+
* @param status - HTTP status.
|
|
75
|
+
* @returns the normalized failure.
|
|
76
|
+
*/
|
|
77
|
+
function searchHttpError(provider, status) {
|
|
78
|
+
return new SearchProviderError(provider, status === 401 || status === 403 ? "AUTH_ERROR" : status === 429 ? "RATE_LIMITED" : status >= 500 ? "NETWORK_ERROR" : "CONFIG_ERROR", status);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Remove fragments and common tracking parameters for duplicate detection.
|
|
82
|
+
* Non-root trailing slashes and meaningful query parameters remain distinct.
|
|
83
|
+
* @param value - source URL.
|
|
84
|
+
* @returns a canonical comparison key, or the unchanged malformed URL.
|
|
85
|
+
*/
|
|
86
|
+
function normalizedSearchUrl(value) {
|
|
87
|
+
try {
|
|
88
|
+
const url = new URL(value);
|
|
89
|
+
url.hash = "";
|
|
90
|
+
for (const key of [...url.searchParams.keys()]) if (/^(utm_.+|fbclid|gclid|dclid|msclkid)$/i.test(key)) url.searchParams.delete(key);
|
|
91
|
+
return url.href;
|
|
92
|
+
} catch {
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region lib/types/index.js
|
|
98
|
+
/**
|
|
99
|
+
* Service Definition for the web access capability seam (`ctx.web`): registries and provider-selecting execution for search and
|
|
100
|
+
* fetch. Duplicate ids are rejected. At execution time, a configured provider must exist and
|
|
101
|
+
* be usable; without one, exactly one usable provider is required, so selection never depends
|
|
102
|
+
* on registration order.
|
|
103
|
+
* @module @hydraharness/harness-web
|
|
104
|
+
*/
|
|
105
|
+
/**
|
|
106
|
+
* The web access service. Registered as `ctx.web` (one instance per context).
|
|
107
|
+
*
|
|
108
|
+
* Selection semantics (resolved at execution time, never order-dependent):
|
|
109
|
+
* - A configured id that is registered and `available()` → that provider.
|
|
110
|
+
* - A configured id not registered → `WEB_PROVIDER_CONFIGURED_MISSING`.
|
|
111
|
+
* - A configured id registered but unavailable →
|
|
112
|
+
* `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`.
|
|
113
|
+
* - No id configured, exactly one registered usable provider → that provider.
|
|
114
|
+
* - No id configured, multiple usable providers → `WEB_PROVIDER_AMBIGUOUS`.
|
|
115
|
+
* - No id configured, no usable provider → `WEB_PROVIDER_UNAVAILABLE`.
|
|
116
|
+
*/
|
|
117
|
+
var WebRuntime = class extends Service {
|
|
118
|
+
/**
|
|
119
|
+
* Provider selection config. Operational env overrides feed the SAME fields:
|
|
120
|
+
* `$HYDRA_WEB_SEARCH_PROVIDER` / `$HYDRA_WEB_FETCH_PROVIDER` are equivalent to
|
|
121
|
+
* `searchProvider` / `fetchProvider` and are NOT a hidden priority chain.
|
|
122
|
+
*/
|
|
123
|
+
static Config = z.object({
|
|
124
|
+
requireSearchSelection: z.boolean().default(false),
|
|
125
|
+
searchProvider: z.string(),
|
|
126
|
+
fetchProvider: z.string(),
|
|
127
|
+
searchFallbackProviders: z.array(z.string()).default([])
|
|
128
|
+
});
|
|
129
|
+
searchProviders = /* @__PURE__ */ new Map();
|
|
130
|
+
fetchProviders = /* @__PURE__ */ new Map();
|
|
131
|
+
searchProviderId;
|
|
132
|
+
fetchProviderId;
|
|
133
|
+
searchFallbackProviders;
|
|
134
|
+
searchSettings;
|
|
135
|
+
requireSearchSelection;
|
|
136
|
+
constructor(ctx, config = {}) {
|
|
137
|
+
super(ctx, "web");
|
|
138
|
+
this.searchProviderId = config.searchProvider ?? process.env.HYDRA_WEB_SEARCH_PROVIDER;
|
|
139
|
+
this.fetchProviderId = config.fetchProvider ?? process.env.HYDRA_WEB_FETCH_PROVIDER;
|
|
140
|
+
this.searchFallbackProviders = [...config.searchFallbackProviders ?? []];
|
|
141
|
+
this.requireSearchSelection = config.requireSearchSelection === true;
|
|
142
|
+
const settings = SearchSettings({ provider: this.searchProviderId ?? "" });
|
|
143
|
+
this.searchSettings = () => settings;
|
|
144
|
+
installSettingsSection(ctx, settingsNamespace("web-search"), SearchSettings, settings, {
|
|
145
|
+
setSource: (source) => {
|
|
146
|
+
this.searchSettings = source;
|
|
147
|
+
},
|
|
148
|
+
onChange: () => {}
|
|
149
|
+
});
|
|
150
|
+
if (this.searchFallbackProviders.length > 0 && this.searchProviderId === void 0) throw new WebError("searchFallbackProviders requires an explicit searchProvider", "WEB_PROVIDER_CONFIGURED_MISSING");
|
|
151
|
+
const chain = [this.searchProviderId, ...this.searchFallbackProviders];
|
|
152
|
+
if (new Set(chain).size !== chain.length || this.searchFallbackProviders.some((id) => id.trim() === "")) throw new WebError("searchFallbackProviders must contain distinct non-empty provider ids excluding the primary", "WEB_DUPLICATE_PROVIDER");
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Read current product limits; standalone compositions retain tool-level limits.
|
|
156
|
+
* @returns saved invocation preferences when product selection is enabled.
|
|
157
|
+
*/
|
|
158
|
+
searchPreferences() {
|
|
159
|
+
return this.requireSearchSelection ? this.searchSettings() : void 0;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* List provider-owned, secret-free Settings descriptors.
|
|
163
|
+
* @returns configurable providers in registration order.
|
|
164
|
+
*/
|
|
165
|
+
listSearchProviders() {
|
|
166
|
+
return [...this.searchProviders.values()].flatMap((provider) => provider.descriptor === void 0 ? [] : [provider.descriptor]);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Persist the first search selection from explicit legacy configuration only.
|
|
170
|
+
* An explicit empty selection also marks a fresh installation as initialized.
|
|
171
|
+
* @param provider - legacy registered id.
|
|
172
|
+
* @param configured - whether a legacy search section contains explicit settings.
|
|
173
|
+
*/
|
|
174
|
+
async migrateSearchSelection(provider, configured) {
|
|
175
|
+
const settings = this.ctx.get("settings");
|
|
176
|
+
if (!this.requireSearchSelection || settings === void 0) return;
|
|
177
|
+
const ns = settingsNamespace("web-search");
|
|
178
|
+
const descriptor = settings.describe().find((item) => item.ns === ns);
|
|
179
|
+
if (descriptor === void 0 || Object.hasOwn(descriptor.user ?? {}, "provider")) return;
|
|
180
|
+
if (!settings.writable) return;
|
|
181
|
+
await settings.update(ns, { provider: this.searchProviderId ?? (configured ? provider : "") }, descriptor.revision);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Execute a small real search with a saved provider without changing selection.
|
|
185
|
+
* @param id - registered provider to test.
|
|
186
|
+
* @param signal - caller cancellation.
|
|
187
|
+
* @returns normalized results after validating the provider response.
|
|
188
|
+
*/
|
|
189
|
+
async testSearchProvider(id, signal) {
|
|
190
|
+
return this.runSearchProvider(id, {
|
|
191
|
+
query: "Hydra search provider connectivity test",
|
|
192
|
+
maxResults: 5
|
|
193
|
+
}, signal);
|
|
194
|
+
}
|
|
195
|
+
async runSearchProvider(id, request, signal) {
|
|
196
|
+
const provider = this.searchProviders.get(id);
|
|
197
|
+
if (provider === void 0 || !provider.available()) throw new SearchProviderError(id, "CONFIG_ERROR");
|
|
198
|
+
const type = request.type ?? "web";
|
|
199
|
+
if (!(provider.descriptor?.capabilities[type] ?? type === "web")) throw new SearchProviderError(id, "CONFIG_ERROR", void 0, `Provider "${id}" does not support ${type} search.`);
|
|
200
|
+
const timeout = AbortSignal.timeout(this.searchSettings().timeoutMs);
|
|
201
|
+
const boundedSignal = signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
|
|
202
|
+
const started = Date.now();
|
|
203
|
+
let result;
|
|
204
|
+
let failure;
|
|
205
|
+
try {
|
|
206
|
+
const raw = await provider.search(request, boundedSignal);
|
|
207
|
+
const seen = /* @__PURE__ */ new Set();
|
|
208
|
+
const sources = raw.sources.filter((source) => {
|
|
209
|
+
const key = normalizedSearchUrl(source.url);
|
|
210
|
+
if (seen.has(key)) return false;
|
|
211
|
+
seen.add(key);
|
|
212
|
+
return true;
|
|
213
|
+
}).map((source) => ({
|
|
214
|
+
...source,
|
|
215
|
+
provider: id
|
|
216
|
+
}));
|
|
217
|
+
result = capSources({
|
|
218
|
+
...raw,
|
|
219
|
+
sources,
|
|
220
|
+
provider: id,
|
|
221
|
+
query: request.query
|
|
222
|
+
}, request.maxResults);
|
|
223
|
+
return result;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (signal?.aborted) throw new WebError("Web search aborted", "WEB_ABORTED");
|
|
226
|
+
failure = timeout.aborted ? new SearchProviderError(id, "TIMEOUT") : error instanceof SearchProviderError ? error : new SearchProviderError(id, "UNKNOWN");
|
|
227
|
+
throw failure;
|
|
228
|
+
} finally {
|
|
229
|
+
this.ctx.logger("web-search").info("provider=%s queries=1 duration=%d results=%d status=%s retries=0 error=%s", id, Date.now() - started, result?.sources.length ?? 0, result?.statusCode ?? failure?.statusCode ?? "-", failure?.code ?? "-");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Register a search provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER`
|
|
234
|
+
* if its id is already registered for search. Returns a disposer; disposed
|
|
235
|
+
* with the calling fiber.
|
|
236
|
+
* @param provider - the provider; its `id` is the registry key.
|
|
237
|
+
* @returns the disposer that unregisters the provider.
|
|
238
|
+
*/
|
|
239
|
+
registerSearchProvider(provider) {
|
|
240
|
+
return this.registerProvider(this.searchProviders, provider);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Register a fetch provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER`
|
|
244
|
+
* if its id is already registered for fetch. Returns a disposer; disposed
|
|
245
|
+
* with the calling fiber.
|
|
246
|
+
* @param provider - the provider; its `id` is the registry key.
|
|
247
|
+
* @returns the disposer that unregisters the provider.
|
|
248
|
+
*/
|
|
249
|
+
registerFetchProvider(provider) {
|
|
250
|
+
return this.registerProvider(this.fetchProviders, provider);
|
|
251
|
+
}
|
|
252
|
+
registerProvider(store, provider) {
|
|
253
|
+
if (store.has(provider.id)) throw new WebError(`a web provider with id "${provider.id}" is already registered`, "WEB_DUPLICATE_PROVIDER");
|
|
254
|
+
const dispose = this.ctx.effect(function* () {
|
|
255
|
+
store.set(provider.id, provider);
|
|
256
|
+
yield () => store.delete(provider.id);
|
|
257
|
+
}, "web.registerProvider()");
|
|
258
|
+
return () => void dispose();
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Run one search through the selected provider. Resolves the provider at call
|
|
262
|
+
* time with the selection rules above; throws {@link WebError} when the
|
|
263
|
+
* capability cannot run. The seam enforces `request.maxResults` on the result:
|
|
264
|
+
* if the provider over-returns, `sources[]` is truncated and `truncated` set.
|
|
265
|
+
* Configured fallbacks share the caller signal and retry only provider or
|
|
266
|
+
* credential unavailability; missing registrations and cancellation stop the chain.
|
|
267
|
+
* @param request - the query and optional result limit.
|
|
268
|
+
* @param signal - optional cancellation signal forwarded to the provider.
|
|
269
|
+
* @returns the provider's results, capped to `request.maxResults`.
|
|
270
|
+
*/
|
|
271
|
+
async search(request, signal) {
|
|
272
|
+
if (this.requireSearchSelection) {
|
|
273
|
+
const settings = this.searchSettings();
|
|
274
|
+
if (!settings.enabled) throw new SearchProviderError(settings.provider, "CONFIG_ERROR", void 0, "Web Search is disabled in Settings > Web Search.");
|
|
275
|
+
if (settings.provider === "") throw new SearchProviderError("", "CONFIG_ERROR", void 0, "Web Search is enabled but no search provider has been configured. Open Settings > Web Search and select a provider.");
|
|
276
|
+
return this.runSearchProvider(settings.provider, request, signal);
|
|
277
|
+
}
|
|
278
|
+
if (this.searchFallbackProviders.length > 0) {
|
|
279
|
+
const providers = [this.searchProviderId, ...this.searchFallbackProviders].map((id) => {
|
|
280
|
+
const provider = this.searchProviders.get(id);
|
|
281
|
+
if (provider === void 0) throw new WebError(`configured web provider "${id}" is not registered`, "WEB_PROVIDER_CONFIGURED_MISSING");
|
|
282
|
+
return provider;
|
|
283
|
+
});
|
|
284
|
+
let failure;
|
|
285
|
+
for (const provider of providers) {
|
|
286
|
+
if (signal?.aborted) throw new WebError("web search aborted", "WEB_ABORTED", { cause: signal.reason });
|
|
287
|
+
try {
|
|
288
|
+
if (!provider.available()) throw new WebError(`configured web provider "${provider.id}" is unavailable`, "WEB_PROVIDER_CONFIGURED_UNAVAILABLE");
|
|
289
|
+
return capSources(await provider.search(request, signal), request.maxResults);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
const retryable = error instanceof SearchProviderError ? error.retryable : error instanceof WebError && [
|
|
292
|
+
"WEB_PROVIDER_ERROR",
|
|
293
|
+
"WEB_PROVIDER_CREDENTIAL_MISSING",
|
|
294
|
+
"WEB_PROVIDER_CONFIGURED_UNAVAILABLE"
|
|
295
|
+
].includes(error.code);
|
|
296
|
+
if (signal?.aborted || !retryable) throw error;
|
|
297
|
+
failure = error;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
throw failure;
|
|
301
|
+
}
|
|
302
|
+
return capSources(await resolveProvider({
|
|
303
|
+
providers: this.searchProviders,
|
|
304
|
+
...this.searchProviderId !== void 0 ? { configuredId: this.searchProviderId } : {}
|
|
305
|
+
}).search(request, signal), request.maxResults);
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Retrieve one URL through the selected provider. Resolves the provider at
|
|
309
|
+
* call time with the selection rules above; throws {@link WebError} when the
|
|
310
|
+
* capability cannot run. A non-2xx response is a result, not a throw.
|
|
311
|
+
* @param request - the URL plus retrieval options.
|
|
312
|
+
* @param signal - optional cancellation signal forwarded to the provider.
|
|
313
|
+
* @returns the retrieval outcome; non-2xx responses resolve descriptively.
|
|
314
|
+
*/
|
|
315
|
+
async fetch(request, signal) {
|
|
316
|
+
return resolveProvider({
|
|
317
|
+
providers: this.fetchProviders,
|
|
318
|
+
...this.fetchProviderId !== void 0 ? { configuredId: this.fetchProviderId } : {}
|
|
319
|
+
}).fetch(request, signal);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
/** Resolve the selected provider or throw the matching {@link WebError}. */
|
|
323
|
+
function resolveProvider(selection) {
|
|
324
|
+
const { configuredId, providers } = selection;
|
|
325
|
+
if (configuredId !== void 0) {
|
|
326
|
+
const provider = providers.get(configuredId);
|
|
327
|
+
if (!provider) throw new WebError(`configured web provider "${configuredId}" is not registered`, "WEB_PROVIDER_CONFIGURED_MISSING");
|
|
328
|
+
if (!provider.available()) throw new WebError(`configured web provider "${configuredId}" is registered but unavailable`, "WEB_PROVIDER_CONFIGURED_UNAVAILABLE");
|
|
329
|
+
return provider;
|
|
330
|
+
}
|
|
331
|
+
const usable = [...providers.values()].filter((provider) => provider.available());
|
|
332
|
+
const [single] = usable;
|
|
333
|
+
if (single === void 0) throw new WebError("no usable web provider is registered", "WEB_PROVIDER_UNAVAILABLE");
|
|
334
|
+
if (usable.length > 1) throw new WebError(`multiple usable web providers are registered (${usable.map((provider) => provider.id).join(", ")}); configure one explicitly`, "WEB_PROVIDER_AMBIGUOUS");
|
|
335
|
+
return single;
|
|
336
|
+
}
|
|
337
|
+
/** Enforce `maxResults` on a search result: truncate `sources[]` and flag it. */
|
|
338
|
+
function capSources(result, maxResults) {
|
|
339
|
+
if (maxResults === void 0 || result.sources.length <= maxResults) return result;
|
|
340
|
+
return {
|
|
341
|
+
...result,
|
|
342
|
+
sources: result.sources.slice(0, maxResults),
|
|
343
|
+
truncated: true
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
//#endregion
|
|
347
|
+
export { SearchProviderError, WEB_SEARCH_CAPABILITIES, WebError, WebRuntime, WebRuntime as default, normalizedSearchUrl, searchHttpError };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@hydraharness/harness-web`.
|
|
4
|
+
* @module @hydraharness/harness-web/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@hydraharness/harness-web";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "web-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: provider maps are private and selection/result caps are enforced on each
|
|
13
|
+
* call; the seam publishes no independent registry or request/result observation stream.
|
|
14
|
+
*/
|
|
15
|
+
const install = () => {};
|
|
16
|
+
/**
|
|
17
|
+
* Register this package's invariant companion.
|
|
18
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
19
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
20
|
+
*/
|
|
21
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
22
|
+
//#endregion
|
|
23
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Definition for the web access capability seam (`ctx.web`): registries and provider-selecting execution for search and
|
|
3
|
+
* fetch. Duplicate ids are rejected. At execution time, a configured provider must exist and
|
|
4
|
+
* be usable; without one, exactly one usable provider is required, so selection never depends
|
|
5
|
+
* on registration order.
|
|
6
|
+
* @module @hydraharness/harness-web
|
|
7
|
+
*/
|
|
8
|
+
import { Context, Service } from '@hydraharness/cordis';
|
|
9
|
+
import z from '@hydraharness/schemastery';
|
|
10
|
+
import { type WebSearchSettings } from './settings.ts';
|
|
11
|
+
import { type WebSearchProviderDescriptor } from './search.ts';
|
|
12
|
+
import type { WebFetchProvider, WebFetchRequest, WebFetchResult, WebSearchProvider, WebSearchRequest, WebSearchResult } from './types.ts';
|
|
13
|
+
export { WebError, } from './types.ts';
|
|
14
|
+
export { normalizedSearchUrl, SearchProviderError, searchHttpError, WEB_SEARCH_CAPABILITIES } from './search.ts';
|
|
15
|
+
export type { SearchConfigField, SearchErrorCode, WebSearchProviderDescriptor, WebSearchType } from './search.ts';
|
|
16
|
+
export type { WebSearchSettings } from './settings.ts';
|
|
17
|
+
export type { WebFetchBody, WebFetchProvider, WebFetchRequest, WebFetchResult, WebSearchProvider, WebSearchRequest, WebSearchResult, WebSearchSource, } from './types.ts';
|
|
18
|
+
declare module '@hydraharness/cordis' {
|
|
19
|
+
interface Context {
|
|
20
|
+
web: WebRuntime;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Config for the web seam. `searchProvider` / `fetchProvider` pin which provider
|
|
25
|
+
* wins for each capability; both are optional (a single registered usable
|
|
26
|
+
* provider auto-selects). Operational overrides such as environment variables
|
|
27
|
+
* must feed these same fields rather than introduce a hidden priority chain.
|
|
28
|
+
*/
|
|
29
|
+
export interface WebRuntimeConfig {
|
|
30
|
+
/** Product Settings owns selection; no single-provider auto-selection. */
|
|
31
|
+
readonly requireSearchSelection?: boolean;
|
|
32
|
+
/** Explicit search provider id. Omitted = auto-select when exactly one usable. */
|
|
33
|
+
readonly searchProvider?: string;
|
|
34
|
+
/** Explicit fetch provider id. Omitted = auto-select when exactly one usable. */
|
|
35
|
+
readonly fetchProvider?: string;
|
|
36
|
+
/** Ordered backup search providers; requires an explicit primary searchProvider. Defaults to none. */
|
|
37
|
+
readonly searchFallbackProviders?: string[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The web access service. Registered as `ctx.web` (one instance per context).
|
|
41
|
+
*
|
|
42
|
+
* Selection semantics (resolved at execution time, never order-dependent):
|
|
43
|
+
* - A configured id that is registered and `available()` → that provider.
|
|
44
|
+
* - A configured id not registered → `WEB_PROVIDER_CONFIGURED_MISSING`.
|
|
45
|
+
* - A configured id registered but unavailable →
|
|
46
|
+
* `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`.
|
|
47
|
+
* - No id configured, exactly one registered usable provider → that provider.
|
|
48
|
+
* - No id configured, multiple usable providers → `WEB_PROVIDER_AMBIGUOUS`.
|
|
49
|
+
* - No id configured, no usable provider → `WEB_PROVIDER_UNAVAILABLE`.
|
|
50
|
+
*/
|
|
51
|
+
export declare class WebRuntime extends Service {
|
|
52
|
+
/**
|
|
53
|
+
* Provider selection config. Operational env overrides feed the SAME fields:
|
|
54
|
+
* `$HYDRA_WEB_SEARCH_PROVIDER` / `$HYDRA_WEB_FETCH_PROVIDER` are equivalent to
|
|
55
|
+
* `searchProvider` / `fetchProvider` and are NOT a hidden priority chain.
|
|
56
|
+
*/
|
|
57
|
+
static Config: z<WebRuntimeConfig>;
|
|
58
|
+
private searchProviders;
|
|
59
|
+
private fetchProviders;
|
|
60
|
+
private readonly searchProviderId;
|
|
61
|
+
private readonly fetchProviderId;
|
|
62
|
+
private readonly searchFallbackProviders;
|
|
63
|
+
private searchSettings;
|
|
64
|
+
private readonly requireSearchSelection;
|
|
65
|
+
constructor(ctx: Context, config?: WebRuntimeConfig);
|
|
66
|
+
/**
|
|
67
|
+
* Read current product limits; standalone compositions retain tool-level limits.
|
|
68
|
+
* @returns saved invocation preferences when product selection is enabled.
|
|
69
|
+
*/
|
|
70
|
+
searchPreferences(): WebSearchSettings | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* List provider-owned, secret-free Settings descriptors.
|
|
73
|
+
* @returns configurable providers in registration order.
|
|
74
|
+
*/
|
|
75
|
+
listSearchProviders(): WebSearchProviderDescriptor[];
|
|
76
|
+
/**
|
|
77
|
+
* Persist the first search selection from explicit legacy configuration only.
|
|
78
|
+
* An explicit empty selection also marks a fresh installation as initialized.
|
|
79
|
+
* @param provider - legacy registered id.
|
|
80
|
+
* @param configured - whether a legacy search section contains explicit settings.
|
|
81
|
+
*/
|
|
82
|
+
migrateSearchSelection(provider: string, configured: boolean): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Execute a small real search with a saved provider without changing selection.
|
|
85
|
+
* @param id - registered provider to test.
|
|
86
|
+
* @param signal - caller cancellation.
|
|
87
|
+
* @returns normalized results after validating the provider response.
|
|
88
|
+
*/
|
|
89
|
+
testSearchProvider(id: string, signal?: AbortSignal): Promise<WebSearchResult>;
|
|
90
|
+
private runSearchProvider;
|
|
91
|
+
/**
|
|
92
|
+
* Register a search provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER`
|
|
93
|
+
* if its id is already registered for search. Returns a disposer; disposed
|
|
94
|
+
* with the calling fiber.
|
|
95
|
+
* @param provider - the provider; its `id` is the registry key.
|
|
96
|
+
* @returns the disposer that unregisters the provider.
|
|
97
|
+
*/
|
|
98
|
+
registerSearchProvider(provider: WebSearchProvider): () => void;
|
|
99
|
+
/**
|
|
100
|
+
* Register a fetch provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER`
|
|
101
|
+
* if its id is already registered for fetch. Returns a disposer; disposed
|
|
102
|
+
* with the calling fiber.
|
|
103
|
+
* @param provider - the provider; its `id` is the registry key.
|
|
104
|
+
* @returns the disposer that unregisters the provider.
|
|
105
|
+
*/
|
|
106
|
+
registerFetchProvider(provider: WebFetchProvider): () => void;
|
|
107
|
+
private registerProvider;
|
|
108
|
+
/**
|
|
109
|
+
* Run one search through the selected provider. Resolves the provider at call
|
|
110
|
+
* time with the selection rules above; throws {@link WebError} when the
|
|
111
|
+
* capability cannot run. The seam enforces `request.maxResults` on the result:
|
|
112
|
+
* if the provider over-returns, `sources[]` is truncated and `truncated` set.
|
|
113
|
+
* Configured fallbacks share the caller signal and retry only provider or
|
|
114
|
+
* credential unavailability; missing registrations and cancellation stop the chain.
|
|
115
|
+
* @param request - the query and optional result limit.
|
|
116
|
+
* @param signal - optional cancellation signal forwarded to the provider.
|
|
117
|
+
* @returns the provider's results, capped to `request.maxResults`.
|
|
118
|
+
*/
|
|
119
|
+
search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
|
|
120
|
+
/**
|
|
121
|
+
* Retrieve one URL through the selected provider. Resolves the provider at
|
|
122
|
+
* call time with the selection rules above; throws {@link WebError} when the
|
|
123
|
+
* capability cannot run. A non-2xx response is a result, not a throw.
|
|
124
|
+
* @param request - the URL plus retrieval options.
|
|
125
|
+
* @param signal - optional cancellation signal forwarded to the provider.
|
|
126
|
+
* @returns the retrieval outcome; non-2xx responses resolve descriptively.
|
|
127
|
+
*/
|
|
128
|
+
fetch(request: WebFetchRequest, signal?: AbortSignal): Promise<WebFetchResult>;
|
|
129
|
+
}
|
|
130
|
+
export default WebRuntime;
|
|
131
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@hydraharness/harness-web`.
|
|
3
|
+
* @module @hydraharness/harness-web/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@hydraharness/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "web-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Shared search errors, provider configuration metadata, and URL normalization. */
|
|
2
|
+
import { WebError } from './types.ts';
|
|
3
|
+
/** Search types a provider may explicitly support. */
|
|
4
|
+
export type WebSearchType = 'web' | 'images' | 'news' | 'videos' | 'academic';
|
|
5
|
+
/** One provider-owned settings control; values live in its settings namespace. */
|
|
6
|
+
export interface SearchConfigField {
|
|
7
|
+
key: string;
|
|
8
|
+
label: string;
|
|
9
|
+
kind: 'text' | 'number' | 'select' | 'json';
|
|
10
|
+
options?: string[];
|
|
11
|
+
advanced?: boolean;
|
|
12
|
+
hint?: string;
|
|
13
|
+
}
|
|
14
|
+
/** Secret-free provider metadata used by the Settings directory. */
|
|
15
|
+
export interface WebSearchProviderDescriptor {
|
|
16
|
+
id: string;
|
|
17
|
+
displayName: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
capabilities: Record<WebSearchType, boolean>;
|
|
20
|
+
configurable: boolean;
|
|
21
|
+
settingsNs: string;
|
|
22
|
+
credentialRef: string;
|
|
23
|
+
fields: SearchConfigField[];
|
|
24
|
+
}
|
|
25
|
+
/** Web-only capabilities; endpoints for other search types are not advertised. */
|
|
26
|
+
export declare const WEB_SEARCH_CAPABILITIES: Record<WebSearchType, boolean>;
|
|
27
|
+
/** Safe categories exposed to tools and configuration UIs. */
|
|
28
|
+
export type SearchErrorCode = 'AUTH_ERROR' | 'RATE_LIMITED' | 'TIMEOUT' | 'NETWORK_ERROR' | 'INVALID_RESPONSE' | 'CONFIG_ERROR' | 'UNKNOWN';
|
|
29
|
+
/** Search failure containing only owned diagnostics, never upstream text or causes. */
|
|
30
|
+
export declare class SearchProviderError extends WebError {
|
|
31
|
+
readonly provider: string;
|
|
32
|
+
readonly code: SearchErrorCode;
|
|
33
|
+
readonly statusCode?: number | undefined;
|
|
34
|
+
/** Whether a later retry may succeed without reconfiguration. */
|
|
35
|
+
readonly retryable: boolean;
|
|
36
|
+
constructor(provider: string, code: SearchErrorCode, statusCode?: number | undefined, message?: string);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Translate an HTTP failure without reading an untrusted error body.
|
|
40
|
+
* @param provider - registered provider id.
|
|
41
|
+
* @param status - HTTP status.
|
|
42
|
+
* @returns the normalized failure.
|
|
43
|
+
*/
|
|
44
|
+
export declare function searchHttpError(provider: string, status: number): SearchProviderError;
|
|
45
|
+
/**
|
|
46
|
+
* Remove fragments and common tracking parameters for duplicate detection.
|
|
47
|
+
* Non-root trailing slashes and meaningful query parameters remain distinct.
|
|
48
|
+
* @param value - source URL.
|
|
49
|
+
* @returns a canonical comparison key, or the unchanged malformed URL.
|
|
50
|
+
*/
|
|
51
|
+
export declare function normalizedSearchUrl(value: string): string;
|
|
52
|
+
//# sourceMappingURL=search.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Product search selection and invocation limits, independent of chat configuration. */
|
|
2
|
+
import z from '@hydraharness/schemastery';
|
|
3
|
+
/** Saved search preferences. Empty provider requires an explicit user selection. */
|
|
4
|
+
export interface WebSearchSettings {
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
provider: string;
|
|
7
|
+
maxQueries: number;
|
|
8
|
+
maxResults: number;
|
|
9
|
+
timeoutMs: number;
|
|
10
|
+
}
|
|
11
|
+
/** Schema shared by registration and composition default resolution. */
|
|
12
|
+
export declare const SearchSettings: z<Partial<WebSearchSettings>, WebSearchSettings>;
|
|
13
|
+
//# sourceMappingURL=settings.d.ts.map
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vocabulary for the web capability seam (`ctx.web`). Search and fetch deliberately share one
|
|
3
|
+
* seam so provider selection, cancellation, errors, and product configuration have one owner,
|
|
4
|
+
* while retaining separate request and result types.
|
|
5
|
+
* @module @hydraharness/harness-web/types
|
|
6
|
+
*/
|
|
7
|
+
import { HarnessError } from '@hydraharness/harness-llm';
|
|
8
|
+
import type { WebSearchProviderDescriptor, WebSearchType } from './search.ts';
|
|
9
|
+
/**
|
|
10
|
+
* What one search-capable backend is asked to search. Each request carries one
|
|
11
|
+
* query; a consumer may issue several requests. `maxResults` is a
|
|
12
|
+
* `@hydraharness/harness-tool-web`-layer bound passed through unchanged and enforced on the way
|
|
13
|
+
* back by the seam (see {@link WebSearchResult}).
|
|
14
|
+
*/
|
|
15
|
+
export interface WebSearchRequest {
|
|
16
|
+
readonly query: string;
|
|
17
|
+
readonly type?: WebSearchType;
|
|
18
|
+
readonly country?: string;
|
|
19
|
+
readonly language?: string;
|
|
20
|
+
readonly date?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Upper bound on returned sources; the seam truncates to it. Omitted = no
|
|
23
|
+
* bound. `@hydraharness/harness-tool-web` always sets it. A provider whose API supports a
|
|
24
|
+
* result-count control (Exa's `numResults`) should apply it at the request
|
|
25
|
+
* layer as a cost/latency optimization; the seam enforces the bound
|
|
26
|
+
* regardless.
|
|
27
|
+
*/
|
|
28
|
+
readonly maxResults?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Normalized search outcome. `content` is optional provider-generated answer
|
|
32
|
+
* text or summary (Exa and DeepSeek return none; Perplexity returns a
|
|
33
|
+
* generated answer).
|
|
34
|
+
* `sources[]` is the portable citation shape. `truncated` is set by the seam
|
|
35
|
+
* when it cut `sources[]` down to `maxResults`.
|
|
36
|
+
*/
|
|
37
|
+
export interface WebSearchResult {
|
|
38
|
+
readonly provider?: string;
|
|
39
|
+
readonly query?: string;
|
|
40
|
+
readonly statusCode?: number;
|
|
41
|
+
/** Optional provider-generated answer text, search context, or summary. */
|
|
42
|
+
readonly content?: string;
|
|
43
|
+
/** Citeable sources, already truncated to the request's `maxResults`. */
|
|
44
|
+
readonly sources: readonly WebSearchSource[];
|
|
45
|
+
/** True when the seam dropped sources to honor `maxResults`. */
|
|
46
|
+
readonly truncated: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* One citeable source. A source always has a URL; `title`, `snippet`, and
|
|
50
|
+
* `publishedAt` are optional because not every provider returns them — forcing
|
|
51
|
+
* adapters to invent them would make the seam lie (Perplexity citations may be
|
|
52
|
+
* URL-only). `@hydraharness/harness-tool-web` renders `title ?? hostname(url)` for display.
|
|
53
|
+
*/
|
|
54
|
+
export interface WebSearchSource {
|
|
55
|
+
readonly url: string;
|
|
56
|
+
readonly title?: string;
|
|
57
|
+
readonly snippet?: string;
|
|
58
|
+
/** Publication/crawl timestamp as a provider-supplied ISO-8601 string. */
|
|
59
|
+
readonly publishedAt?: string;
|
|
60
|
+
readonly provider?: string;
|
|
61
|
+
readonly position?: number;
|
|
62
|
+
readonly score?: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* What one fetch-capable backend is asked to retrieve. The request deliberately
|
|
66
|
+
* omits timeout, format, prompt, and extraction controls: cancellation is a
|
|
67
|
+
* direct execution argument, while presentation and higher-level LLM concerns
|
|
68
|
+
* belong outside safe retrieval.
|
|
69
|
+
*/
|
|
70
|
+
export interface WebFetchRequest {
|
|
71
|
+
readonly url: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Normalized fetch outcome. A successful network fetch of a non-2xx response is
|
|
75
|
+
* a result, not an error: the status code is part of the fetched resource
|
|
76
|
+
* state. {@link WebError} is reserved for failures to safely retrieve or
|
|
77
|
+
* represent the resource.
|
|
78
|
+
*/
|
|
79
|
+
export interface WebFetchResult {
|
|
80
|
+
/** The final URL after allowed redirects (the request URL is in the request). */
|
|
81
|
+
readonly url: string;
|
|
82
|
+
/** HTTP status code of the fetched response. */
|
|
83
|
+
readonly statusCode: number;
|
|
84
|
+
/** Decoded body, classified by content kind. */
|
|
85
|
+
readonly body: WebFetchBody;
|
|
86
|
+
/** True when the provider capped the decoded body. */
|
|
87
|
+
readonly truncated: boolean;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The decoded body of a fetched resource. A CLOSED discriminated union owned by
|
|
91
|
+
* `@hydraharness/harness-web`: the provider decodes the kind and `@hydraharness/harness-tool-web` renders it, so a
|
|
92
|
+
* new kind is a coordinated change across known packages, not a plugin
|
|
93
|
+
* extension. Consumers `switch` on `kind` ending in `default: assertNever(...)`
|
|
94
|
+
* so adding a kind breaks compilation at every consumer until handled. Each arm
|
|
95
|
+
* stays its own object literal even where fields coincide, so an arm can gain
|
|
96
|
+
* fields the others lack.
|
|
97
|
+
*/
|
|
98
|
+
export type WebFetchBody = {
|
|
99
|
+
readonly kind: 'html';
|
|
100
|
+
readonly content: string;
|
|
101
|
+
} | {
|
|
102
|
+
readonly kind: 'text';
|
|
103
|
+
readonly content: string;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* A search-capable backend. Registered with `ctx.web.registerSearchProvider`.
|
|
107
|
+
* `id` is a stable string, unique within the search capability kind.
|
|
108
|
+
*/
|
|
109
|
+
export interface WebSearchProvider {
|
|
110
|
+
readonly id: string;
|
|
111
|
+
/** Configurable providers expose their own metadata; others stay out of Settings. */
|
|
112
|
+
readonly descriptor?: WebSearchProviderDescriptor;
|
|
113
|
+
/** Cheap local usability check; must not make network calls. */
|
|
114
|
+
available(): boolean;
|
|
115
|
+
/** Run one search; honor `signal` for cancellation. */
|
|
116
|
+
search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* A fetch-capable backend. Registered with `ctx.web.registerFetchProvider`.
|
|
120
|
+
* `id` is a stable string, unique within the fetch capability kind.
|
|
121
|
+
*/
|
|
122
|
+
export interface WebFetchProvider {
|
|
123
|
+
readonly id: string;
|
|
124
|
+
/** Cheap local usability check; must not make network calls. */
|
|
125
|
+
available(): boolean;
|
|
126
|
+
/** Retrieve one URL; honor `signal` for cancellation. */
|
|
127
|
+
fetch(request: WebFetchRequest, signal?: AbortSignal): Promise<WebFetchResult>;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Typed web error with a machine-routable, open-string `code` and chained `cause`.
|
|
131
|
+
* Consumers must tolerate provider-specific codes. Shared codes cover unavailable,
|
|
132
|
+
* missing, unusable, ambiguous, or duplicate providers, cancellation, and provider failure;
|
|
133
|
+
* the local fetch provider additionally distinguishes invalid or blocked URLs, redirects,
|
|
134
|
+
* size and timeout limits, and unsupported content types. Tool execution exposes the code in
|
|
135
|
+
* structured error metadata.
|
|
136
|
+
*/
|
|
137
|
+
export declare class WebError extends HarnessError {
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=types.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hydraharness/harness-web",
|
|
3
|
+
"description": "Abstract web access capability seam (ctx.web) for the Hydra harness — search/fetch provider registry, registration-order-independent selection, request/result vocabulary, and the WebError taxonomy",
|
|
4
|
+
"hydra": {
|
|
5
|
+
"plugin": {
|
|
6
|
+
"application": "Route web searches and page fetches to the providers configured in the profile."
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"version": "0.1.1-rc.10",
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/MaiHongPhong2010/Hydra-Harness.git",
|
|
16
|
+
"directory": "packages/web/web"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "lib/index.js",
|
|
20
|
+
"types": "lib/types/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./lib/types/index.d.ts",
|
|
24
|
+
"default": "./lib/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./invariant": {
|
|
27
|
+
"types": "./lib/types/invariant.d.ts",
|
|
28
|
+
"default": "./lib/invariant.js"
|
|
29
|
+
},
|
|
30
|
+
"./src/*": "./src/*",
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"lib/index.js",
|
|
35
|
+
"lib/invariant.js",
|
|
36
|
+
"lib/types/**/*.d.ts"
|
|
37
|
+
],
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@hydraharness/harness-invariants": "^0.1.1-rc.10",
|
|
41
|
+
"@hydraharness/harness-llm": "^0.1.1-rc.10",
|
|
42
|
+
"@hydraharness/cordis": "^4.0.2",
|
|
43
|
+
"@hydraharness/harness-settings": "^0.1.1-rc.10"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@hydraharness/schemastery": "^3.18.2"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@hydraharness/harness-invariants": "^0.1.1-rc.10",
|
|
50
|
+
"@hydraharness/harness-llm": "^0.1.1-rc.10",
|
|
51
|
+
"@hydraharness/cordis": "^4.0.2",
|
|
52
|
+
"@hydraharness/harness-settings": "^0.1.1-rc.10"
|
|
53
|
+
}
|
|
54
|
+
}
|