@mcoda/codali 0.1.118 → 0.1.120
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/gateway/GatewayPlanner.d.ts.map +1 -1
- package/dist/gateway/GatewayPlanner.js +17 -1
- package/dist/gateway/GroundingMode.d.ts +10 -0
- package/dist/gateway/GroundingMode.d.ts.map +1 -1
- package/dist/gateway/GroundingMode.js +23 -0
- package/dist/tools/docdex/DocdexTools.d.ts +2 -0
- package/dist/tools/docdex/DocdexTools.d.ts.map +1 -1
- package/dist/tools/docdex/DocdexTools.js +89 -11
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GatewayPlanner.d.ts","sourceRoot":"","sources":["../../src/gateway/GatewayPlanner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,eAAe,EAEhB,MAAM,+BAA+B,CAAC;AAIvC,OAAO,KAAK,EACV,6BAA6B,EAC7B,0BAA0B,EAC1B,oBAAoB,EAEpB,4BAA4B,EAE7B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAEL,KAAK,wBAAwB,EAC9B,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"GatewayPlanner.d.ts","sourceRoot":"","sources":["../../src/gateway/GatewayPlanner.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { validateCodaliGatewayPlannerOutput, } from "./CodaliGatewaySchemas.js";
|
|
2
2
|
import { compileCodaliGatewayPolicy, } from "./GatewayPolicyCompiler.js";
|
|
3
3
|
import { CODALI_GATEWAY_SECURITY_PROMPT_HARDENING } from "./GatewaySecurityPolicy.js";
|
|
4
|
+
import { requestTouchesOwnData } from "./GroundingMode.js";
|
|
4
5
|
import { renderCapabilityLines, renderToolLines } from "./ToolExposure.js";
|
|
5
6
|
import { buildTemporalContext, renderTemporalContext } from "./TemporalContext.js";
|
|
6
7
|
export class CodaliGatewayPlannerError extends Error {
|
|
@@ -417,6 +418,12 @@ export const resolveToolNameAgainst = (candidate, available) => {
|
|
|
417
418
|
const bySuffix = available.filter((tool) => tool.toLowerCase().endsWith(`:${lower}`));
|
|
418
419
|
return bySuffix.length === 1 ? bySuffix[0] : undefined;
|
|
419
420
|
};
|
|
421
|
+
/**
|
|
422
|
+
* Whether a tool name belongs to a connector into someone's account, as opposed
|
|
423
|
+
* to a repository search or the public web. Namespaced transports are the
|
|
424
|
+
* connectors: `mcp:github:list_issues`, `http:jira:search_issues`.
|
|
425
|
+
*/
|
|
426
|
+
const isOwnedSystemConnector = (tool) => tool.startsWith("mcp:") || tool.startsWith("http:");
|
|
420
427
|
const allowedToolNames = (compilation) => [...compilation.effectiveAllowedTools].sort();
|
|
421
428
|
/**
|
|
422
429
|
* Capability names the classifier selected for stage-2 tool expansion. Absent
|
|
@@ -518,7 +525,16 @@ export const buildCodaliGatewayClassifierMessages = (input) => {
|
|
|
518
525
|
};
|
|
519
526
|
export const buildCodaliGatewayPlannerMessages = (input, classifier) => {
|
|
520
527
|
const policy = input.policyCompilation ?? compileCodaliGatewayPolicy({ request: input.request });
|
|
521
|
-
|
|
528
|
+
// A connector reaches into the user's own account, so it is only worth
|
|
529
|
+
// showing when the request is about their data. Left in view for every
|
|
530
|
+
// question, the planner picked by surface resemblance — a question about a
|
|
531
|
+
// company's chief executive went to a GitHub user search, and one about a
|
|
532
|
+
// language's newest release went to `get_latest_release`. Withholding them
|
|
533
|
+
// is a narrowing, not a denial: an explicitly allowed tool is still allowed,
|
|
534
|
+
// this only stops it being suggested for work it cannot do.
|
|
535
|
+
const tools = requestTouchesOwnData(input.request.query, classifier)
|
|
536
|
+
? allowedToolNames(policy)
|
|
537
|
+
: allowedToolNames(policy).filter((tool) => !isOwnedSystemConnector(tool));
|
|
522
538
|
// Stage 2 of two-level exposure: expand full schemas, but only for the
|
|
523
539
|
// capabilities the classifier selected.
|
|
524
540
|
const exposure = renderToolLines(tools, input.toolDescriptions, classifier.capabilities);
|
|
@@ -32,4 +32,14 @@ export interface GroundingDecisionInput {
|
|
|
32
32
|
classifier: Pick<CodaliGatewayClassifierOutput, "needsPrivateData" | "needsFreshData" | "needsDocdex" | "needsAppTools">;
|
|
33
33
|
}
|
|
34
34
|
export declare const decideGroundingMode: (input: GroundingDecisionInput) => GroundingDecision;
|
|
35
|
+
/**
|
|
36
|
+
* Whether the request is about data the user owns, and so whether the
|
|
37
|
+
* connectors that reach their accounts should be offered at all.
|
|
38
|
+
*
|
|
39
|
+
* Deliberately generous: a wrong "yes" offers a tool the planner may ignore,
|
|
40
|
+
* while a wrong "no" hides the only tool that could have answered. So the
|
|
41
|
+
* classifier's own judgement is honoured first, and either naming the system or
|
|
42
|
+
* claiming the data is enough.
|
|
43
|
+
*/
|
|
44
|
+
export declare const requestTouchesOwnData: (query: string, classifier: Pick<GroundingDecisionInput["classifier"], "needsPrivateData" | "needsAppTools">) => boolean;
|
|
35
45
|
//# sourceMappingURL=GroundingMode.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GroundingMode.d.ts","sourceRoot":"","sources":["../../src/gateway/GroundingMode.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,yBAAyB,CAAC;AAE7E;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,MAAM,mBAAmB,GAAG,UAAU,GAAG,MAAM,CAAC;AAEtD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,mBAAmB,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,IAAI,CACd,6BAA6B,EAC7B,kBAAkB,GAAG,gBAAgB,GAAG,aAAa,GAAG,eAAe,CACxE,CAAC;CACH;AAiDD,eAAO,MAAM,mBAAmB,GAC9B,OAAO,sBAAsB,KAC5B,iBAoCF,CAAC"}
|
|
1
|
+
{"version":3,"file":"GroundingMode.d.ts","sourceRoot":"","sources":["../../src/gateway/GroundingMode.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,yBAAyB,CAAC;AAE7E;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,MAAM,mBAAmB,GAAG,UAAU,GAAG,MAAM,CAAC;AAEtD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,mBAAmB,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,IAAI,CACd,6BAA6B,EAC7B,kBAAkB,GAAG,gBAAgB,GAAG,aAAa,GAAG,eAAe,CACxE,CAAC;CACH;AAiDD,eAAO,MAAM,mBAAmB,GAC9B,OAAO,sBAAsB,KAC5B,iBAoCF,CAAC;AAcF;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,GAChC,OAAO,MAAM,EACb,YAAY,IAAI,CAAC,sBAAsB,CAAC,YAAY,CAAC,EAAE,kBAAkB,GAAG,eAAe,CAAC,KAC3F,OAImC,CAAC"}
|
|
@@ -67,3 +67,26 @@ export const decideGroundingMode = (input) => {
|
|
|
67
67
|
}
|
|
68
68
|
return { mode: "grounded", reason: "default" };
|
|
69
69
|
};
|
|
70
|
+
/**
|
|
71
|
+
* Systems a request has to name before a connector to them is worth offering.
|
|
72
|
+
*
|
|
73
|
+
* A product connector reaches into somebody's account. Offering every one of
|
|
74
|
+
* them to every question invited the planner to pick by surface resemblance:
|
|
75
|
+
* asked who currently runs Microsoft it called `mcp:github:search_users`, and
|
|
76
|
+
* asked for the newest Node.js release it called `get_latest_release`. Both are
|
|
77
|
+
* public facts about a company, and neither is in anyone's account.
|
|
78
|
+
*/
|
|
79
|
+
const OWNED_SYSTEM_NOUNS = /\b(?:github|gitlab|bitbucket|jira|confluence|outlook|teams|calendar|inbox|mailbox|repository|repositories|repo|commit|commits|branch|branches|pull request|merge request|issue|issues|ticket|tickets|workspace|tenant)\b/i;
|
|
80
|
+
/**
|
|
81
|
+
* Whether the request is about data the user owns, and so whether the
|
|
82
|
+
* connectors that reach their accounts should be offered at all.
|
|
83
|
+
*
|
|
84
|
+
* Deliberately generous: a wrong "yes" offers a tool the planner may ignore,
|
|
85
|
+
* while a wrong "no" hides the only tool that could have answered. So the
|
|
86
|
+
* classifier's own judgement is honoured first, and either naming the system or
|
|
87
|
+
* claiming the data is enough.
|
|
88
|
+
*/
|
|
89
|
+
export const requestTouchesOwnData = (query, classifier) => classifier.needsPrivateData === true ||
|
|
90
|
+
classifier.needsAppTools === true ||
|
|
91
|
+
POSSESSIVE.test(query ?? "") ||
|
|
92
|
+
OWNED_SYSTEM_NOUNS.test(query ?? "");
|
|
@@ -13,6 +13,8 @@ import type { DocdexClient } from "../../docdex/DocdexClient.js";
|
|
|
13
13
|
* no path is given.
|
|
14
14
|
*/
|
|
15
15
|
export declare const toRepoRelativePath: (value: string | undefined, workspaceRoot?: string) => string | undefined;
|
|
16
|
+
/** Test seam: forget any open circuit. */
|
|
17
|
+
export declare const resetWebResearchBackoff: () => void;
|
|
16
18
|
/**
|
|
17
19
|
* Judges whether search results actually answer the question.
|
|
18
20
|
*
|
|
@@ -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;AAyBF,0CAA0C;AAC1C,eAAO,MAAM,uBAAuB,QAAO,IAG1C,CAAC;AAqJF;;;;;;;;;;;;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"}
|
|
@@ -107,6 +107,60 @@ export const toRepoRelativePath = (value, workspaceRoot) => {
|
|
|
107
107
|
candidate = candidate.replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/{2,}/g, "/");
|
|
108
108
|
return candidate || undefined;
|
|
109
109
|
};
|
|
110
|
+
/**
|
|
111
|
+
* Stops paying for a quota that is already spent.
|
|
112
|
+
*
|
|
113
|
+
* Web discovery is metered upstream. When the provider answers 402 — or 401,
|
|
114
|
+
* 403, 429 — the answer will not change for the next caller, but nothing
|
|
115
|
+
* remembered that: every question kept asking, and each question asks more than
|
|
116
|
+
* once, because a search that finds nothing locally falls through to the web
|
|
117
|
+
* and a cache hit with no content is retried uncached. Measured upstream at
|
|
118
|
+
* roughly 366 attempts in two hours, about 4,400 a day, which empties a monthly
|
|
119
|
+
* allowance of 5,000 in a little over a day.
|
|
120
|
+
*
|
|
121
|
+
* So the first refusal opens the circuit for everyone until it is due to close.
|
|
122
|
+
* `Retry-After` decides when, since the provider knows and we are guessing;
|
|
123
|
+
* the fallback is deliberately short, because staying closed too long is a
|
|
124
|
+
* self-inflicted outage and reopening early costs one wasted call.
|
|
125
|
+
*/
|
|
126
|
+
const WEB_QUOTA_STATUSES = new Set([401, 402, 403, 429]);
|
|
127
|
+
const DEFAULT_WEB_BACKOFF_MS = 15 * 60000;
|
|
128
|
+
const MAX_WEB_BACKOFF_MS = 60 * 60000;
|
|
129
|
+
let webUnavailableUntilMs = 0;
|
|
130
|
+
let webUnavailableReason = "";
|
|
131
|
+
/** Test seam: forget any open circuit. */
|
|
132
|
+
export const resetWebResearchBackoff = () => {
|
|
133
|
+
webUnavailableUntilMs = 0;
|
|
134
|
+
webUnavailableReason = "";
|
|
135
|
+
};
|
|
136
|
+
const retryAfterMsFrom = (error) => {
|
|
137
|
+
const details = isRecord(error) && isRecord(error.details) ? error.details : undefined;
|
|
138
|
+
const raw = details?.retry_after_ms ??
|
|
139
|
+
details?.retryAfterMs ??
|
|
140
|
+
details?.["retry-after"] ??
|
|
141
|
+
details?.retryAfter;
|
|
142
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) {
|
|
143
|
+
// Seconds or milliseconds, depending on who wrote it.
|
|
144
|
+
return raw < 10000 ? raw * 1000 : raw;
|
|
145
|
+
}
|
|
146
|
+
if (typeof raw === "string") {
|
|
147
|
+
const seconds = Number(raw.trim());
|
|
148
|
+
if (Number.isFinite(seconds) && seconds > 0)
|
|
149
|
+
return seconds * 1000;
|
|
150
|
+
}
|
|
151
|
+
return undefined;
|
|
152
|
+
};
|
|
153
|
+
const isWebQuotaRefusal = (error) => {
|
|
154
|
+
const status = isRecord(error) && typeof error.status === "number" ? error.status : undefined;
|
|
155
|
+
return status !== undefined && WEB_QUOTA_STATUSES.has(status);
|
|
156
|
+
};
|
|
157
|
+
const noteWebRefusal = (error) => {
|
|
158
|
+
const backoff = Math.min(retryAfterMsFrom(error) ?? DEFAULT_WEB_BACKOFF_MS, MAX_WEB_BACKOFF_MS);
|
|
159
|
+
webUnavailableUntilMs = Date.now() + backoff;
|
|
160
|
+
const status = isRecord(error) && typeof error.status === "number" ? error.status : "unknown";
|
|
161
|
+
webUnavailableReason = `provider returned ${status}; not retrying for ${Math.round(backoff / 60000)}m`;
|
|
162
|
+
};
|
|
163
|
+
const webResearchBlocked = () => Date.now() < webUnavailableUntilMs ? webUnavailableReason : undefined;
|
|
110
164
|
/** Pages to keep from a web research call, and how much of each. */
|
|
111
165
|
const MAX_WEB_PAGES = 6;
|
|
112
166
|
const MAX_WEB_PAGE_CHARS = 6000;
|
|
@@ -275,16 +329,20 @@ const docdexToolDefinitions = (client, options) => [
|
|
|
275
329
|
// A judge that fails must not block a search that already succeeded.
|
|
276
330
|
}
|
|
277
331
|
}
|
|
278
|
-
if (!localAnswers) {
|
|
332
|
+
if (!localAnswers && !webResearchBlocked()) {
|
|
279
333
|
try {
|
|
280
334
|
const web = await client.search(query, { limit, forceWeb: true });
|
|
281
335
|
if (hasWebFindings(web)) {
|
|
282
336
|
return toOutput(web);
|
|
283
337
|
}
|
|
284
338
|
}
|
|
285
|
-
catch {
|
|
339
|
+
catch (error) {
|
|
286
340
|
// Web discovery being unavailable must not fail a local search that
|
|
287
|
-
// simply found nothing. Return the local (empty) result instead
|
|
341
|
+
// simply found nothing. Return the local (empty) result instead — and
|
|
342
|
+
// remember a refusal, because every search would otherwise pay to
|
|
343
|
+
// rediscover it.
|
|
344
|
+
if (isWebQuotaRefusal(error))
|
|
345
|
+
noteWebRefusal(error);
|
|
288
346
|
}
|
|
289
347
|
}
|
|
290
348
|
return toOutput(await withQueryCentredSnippets(client, query, local));
|
|
@@ -546,12 +604,28 @@ const docdexToolDefinitions = (client, options) => [
|
|
|
546
604
|
// a central bank's interest rate came back sourced to this repository's
|
|
547
605
|
// own documentation. Anything genuinely local is what `docdex_search` is
|
|
548
606
|
// for; a caller can still ask for the mixed behaviour explicitly.
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
607
|
+
const blocked = webResearchBlocked();
|
|
608
|
+
if (blocked) {
|
|
609
|
+
// Answering "the web is unavailable" costs nothing; asking again costs
|
|
610
|
+
// a request from an allowance that is already spent.
|
|
611
|
+
return toOutput({ query, pages: [], unavailable: blocked });
|
|
612
|
+
}
|
|
613
|
+
let result;
|
|
614
|
+
try {
|
|
615
|
+
result = await client.webResearch(query, {
|
|
616
|
+
forceWeb: forceWeb ?? true,
|
|
617
|
+
skipLocalSearch,
|
|
618
|
+
webLimit,
|
|
619
|
+
noCache,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
if (isWebQuotaRefusal(error)) {
|
|
624
|
+
noteWebRefusal(error);
|
|
625
|
+
return toOutput({ query, pages: [], unavailable: webResearchBlocked() });
|
|
626
|
+
}
|
|
627
|
+
throw error;
|
|
628
|
+
}
|
|
555
629
|
// A cache hit returns discovery metadata with no page content at all —
|
|
556
630
|
// no web_context, no hits — so the run sees that a search happened and
|
|
557
631
|
// nothing it can quote. Retrying uncached fetches the pages the cache
|
|
@@ -568,8 +642,12 @@ const docdexToolDefinitions = (client, options) => [
|
|
|
568
642
|
result = fresh;
|
|
569
643
|
}
|
|
570
644
|
}
|
|
571
|
-
catch {
|
|
572
|
-
// Keep the cached response rather than failing a search that worked
|
|
645
|
+
catch (error) {
|
|
646
|
+
// Keep the cached response rather than failing a search that worked —
|
|
647
|
+
// but a refusal is worth remembering, so the next question does not
|
|
648
|
+
// spend another request discovering the same thing.
|
|
649
|
+
if (isWebQuotaRefusal(error))
|
|
650
|
+
noteWebRefusal(error);
|
|
573
651
|
}
|
|
574
652
|
}
|
|
575
653
|
return toOutput(toWebResearchOutput(result));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcoda/codali",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.120",
|
|
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.120",
|
|
39
|
+
"@mcoda/shared": "0.1.120"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "tsc -p tsconfig.json",
|