@mcoda/codali 0.1.120 → 0.1.122
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.
|
@@ -15,6 +15,7 @@ import type { DocdexClient } from "../../docdex/DocdexClient.js";
|
|
|
15
15
|
export declare const toRepoRelativePath: (value: string | undefined, workspaceRoot?: string) => string | undefined;
|
|
16
16
|
/** Test seam: forget any open circuit. */
|
|
17
17
|
export declare const resetWebResearchBackoff: () => void;
|
|
18
|
+
export declare const normalizeWebQuery: (value: unknown) => string | undefined;
|
|
18
19
|
/**
|
|
19
20
|
* Judges whether search results actually answer the question.
|
|
20
21
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DocdexTools.d.ts","sourceRoot":"","sources":["../../../src/tools/docdex/DocdexTools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAuFjE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB,GAC7B,OAAO,MAAM,GAAG,SAAS,EACzB,gBAAgB,MAAM,KACrB,MAAM,GAAG,SAWX,CAAC;
|
|
1
|
+
{"version":3,"file":"DocdexTools.d.ts","sourceRoot":"","sources":["../../../src/tools/docdex/DocdexTools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAuFjE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB,GAC7B,OAAO,MAAM,GAAG,SAAS,EACzB,gBAAgB,MAAM,KACrB,MAAM,GAAG,SAWX,CAAC;AAwCF,0CAA0C;AAC1C,eAAO,MAAM,uBAAuB,QAAO,IAG1C,CAAC;AA0DF,eAAO,MAAM,iBAAiB,GAAI,OAAO,OAAO,KAAG,MAAM,GAAG,SAO3D,CAAC;AAoHF;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAEvB,MAAM,WAAW,iBAAiB;IAChC,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC;AAED,eAAO,MAAM,iBAAiB,GAC5B,QAAQ,YAAY,EACpB,UAAS,iBAAsB,KAC9B,cAAc,EAAgE,CAAC"}
|
|
@@ -123,8 +123,23 @@ export const toRepoRelativePath = (value, workspaceRoot) => {
|
|
|
123
123
|
* the fallback is deliberately short, because staying closed too long is a
|
|
124
124
|
* self-inflicted outage and reopening early costs one wasted call.
|
|
125
125
|
*/
|
|
126
|
-
|
|
126
|
+
/**
|
|
127
|
+
* Two kinds of refusal, and they deserve different patience.
|
|
128
|
+
*
|
|
129
|
+
* A spent quota or a rejected credential will still be spent and still be
|
|
130
|
+
* rejected in a minute, so the sensible default is long. A rate limit or a
|
|
131
|
+
* service restarting is over in seconds, and blocking for a quarter of an hour
|
|
132
|
+
* because a container was recreated would turn a blip into an outage of our
|
|
133
|
+
* own making.
|
|
134
|
+
*
|
|
135
|
+
* `Retry-After` overrides both, because the provider knows and we are guessing.
|
|
136
|
+
* The upstream gateway now answers a cold start with 503 and a `Retry-After`
|
|
137
|
+
* rather than a bodyless 502, which is precisely the case this handles.
|
|
138
|
+
*/
|
|
139
|
+
const WEB_EXHAUSTED_STATUSES = new Set([401, 402, 403]);
|
|
140
|
+
const WEB_TRANSIENT_STATUSES = new Set([429, 503]);
|
|
127
141
|
const DEFAULT_WEB_BACKOFF_MS = 15 * 60000;
|
|
142
|
+
const TRANSIENT_WEB_BACKOFF_MS = 30000;
|
|
128
143
|
const MAX_WEB_BACKOFF_MS = 60 * 60000;
|
|
129
144
|
let webUnavailableUntilMs = 0;
|
|
130
145
|
let webUnavailableReason = "";
|
|
@@ -150,17 +165,49 @@ const retryAfterMsFrom = (error) => {
|
|
|
150
165
|
}
|
|
151
166
|
return undefined;
|
|
152
167
|
};
|
|
153
|
-
const
|
|
168
|
+
const webRefusalStatus = (error) => {
|
|
154
169
|
const status = isRecord(error) && typeof error.status === "number" ? error.status : undefined;
|
|
155
|
-
|
|
170
|
+
if (status === undefined)
|
|
171
|
+
return undefined;
|
|
172
|
+
return WEB_EXHAUSTED_STATUSES.has(status) || WEB_TRANSIENT_STATUSES.has(status)
|
|
173
|
+
? status
|
|
174
|
+
: undefined;
|
|
156
175
|
};
|
|
176
|
+
const isWebQuotaRefusal = (error) => webRefusalStatus(error) !== undefined;
|
|
157
177
|
const noteWebRefusal = (error) => {
|
|
158
|
-
const
|
|
178
|
+
const status = webRefusalStatus(error);
|
|
179
|
+
const fallback = status !== undefined && WEB_TRANSIENT_STATUSES.has(status)
|
|
180
|
+
? TRANSIENT_WEB_BACKOFF_MS
|
|
181
|
+
: DEFAULT_WEB_BACKOFF_MS;
|
|
182
|
+
const backoff = Math.min(retryAfterMsFrom(error) ?? fallback, MAX_WEB_BACKOFF_MS);
|
|
159
183
|
webUnavailableUntilMs = Date.now() + backoff;
|
|
160
|
-
|
|
161
|
-
|
|
184
|
+
webUnavailableReason =
|
|
185
|
+
`provider returned ${status ?? "unknown"}; not retrying for ` +
|
|
186
|
+
(backoff < 60000 ? `${Math.round(backoff / 1000)}s` : `${Math.round(backoff / 60000)}m`);
|
|
162
187
|
};
|
|
163
188
|
const webResearchBlocked = () => Date.now() < webUnavailableUntilMs ? webUnavailableReason : undefined;
|
|
189
|
+
/**
|
|
190
|
+
* Normalises a query before it costs a metered request.
|
|
191
|
+
*
|
|
192
|
+
* The schema asks for a string and a model can satisfy that with whitespace, or
|
|
193
|
+
* with a whole paragraph of pasted context. Both are rejected upstream, and a
|
|
194
|
+
* rejected request still spends the allowance: measured at the gateway, a third
|
|
195
|
+
* of an hour's traffic was 400s rather than answers.
|
|
196
|
+
*
|
|
197
|
+
* A search query is a phrase. Beyond a couple of hundred characters it is not a
|
|
198
|
+
* query any more, and the useful part is at the front.
|
|
199
|
+
*/
|
|
200
|
+
const MAX_WEB_QUERY_CHARS = 400;
|
|
201
|
+
export const normalizeWebQuery = (value) => {
|
|
202
|
+
if (typeof value !== "string")
|
|
203
|
+
return undefined;
|
|
204
|
+
const collapsed = value.replace(/\s+/g, " ").trim();
|
|
205
|
+
if (!collapsed)
|
|
206
|
+
return undefined;
|
|
207
|
+
return collapsed.length > MAX_WEB_QUERY_CHARS
|
|
208
|
+
? collapsed.slice(0, MAX_WEB_QUERY_CHARS).trimEnd()
|
|
209
|
+
: collapsed;
|
|
210
|
+
};
|
|
164
211
|
/** Pages to keep from a web research call, and how much of each. */
|
|
165
212
|
const MAX_WEB_PAGES = 6;
|
|
166
213
|
const MAX_WEB_PAGE_CHARS = 6000;
|
|
@@ -329,9 +376,10 @@ const docdexToolDefinitions = (client, options) => [
|
|
|
329
376
|
// A judge that fails must not block a search that already succeeded.
|
|
330
377
|
}
|
|
331
378
|
}
|
|
332
|
-
|
|
379
|
+
const webQuery = normalizeWebQuery(query);
|
|
380
|
+
if (!localAnswers && webQuery && !webResearchBlocked()) {
|
|
333
381
|
try {
|
|
334
|
-
const web = await client.search(
|
|
382
|
+
const web = await client.search(webQuery, { limit, forceWeb: true });
|
|
335
383
|
if (hasWebFindings(web)) {
|
|
336
384
|
return toOutput(web);
|
|
337
385
|
}
|
|
@@ -604,6 +652,11 @@ const docdexToolDefinitions = (client, options) => [
|
|
|
604
652
|
// a central bank's interest rate came back sourced to this repository's
|
|
605
653
|
// own documentation. Anything genuinely local is what `docdex_search` is
|
|
606
654
|
// for; a caller can still ask for the mixed behaviour explicitly.
|
|
655
|
+
const normalizedQuery = normalizeWebQuery(query);
|
|
656
|
+
if (!normalizedQuery) {
|
|
657
|
+
// Spending a metered request to be told this is malformed helps nobody.
|
|
658
|
+
return toOutput({ query, pages: [], unavailable: "empty query" });
|
|
659
|
+
}
|
|
607
660
|
const blocked = webResearchBlocked();
|
|
608
661
|
if (blocked) {
|
|
609
662
|
// Answering "the web is unavailable" costs nothing; asking again costs
|
|
@@ -612,7 +665,7 @@ const docdexToolDefinitions = (client, options) => [
|
|
|
612
665
|
}
|
|
613
666
|
let result;
|
|
614
667
|
try {
|
|
615
|
-
result = await client.webResearch(
|
|
668
|
+
result = await client.webResearch(normalizedQuery, {
|
|
616
669
|
forceWeb: forceWeb ?? true,
|
|
617
670
|
skipLocalSearch,
|
|
618
671
|
webLimit,
|
|
@@ -632,7 +685,7 @@ const docdexToolDefinitions = (client, options) => [
|
|
|
632
685
|
// was standing in for.
|
|
633
686
|
if (!noCache && !hasWebContent(result)) {
|
|
634
687
|
try {
|
|
635
|
-
const fresh = await client.webResearch(
|
|
688
|
+
const fresh = await client.webResearch(normalizedQuery, {
|
|
636
689
|
forceWeb: forceWeb ?? true,
|
|
637
690
|
skipLocalSearch,
|
|
638
691
|
webLimit,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcoda/codali",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.122",
|
|
4
4
|
"description": "Standalone tool-runner adapter for mcoda.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
38
|
-
"@mcoda/db": "0.1.
|
|
39
|
-
"@mcoda/shared": "0.1.
|
|
38
|
+
"@mcoda/db": "0.1.122",
|
|
39
|
+
"@mcoda/shared": "0.1.122"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "tsc -p tsconfig.json",
|