smart-context-mcp 1.19.0 → 1.20.0
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/README.md +1 -1
- package/package.json +5 -2
- package/server.json +2 -2
- package/src/global-memory/store.js +101 -1
- package/src/orchestration/base-orchestrator.js +37 -1
- package/src/server.js +59 -15
- package/src/storage/sqlite.js +75 -1
- package/src/task-runner.js +4 -0
- package/src/tools/global-memory.js +12 -1
- package/src/tools/smart-context.js +18 -4
- package/src/tools/smart-read-batch.js +26 -3
- package/src/tools/smart-read.js +128 -15
- package/src/tools/smart-search.js +665 -57
- package/src/tools/smart-turn.js +88 -4
- package/src/utils/task-budget.js +116 -0
|
@@ -11,6 +11,7 @@ import { buildPathsResult } from '../graph-paths.js';
|
|
|
11
11
|
import { projectRoot } from '../utils/paths.js';
|
|
12
12
|
import { resolveSafePath } from '../utils/fs.js';
|
|
13
13
|
import { countTokens } from '../tokenCounter.js';
|
|
14
|
+
import { consumeTokenBudget, normalizeTokenBudget, resolveTokenBudgetWindow } from '../utils/task-budget.js';
|
|
14
15
|
import { persistMetrics } from '../metrics.js';
|
|
15
16
|
import { predictContextFiles, recordContextAccess } from '../context-patterns.js';
|
|
16
17
|
import { recordToolUsage } from '../usage-feedback.js';
|
|
@@ -411,6 +412,7 @@ export const smartContext = async ({
|
|
|
411
412
|
task,
|
|
412
413
|
intent,
|
|
413
414
|
maxTokens = 12000,
|
|
415
|
+
tokenBudget,
|
|
414
416
|
entryFile,
|
|
415
417
|
diff,
|
|
416
418
|
detail = 'balanced',
|
|
@@ -423,6 +425,9 @@ export const smartContext = async ({
|
|
|
423
425
|
}) => {
|
|
424
426
|
const progress = enableProgress ? createProgressReporter('smart_context') : null;
|
|
425
427
|
const startTime = Date.now();
|
|
428
|
+
const normalizedTokenBudget = normalizeTokenBudget(tokenBudget);
|
|
429
|
+
const budgetWindow = resolveTokenBudgetWindow({ tokenBudget: normalizedTokenBudget, maxTokens });
|
|
430
|
+
const effectiveMaxTokens = budgetWindow.effectiveMaxTokens ?? 12000;
|
|
426
431
|
|
|
427
432
|
if (paths && typeof paths === 'object' && paths.from && paths.to && !task) {
|
|
428
433
|
if (progress) {
|
|
@@ -650,7 +655,7 @@ export const smartContext = async ({
|
|
|
650
655
|
attachSymbolEvidence(expanded, index, symbolCandidates);
|
|
651
656
|
normalizePrimaryCandidate(expanded, task, resolvedIntent);
|
|
652
657
|
|
|
653
|
-
const readPlan = allocateReads(expanded,
|
|
658
|
+
const readPlan = allocateReads(expanded, effectiveMaxTokens, resolvedIntent, detailMode);
|
|
654
659
|
|
|
655
660
|
const context = [];
|
|
656
661
|
let totalRawTokens = 0;
|
|
@@ -661,7 +666,7 @@ export const smartContext = async ({
|
|
|
661
666
|
for (const item of readPlan) {
|
|
662
667
|
const basePayload = buildContextItemPayload(item, index, detailMode);
|
|
663
668
|
const baseTokens = countTokens(JSON.stringify(basePayload));
|
|
664
|
-
if (totalCompressedTokens + baseTokens >
|
|
669
|
+
if (totalCompressedTokens + baseTokens > effectiveMaxTokens && context.length > 0) break;
|
|
665
670
|
|
|
666
671
|
const contextIndex = context.length;
|
|
667
672
|
context.push(basePayload);
|
|
@@ -700,7 +705,7 @@ export const smartContext = async ({
|
|
|
700
705
|
const newTokens = countTokens(JSON.stringify(enrichedPayload));
|
|
701
706
|
const tokenDelta = newTokens - oldTokens;
|
|
702
707
|
|
|
703
|
-
if (totalCompressedTokens + tokenDelta >
|
|
708
|
+
if (totalCompressedTokens + tokenDelta > effectiveMaxTokens && pending.contextIndex > 0) continue;
|
|
704
709
|
|
|
705
710
|
context[pending.contextIndex] = enrichedPayload;
|
|
706
711
|
filesWithContent.add(pending.item.rel);
|
|
@@ -727,7 +732,7 @@ export const smartContext = async ({
|
|
|
727
732
|
content: filtered,
|
|
728
733
|
};
|
|
729
734
|
const symbolTokens = countTokens(JSON.stringify(symbolPayload));
|
|
730
|
-
if (totalCompressedTokens + symbolTokens <=
|
|
735
|
+
if (totalCompressedTokens + symbolTokens <= effectiveMaxTokens) {
|
|
731
736
|
context.push(symbolPayload);
|
|
732
737
|
totalCompressedTokens += symbolTokens;
|
|
733
738
|
|
|
@@ -881,6 +886,7 @@ export const smartContext = async ({
|
|
|
881
886
|
filesIncluded,
|
|
882
887
|
filesEvaluated: expanded.size,
|
|
883
888
|
detailMode,
|
|
889
|
+
maxTokens: effectiveMaxTokens,
|
|
884
890
|
totalTokens: countTokens(context.map((c) => c.content || '').join('')),
|
|
885
891
|
...(prefetchResult ? {
|
|
886
892
|
prefetch: {
|
|
@@ -894,6 +900,14 @@ export const smartContext = async ({
|
|
|
894
900
|
...(includeSet.has('hints') ? { hints } : {}),
|
|
895
901
|
};
|
|
896
902
|
|
|
903
|
+
if (normalizedTokenBudget) {
|
|
904
|
+
result.taskBudget = normalizedTokenBudget;
|
|
905
|
+
result.remainingBudget = consumeTokenBudget({
|
|
906
|
+
tokenBudget: normalizedTokenBudget,
|
|
907
|
+
usedTokens: totalCompressedTokens,
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
897
911
|
if (diffSummary) {
|
|
898
912
|
diffSummary.included = context.filter((c) => c.role === 'primary').length;
|
|
899
913
|
result.diffSummary = diffSummary;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { smartRead } from './smart-read.js';
|
|
2
2
|
import { countTokens } from '../tokenCounter.js';
|
|
3
3
|
|
|
4
|
-
export const smartReadBatch = async ({ files, maxTokens }) => {
|
|
4
|
+
export const smartReadBatch = async ({ files, maxTokens, tokenBudget }) => {
|
|
5
5
|
const results = [];
|
|
6
6
|
let totalTokens = 0;
|
|
7
7
|
let filesSkipped = 0;
|
|
8
|
+
let budgetStoppedAt = null;
|
|
8
9
|
|
|
9
10
|
for (const item of files) {
|
|
10
11
|
try {
|
|
@@ -15,6 +16,7 @@ export const smartReadBatch = async ({ files, maxTokens }) => {
|
|
|
15
16
|
startLine: item.startLine,
|
|
16
17
|
endLine: item.endLine,
|
|
17
18
|
maxTokens: item.maxTokens,
|
|
19
|
+
tokenBudget,
|
|
18
20
|
});
|
|
19
21
|
|
|
20
22
|
if (readResult.error) {
|
|
@@ -30,6 +32,7 @@ export const smartReadBatch = async ({ files, maxTokens }) => {
|
|
|
30
32
|
|
|
31
33
|
if (maxTokens && totalTokens + itemTokens > maxTokens && results.length > 0) {
|
|
32
34
|
filesSkipped = files.length - results.length;
|
|
35
|
+
budgetStoppedAt = item.path;
|
|
33
36
|
break;
|
|
34
37
|
}
|
|
35
38
|
|
|
@@ -40,7 +43,8 @@ export const smartReadBatch = async ({ files, maxTokens }) => {
|
|
|
40
43
|
truncated: readResult.truncated,
|
|
41
44
|
content: readResult.content,
|
|
42
45
|
...(readResult.indexHint !== undefined ? { indexHint: readResult.indexHint } : {}),
|
|
43
|
-
...(readResult.chosenMode ? { chosenMode: readResult.chosenMode
|
|
46
|
+
...(readResult.chosenMode ? { chosenMode: readResult.chosenMode } : {}),
|
|
47
|
+
...(readResult.budgetApplied ? { budgetApplied: true, budgetDetails: readResult.budgetDetails } : {}),
|
|
44
48
|
});
|
|
45
49
|
|
|
46
50
|
totalTokens += itemTokens;
|
|
@@ -53,7 +57,7 @@ export const smartReadBatch = async ({ files, maxTokens }) => {
|
|
|
53
57
|
}
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
|
|
60
|
+
const response = {
|
|
57
61
|
results,
|
|
58
62
|
metrics: {
|
|
59
63
|
totalTokens,
|
|
@@ -61,4 +65,23 @@ export const smartReadBatch = async ({ files, maxTokens }) => {
|
|
|
61
65
|
filesSkipped,
|
|
62
66
|
},
|
|
63
67
|
};
|
|
68
|
+
|
|
69
|
+
if (maxTokens && filesSkipped > 0) {
|
|
70
|
+
response.budgetApplied = true;
|
|
71
|
+
response.budgetDetails = {
|
|
72
|
+
scope: 'batch',
|
|
73
|
+
maxTokens,
|
|
74
|
+
actions: ['batch_stopped_early'],
|
|
75
|
+
filesRead: results.length,
|
|
76
|
+
filesSkipped,
|
|
77
|
+
stopReason: 'batch_token_limit',
|
|
78
|
+
...(budgetStoppedAt ? { stoppedBefore: budgetStoppedAt } : {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (tokenBudget) {
|
|
83
|
+
response.taskBudget = tokenBudget;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return response;
|
|
64
87
|
};
|
package/src/tools/smart-read.js
CHANGED
|
@@ -9,10 +9,13 @@ import { isDockerfile, readTextFile } from '../utils/fs.js';
|
|
|
9
9
|
import { projectRoot } from '../utils/paths.js';
|
|
10
10
|
import { truncate } from '../utils/text.js';
|
|
11
11
|
import { countTokens } from '../tokenCounter.js';
|
|
12
|
+
import { consumeTokenBudget, normalizeTokenBudget, resolveTokenBudgetWindow } from '../utils/task-budget.js';
|
|
12
13
|
import { recordToolUsage } from '../usage-feedback.js';
|
|
13
14
|
import { recordDecision, DECISION_REASONS, EXPECTED_BENEFITS } from '../decision-explainer.js';
|
|
14
15
|
import { recordDevctxOperation } from '../missed-opportunities.js';
|
|
15
16
|
import { createProgressReporter } from '../streaming.js';
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
18
|
+
import { getReadCache, setReadCache } from '../storage/sqlite.js';
|
|
16
19
|
|
|
17
20
|
const execFile = promisify(execFileCb);
|
|
18
21
|
import { summarizeGo, summarizeRust, summarizeJava, summarizeShell, summarizeTerraform, summarizeDockerfile, summarizeSql, extractGoSymbol, extractRustSymbol, extractJavaSymbol, summarizeCsharp, extractCsharpSymbol, summarizeKotlin, extractKotlinSymbol, summarizePhp, extractPhpSymbol, summarizeSwift, extractSwiftSymbol } from './smart-read/additional-languages.js';
|
|
@@ -44,6 +47,8 @@ const MAX_CACHE_ENTRIES = 200;
|
|
|
44
47
|
const buildCacheKey = (fullPath, mode, extra) =>
|
|
45
48
|
extra ? `${fullPath}::${mode}::${extra}` : `${fullPath}::${mode}`;
|
|
46
49
|
|
|
50
|
+
const buildContentHash = (content) => createHash('sha256').update(content).digest('hex');
|
|
51
|
+
|
|
47
52
|
const getFileMtime = (fullPath) => Math.floor(fs.statSync(fullPath).mtimeMs);
|
|
48
53
|
|
|
49
54
|
const getCached = (key, mtime) => {
|
|
@@ -154,7 +159,55 @@ const resolveParserType = (extension, fullPath) => {
|
|
|
154
159
|
return 'fallback';
|
|
155
160
|
};
|
|
156
161
|
|
|
157
|
-
const
|
|
162
|
+
const MODE_BUDGET_CASCADE = {
|
|
163
|
+
full: ['signatures', 'outline'],
|
|
164
|
+
signatures: ['signatures', 'outline'],
|
|
165
|
+
outline: ['outline'],
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const getBudgetCascade = (mode) => MODE_BUDGET_CASCADE[mode] ?? [mode];
|
|
169
|
+
|
|
170
|
+
const buildFullModeMetadata = ({ requestedMode, effectiveMode, validBudget }) => {
|
|
171
|
+
if (requestedMode !== 'full') {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (effectiveMode === 'full') {
|
|
176
|
+
return {
|
|
177
|
+
requested: true,
|
|
178
|
+
used: true,
|
|
179
|
+
reason: 'explicit_request',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
requested: true,
|
|
185
|
+
used: false,
|
|
186
|
+
reason: validBudget ? 'degraded_for_budget' : 'not_used',
|
|
187
|
+
fallbackMode: effectiveMode,
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const buildBudgetDetails = ({ requestedMode, effectiveMode, validBudget, truncated }) => {
|
|
192
|
+
if (!validBudget) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const actions = [];
|
|
197
|
+
if (effectiveMode !== requestedMode) actions.push('mode_degraded');
|
|
198
|
+
if (truncated) actions.push('content_truncated');
|
|
199
|
+
|
|
200
|
+
if (actions.length === 0) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
scope: 'content',
|
|
206
|
+
maxTokens: validBudget,
|
|
207
|
+
finalMode: effectiveMode,
|
|
208
|
+
actions,
|
|
209
|
+
};
|
|
210
|
+
};
|
|
158
211
|
|
|
159
212
|
const generateContent = (fullPath, extension, content, mode) => {
|
|
160
213
|
if (mode === 'full') return truncate(content, 12000);
|
|
@@ -207,27 +260,52 @@ const truncateByTokens = (text, maxTokens) => {
|
|
|
207
260
|
tokens += lineTokens;
|
|
208
261
|
}
|
|
209
262
|
|
|
210
|
-
kept.
|
|
211
|
-
|
|
263
|
+
let result = `${kept.join('\n')}${marker}`;
|
|
264
|
+
while (kept.length > 0 && countTokens(result) > maxTokens) {
|
|
265
|
+
kept.pop();
|
|
266
|
+
result = `${kept.join('\n')}${marker}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return result;
|
|
212
270
|
};
|
|
213
271
|
|
|
214
|
-
const cachedGenerate = (fullPath, extension, content, mode, mtime) => {
|
|
272
|
+
const cachedGenerate = async (fullPath, extension, content, mode, mtime, root = projectRoot, selector = '') => {
|
|
215
273
|
const key = buildCacheKey(fullPath, mode);
|
|
216
274
|
const hit = getCached(key, mtime);
|
|
217
275
|
if (hit !== null) return { text: hit, cached: true };
|
|
276
|
+
|
|
277
|
+
const relPath = path.relative(root, fullPath).replace(/\\/g, '/');
|
|
278
|
+
const contentHash = buildContentHash(content);
|
|
279
|
+
const persistent = await getReadCache({ relPath, mode, selector, contentHash });
|
|
280
|
+
if (persistent?.payload?.text) {
|
|
281
|
+
setCache(key, mtime, persistent.payload.text);
|
|
282
|
+
return { text: persistent.payload.text, cached: true };
|
|
283
|
+
}
|
|
284
|
+
|
|
218
285
|
const text = generateContent(fullPath, extension, content, mode);
|
|
219
286
|
setCache(key, mtime, text);
|
|
287
|
+
await setReadCache({ relPath, mode, selector, contentHash, payload: { text }, tokens: countTokens(text) });
|
|
220
288
|
return { text, cached: false };
|
|
221
289
|
};
|
|
222
290
|
|
|
223
|
-
const cachedSymbol = (fullPath, content, symbol, mtime) => {
|
|
291
|
+
const cachedSymbol = async (fullPath, content, symbol, mtime, root = projectRoot) => {
|
|
224
292
|
const symbols = Array.isArray(symbol) ? symbol : [symbol];
|
|
225
293
|
const extra = symbols.join(',');
|
|
226
294
|
const key = buildCacheKey(fullPath, 'symbol', extra);
|
|
227
295
|
const hit = getCached(key, mtime);
|
|
228
296
|
if (hit !== null) return { text: hit.text, indexHint: hit.indexHint, cached: true };
|
|
297
|
+
|
|
298
|
+
const relPath = path.relative(root, fullPath).replace(/\\/g, '/');
|
|
299
|
+
const contentHash = buildContentHash(content);
|
|
300
|
+
const persistent = await getReadCache({ relPath, mode: 'symbol', selector: extra, contentHash });
|
|
301
|
+
if (persistent?.payload?.text) {
|
|
302
|
+
setCache(key, mtime, { text: persistent.payload.text, indexHint: persistent.payload.indexHint });
|
|
303
|
+
return { text: persistent.payload.text, indexHint: persistent.payload.indexHint, cached: true };
|
|
304
|
+
}
|
|
305
|
+
|
|
229
306
|
const result = generateSymbolContent(fullPath, content, symbol);
|
|
230
307
|
setCache(key, mtime, { text: result.text, indexHint: result.indexHint });
|
|
308
|
+
await setReadCache({ relPath, mode: 'symbol', selector: extra, contentHash, payload: result, tokens: countTokens(result.text) });
|
|
231
309
|
return { ...result, cached: false };
|
|
232
310
|
};
|
|
233
311
|
|
|
@@ -410,9 +488,11 @@ const formatContextSections = (sections) => {
|
|
|
410
488
|
return parts.length > 0 ? '\n' + parts.join('\n') : '';
|
|
411
489
|
};
|
|
412
490
|
|
|
413
|
-
export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine, symbol, maxTokens, context: includeContext, cwd, progress: enableProgress = false }) => {
|
|
491
|
+
export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine, symbol, maxTokens, tokenBudget, context: includeContext, cwd, progress: enableProgress = false }) => {
|
|
414
492
|
const progress = enableProgress ? createProgressReporter('smart_read') : null;
|
|
415
493
|
const startTime = Date.now();
|
|
494
|
+
const normalizedTokenBudget = normalizeTokenBudget(tokenBudget);
|
|
495
|
+
const budgetWindow = resolveTokenBudgetWindow({ tokenBudget: normalizedTokenBudget, maxTokens });
|
|
416
496
|
|
|
417
497
|
let fullPath, content;
|
|
418
498
|
const effectiveRoot = cwd || projectRoot;
|
|
@@ -448,11 +528,13 @@ export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine
|
|
|
448
528
|
const extension = path.extname(fullPath).toLowerCase();
|
|
449
529
|
const mtime = getFileMtime(fullPath);
|
|
450
530
|
|
|
451
|
-
const
|
|
531
|
+
const effectiveMaxTokens = budgetWindow.effectiveMaxTokens;
|
|
532
|
+
const validBudget = Number.isFinite(effectiveMaxTokens) && effectiveMaxTokens >= 1 ? effectiveMaxTokens : null;
|
|
452
533
|
let effectiveMode = mode;
|
|
453
534
|
let indexHintUsed = false;
|
|
454
535
|
let compressedText;
|
|
455
536
|
let cacheHit = false;
|
|
537
|
+
let fullModeMetadata = null;
|
|
456
538
|
|
|
457
539
|
if (mode === 'range') {
|
|
458
540
|
const r = cachedRange(content, startLine, endLine, fullPath, mtime);
|
|
@@ -463,15 +545,24 @@ export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine
|
|
|
463
545
|
const start = Math.max(0, (startLine ?? 1) - 1);
|
|
464
546
|
const end = endLine ?? lines.length;
|
|
465
547
|
const rangeContent = lines.slice(start, end).join('\n');
|
|
466
|
-
const g = cachedGenerate(fullPath, extension, rangeContent, 'outline', mtime);
|
|
548
|
+
const g = await cachedGenerate(fullPath, extension, rangeContent, 'outline', mtime, effectiveRoot, `${startLine ?? 1}-${endLine ?? ''}`);
|
|
467
549
|
compressedText = g.text;
|
|
468
550
|
cacheHit = g.cached;
|
|
469
551
|
effectiveMode = 'outline';
|
|
470
552
|
} else if (mode === 'symbol') {
|
|
471
|
-
const sym = cachedSymbol(fullPath, content, symbol, mtime);
|
|
553
|
+
const sym = await cachedSymbol(fullPath, content, symbol, mtime, effectiveRoot);
|
|
472
554
|
compressedText = sym.text;
|
|
473
555
|
indexHintUsed = sym.indexHint;
|
|
474
556
|
cacheHit = sym.cached;
|
|
557
|
+
if (validBudget && normalizedTokenBudget?.shared && countTokens(compressedText) > validBudget) {
|
|
558
|
+
for (const candidate of ['signatures', 'outline']) {
|
|
559
|
+
const g = await cachedGenerate(fullPath, extension, content, candidate, mtime, effectiveRoot);
|
|
560
|
+
compressedText = g.text;
|
|
561
|
+
if (g.cached) cacheHit = true;
|
|
562
|
+
effectiveMode = candidate;
|
|
563
|
+
if (countTokens(compressedText) <= validBudget) break;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
475
566
|
} else if (mode === 'explain') {
|
|
476
567
|
if (!symbol) {
|
|
477
568
|
compressedText = 'Error: symbol parameter is required for explain mode';
|
|
@@ -488,12 +579,20 @@ export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine
|
|
|
488
579
|
if (anyCached) cacheHit = true;
|
|
489
580
|
indexHintUsed = anyIndex;
|
|
490
581
|
}
|
|
582
|
+
if (validBudget && normalizedTokenBudget?.shared && countTokens(compressedText) > validBudget) {
|
|
583
|
+
for (const candidate of ['signatures', 'outline']) {
|
|
584
|
+
const g = await cachedGenerate(fullPath, extension, content, candidate, mtime, effectiveRoot);
|
|
585
|
+
compressedText = g.text;
|
|
586
|
+
if (g.cached) cacheHit = true;
|
|
587
|
+
effectiveMode = candidate;
|
|
588
|
+
if (countTokens(compressedText) <= validBudget) break;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
491
591
|
} else if (validBudget) {
|
|
492
|
-
const
|
|
493
|
-
const cascade = cascadeFrom >= 0 ? MODE_CASCADE.slice(cascadeFrom) : [effectiveMode];
|
|
592
|
+
const cascade = getBudgetCascade(effectiveMode);
|
|
494
593
|
|
|
495
594
|
for (const candidate of cascade) {
|
|
496
|
-
const g = cachedGenerate(fullPath, extension, content, candidate, mtime);
|
|
595
|
+
const g = await cachedGenerate(fullPath, extension, content, candidate, mtime, effectiveRoot);
|
|
497
596
|
compressedText = g.text;
|
|
498
597
|
if (g.cached) cacheHit = true;
|
|
499
598
|
effectiveMode = candidate;
|
|
@@ -504,11 +603,13 @@ export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine
|
|
|
504
603
|
compressedText = truncateByTokens(compressedText, validBudget);
|
|
505
604
|
}
|
|
506
605
|
} else {
|
|
507
|
-
const g = cachedGenerate(fullPath, extension, content, mode, mtime);
|
|
606
|
+
const g = await cachedGenerate(fullPath, extension, content, mode, mtime, effectiveRoot);
|
|
508
607
|
compressedText = g.text;
|
|
509
608
|
cacheHit = g.cached;
|
|
510
609
|
}
|
|
511
610
|
|
|
611
|
+
fullModeMetadata = buildFullModeMetadata({ requestedMode: mode, effectiveMode, validBudget });
|
|
612
|
+
|
|
512
613
|
if (progress) {
|
|
513
614
|
const compressedTokens = countTokens(compressedText);
|
|
514
615
|
const rawTokens = countTokens(content);
|
|
@@ -542,6 +643,7 @@ export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine
|
|
|
542
643
|
const rawMode = effectiveMode === 'full' || effectiveMode === 'range';
|
|
543
644
|
const parser = mode === 'explain' ? 'structural' : (rawMode ? 'raw' : resolveParserType(extension, fullPath));
|
|
544
645
|
const truncated = compressedText.includes('[truncated ');
|
|
646
|
+
const budgetDetails = buildBudgetDetails({ requestedMode: mode, effectiveMode, validBudget, truncated });
|
|
545
647
|
|
|
546
648
|
const metrics = buildMetrics({
|
|
547
649
|
tool: 'smart_read',
|
|
@@ -598,12 +700,23 @@ export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine
|
|
|
598
700
|
content: compressedText,
|
|
599
701
|
};
|
|
600
702
|
if (mode === 'symbol' || mode === 'explain') result.indexHint = indexHintUsed;
|
|
601
|
-
|
|
602
|
-
if (
|
|
703
|
+
result.cached = cacheHit;
|
|
704
|
+
if (normalizedTokenBudget) result.taskBudget = normalizedTokenBudget;
|
|
705
|
+
if (normalizedTokenBudget) {
|
|
706
|
+
result.remainingBudget = consumeTokenBudget({
|
|
707
|
+
tokenBudget: normalizedTokenBudget,
|
|
708
|
+
usedTokens: countTokens(compressedText),
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
if (effectiveMode !== mode) {
|
|
603
712
|
result.chosenMode = effectiveMode;
|
|
713
|
+
}
|
|
714
|
+
if (budgetDetails) {
|
|
604
715
|
result.budgetApplied = true;
|
|
716
|
+
result.budgetDetails = budgetDetails;
|
|
605
717
|
}
|
|
606
718
|
if (contextResult) Object.assign(result, contextResult);
|
|
719
|
+
if (fullModeMetadata) result.fullMode = fullModeMetadata;
|
|
607
720
|
|
|
608
721
|
return result;
|
|
609
722
|
};
|