@mcoda/mswarm 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.
Files changed (33) hide show
  1. package/dist/vendor/codali/cli/AskCommand.d.ts.map +1 -1
  2. package/dist/vendor/codali/cli/AskCommand.js +20 -1
  3. package/dist/vendor/codali/docdex/DocdexClient.d.ts +6 -0
  4. package/dist/vendor/codali/docdex/DocdexClient.d.ts.map +1 -1
  5. package/dist/vendor/codali/docdex/DocdexClient.js +2 -0
  6. package/dist/vendor/codali/gateway/CodaliGateway.d.ts +2 -1
  7. package/dist/vendor/codali/gateway/CodaliGateway.d.ts.map +1 -1
  8. package/dist/vendor/codali/gateway/CodaliGateway.js +117 -37
  9. package/dist/vendor/codali/gateway/EvidenceNormalizer.d.ts.map +1 -1
  10. package/dist/vendor/codali/gateway/EvidenceNormalizer.js +11 -0
  11. package/dist/vendor/codali/gateway/GatewayPlanner.d.ts.map +1 -1
  12. package/dist/vendor/codali/gateway/GatewayPlanner.js +37 -1
  13. package/dist/vendor/codali/gateway/GroundingMode.d.ts +35 -0
  14. package/dist/vendor/codali/gateway/GroundingMode.d.ts.map +1 -0
  15. package/dist/vendor/codali/gateway/GroundingMode.js +69 -0
  16. package/dist/vendor/codali/gateway/LocalGatewayProvider.d.ts +13 -0
  17. package/dist/vendor/codali/gateway/LocalGatewayProvider.d.ts.map +1 -1
  18. package/dist/vendor/codali/gateway/LocalGatewayProvider.js +37 -3
  19. package/dist/vendor/codali/gateway/LocalGatewayTaskRunner.d.ts.map +1 -1
  20. package/dist/vendor/codali/gateway/LocalGatewayTaskRunner.js +18 -2
  21. package/dist/vendor/codali/gateway/RelevanceJudge.d.ts +11 -0
  22. package/dist/vendor/codali/gateway/RelevanceJudge.d.ts.map +1 -1
  23. package/dist/vendor/codali/gateway/RelevanceJudge.js +54 -0
  24. package/dist/vendor/codali/providers/OpenAiCompatibleProvider.d.ts +1 -0
  25. package/dist/vendor/codali/providers/OpenAiCompatibleProvider.d.ts.map +1 -1
  26. package/dist/vendor/codali/providers/OpenAiCompatibleProvider.js +45 -2
  27. package/dist/vendor/codali/tools/ToolRegistry.d.ts +5 -1
  28. package/dist/vendor/codali/tools/ToolRegistry.d.ts.map +1 -1
  29. package/dist/vendor/codali/tools/ToolRegistry.js +79 -2
  30. package/dist/vendor/codali/tools/docdex/DocdexTools.d.ts +13 -0
  31. package/dist/vendor/codali/tools/docdex/DocdexTools.d.ts.map +1 -1
  32. package/dist/vendor/codali/tools/docdex/DocdexTools.js +206 -9
  33. 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: isRecord(nested.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;AAEzD,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;AA6DN,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;CA+S7C;AAED,eAAO,MAAM,4BAA4B,GACvC,SAAS,6BAA6B,KACrC,sBAA6D,CAAC;AAEjE,eAAO,MAAM,aAAa,UArXD,OAAO,KAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAqX7B,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
- ? "Call the tools you need in a single batch. You get exactly one opportunity to call tools, so request everything you need at once."
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
- ? truncate(result.output)
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;AAED,eAAO,MAAM,oBAAoB,GAC/B,SAAS,qBAAqB,KAC7B,cAsCF,CAAC"}
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;AAsH5B,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;IAsMnE,OAAO,CAAC,WAAW;CAepB"}
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 fetch(url, {
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,EAGzB,MAAM,gBAAgB,CAAC;AAExB;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAI,MAAM,cAAc,KAAG,cAOvD,CAAC;AA4QH,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;CAkC/F"}
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 validationError = tool.inputSchema ? validateValue(args, tool.inputSchema) : undefined;
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(args, context);
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;AAwEjE;;;;;;;;;;;;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"}
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"}