@mcoda/mswarm 0.1.110 → 0.1.112

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 (36) 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 +104 -37
  9. package/dist/vendor/codali/gateway/CodaliGatewayTypes.d.ts +10 -0
  10. package/dist/vendor/codali/gateway/CodaliGatewayTypes.d.ts.map +1 -1
  11. package/dist/vendor/codali/gateway/EvidenceNormalizer.d.ts.map +1 -1
  12. package/dist/vendor/codali/gateway/EvidenceNormalizer.js +3 -0
  13. package/dist/vendor/codali/gateway/GatewayPlanner.d.ts.map +1 -1
  14. package/dist/vendor/codali/gateway/GatewayPlanner.js +37 -1
  15. package/dist/vendor/codali/gateway/GroundingMode.d.ts +35 -0
  16. package/dist/vendor/codali/gateway/GroundingMode.d.ts.map +1 -0
  17. package/dist/vendor/codali/gateway/GroundingMode.js +69 -0
  18. package/dist/vendor/codali/gateway/LocalGatewayProvider.d.ts +16 -0
  19. package/dist/vendor/codali/gateway/LocalGatewayProvider.d.ts.map +1 -1
  20. package/dist/vendor/codali/gateway/LocalGatewayProvider.js +24 -1
  21. package/dist/vendor/codali/gateway/LocalGatewayTaskRunner.d.ts.map +1 -1
  22. package/dist/vendor/codali/gateway/LocalGatewayTaskRunner.js +10 -1
  23. package/dist/vendor/codali/gateway/RelevanceJudge.d.ts.map +1 -1
  24. package/dist/vendor/codali/gateway/RelevanceJudge.js +25 -8
  25. package/dist/vendor/codali/providers/OpenAiCompatibleProvider.d.ts +1 -0
  26. package/dist/vendor/codali/providers/OpenAiCompatibleProvider.d.ts.map +1 -1
  27. package/dist/vendor/codali/providers/OpenAiCompatibleProvider.js +55 -3
  28. package/dist/vendor/codali/providers/ProviderTypes.d.ts +2 -0
  29. package/dist/vendor/codali/providers/ProviderTypes.d.ts.map +1 -1
  30. package/dist/vendor/codali/tools/ToolRegistry.d.ts +5 -1
  31. package/dist/vendor/codali/tools/ToolRegistry.d.ts.map +1 -1
  32. package/dist/vendor/codali/tools/ToolRegistry.js +79 -2
  33. package/dist/vendor/codali/tools/docdex/DocdexTools.d.ts +13 -0
  34. package/dist/vendor/codali/tools/docdex/DocdexTools.d.ts.map +1 -1
  35. package/dist/vendor/codali/tools/docdex/DocdexTools.js +167 -12
  36. package/package.json +3 -3
