@stratusagent/search 0.11.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/LICENSE +21 -0
- package/README.md +271 -0
- package/dist/contract.d.ts +171 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +40 -0
- package/dist/contract.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/options.d.ts +78 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +186 -0
- package/dist/options.js.map +1 -0
- package/dist/results.d.ts +53 -0
- package/dist/results.d.ts.map +1 -0
- package/dist/results.js +224 -0
- package/dist/results.js.map +1 -0
- package/dist/tool.d.ts +62 -0
- package/dist/tool.d.ts.map +1 -0
- package/dist/tool.js +148 -0
- package/dist/tool.js.map +1 -0
- package/package.json +53 -0
package/dist/options.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every option pinned to a meaning.
|
|
3
|
+
*
|
|
4
|
+
* Naming the options is not specifying them: two adapters can both satisfy
|
|
5
|
+
* `{ search(query, options) }` while accepting incompatible calls, which
|
|
6
|
+
* defeats the one criterion this package exists for. So the parsing lives
|
|
7
|
+
* here, once, in front of every backend — a call that means one thing on
|
|
8
|
+
* Brave means the same thing on SearXNG because neither of them parsed it.
|
|
9
|
+
*/
|
|
10
|
+
import { WEB_SEARCH_TOOL_NAME } from "./contract.js";
|
|
11
|
+
export const SEARCH_COUNT_DEFAULT = 10;
|
|
12
|
+
export const SEARCH_COUNT_MAX = 50;
|
|
13
|
+
/**
|
|
14
|
+
* A call this contract refuses. Exported because a backend throws it too:
|
|
15
|
+
* an option it declared `unsupported` and a value its vendor rejects are
|
|
16
|
+
* the same kind of answer to the agent.
|
|
17
|
+
*/
|
|
18
|
+
export class SearchOptionError extends Error {
|
|
19
|
+
constructor(message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = 'SearchOptionError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Fixed-length designators only. `W` and `D` before the `T`, `H`, `M` and
|
|
26
|
+
* `S` after it — and note that `M` is absent from the date portion on
|
|
27
|
+
* purpose, which is what makes the rejection of `P1M` positional rather
|
|
28
|
+
* than a ban on the letter.
|
|
29
|
+
*
|
|
30
|
+
* The lookahead after `T` is what rejects a bare `P1DT`: ISO 8601 requires
|
|
31
|
+
* at least one component after the designator, and without it the optional
|
|
32
|
+
* group matches nothing and the trailing `T` is silently ignored.
|
|
33
|
+
*/
|
|
34
|
+
const FIXED_DURATION = /^P(?:(\d+)W)?(?:(\d+)D)?(?:T(?=\d)(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/;
|
|
35
|
+
/** A year or a month in the date portion — the two ambiguous ones. */
|
|
36
|
+
const CALENDAR_DURATION = /^P\d+[YM]/;
|
|
37
|
+
const UNITS = ['W', 'D', 'H', 'M', 'S'];
|
|
38
|
+
const MS = { W: 604_800_000, D: 86_400_000, H: 3_600_000, M: 60_000, S: 1000 };
|
|
39
|
+
/**
|
|
40
|
+
* An ISO 8601 duration of fixed length, in milliseconds.
|
|
41
|
+
*
|
|
42
|
+
* **Fixed-length only, because `P1M` and `P1Y` are not one length.**
|
|
43
|
+
* Calendar arithmetic is where two conforming adapters part company:
|
|
44
|
+
* subtracting a month from 31 March, or a year from 29 February, has more
|
|
45
|
+
* than one defensible answer, and an adapter that converts to 30 or 365
|
|
46
|
+
* days instead disagrees with one that does not — at boundaries,
|
|
47
|
+
* intermittently, which is the worst way for a difference to show up.
|
|
48
|
+
* Rejecting the two ambiguous designators costs nothing a caller wants:
|
|
49
|
+
* `P30D` and `P365D` say what someone asking for "a month" or "a year" of
|
|
50
|
+
* search results actually means, and say it identically everywhere.
|
|
51
|
+
*
|
|
52
|
+
* `M` means two things and only one of them is rejected. ISO 8601 uses the
|
|
53
|
+
* same letter for months in the date portion and minutes after the `T`, so
|
|
54
|
+
* "reject `M`" would throw out `PT30M`, which is fixed-length and perfectly
|
|
55
|
+
* well defined.
|
|
56
|
+
*/
|
|
57
|
+
export const parseFreshnessDuration = (raw) => {
|
|
58
|
+
const value = raw.trim().toUpperCase();
|
|
59
|
+
const match = FIXED_DURATION.exec(value);
|
|
60
|
+
const total = UNITS.reduce((sum, unit, index) => {
|
|
61
|
+
const digits = match?.[index + 1];
|
|
62
|
+
return digits === undefined ? sum : sum + Number(digits) * MS[unit];
|
|
63
|
+
}, 0);
|
|
64
|
+
if (!match || total === 0) {
|
|
65
|
+
if (CALENDAR_DURATION.test(value)) {
|
|
66
|
+
throw new SearchOptionError(`freshness ${JSON.stringify(raw)} is a calendar length, which is not one length: a month from 31 March and a year `
|
|
67
|
+
+ 'from 29 February each have more than one defensible answer. Write P30D or P365D instead. '
|
|
68
|
+
+ '(PT30M, meaning thirty minutes, is fine — the rejection is about the date portion.)');
|
|
69
|
+
}
|
|
70
|
+
throw new SearchOptionError(`freshness must be a positive ISO 8601 duration of weeks, days, hours, minutes, or seconds — P7D, P4W, PT12H, PT30M. `
|
|
71
|
+
+ `Got ${JSON.stringify(raw)}.`);
|
|
72
|
+
}
|
|
73
|
+
return total;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* One hostname, normalized so two adapters compare the same string.
|
|
77
|
+
*
|
|
78
|
+
* **Hostname with label-boundary matching, not "registrable domain".** A
|
|
79
|
+
* registrable domain is the public-suffix-plus-one form, so
|
|
80
|
+
* `docs.example.com` is not one — an adapter enforcing that term would
|
|
81
|
+
* reject it or widen it up to `example.com`, silently broadening a search
|
|
82
|
+
* another adapter would keep narrow. This needs no public-suffix list,
|
|
83
|
+
* which is a dependency worth not acquiring. The two-label minimum is what
|
|
84
|
+
* stops `site: com`.
|
|
85
|
+
*/
|
|
86
|
+
export const normalizeSearchSite = (raw) => {
|
|
87
|
+
const trimmed = raw.trim().replace(/\.$/, '');
|
|
88
|
+
if (trimmed.length === 0) {
|
|
89
|
+
throw new SearchOptionError('site must be a hostname like example.com; it was empty.');
|
|
90
|
+
}
|
|
91
|
+
if (/[\s/\\?#@:]/.test(trimmed)) {
|
|
92
|
+
throw new SearchOptionError(`site must be a bare hostname — no scheme, no port, no path. Got ${JSON.stringify(raw)}; write example.com, not https://example.com/docs.`);
|
|
93
|
+
}
|
|
94
|
+
let hostname;
|
|
95
|
+
try {
|
|
96
|
+
// The URL parser is what lower-cases and converts an IDN to its A-label
|
|
97
|
+
// form, so `Bücher.de` and `bücher.de` and `xn--bcher-kva.de` are one
|
|
98
|
+
// string by the time anything compares them.
|
|
99
|
+
hostname = new URL(`https://${trimmed}`).hostname;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
throw new SearchOptionError(`site is not a hostname: ${JSON.stringify(raw)}.`);
|
|
103
|
+
}
|
|
104
|
+
const labels = hostname.split('.');
|
|
105
|
+
if (labels.length < 2 || labels.some((label) => label.length === 0)) {
|
|
106
|
+
throw new SearchOptionError(`site must name a host with at least two labels — example.com or docs.example.com, not ${JSON.stringify(raw)}.`);
|
|
107
|
+
}
|
|
108
|
+
return hostname;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Whether one result's host is inside a `site` filter.
|
|
112
|
+
*
|
|
113
|
+
* On label boundaries, which is the whole rule: `notexample.com` ends with
|
|
114
|
+
* `example.com` as a string and is a different company.
|
|
115
|
+
*/
|
|
116
|
+
export const hostMatchesSite = (host, site) => {
|
|
117
|
+
const normalized = host.toLowerCase().replace(/^\[|\]$/g, '').replace(/\.$/, '');
|
|
118
|
+
return normalized === site || normalized.endsWith(`.${site}`);
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Every field a call may carry. A misspelled option is refused rather than
|
|
122
|
+
* ignored, for the reason the rest of this file is strict: `freshnes: "P7D"`
|
|
123
|
+
* read as "no freshness filter" runs an unrestricted search that *succeeds*,
|
|
124
|
+
* and the agent then says the answer is recent. A silently dropped option is
|
|
125
|
+
* the same failure as a silently ignored one, arriving one step earlier.
|
|
126
|
+
*/
|
|
127
|
+
const KNOWN_FIELDS = ['query', 'count', 'site', 'freshness'];
|
|
128
|
+
const requireString = (value, field) => {
|
|
129
|
+
if (typeof value !== 'string') {
|
|
130
|
+
throw new SearchOptionError(`${field} must be a string; got ${value === null ? 'null' : typeof value}.`);
|
|
131
|
+
}
|
|
132
|
+
return value;
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* One tool call's input, turned into the options a backend receives.
|
|
136
|
+
*
|
|
137
|
+
* The query is passed through as **literal text**. "Verbatim" was the wrong
|
|
138
|
+
* word for it and the two halves could not both be true: most search APIs
|
|
139
|
+
* parse their own operators out of the query field, so handing the string
|
|
140
|
+
* over untouched is precisely what lets the upstream reinterpret `site:`,
|
|
141
|
+
* `OR`, a minus sign, or a quote — while a backend that escapes them is no
|
|
142
|
+
* longer passing anything verbatim. What is required is the *meaning*: the
|
|
143
|
+
* query is literal, and each backend does whatever its provider needs to
|
|
144
|
+
* make that true. An unbalanced quote is a search for a quote character,
|
|
145
|
+
* not a syntax error.
|
|
146
|
+
*/
|
|
147
|
+
export const parseSearchCall = (input, now) => {
|
|
148
|
+
const unknown = Object.keys(input).filter((key) => !KNOWN_FIELDS.includes(key));
|
|
149
|
+
if (unknown.length > 0) {
|
|
150
|
+
throw new SearchOptionError(`${unknown.join(', ')} ${unknown.length === 1 ? 'is not a field' : 'are not fields'} of ${WEB_SEARCH_TOOL_NAME}. `
|
|
151
|
+
+ `It takes ${KNOWN_FIELDS.join(', ')}.`);
|
|
152
|
+
}
|
|
153
|
+
const query = requireString(input.query, 'query').trim();
|
|
154
|
+
if (query.length === 0) {
|
|
155
|
+
throw new SearchOptionError('query is required and must not be empty.');
|
|
156
|
+
}
|
|
157
|
+
let count = SEARCH_COUNT_DEFAULT;
|
|
158
|
+
if (input.count !== undefined) {
|
|
159
|
+
if (typeof input.count !== 'number' || !Number.isInteger(input.count)) {
|
|
160
|
+
throw new SearchOptionError(`count must be a whole number between 1 and ${SEARCH_COUNT_MAX}; got ${JSON.stringify(input.count)}.`);
|
|
161
|
+
}
|
|
162
|
+
if (input.count < 1 || input.count > SEARCH_COUNT_MAX) {
|
|
163
|
+
throw new SearchOptionError(`count must be between 1 and ${SEARCH_COUNT_MAX}; got ${input.count}.`);
|
|
164
|
+
}
|
|
165
|
+
count = input.count;
|
|
166
|
+
}
|
|
167
|
+
const site = input.site === undefined ? undefined : normalizeSearchSite(requireString(input.site, 'site'));
|
|
168
|
+
let freshness;
|
|
169
|
+
if (input.freshness !== undefined) {
|
|
170
|
+
const written = requireString(input.freshness, 'freshness');
|
|
171
|
+
const ms = parseFreshnessDuration(written);
|
|
172
|
+
// Measured back from the instant the request is made, in UTC, and
|
|
173
|
+
// resolved once so the backend's native filter and the filter applied
|
|
174
|
+
// afterwards cannot disagree about when "now" was.
|
|
175
|
+
freshness = { duration: written.trim().toUpperCase(), ms, since: new Date(now.getTime() - ms).toISOString() };
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
query,
|
|
179
|
+
options: {
|
|
180
|
+
count,
|
|
181
|
+
...(site !== undefined ? { site } : {}),
|
|
182
|
+
...(freshness !== undefined ? { freshness } : {}),
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
//# sourceMappingURL=options.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an agent actually receives, whichever backend answered.
|
|
3
|
+
*
|
|
4
|
+
* Normalized results are part of the contract, not an implementation
|
|
5
|
+
* detail, so the enforcement is here rather than trusted to each adapter.
|
|
6
|
+
* That is deliberately belt and braces: a backend passes `count`, `site`,
|
|
7
|
+
* and `freshness` upstream because a provider filtering natively ranks
|
|
8
|
+
* better than a filter applied afterwards — and this package applies them
|
|
9
|
+
* again because "two backends answer the same call the same way" cannot
|
|
10
|
+
* rest on every ecosystem author having got it right.
|
|
11
|
+
*/
|
|
12
|
+
import { type EgressPolicy } from '@stratusagent/egress';
|
|
13
|
+
import type { SearchOptions, SearchResult } from './contract.ts';
|
|
14
|
+
export declare const plainSnippet: (value: string) => string;
|
|
15
|
+
/**
|
|
16
|
+
* An instant, or nothing.
|
|
17
|
+
*
|
|
18
|
+
* `publishedAt` is an ISO 8601 instant in UTC when the backend supplies one
|
|
19
|
+
* and absent when it does not — never a guess, never a locale-formatted
|
|
20
|
+
* date. Under `freshness` that means the result is dropped, which is why
|
|
21
|
+
* this has to be strict rather than merely tolerant: **`new Date(value)` is
|
|
22
|
+
* not a validator.** It reads `03/04/2026` as 4 March without ever saying
|
|
23
|
+
* whether the backend meant March or April, and it silently *corrects*
|
|
24
|
+
* `2026-02-30` into 2 March — so a lenient parse does not fail to date a
|
|
25
|
+
* result, it invents a date, and then a freshness filter keeps or drops the
|
|
26
|
+
* result on the strength of it. The shape is checked first, and the
|
|
27
|
+
* calendar date is round-tripped so a day that does not exist is refused
|
|
28
|
+
* instead of rolled forward.
|
|
29
|
+
*
|
|
30
|
+
* A date with no time is accepted and read as UTC midnight, which is what
|
|
31
|
+
* ISO 8601 and JavaScript both say it is. Plenty of backends only have a
|
|
32
|
+
* day, and refusing them would silently empty every `freshness` search
|
|
33
|
+
* against such a backend; reading it as midnight errs towards calling a
|
|
34
|
+
* page slightly older than it is, which is the safe direction here.
|
|
35
|
+
*/
|
|
36
|
+
export declare const normalizePublishedAt: (value: string | undefined) => string | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Hold a backend's answer to the contract: absolute permitted URLs, plain
|
|
39
|
+
* snippets, real instants, the `site` and `freshness` filters, and `count`
|
|
40
|
+
* as a maximum.
|
|
41
|
+
*
|
|
42
|
+
* An undated result does not survive a `freshness` filter. Backends omit
|
|
43
|
+
* dates for some hits routinely, so leaving this to each of them lets two
|
|
44
|
+
* conforming adapters return different sets for identical calls — the
|
|
45
|
+
* interchangeability criterion failing on the most ordinary input there is.
|
|
46
|
+
* Exclusion is the right side to land on: the agent asked for recent
|
|
47
|
+
* results and will say the answer is recent, and "we could not determine a
|
|
48
|
+
* date" is not evidence that it is. The cost is real — a good undated page
|
|
49
|
+
* is dropped — which is why the exclusion applies *only* when `freshness`
|
|
50
|
+
* is set, and a call that does not ask about time sees every result.
|
|
51
|
+
*/
|
|
52
|
+
export declare const normalizeSearchResults: (raw: readonly SearchResult[], options: SearchOptions, policy?: EgressPolicy) => SearchResult[];
|
|
53
|
+
//# sourceMappingURL=results.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"results.d.ts","sourceRoot":"","sources":["../src/results.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAwB,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAE/E,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AA+EjE,eAAO,MAAM,YAAY,GAAI,OAAO,MAAM,KAAG,MAWlC,CAAC;AAaZ;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,oBAAoB,GAAI,OAAO,MAAM,GAAG,SAAS,KAAG,MAAM,GAAG,SA8BzE,CAAC;AAmBF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,sBAAsB,GACjC,KAAK,SAAS,YAAY,EAAE,EAC5B,SAAS,aAAa,EACtB,SAAQ,YAAiB,KACxB,YAAY,EAkCd,CAAC"}
|
package/dist/results.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an agent actually receives, whichever backend answered.
|
|
3
|
+
*
|
|
4
|
+
* Normalized results are part of the contract, not an implementation
|
|
5
|
+
* detail, so the enforcement is here rather than trusted to each adapter.
|
|
6
|
+
* That is deliberately belt and braces: a backend passes `count`, `site`,
|
|
7
|
+
* and `freshness` upstream because a provider filtering natively ranks
|
|
8
|
+
* better than a filter applied afterwards — and this package applies them
|
|
9
|
+
* again because "two backends answer the same call the same way" cannot
|
|
10
|
+
* rest on every ecosystem author having got it right.
|
|
11
|
+
*/
|
|
12
|
+
import { assertRequestAllowed } from '@stratusagent/egress';
|
|
13
|
+
import { hostMatchesSite } from "./options.js";
|
|
14
|
+
/**
|
|
15
|
+
* A snippet as plain text.
|
|
16
|
+
*
|
|
17
|
+
* Vendors decorate their snippets with markup — Brave wraps matched terms
|
|
18
|
+
* in `<strong>` unless told not to — and a model reading `<strong>` learns
|
|
19
|
+
* nothing except that there was markup. Tags out, whitespace collapsed to
|
|
20
|
+
* one line.
|
|
21
|
+
*
|
|
22
|
+
* The pattern requires a letter or a slash after the `<`, which is what
|
|
23
|
+
* keeps `5 < 10 and 20 > 15` intact: a blanket `<[^>]*>` reads everything
|
|
24
|
+
* between a less-than and the next greater-than as a tag and deletes the
|
|
25
|
+
* sentence between them. Snippets are prose about arbitrary subjects, so
|
|
26
|
+
* that case is not hypothetical.
|
|
27
|
+
*
|
|
28
|
+
* **A formatting tag is removed, not replaced with a space.** Vendors
|
|
29
|
+
* highlight the matched *substring*, so `un<strong>expected</strong>`
|
|
30
|
+
* arrives routinely — and a space there produces `un expected`, a word the
|
|
31
|
+
* page does not contain. The same space detaches punctuation, turning
|
|
32
|
+
* `<strong>kettle</strong>.` into `kettle .`. Anything that is not inline
|
|
33
|
+
* formatting becomes a space, because removing a cell or list boundary
|
|
34
|
+
* glues two values into one instead — see `INLINE_ELEMENTS`.
|
|
35
|
+
*
|
|
36
|
+
* Entity *decoding* is deliberately not done here: a snippet arrives as a
|
|
37
|
+
* string a backend parsed out of its vendor's JSON, so whatever encoding
|
|
38
|
+
* that payload used is the backend's to undo, and this package guessing at
|
|
39
|
+
* it would corrupt a snippet that legitimately contains an ampersand
|
|
40
|
+
* followed by a word.
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* Formatting that wraps text which was already adjacent. Everything not
|
|
44
|
+
* named here becomes a space instead, which is the safe direction: naming
|
|
45
|
+
* the *separators* looks equivalent and is not, because that list is never
|
|
46
|
+
* complete — a custom element is missing from every list anybody will
|
|
47
|
+
* write, and deleting one glues `AlphaBeta` out of two cells. Inline
|
|
48
|
+
* formatting is a closed set, so it is the half that can be finished.
|
|
49
|
+
*
|
|
50
|
+
* The name is captured **whole**, punctuation included, before it is looked
|
|
51
|
+
* up. Stopping at the first character outside `[a-zA-Z0-9-]` reads
|
|
52
|
+
* `<a:widget>` as the inline `a` and deletes it — and a namespaced or
|
|
53
|
+
* framework-generated element is precisely the unknown tag the separator
|
|
54
|
+
* default exists for, so misreading one defeats the rule at the only point
|
|
55
|
+
* where it matters.
|
|
56
|
+
*
|
|
57
|
+
* `@stratusagent/tool-web` keeps the same set for the same reason. They are
|
|
58
|
+
* not shared because no dependency direction exists between a contract
|
|
59
|
+
* package and a plugin: `search` must not depend on `tool-web`, and the
|
|
60
|
+
* reverse would be backwards.
|
|
61
|
+
*/
|
|
62
|
+
const INLINE_ELEMENTS = new Set([
|
|
63
|
+
'a', 'abbr', 'b', 'bdi', 'bdo', 'big', 'cite', 'code', 'data', 'del', 'dfn',
|
|
64
|
+
'em', 'font', 'i', 'ins', 'kbd', 'mark', 'nobr', 'ruby',
|
|
65
|
+
's', 'samp', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'time',
|
|
66
|
+
'tt', 'u', 'var', 'wbr',
|
|
67
|
+
]);
|
|
68
|
+
// `rt` and `rp` are deliberately absent, though they are inline elements.
|
|
69
|
+
// They hold a ruby annotation — a pronunciation printed *above* the base
|
|
70
|
+
// text, not beside it — so joining them produces a token the page never
|
|
71
|
+
// shows: `<ruby>東京<rt>とうきょう</rt></ruby>` reads back as 東京とうきょう,
|
|
72
|
+
// fusing a word with its own furigana. Separating loses nothing and fuses
|
|
73
|
+
// nothing, which is the safer of the two ways to be wrong here; dropping
|
|
74
|
+
// the annotation outright would read better still and would be a guess
|
|
75
|
+
// about whether the pronunciation is content.
|
|
76
|
+
//
|
|
77
|
+
// `sub` and `sup` stay, and are the deliberate contrast: `H<sub>2</sub>O`
|
|
78
|
+
// is one token and joining it is the whole point.
|
|
79
|
+
/**
|
|
80
|
+
* `q` is the one element that renders a character of its own: a browser
|
|
81
|
+
* draws quotation marks around it from the stylesheet, so neither answer
|
|
82
|
+
* above is right. Removing it fuses `<q>yes</q><q>no</q>` into `yesno`,
|
|
83
|
+
* and spacing it detaches the comma in `<q>yes</q>, then left` — the very
|
|
84
|
+
* defect this file exists to fix. Emitting the mark the reader sees costs
|
|
85
|
+
* nothing and is the more faithful extraction of the two.
|
|
86
|
+
*/
|
|
87
|
+
const QUOTE_ELEMENT = 'q';
|
|
88
|
+
export const plainSnippet = (value) => value
|
|
89
|
+
.replace(/<\/?([a-zA-Z][^\s/>]*)[^>]*>/g, (_tag, rawName) => {
|
|
90
|
+
const name = rawName.toLowerCase();
|
|
91
|
+
if (name === QUOTE_ELEMENT)
|
|
92
|
+
return '"';
|
|
93
|
+
return INLINE_ELEMENTS.has(name) ? '' : ' ';
|
|
94
|
+
})
|
|
95
|
+
.replace(/\s+/g, ' ')
|
|
96
|
+
.trim();
|
|
97
|
+
/**
|
|
98
|
+
* A date-time this contract will accept: a calendar date, optionally with a
|
|
99
|
+
* time that carries an explicit zone.
|
|
100
|
+
*
|
|
101
|
+
* The zone is mandatory once there is a time, because a zone-less
|
|
102
|
+
* `2026-09-01T10:00:00` is *local* time to whatever machine parses it — the
|
|
103
|
+
* same string becomes two different instants on two daemons, which is the
|
|
104
|
+
* disagreement this package exists to remove.
|
|
105
|
+
*/
|
|
106
|
+
const ISO_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,9})?)?(Z|[+-]\d{2}:?\d{2}))?$/;
|
|
107
|
+
/**
|
|
108
|
+
* An instant, or nothing.
|
|
109
|
+
*
|
|
110
|
+
* `publishedAt` is an ISO 8601 instant in UTC when the backend supplies one
|
|
111
|
+
* and absent when it does not — never a guess, never a locale-formatted
|
|
112
|
+
* date. Under `freshness` that means the result is dropped, which is why
|
|
113
|
+
* this has to be strict rather than merely tolerant: **`new Date(value)` is
|
|
114
|
+
* not a validator.** It reads `03/04/2026` as 4 March without ever saying
|
|
115
|
+
* whether the backend meant March or April, and it silently *corrects*
|
|
116
|
+
* `2026-02-30` into 2 March — so a lenient parse does not fail to date a
|
|
117
|
+
* result, it invents a date, and then a freshness filter keeps or drops the
|
|
118
|
+
* result on the strength of it. The shape is checked first, and the
|
|
119
|
+
* calendar date is round-tripped so a day that does not exist is refused
|
|
120
|
+
* instead of rolled forward.
|
|
121
|
+
*
|
|
122
|
+
* A date with no time is accepted and read as UTC midnight, which is what
|
|
123
|
+
* ISO 8601 and JavaScript both say it is. Plenty of backends only have a
|
|
124
|
+
* day, and refusing them would silently empty every `freshness` search
|
|
125
|
+
* against such a backend; reading it as midnight errs towards calling a
|
|
126
|
+
* page slightly older than it is, which is the safe direction here.
|
|
127
|
+
*/
|
|
128
|
+
export const normalizePublishedAt = (value) => {
|
|
129
|
+
if (value === undefined) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
const trimmed = value.trim();
|
|
133
|
+
const match = ISO_DATE_TIME.exec(trimmed);
|
|
134
|
+
if (!match) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
const [, year = '', month = '', day = '', hour = '0', minute = '0', second = '0', zone = 'Z'] = match;
|
|
138
|
+
// Round-tripped rather than trusted: `Date.UTC(2026, 1, 30)` answers
|
|
139
|
+
// 2 March quite happily, and a date the backend did not give is worse
|
|
140
|
+
// than no date at all.
|
|
141
|
+
const probe = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
|
|
142
|
+
if (probe.getUTCFullYear() !== Number(year)
|
|
143
|
+
|| probe.getUTCMonth() !== Number(month) - 1
|
|
144
|
+
|| probe.getUTCDate() !== Number(day)
|
|
145
|
+
|| Number(hour) > 23
|
|
146
|
+
|| Number(minute) > 59
|
|
147
|
+
|| Number(second) > 60) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
// `+0200` and `+02:00` are the same offset in ISO 8601, and only the
|
|
151
|
+
// second is in JavaScript's own grammar. Spelling it the way the parser
|
|
152
|
+
// requires is not a guess about what the backend meant.
|
|
153
|
+
const parsed = new Date(trimmed.replace(/([+-]\d{2})(\d{2})$/, '$1:$2'));
|
|
154
|
+
const time = parsed.getTime();
|
|
155
|
+
return Number.isFinite(time) ? parsed.toISOString() : undefined;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* A result's URL, or nothing when an agent must not be handed it.
|
|
159
|
+
*
|
|
160
|
+
* The same address policy `web.fetch` uses, applied before the URL reaches
|
|
161
|
+
* a model that is about to fetch it: a ranker that returned an intranet
|
|
162
|
+
* host, a `javascript:` URL, or the metadata endpoint has returned one bad
|
|
163
|
+
* row, not a reason to fail a search — so the row is dropped and the rest
|
|
164
|
+
* of the page stands.
|
|
165
|
+
*/
|
|
166
|
+
const permittedUrl = (raw, policy) => {
|
|
167
|
+
try {
|
|
168
|
+
return assertRequestAllowed(raw, policy).href;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Hold a backend's answer to the contract: absolute permitted URLs, plain
|
|
176
|
+
* snippets, real instants, the `site` and `freshness` filters, and `count`
|
|
177
|
+
* as a maximum.
|
|
178
|
+
*
|
|
179
|
+
* An undated result does not survive a `freshness` filter. Backends omit
|
|
180
|
+
* dates for some hits routinely, so leaving this to each of them lets two
|
|
181
|
+
* conforming adapters return different sets for identical calls — the
|
|
182
|
+
* interchangeability criterion failing on the most ordinary input there is.
|
|
183
|
+
* Exclusion is the right side to land on: the agent asked for recent
|
|
184
|
+
* results and will say the answer is recent, and "we could not determine a
|
|
185
|
+
* date" is not evidence that it is. The cost is real — a good undated page
|
|
186
|
+
* is dropped — which is why the exclusion applies *only* when `freshness`
|
|
187
|
+
* is set, and a call that does not ask about time sees every result.
|
|
188
|
+
*/
|
|
189
|
+
export const normalizeSearchResults = (raw, options, policy = {}) => {
|
|
190
|
+
const kept = [];
|
|
191
|
+
// Compared as instants rather than as strings: ISO text sorts
|
|
192
|
+
// chronologically only for four-digit years, and a filter that is right
|
|
193
|
+
// "almost always" is the kind of difference between two backends this
|
|
194
|
+
// package exists to remove.
|
|
195
|
+
const since = options.freshness === undefined ? undefined : Date.parse(options.freshness.since);
|
|
196
|
+
for (const entry of raw) {
|
|
197
|
+
if (kept.length >= options.count) {
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
if (typeof entry?.title !== 'string' || typeof entry.url !== 'string') {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const url = permittedUrl(entry.url, policy);
|
|
204
|
+
if (url === undefined) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (options.site !== undefined && !hostMatchesSite(new URL(url).hostname, options.site)) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const publishedAt = normalizePublishedAt(typeof entry.publishedAt === 'string' ? entry.publishedAt : undefined);
|
|
211
|
+
if (since !== undefined && (publishedAt === undefined || Date.parse(publishedAt) < since)) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const snippet = typeof entry.snippet === 'string' ? plainSnippet(entry.snippet) : '';
|
|
215
|
+
kept.push({
|
|
216
|
+
title: entry.title.trim(),
|
|
217
|
+
url,
|
|
218
|
+
...(snippet.length > 0 ? { snippet } : {}),
|
|
219
|
+
...(publishedAt !== undefined ? { publishedAt } : {}),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return kept;
|
|
223
|
+
};
|
|
224
|
+
//# sourceMappingURL=results.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"results.js","sourceRoot":"","sources":["../src/results.ts"],"names":[],"mappings":"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"}
|
package/dist/tool.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `web.search` tool: one definition, whichever backend is installed.
|
|
3
|
+
*
|
|
4
|
+
* A backend supplies `search(query, options, context)` and nothing else.
|
|
5
|
+
* The name, the input schema, the option meanings, the result envelope, the
|
|
6
|
+
* credential seam, and the address policy are all here — which is what
|
|
7
|
+
* makes swapping vendors change no soul, no skill, and no result shape.
|
|
8
|
+
*/
|
|
9
|
+
import { type CredentialResolver, type JsonObject, type Tool } from '@stratusagent/core';
|
|
10
|
+
import { type SearchProvider } from './contract.ts';
|
|
11
|
+
/**
|
|
12
|
+
* What an agent may spend in a day before someone has to say so.
|
|
13
|
+
*
|
|
14
|
+
* A query acts on nothing, which is why `web.search` is `safe` — but a
|
|
15
|
+
* metered API costs money, and an agent in a loop can spend it unattended.
|
|
16
|
+
* That is a budget control rather than a permission, so it is a cap here
|
|
17
|
+
* and not an approval prompt on every search.
|
|
18
|
+
*/
|
|
19
|
+
export declare const DEFAULT_MAX_SEARCHES_PER_DAY = 200;
|
|
20
|
+
/**
|
|
21
|
+
* Said once, in the envelope, rather than left for each backend to word.
|
|
22
|
+
*
|
|
23
|
+
* Snippets are attacker-controlled text: written by whoever owns the page,
|
|
24
|
+
* selected by a third-party ranker, and handed to a model that is about to
|
|
25
|
+
* decide what to do next. This does not solve prompt injection and does not
|
|
26
|
+
* claim to — it marks the boundary, because doing so is nearly free now and
|
|
27
|
+
* a retrofit across every tool that returns third-party text is not.
|
|
28
|
+
*/
|
|
29
|
+
export declare const UNTRUSTED_RESULT_NOTE: string;
|
|
30
|
+
/** What the daemon records about a search. Deliberately not the query. */
|
|
31
|
+
export interface SearchLogRecord {
|
|
32
|
+
agentId: string;
|
|
33
|
+
/** Which backend answered, so an operator can tell where the bill came from. */
|
|
34
|
+
provider: string;
|
|
35
|
+
results: number;
|
|
36
|
+
}
|
|
37
|
+
export interface WebSearchToolOptions {
|
|
38
|
+
provider: SearchProvider;
|
|
39
|
+
/**
|
|
40
|
+
* The plugin's own config block, resolved per agent on every call. A
|
|
41
|
+
* plugin whose settings are an access boundary must not close over one
|
|
42
|
+
* agent's answer at setup, and the address policy is exactly that.
|
|
43
|
+
*/
|
|
44
|
+
config?: JsonObject;
|
|
45
|
+
/**
|
|
46
|
+
* How the calling agent's `search.apiKey` is found. A host that omits it
|
|
47
|
+
* gives up per-agent credentials entirely: every call reports the key as
|
|
48
|
+
* missing, naming what to add, rather than quietly searching with
|
|
49
|
+
* somebody else's.
|
|
50
|
+
*/
|
|
51
|
+
credentials?: CredentialResolver;
|
|
52
|
+
/**
|
|
53
|
+
* The daemon's structured log. It records that a search ran and against
|
|
54
|
+
* which provider — **never the query**, which is user content, in a log
|
|
55
|
+
* that is a trace rather than a second transcript.
|
|
56
|
+
*/
|
|
57
|
+
onSearch?: (record: SearchLogRecord) => void;
|
|
58
|
+
/** The clock, so a test can hold one still. */
|
|
59
|
+
now?: () => Date;
|
|
60
|
+
}
|
|
61
|
+
export declare const createWebSearchTool: (options: WebSearchToolOptions) => Tool;
|
|
62
|
+
//# sourceMappingURL=tool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../src/tool.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,UAAU,EAGf,KAAK,IAAI,EACV,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAI1E;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,MAAM,CAAC;AAEhD;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,QAE2D,CAAC;AAE9F,0EAA0E;AAC1E,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,gFAAgF;IAChF,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,cAAc,CAAC;IACzB;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IAC7C,+CAA+C;IAC/C,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AA0DD,eAAO,MAAM,mBAAmB,GAAI,SAAS,oBAAoB,KAAG,IAiFnE,CAAC"}
|