@mcoda/codali 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.
- 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 +104 -37
- package/dist/gateway/CodaliGatewayTypes.d.ts +10 -0
- package/dist/gateway/CodaliGatewayTypes.d.ts.map +1 -1
- package/dist/gateway/EvidenceNormalizer.d.ts.map +1 -1
- package/dist/gateway/EvidenceNormalizer.js +3 -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 +16 -0
- package/dist/gateway/LocalGatewayProvider.d.ts.map +1 -1
- package/dist/gateway/LocalGatewayProvider.js +24 -1
- package/dist/gateway/LocalGatewayTaskRunner.d.ts.map +1 -1
- package/dist/gateway/LocalGatewayTaskRunner.js +10 -1
- package/dist/gateway/RelevanceJudge.d.ts.map +1 -1
- package/dist/gateway/RelevanceJudge.js +25 -8
- package/dist/providers/OpenAiCompatibleProvider.d.ts +1 -0
- package/dist/providers/OpenAiCompatibleProvider.d.ts.map +1 -1
- package/dist/providers/OpenAiCompatibleProvider.js +55 -3
- package/dist/providers/ProviderTypes.d.ts +2 -0
- package/dist/providers/ProviderTypes.d.ts.map +1 -1
- 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 +167 -12
- 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
|
-
|
|
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
|
|
115
|
-
"
|
|
116
|
-
"
|
|
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: "
|
|
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
|
|
294
|
-
|
|
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
|
-
|
|
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/codali",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.112",
|
|
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.112",
|
|
39
|
+
"@mcoda/shared": "0.1.112"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "tsc -p tsconfig.json",
|