@@ -67,13 +67,146 @@ const hasWebFindings = (payload) => {
67
67
  const hasWebContent = (payload) => {
68
68
  if (!isRecord(payload))
69
69
  return false;
70
- for (const key of ["web_context", "webContext", "hits", "results"]) {
70
+ // Fetched pages first. `hits` is not evidence of a successful fetch: with
71
+ // forceWeb it still carries local index matches, so judging by it alone
72
+ // reports success for a call that downloaded nothing.
73
+ const discovery = isRecord(payload.webDiscovery) ? payload.webDiscovery : undefined;
74
+ const fetches = Array.isArray(discovery?.fetches) ? discovery.fetches : [];
75
+ if (fetches.some((page) => isRecord(page) && (page.ai_digested_content || page.content))) {
76
+ return true;
77
+ }
78
+ for (const key of ["web_context", "webContext"]) {
71
79
  const value = payload[key];
72
80
  if (Array.isArray(value) && value.length > 0)
73
81
  return true;
74
82
  }
75
83
  return false;
76
84
  };
85
+ /**
86
+ * Rewrites a path into the repo-relative form docdex accepts.
87
+ *
88
+ * Docdex answers `invalid path` for a leading slash, a `./` prefix, or an
89
+ * absolute path, and a model asked to list a repository reaches for exactly
90
+ * those. That took out every "which files are in X" question: the tool was
91
+ * chosen correctly and then rejected on a formatting detail. Normalising here
92
+ * is the adapter's job — the caller meant the same directory either way.
93
+ *
94
+ * Returns undefined for "the whole repository", which is what docdex wants when
95
+ * no path is given.
96
+ */
97
+ export const toRepoRelativePath = (value, workspaceRoot) => {
98
+ if (typeof value !== "string")
99
+ return undefined;
100
+ let candidate = value.trim();
101
+ if (!candidate || candidate === "." || candidate === "./" || candidate === "/") {
102
+ return undefined;
103
+ }
104
+ if (workspaceRoot && candidate.startsWith(workspaceRoot)) {
105
+ candidate = candidate.slice(workspaceRoot.length);
106
+ }
107
+ candidate = candidate.replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/{2,}/g, "/");
108
+ return candidate || undefined;
109
+ };
110
+ /** Pages to keep from a web research call, and how much of each. */
111
+ const MAX_WEB_PAGES = 6;
112
+ const MAX_WEB_PAGE_CHARS = 6000;
113
+ /**
114
+ * Reduces a web-research response to the pages it actually fetched.
115
+ *
116
+ * The raw response is the wrong shape twice over. The fetched pages — the whole
117
+ * point of the call — are buried at `webDiscovery.fetches`, where the evidence
118
+ * normalizer does not look, while `hits` holds whatever the *local* index
119
+ * matched. For an external question those hits are repo source files, so a
120
+ * question about a central bank's interest rate came back citing eight files
121
+ * from this repository and none of the eight pages just downloaded.
122
+ *
123
+ * Suppressing local hits by asking docdex to skip its local search does not
124
+ * work: docdex indexes the pages it fetches and serves them back through that
125
+ * same path, so skipping it returns nothing at all. The response has to be
126
+ * reshaped here instead.
127
+ */
128
+ const toWebResearchOutput = (payload) => {
129
+ if (!isRecord(payload))
130
+ return payload;
131
+ const discovery = isRecord(payload.webDiscovery) ? payload.webDiscovery : undefined;
132
+ const fetches = Array.isArray(discovery?.fetches) ? discovery.fetches : [];
133
+ if (fetches.length === 0)
134
+ return payload;
135
+ const pages = fetches
136
+ .filter(isRecord)
137
+ .filter((page) => typeof page.url === "string")
138
+ .sort((a, b) => Number(b.relevance_score ?? 0) - Number(a.relevance_score ?? 0))
139
+ .slice(0, MAX_WEB_PAGES)
140
+ .map((page) => {
141
+ // The digest is the model-written summary docdex already produced; it
142
+ // says more per character than the raw page and costs the run less.
143
+ const body = (typeof page.ai_digested_content === "string" && page.ai_digested_content) ||
144
+ (typeof page.content === "string" && page.content) ||
145
+ "";
146
+ return {
147
+ url: page.url,
148
+ content: body.slice(0, MAX_WEB_PAGE_CHARS),
149
+ ...(page.relevance_score !== undefined ? { relevanceScore: page.relevance_score } : {}),
150
+ };
151
+ })
152
+ .filter((page) => page.content.trim().length > 0);
153
+ if (pages.length === 0)
154
+ return payload;
155
+ return {
156
+ query: isRecord(discovery?.discovery) ? discovery.discovery.query : undefined,
157
+ pages,
158
+ };
159
+ };
160
+ /** Hits to re-snippet. Enough to cover the answer, few enough to stay quick. */
161
+ const QUERY_SNIPPET_HITS = 3;
162
+ const QUERY_SNIPPET_WINDOW = 8;
163
+ /**
164
+ * Replaces each hit's snippet with the part of the file that matches the query.
165
+ *
166
+ * Search returns a window starting at line one, which for a code file is the
167
+ * import block. Asked for the value of a constant, a worker therefore received
168
+ * three files' worth of imports and reported that it could not find the value —
169
+ * correct about what it was given, useless as an answer. The follow-up tools
170
+ * exist (`docdex_open`, `docdex_symbols`) and the tool description recommends
171
+ * them, but a small model reliably searches once and stops, so the first result
172
+ * has to carry the content.
173
+ *
174
+ * Best-effort throughout: a failed re-snippet leaves the original hit alone,
175
+ * because a worse snippet is much better than a failed search.
176
+ */
177
+ const withQueryCentredSnippets = async (client, query, payload) => {
178
+ if (!isRecord(payload))
179
+ return payload;
180
+ const key = ["hits", "results", "matches"].find((candidate) => Array.isArray(payload[candidate]));
181
+ if (!key)
182
+ return payload;
183
+ const hits = payload[key];
184
+ if (hits.length === 0)
185
+ return payload;
186
+ const enriched = await Promise.all(hits.map(async (hit, index) => {
187
+ if (index >= QUERY_SNIPPET_HITS || !isRecord(hit))
188
+ return hit;
189
+ const docId = hit.doc_id ?? hit.docId ?? hit.rel_path ?? hit.relPath;
190
+ if (typeof docId !== "string" || !docId)
191
+ return hit;
192
+ try {
193
+ const snippet = await client.openSnippet(docId, {
194
+ query,
195
+ window: QUERY_SNIPPET_WINDOW,
196
+ });
197
+ const text = isRecord(snippet) && isRecord(snippet.snippet)
198
+ ? snippet.snippet.text
199
+ : undefined;
200
+ if (typeof text !== "string" || text.trim().length === 0)
201
+ return hit;
202
+ return { ...hit, snippet: text, snippet_origin: "query_centred" };
203
+ }
204
+ catch {
205
+ return hit;
206
+ }
207
+ }));
208
+ return { ...payload, [key]: enriched };
209
+ };
77
210
  const withDocdexMetadata = (tools) => tools.map((tool) => ({
78
211
  ...tool,
79
212
  readOnly: tool.readOnly ?? !DOCDEX_WRITE_TOOLS.has(tool.name),
@@ -111,9 +244,9 @@ const docdexToolDefinitions = (client, options) => [
111
244
  description: "Search the indexed repository and organizational memory for code, docs, and prior decisions. " +
112
245
  "Searches locally first and falls back to the web when the local index has nothing relevant, " +
113
246
  "so this is the default tool for both repo questions and general external questions. " +
114
- "Returns file paths, doc ids, and a snippet from the START of each file — usually imports, " +
115
- "not the part that answers the question. To explain what something does, follow up with " +
116
- "docdex_symbols for its signature or docdex_open for the relevant lines.",
247
+ "Returns file paths, doc ids, and for the top results a snippet taken from the part of the " +
248
+ "file that matches the query. For more of a file, follow up with docdex_symbols for its " +
249
+ "signatures or docdex_open for a specific range of lines.",
117
250
  inputSchema: {
118
251
  type: "object",
119
252
  required: ["query"],
@@ -154,7 +287,7 @@ const docdexToolDefinitions = (client, options) => [
154
287
  // simply found nothing. Return the local (empty) result instead.
155
288
  }
156
289
  }
157
- return toOutput(local);
290
+ return toOutput(await withQueryCentredSnippets(client, query, local));
158
291
  },
159
292
  },
160
293
  {
@@ -277,7 +410,7 @@ const docdexToolDefinitions = (client, options) => [
277
410
  },
278
411
  {
279
412
  name: "docdex_tree",
280
- description: "Render the repository folder tree with build output and vendor directories excluded. Use to orient in an unfamiliar repo instead of listing directories manually.",
413
+ description: "List the files and folders in the repository, or under one directory, with build output and vendor directories excluded. This is the tool for \"which files are in X\" and \"what packages exist\" — a text search ranks files by content and will not enumerate them.",
281
414
  inputSchema: {
282
415
  type: "object",
283
416
  properties: {
@@ -288,10 +421,22 @@ const docdexToolDefinitions = (client, options) => [
288
421
  extraExcludes: { type: "array", items: { type: "string" } },
289
422
  },
290
423
  },
291
- handler: async (args) => {
424
+ handler: async (args, context) => {
292
425
  const { path, maxDepth, dirsOnly, includeHidden, extraExcludes } = args;
293
- const result = await client.tree({ path, maxDepth, dirsOnly, includeHidden, extraExcludes });
294
- return toOutput(result);
426
+ const scoped = toRepoRelativePath(path, context?.workspaceRoot);
427
+ const options = { maxDepth, dirsOnly, includeHidden, extraExcludes };
428
+ try {
429
+ return toOutput(await client.tree({ path: scoped, ...options }));
430
+ }
431
+ catch (error) {
432
+ // Docdex fails a directory that does not exist, and a model asked to
433
+ // list a repository guesses names — "mcoda", "monorepo", "root". The
434
+ // request was still "show me what is here", so answer that from the
435
+ // top rather than returning nothing.
436
+ if (!scoped)
437
+ throw error;
438
+ return toOutput(await client.tree(options));
439
+ }
295
440
  },
296
441
  },
297
442
  {
@@ -396,7 +541,17 @@ const docdexToolDefinitions = (client, options) => [
396
541
  },
397
542
  handler: async (args) => {
398
543
  const { query, forceWeb, skipLocalSearch, webLimit, noCache } = args;
399
- let result = await client.webResearch(query, { forceWeb, skipLocalSearch, webLimit, noCache });
544
+ // This tool means "go and look outside". Left to default, docdex searches
545
+ // the local index first and returns whatever it holds, so a question about
546
+ // a central bank's interest rate came back sourced to this repository's
547
+ // own documentation. Anything genuinely local is what `docdex_search` is
548
+ // for; a caller can still ask for the mixed behaviour explicitly.
549
+ let result = await client.webResearch(query, {
550
+ forceWeb: forceWeb ?? true,
551
+ skipLocalSearch,
552
+ webLimit,
553
+ noCache,
554
+ });
400
555
  // A cache hit returns discovery metadata with no page content at all —
401
556
  // no web_context, no hits — so the run sees that a search happened and
402
557
  // nothing it can quote. Retrying uncached fetches the pages the cache
@@ -404,7 +559,7 @@ const docdexToolDefinitions = (client, options) => [
404
559
  if (!noCache && !hasWebContent(result)) {
405
560
  try {
406
561
  const fresh = await client.webResearch(query, {
407
- forceWeb,
562
+ forceWeb: forceWeb ?? true,
408
563
  skipLocalSearch,
409
564
  webLimit,
410
565
  noCache: true,
@@ -417,7 +572,7 @@ const docdexToolDefinitions = (client, options) => [
417
572
  // Keep the cached response rather than failing a search that worked.
418
573
  }
419
574
  }
420
- return toOutput(result);
575
+ return toOutput(toWebResearchOutput(result));
421
576
  },
422
577
  },
423
578
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcoda/mswarm",
3
- "version": "0.1.110",
3
+ "version": "0.1.112",
4
4
  "description": "Owner-run mswarm node for exposing local mcoda agents through the mswarm self-hosted gateway.",
5
5
  "type": "module",
6
6
  "main": "dist/server.js",
@@ -48,8 +48,8 @@
48
48
  "dependencies": {
49
49
  "fastify": "^5.7.4",
50
50
  "undici": "^6.28.0",
51
- "@mcoda/db": "0.1.110",
52
- "@mcoda/shared": "0.1.110"
51
+ "@mcoda/shared": "0.1.112",
52
+ "@mcoda/db": "0.1.112"
53
53
  },
54
54
  "scripts": {
55
55
  "build": "pnpm --filter @mcoda/shared run build && pnpm --filter @mcoda/codali run build && tsc -p tsconfig.json && node ../../scripts/copy-mswarm-codali-vendor.js",