@mcoda/codali 0.1.109 → 0.1.111
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/cli/AskCommand.d.ts.map +1 -1
- package/dist/cli/AskCommand.js +20 -1
- package/dist/docdex/DocdexClient.d.ts +6 -0
- package/dist/docdex/DocdexClient.d.ts.map +1 -1
- package/dist/docdex/DocdexClient.js +2 -0
- package/dist/gateway/CodaliGateway.d.ts +2 -1
- package/dist/gateway/CodaliGateway.d.ts.map +1 -1
- package/dist/gateway/CodaliGateway.js +117 -37
- package/dist/gateway/EvidenceNormalizer.d.ts.map +1 -1
- package/dist/gateway/EvidenceNormalizer.js +11 -0
- package/dist/gateway/GatewayPlanner.d.ts.map +1 -1
- package/dist/gateway/GatewayPlanner.js +37 -1
- package/dist/gateway/GroundingMode.d.ts +35 -0
- package/dist/gateway/GroundingMode.d.ts.map +1 -0
- package/dist/gateway/GroundingMode.js +69 -0
- package/dist/gateway/LocalGatewayProvider.d.ts +13 -0
- package/dist/gateway/LocalGatewayProvider.d.ts.map +1 -1
- package/dist/gateway/LocalGatewayProvider.js +37 -3
- package/dist/gateway/LocalGatewayTaskRunner.d.ts.map +1 -1
- package/dist/gateway/LocalGatewayTaskRunner.js +18 -2
- package/dist/gateway/RelevanceJudge.d.ts +11 -0
- package/dist/gateway/RelevanceJudge.d.ts.map +1 -1
- package/dist/gateway/RelevanceJudge.js +54 -0
- package/dist/providers/OpenAiCompatibleProvider.d.ts +1 -0
- package/dist/providers/OpenAiCompatibleProvider.d.ts.map +1 -1
- package/dist/providers/OpenAiCompatibleProvider.js +45 -2
- package/dist/tools/ToolRegistry.d.ts +5 -1
- package/dist/tools/ToolRegistry.d.ts.map +1 -1
- package/dist/tools/ToolRegistry.js +79 -2
- package/dist/tools/docdex/DocdexTools.d.ts +13 -0
- package/dist/tools/docdex/DocdexTools.d.ts.map +1 -1
- package/dist/tools/docdex/DocdexTools.js +206 -9
- package/package.json +3 -3
|
@@ -5,6 +5,31 @@ import { ClaudeCliProvider } from "../providers/ClaudeCliProvider.js";
|
|
|
5
5
|
import { CodexCliProvider } from "../providers/CodexCliProvider.js";
|
|
6
6
|
import { OllamaRemoteProvider } from "../providers/OllamaRemoteProvider.js";
|
|
7
7
|
import { OpenAiCompatibleProvider } from "../providers/OpenAiCompatibleProvider.js";
|
|
8
|
+
/**
|
|
9
|
+
* Attaches the scheduling priority mswarm reads from `scheduling.priority`,
|
|
10
|
+
* without disturbing any runner settings the agent already carries.
|
|
11
|
+
*/
|
|
12
|
+
const buildLocalRunner = (nested, baseUrl, priority) => {
|
|
13
|
+
const existing = isRecord(nested.localRunner)
|
|
14
|
+
? nested.localRunner
|
|
15
|
+
: undefined;
|
|
16
|
+
if (!isMswarmEndpoint(baseUrl)) {
|
|
17
|
+
return existing;
|
|
18
|
+
}
|
|
19
|
+
const existingExtra = isRecord(existing?.extraBody) ? existing.extraBody : {};
|
|
20
|
+
const existingScheduling = isRecord(existingExtra.scheduling) ? existingExtra.scheduling : {};
|
|
21
|
+
return {
|
|
22
|
+
...(existing ?? {}),
|
|
23
|
+
extraBody: {
|
|
24
|
+
...existingExtra,
|
|
25
|
+
scheduling: {
|
|
26
|
+
...existingScheduling,
|
|
27
|
+
// An explicit value in the agent config wins over the default.
|
|
28
|
+
priority: existingScheduling.priority ?? priority ?? CODALI_MSWARM_PRIORITY,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
};
|
|
8
33
|
/**
|
|
9
34
|
* Request timeout for a locally-hosted model.
|
|
10
35
|
*
|
|
@@ -14,6 +39,17 @@ import { OpenAiCompatibleProvider } from "../providers/OpenAiCompatibleProvider.
|
|
|
14
39
|
* unavailable" — a timeout wearing the costume of an outage.
|
|
15
40
|
*/
|
|
16
41
|
const LOCAL_MODEL_TIMEOUT_MS = 600000;
|
|
42
|
+
/**
|
|
43
|
+
* Scheduling priority Codali asks mswarm for.
|
|
44
|
+
*
|
|
45
|
+
* mswarm sorts ascending, so a lower number runs sooner, and its nodes reserve
|
|
46
|
+
* capacity for priority <= -1. Codali runs are interactive — someone is waiting
|
|
47
|
+
* at a terminal or in a chat window — so they should not queue behind batch
|
|
48
|
+
* work. -10 sits comfortably inside the reserved band without claiming the
|
|
49
|
+
* extreme of the -100..100 range, leaving room for anything genuinely more
|
|
50
|
+
* urgent.
|
|
51
|
+
*/
|
|
52
|
+
export const CODALI_MSWARM_PRIORITY = -10;
|
|
17
53
|
const isRecord = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
18
54
|
const readString = (record, keys) => {
|
|
19
55
|
for (const key of keys) {
|
|
@@ -125,9 +161,7 @@ export const createProviderForAssignment = (assignment, options = {}) => {
|
|
|
125
161
|
timeoutMs: options.timeoutMs ?? LOCAL_MODEL_TIMEOUT_MS,
|
|
126
162
|
runnerKind: readString(nested, ["runnerKind", "runner_kind"]),
|
|
127
163
|
authMode: readString(nested, ["authMode", "auth_mode"]),
|
|
128
|
-
localRunner:
|
|
129
|
-
? nested.localRunner
|
|
130
|
-
: undefined,
|
|
164
|
+
localRunner: buildLocalRunner(nested, baseUrl, options.priority),
|
|
131
165
|
supportsTools: assignment.candidate.supportsTools ?? readBoolean(raw, ["supportsTools"]),
|
|
132
166
|
supportsJsonSchema: assignment.candidate.supportsJsonSchema ?? readBoolean(raw, ["supportsJsonSchema"]),
|
|
133
167
|
supportsStreaming: assignment.candidate.supportsStreaming ?? readBoolean(raw, ["supportsStreaming"]),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocalGatewayTaskRunner.d.ts","sourceRoot":"","sources":["../../src/gateway/LocalGatewayTaskRunner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EAGT,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"LocalGatewayTaskRunner.d.ts","sourceRoot":"","sources":["../../src/gateway/LocalGatewayTaskRunner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EAGT,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAGzD,OAAO,KAAK,EAEV,+BAA+B,EAC/B,gCAAgC,EAChC,6BAA6B,EAE9B,MAAM,0BAA0B,CAAC;AAElC;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,8EAA8E;AAC9E,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,2EAA2E;AAC3E,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAe1C,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,EAAE,QAAQ,CAAC;IACnB,QAAQ,EAAE,YAAY,CAAC;IACvB,WAAW,EAAE,WAAW,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+CAA+C;IAC/C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,2BAA2B,KAAK,IAAI,CAAC;CACxD;AAED,MAAM,MAAM,2BAA2B,GACnC;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CAAE,GAClF;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAClE;IACE,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,OAAO,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GACD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,WAAW,GAAG,QAAQ,CAAC;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAsEN,qBAAa,sBAAuB,YAAW,6BAA6B;IAC9D,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,6BAA6B;IAE7D,GAAG,CACP,KAAK,EAAE,+BAA+B,GACrC,OAAO,CAAC,gCAAgC,CAAC;CAqT7C;AAED,eAAO,MAAM,4BAA4B,GACvC,SAAS,6BAA6B,KACrC,sBAA6D,CAAC;AAEjE,eAAO,MAAM,aAAa,UApYD,OAAO,KAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAoY7B,CAAC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { truncateToolResult } from "../tools/TruncateResult.js";
|
|
1
2
|
import { CODALI_GATEWAY_SECURITY_PROMPT_HARDENING } from "./GatewaySecurityPolicy.js";
|
|
2
3
|
/**
|
|
3
4
|
* Bounded worker-task runner.
|
|
@@ -69,7 +70,16 @@ const buildTaskMessages = (input, hasTools) => {
|
|
|
69
70
|
"You are a Codali gateway worker.",
|
|
70
71
|
"Gather evidence for one bounded sub-task. Do not answer the user's overall question.",
|
|
71
72
|
hasTools
|
|
72
|
-
?
|
|
73
|
+
? [
|
|
74
|
+
"Call the tools you need in a single batch. You get exactly one opportunity to call tools, so request everything you need at once.",
|
|
75
|
+
// Told only that it had one chance, the worker reliably issued a
|
|
76
|
+
// single search and stopped. A search ranks whole files; it does not
|
|
77
|
+
// return the value of a constant or the contents of a directory, so
|
|
78
|
+
// questions of that shape came back unanswered with the budget for
|
|
79
|
+
// seven more calls unspent.
|
|
80
|
+
"One search is rarely enough. In the same batch, add the tools that cover what a search cannot: listing a directory's files, reading a named file, or looking up a symbol's definition.",
|
|
81
|
+
"Prefer several complementary calls over one broad one — unused calls cost nothing, a missing one costs the answer.",
|
|
82
|
+
].join("\n")
|
|
73
83
|
: "No tools are available. Answer from the task description alone or state what is missing.",
|
|
74
84
|
"Report only what the tool results actually support. Never invent file paths, identifiers, figures, or quotations.",
|
|
75
85
|
"If the results are insufficient, say so plainly instead of guessing; another round may be scheduled.",
|
|
@@ -246,8 +256,14 @@ export class LocalGatewayTaskRunner {
|
|
|
246
256
|
errorCode: result.error?.code,
|
|
247
257
|
errorMessage: result.error?.message,
|
|
248
258
|
},
|
|
259
|
+
// Structural where the tool returned data: a 258 KB web-research
|
|
260
|
+
// payload cut at a character count is unparseable, and the model
|
|
261
|
+
// then reports it found nothing. Applied here so it covers docdex
|
|
262
|
+
// and built-in tools too, not just the connectors.
|
|
249
263
|
output: result.ok
|
|
250
|
-
?
|
|
264
|
+
? (result.data !== undefined
|
|
265
|
+
? truncateToolResult(result.data, result.output, MAX_TOOL_OUTPUT_CHARS).text
|
|
266
|
+
: truncate(result.output))
|
|
251
267
|
: `Tool failed (${result.error?.code ?? "unknown"}): ${result.error?.message ?? ""}`,
|
|
252
268
|
};
|
|
253
269
|
}));
|
|
@@ -10,5 +10,16 @@ export interface RelevanceJudgeOptions {
|
|
|
10
10
|
provider: Provider;
|
|
11
11
|
maxTokens?: number;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Whether any result title shares a distinctive word with the question.
|
|
15
|
+
*
|
|
16
|
+
* A cheap pre-check that exists because the judge was answering obvious cases
|
|
17
|
+
* wrongly and expensively: asked whether `LocalGatewayTaskRunner.ts` could
|
|
18
|
+
* answer "what does LocalGatewayTaskRunner do", a small model said no, which
|
|
19
|
+
* forced a 45-second web search for a local class. Lexical overlap settles
|
|
20
|
+
* that case without a model call, leaving the judge for genuinely ambiguous
|
|
21
|
+
* results — which is where it earns its keep.
|
|
22
|
+
*/
|
|
23
|
+
export declare const titlesShareTermsWithQuery: (query: string, titles: readonly string[]) => boolean;
|
|
13
24
|
export declare const createRelevanceJudge: (options: RelevanceJudgeOptions) => RelevanceJudge;
|
|
14
25
|
//# sourceMappingURL=RelevanceJudge.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RelevanceJudge.d.ts","sourceRoot":"","sources":["../../src/gateway/RelevanceJudge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAyBrE;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,GAAI,SAAS,OAAO,KAAG,MAAM,EAqB9D,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,QAAQ,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;
|
|
1
|
+
{"version":3,"file":"RelevanceJudge.d.ts","sourceRoot":"","sources":["../../src/gateway/RelevanceJudge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAyBrE;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,GAAI,SAAS,OAAO,KAAG,MAAM,EAqB9D,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,QAAQ,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAoCD;;;;;;;;;GASG;AACH,eAAO,MAAM,yBAAyB,GACpC,OAAO,MAAM,EACb,QAAQ,SAAS,MAAM,EAAE,KACxB,OAWF,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,SAAS,qBAAqB,KAC7B,cAyCF,CAAC"}
|
|
@@ -46,11 +46,65 @@ export const summarizeResultTitles = (results) => {
|
|
|
46
46
|
}
|
|
47
47
|
return titles;
|
|
48
48
|
};
|
|
49
|
+
/**
|
|
50
|
+
* Words too common to signal relevance on their own.
|
|
51
|
+
*
|
|
52
|
+
* Not a keyword rule about any subject — just the connectives that appear in
|
|
53
|
+
* every question and would otherwise match any file path.
|
|
54
|
+
*/
|
|
55
|
+
const STOPWORDS = new Set([
|
|
56
|
+
"what", "which", "where", "when", "does", "did", "the", "and", "for", "with",
|
|
57
|
+
"from", "that", "this", "how", "why", "who", "are", "was", "were", "its",
|
|
58
|
+
"class", "file", "code", "show", "list", "find", "tell", "give", "about",
|
|
59
|
+
"purpose", "exists", "existence", "summary", "summarize", "explain",
|
|
60
|
+
]);
|
|
61
|
+
/**
|
|
62
|
+
* Splits text into comparable words, breaking CamelCase as well as punctuation.
|
|
63
|
+
*
|
|
64
|
+
* A symbol name is one word to a human and several to a path. Asked "which file
|
|
65
|
+
* defines CodaliGatewayPlannerError", the question yields the single term
|
|
66
|
+
* `codaligatewayplannererror`, while `gateway/GatewayPlanner.ts` flattens to
|
|
67
|
+
* `gateway gatewayplanner ts` — no overlap, so the correct file was discarded.
|
|
68
|
+
* Splitting both sides on case boundaries puts `gateway` and `planner` on each
|
|
69
|
+
* side, and the match succeeds.
|
|
70
|
+
*/
|
|
71
|
+
const wordsIn = (value) => value
|
|
72
|
+
// Insert a break between a lowercase/digit and a following uppercase, so
|
|
73
|
+
// `GatewayPlanner` becomes `Gateway Planner` before the lowercase pass.
|
|
74
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
75
|
+
// And between an acronym and a following word: `HTTPClient` -> `HTTP Client`.
|
|
76
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
|
|
77
|
+
.toLowerCase()
|
|
78
|
+
.split(/[^a-z0-9]+/)
|
|
79
|
+
.filter(Boolean);
|
|
80
|
+
/**
|
|
81
|
+
* Whether any result title shares a distinctive word with the question.
|
|
82
|
+
*
|
|
83
|
+
* A cheap pre-check that exists because the judge was answering obvious cases
|
|
84
|
+
* wrongly and expensively: asked whether `LocalGatewayTaskRunner.ts` could
|
|
85
|
+
* answer "what does LocalGatewayTaskRunner do", a small model said no, which
|
|
86
|
+
* forced a 45-second web search for a local class. Lexical overlap settles
|
|
87
|
+
* that case without a model call, leaving the judge for genuinely ambiguous
|
|
88
|
+
* results — which is where it earns its keep.
|
|
89
|
+
*/
|
|
90
|
+
export const titlesShareTermsWithQuery = (query, titles) => {
|
|
91
|
+
const terms = wordsIn(query).filter((word) => word.length > 3 && !STOPWORDS.has(word));
|
|
92
|
+
if (terms.length === 0)
|
|
93
|
+
return false;
|
|
94
|
+
// Whole words only. Splitting both sides the same way already lets a symbol
|
|
95
|
+
// in the question meet the same symbol in a path, so a substring pass would
|
|
96
|
+
// add nothing but false matches ("string" hitting `StringUtils.ts`).
|
|
97
|
+
const haystack = ` ${wordsIn(titles.join(" ")).join(" ")} `;
|
|
98
|
+
return terms.some((term) => haystack.includes(` ${term} `));
|
|
99
|
+
};
|
|
49
100
|
export const createRelevanceJudge = (options) => async ({ query, results }) => {
|
|
50
101
|
const titles = summarizeResultTitles(results);
|
|
51
102
|
// Nothing to judge; the caller's presence check already decided.
|
|
52
103
|
if (titles.length === 0)
|
|
53
104
|
return true;
|
|
105
|
+
// An obvious match needs no model call, and no chance of getting it wrong.
|
|
106
|
+
if (titlesShareTermsWithQuery(query, titles))
|
|
107
|
+
return true;
|
|
54
108
|
const response = await options.provider.generate({
|
|
55
109
|
messages: [
|
|
56
110
|
{
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Provider, ProviderConfig, ProviderRequest, ProviderResponse } from "./ProviderTypes.js";
|
|
2
|
+
export declare const fetchWithTransientRetry: (url: string, init: RequestInit, controller?: AbortController) => Promise<Response>;
|
|
2
3
|
export declare class OpenAiCompatibleProvider implements Provider {
|
|
3
4
|
private config;
|
|
4
5
|
/** OpenAI-style function calling is supported and parsed from `tool_calls`. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OpenAiCompatibleProvider.d.ts","sourceRoot":"","sources":["../../src/providers/OpenAiCompatibleProvider.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,QAAQ,EACR,cAAc,EAEd,eAAe,EACf,gBAAgB,EAGjB,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"OpenAiCompatibleProvider.d.ts","sourceRoot":"","sources":["../../src/providers/OpenAiCompatibleProvider.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,QAAQ,EACR,cAAc,EAEd,eAAe,EACf,gBAAgB,EAGjB,MAAM,oBAAoB,CAAC;AAgJ5B,eAAO,MAAM,uBAAuB,GAClC,KAAK,MAAM,EACX,MAAM,WAAW,EACjB,aAAa,eAAe,KAC3B,OAAO,CAAC,QAAQ,CAclB,CAAC;AAEF,qBAAa,wBAAyB,YAAW,QAAQ;IAS3C,OAAO,CAAC,MAAM;IAR1B,+EAA+E;IAC/E,QAAQ,CAAC,iBAAiB,QAAQ;IAElC,IAAI,SAAuB;IAC3B,OAAO,CAAC,WAAW,CAAoC;IACvD,OAAO,CAAC,QAAQ,CAAsB;IACtC,OAAO,CAAC,sBAAsB,CAAoC;gBAE9C,MAAM,EAAE,cAAc;IAWpC,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA0MnE,OAAO,CAAC,WAAW;CAepB"}
|
|
@@ -80,6 +80,49 @@ const buildPromptOnlyFormatInstruction = (format) => {
|
|
|
80
80
|
}
|
|
81
81
|
return undefined;
|
|
82
82
|
};
|
|
83
|
+
/**
|
|
84
|
+
* A single dropped connection must not fail a whole run.
|
|
85
|
+
*
|
|
86
|
+
* Self-hosted llama.cpp behind a relay closes sockets under load, surfacing as
|
|
87
|
+
* a bare `TypeError: fetch failed` with no status. Measured over fifty live
|
|
88
|
+
* questions this took out four runs outright — the caller saw an empty result,
|
|
89
|
+
* not a slow one. Retried, each of them succeeded.
|
|
90
|
+
*
|
|
91
|
+
* Deliberately narrow: only connection-level errors are retried. An HTTP error
|
|
92
|
+
* status is the server's considered answer and repeating it just wastes a
|
|
93
|
+
* minute, and an abort is our own timeout firing, where retrying would double
|
|
94
|
+
* the wait the caller already declined.
|
|
95
|
+
*/
|
|
96
|
+
const TRANSIENT_RETRIES = 2;
|
|
97
|
+
const TRANSIENT_RETRY_DELAY_MS = 750;
|
|
98
|
+
const isTransientNetworkError = (error) => {
|
|
99
|
+
if (error instanceof Error && error.name === "AbortError")
|
|
100
|
+
return false;
|
|
101
|
+
const cause = error?.cause;
|
|
102
|
+
const code = cause?.code ?? error?.code;
|
|
103
|
+
if (typeof code === "string") {
|
|
104
|
+
return ["ECONNRESET", "ECONNREFUSED", "EPIPE", "ETIMEDOUT", "UND_ERR_SOCKET"].includes(code);
|
|
105
|
+
}
|
|
106
|
+
return error instanceof TypeError && /fetch failed|network|socket/i.test(error.message);
|
|
107
|
+
};
|
|
108
|
+
export const fetchWithTransientRetry = async (url, init, controller) => {
|
|
109
|
+
let lastError;
|
|
110
|
+
for (let attempt = 0; attempt <= TRANSIENT_RETRIES; attempt += 1) {
|
|
111
|
+
try {
|
|
112
|
+
return await fetch(url, init);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
lastError = error;
|
|
116
|
+
// Our own timeout already fired; there is no time left to spend.
|
|
117
|
+
if (controller?.signal.aborted || !isTransientNetworkError(error))
|
|
118
|
+
throw error;
|
|
119
|
+
if (attempt === TRANSIENT_RETRIES)
|
|
120
|
+
break;
|
|
121
|
+
await new Promise((resolve) => setTimeout(resolve, TRANSIENT_RETRY_DELAY_MS * (attempt + 1)));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
throw lastError;
|
|
125
|
+
};
|
|
83
126
|
export class OpenAiCompatibleProvider {
|
|
84
127
|
constructor(config) {
|
|
85
128
|
this.config = config;
|
|
@@ -156,12 +199,12 @@ export class OpenAiCompatibleProvider {
|
|
|
156
199
|
const timeoutMs = this.config.timeoutMs ?? 60000;
|
|
157
200
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
158
201
|
try {
|
|
159
|
-
const response = await
|
|
202
|
+
const response = await fetchWithTransientRetry(url, {
|
|
160
203
|
method: "POST",
|
|
161
204
|
headers,
|
|
162
205
|
body: JSON.stringify(body),
|
|
163
206
|
signal: controller.signal,
|
|
164
|
-
});
|
|
207
|
+
}, controller);
|
|
165
208
|
if (!response.ok) {
|
|
166
209
|
const errorBody = await response.text();
|
|
167
210
|
throw new Error(`OpenAI-compatible error ${response.status}: ${errorBody}`);
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { type ToolCapability, type ToolContext, type ToolDefinition, type ToolDescriptor, type ToolExecutionResult } from "./ToolTypes.js";
|
|
1
|
+
import { type ToolCapability, type ToolContext, type ToolDefinition, type ToolDescriptor, type ToolExecutionResult, type ToolSchemaDefinition } from "./ToolTypes.js";
|
|
2
2
|
/**
|
|
3
3
|
* Projects a registered tool into its model-facing descriptor. Kept as a free
|
|
4
4
|
* function so connector adapters can reuse the same defaulting rules.
|
|
5
5
|
*/
|
|
6
6
|
export declare const toolDescriptorFor: (tool: ToolDefinition) => ToolDescriptor;
|
|
7
|
+
export declare const sanitizeToolArguments: (value: unknown, schema: ToolSchemaDefinition | undefined) => {
|
|
8
|
+
value: unknown;
|
|
9
|
+
dropped: string[];
|
|
10
|
+
};
|
|
7
11
|
export declare class ToolRegistry {
|
|
8
12
|
private tools;
|
|
9
13
|
register(tool: ToolDefinition): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ToolRegistry.d.ts","sourceRoot":"","sources":["../../src/tools/ToolRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,cAAc,EAGnB,KAAK,mBAAmB,
|
|
1
|
+
{"version":3,"file":"ToolRegistry.d.ts","sourceRoot":"","sources":["../../src/tools/ToolRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,cAAc,EAGnB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EAE1B,MAAM,gBAAgB,CAAC;AAExB;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAI,MAAM,cAAc,KAAG,cAOvD,CAAC;AA4HH,eAAO,MAAM,qBAAqB,GAChC,OAAO,OAAO,EACd,QAAQ,oBAAoB,GAAG,SAAS,KACvC;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAwBrC,CAAC;AAyLF,qBAAa,YAAY;IACvB,OAAO,CAAC,KAAK,CAAqC;IAElD,QAAQ,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI;IAOpC,IAAI,IAAI,cAAc,EAAE;IAIxB,QAAQ,IAAI,KAAK,CAAC,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,aAAa,GAAG,aAAa,CAAC,CAAC;IAQ/E;;;;OAIG;IACH,OAAO,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,cAAc,EAAE;IAOpD,2EAA2E;IAC3E,YAAY,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,GAAG,CAAC,cAAc,EAAE,cAAc,EAAE,CAAC;IAaxE,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,mBAAmB,CAAC;CAwC/F"}
|
|
@@ -79,6 +79,78 @@ const validateSchema = (schema, path = "$") => {
|
|
|
79
79
|
}
|
|
80
80
|
return undefined;
|
|
81
81
|
};
|
|
82
|
+
/**
|
|
83
|
+
* Repairs arguments that are nearly right before validating them.
|
|
84
|
+
*
|
|
85
|
+
* A worker gets one batch of tool calls, so a rejected call is not retried — it
|
|
86
|
+
* takes the whole sub-task down. Measured against the live suite, small models
|
|
87
|
+
* lost calls to two trivial mistakes: naming a field `depth` when the schema
|
|
88
|
+
* says `maxDepth`, and sending `"2"` where a number was wanted. Both were
|
|
89
|
+
* reported as `tool_invalid_args` and the question came back unanswered.
|
|
90
|
+
*
|
|
91
|
+
* Two repairs, both safe:
|
|
92
|
+
*
|
|
93
|
+
* - **Unknown properties are dropped.** The handler would ignore them anyway;
|
|
94
|
+
* failing instead only converts a usable call into no call at all. The tool
|
|
95
|
+
* still runs with its documented defaults, which beats returning nothing.
|
|
96
|
+
* - **Scalar strings are coerced** to the declared type when they parse
|
|
97
|
+
* exactly. `"2"` becomes `2`; `"two"` stays a string and is still rejected.
|
|
98
|
+
*
|
|
99
|
+
* Nothing here weakens validation: a missing required argument, or a value that
|
|
100
|
+
* cannot be coerced, fails exactly as before.
|
|
101
|
+
*/
|
|
102
|
+
const coerceScalar = (value, schema) => {
|
|
103
|
+
if (typeof value !== "string")
|
|
104
|
+
return value;
|
|
105
|
+
const types = readSchemaTypes(schema);
|
|
106
|
+
if (!types || types.some((type) => type === "string"))
|
|
107
|
+
return value;
|
|
108
|
+
const trimmed = value.trim();
|
|
109
|
+
if (types.includes("number") || types.includes("integer")) {
|
|
110
|
+
const parsed = Number(trimmed);
|
|
111
|
+
if (trimmed !== "" && Number.isFinite(parsed)) {
|
|
112
|
+
if (types.includes("integer") && !Number.isInteger(parsed))
|
|
113
|
+
return value;
|
|
114
|
+
return parsed;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (types.includes("boolean")) {
|
|
118
|
+
if (trimmed.toLowerCase() === "true")
|
|
119
|
+
return true;
|
|
120
|
+
if (trimmed.toLowerCase() === "false")
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
};
|
|
125
|
+
export const sanitizeToolArguments = (value, schema) => {
|
|
126
|
+
const dropped = [];
|
|
127
|
+
const walk = (input, node, path) => {
|
|
128
|
+
if (!node)
|
|
129
|
+
return input;
|
|
130
|
+
if (Array.isArray(input)) {
|
|
131
|
+
return node.items ? input.map((entry, index) => walk(entry, node.items, `${path}[${index}]`)) : input;
|
|
132
|
+
}
|
|
133
|
+
if (!isObject(input))
|
|
134
|
+
return coerceScalar(input, node);
|
|
135
|
+
const properties = node.properties;
|
|
136
|
+
if (!properties)
|
|
137
|
+
return input;
|
|
138
|
+
const output = {};
|
|
139
|
+
for (const [key, entry] of Object.entries(input)) {
|
|
140
|
+
const propertySchema = properties[key];
|
|
141
|
+
if (!propertySchema) {
|
|
142
|
+
if (node.additionalProperties === true)
|
|
143
|
+
output[key] = entry;
|
|
144
|
+
else
|
|
145
|
+
dropped.push(path ? `${path}.${key}` : key);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
output[key] = walk(entry, propertySchema, path ? `${path}.${key}` : key);
|
|
149
|
+
}
|
|
150
|
+
return output;
|
|
151
|
+
};
|
|
152
|
+
return { value: walk(value, schema, ""), dropped };
|
|
153
|
+
};
|
|
82
154
|
const runtimeTypeOf = (value) => {
|
|
83
155
|
if (value === null)
|
|
84
156
|
return "null";
|
|
@@ -315,12 +387,17 @@ export class ToolRegistry {
|
|
|
315
387
|
if (schemaError) {
|
|
316
388
|
return { ok: false, output: "", error: schemaError };
|
|
317
389
|
}
|
|
318
|
-
const
|
|
390
|
+
const sanitized = tool.inputSchema
|
|
391
|
+
? sanitizeToolArguments(args, tool.inputSchema)
|
|
392
|
+
: { value: args, dropped: [] };
|
|
393
|
+
const validationError = tool.inputSchema
|
|
394
|
+
? validateValue(sanitized.value, tool.inputSchema)
|
|
395
|
+
: undefined;
|
|
319
396
|
if (validationError) {
|
|
320
397
|
return { ok: false, output: "", error: validationError };
|
|
321
398
|
}
|
|
322
399
|
try {
|
|
323
|
-
const result = await tool.handler(
|
|
400
|
+
const result = await tool.handler(sanitized.value, context);
|
|
324
401
|
return { ok: true, output: result.output, data: result.data };
|
|
325
402
|
}
|
|
326
403
|
catch (error) {
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import type { ToolDefinition } from "../ToolTypes.js";
|
|
2
2
|
import type { DocdexClient } from "../../docdex/DocdexClient.js";
|
|
3
|
+
/**
|
|
4
|
+
* Rewrites a path into the repo-relative form docdex accepts.
|
|
5
|
+
*
|
|
6
|
+
* Docdex answers `invalid path` for a leading slash, a `./` prefix, or an
|
|
7
|
+
* absolute path, and a model asked to list a repository reaches for exactly
|
|
8
|
+
* those. That took out every "which files are in X" question: the tool was
|
|
9
|
+
* chosen correctly and then rejected on a formatting detail. Normalising here
|
|
10
|
+
* is the adapter's job — the caller meant the same directory either way.
|
|
11
|
+
*
|
|
12
|
+
* Returns undefined for "the whole repository", which is what docdex wants when
|
|
13
|
+
* no path is given.
|
|
14
|
+
*/
|
|
15
|
+
export declare const toRepoRelativePath: (value: string | undefined, workspaceRoot?: string) => string | undefined;
|
|
3
16
|
/**
|
|
4
17
|
* Judges whether search results actually answer the question.
|
|
5
18
|
*
|
|
@@ -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;
|
|
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;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"}
|