sweet-search 2.6.16 → 2.7.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 +7 -3
- package/core/graph/structural-context-format.js +5 -0
- package/core/graph/structural-context.js +13 -3
- package/core/infrastructure/code-graph-repository.js +58 -0
- package/core/infrastructure/language-patterns/registry-core.js +14 -0
- package/core/infrastructure/model-fetcher.js +7 -0
- package/core/infrastructure/structural-context-repository.js +73 -7
- package/core/infrastructure/structural-context-utils.js +11 -0
- package/core/infrastructure/structural-qualified-resolution.js +29 -0
- package/core/infrastructure/tree-sitter-provider.js +77 -3
- package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt-mcp.md +12 -6
- package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt.md +4 -2
- package/core/search/agent-pack-completion.js +493 -0
- package/core/search/agent-span-client.js +87 -0
- package/core/search/agent-span-ledger.js +447 -0
- package/core/search/context-expander.js +29 -0
- package/core/search/grep-output-shaping.js +28 -0
- package/core/search/regex-dialect.js +328 -0
- package/core/search/search-format.js +96 -0
- package/core/search/search-pattern.js +62 -10
- package/core/search/search-read.js +57 -10
- package/core/search/search-server.js +375 -7
- package/core/search/session-daemon-prewarm.mjs +75 -0
- package/core/search/sweet-search.js +7 -1
- package/core/search/unread-symbol-ranking.js +90 -0
- package/eval/agent-read-workflows/bin/_ss-helpers.mjs +249 -45
- package/mcp/read-tool.js +34 -3
- package/mcp/server.js +55 -9
- package/mcp/tool-handlers.js +60 -7
- package/package.json +9 -9
- package/scripts/init.js +30 -5
- package/scripts/inject-agent-instructions.js +4 -2
- package/scripts/uninstall.js +21 -19
|
@@ -18,7 +18,24 @@ import {
|
|
|
18
18
|
buildGrepPattern, stripInertFlags, normalizeArgs, extractPositional,
|
|
19
19
|
parseLineRange, looksLikeOption, renderSufficiency,
|
|
20
20
|
} from './_ss-argparse.mjs';
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
reallocateGrepTailForManifest,
|
|
23
|
+
renderGrepBody,
|
|
24
|
+
} from '../../../core/search/grep-output-shaping.js';
|
|
25
|
+
import { formatRouteMetadata } from '../../../core/search/search-format.js';
|
|
26
|
+
import { renderRegexDialectHint } from '../../../core/search/regex-dialect.js';
|
|
27
|
+
import {
|
|
28
|
+
applyReadOmissionDecisions,
|
|
29
|
+
collectAgentShownSpans,
|
|
30
|
+
collectReadShownSpans,
|
|
31
|
+
collectSemanticShownSpans,
|
|
32
|
+
exactRereadOmissionEnabled,
|
|
33
|
+
renderReadOmission,
|
|
34
|
+
renderShownFullTrailer,
|
|
35
|
+
resolveAgentSessionId,
|
|
36
|
+
shownSpanTrailerEnabled,
|
|
37
|
+
} from '../../../core/search/agent-span-ledger.js';
|
|
38
|
+
import { sendAgentSpanOperation } from '../../../core/search/agent-span-client.js';
|
|
22
39
|
|
|
23
40
|
// Diagnostic-log isolation (agent-facing tools). The Sweet Search engine emits
|
|
24
41
|
// model/index load banners via console.log → stdout ("LateInteraction: Loaded…",
|
|
@@ -54,6 +71,29 @@ if (!existsSync(path.join(PROJECT_ROOT, '.sweet-search', 'codebase.db'))) {
|
|
|
54
71
|
}
|
|
55
72
|
process.env.SWEET_SEARCH_PROJECT_ROOT = PROJECT_ROOT;
|
|
56
73
|
|
|
74
|
+
const AGENT_SESSION_ID = resolveAgentSessionId();
|
|
75
|
+
const EXACT_REREAD_OMISSION = exactRereadOmissionEnabled();
|
|
76
|
+
const SHOWN_SPAN_TRAILER = shownSpanTrailerEnabled();
|
|
77
|
+
const SPAN_POLICY_ENABLED = EXACT_REREAD_OMISSION || SHOWN_SPAN_TRAILER;
|
|
78
|
+
|
|
79
|
+
async function recordAgentToolCall({
|
|
80
|
+
operation = 'observe',
|
|
81
|
+
spans = [],
|
|
82
|
+
force = false,
|
|
83
|
+
query,
|
|
84
|
+
regex,
|
|
85
|
+
} = {}) {
|
|
86
|
+
if (!EXACT_REREAD_OMISSION || !AGENT_SESSION_ID) return null;
|
|
87
|
+
return sendAgentSpanOperation({
|
|
88
|
+
operation,
|
|
89
|
+
spans,
|
|
90
|
+
force,
|
|
91
|
+
sessionId: AGENT_SESSION_ID,
|
|
92
|
+
query,
|
|
93
|
+
regex,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
57
97
|
const subcommand = process.argv[2];
|
|
58
98
|
const rest = process.argv.slice(3);
|
|
59
99
|
|
|
@@ -114,6 +154,26 @@ async function ensureWarmServerReady({ timeoutMs = 60000, intervalMs = 500 } = {
|
|
|
114
154
|
return false;
|
|
115
155
|
}
|
|
116
156
|
|
|
157
|
+
async function queryWarmSearch(query, options) {
|
|
158
|
+
if (!await ensureWarmServerReady({ timeoutMs: 5000 })) {
|
|
159
|
+
throw new Error('warm server is not ready');
|
|
160
|
+
}
|
|
161
|
+
const { queryServer } = await import(path.join(REPO_ROOT, 'core/search/search-server.js'));
|
|
162
|
+
const response = await queryServer(query, {
|
|
163
|
+
...options,
|
|
164
|
+
projectRoot: PROJECT_ROOT,
|
|
165
|
+
trackAgentSpans: false,
|
|
166
|
+
_isAgentFormat: options._isAgentFormat ?? true,
|
|
167
|
+
});
|
|
168
|
+
if (response?.error) throw new Error(response.error);
|
|
169
|
+
return response;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function writeRegexDialectHint(stats) {
|
|
173
|
+
const note = renderRegexDialectHint(stats?.regexDialectHint);
|
|
174
|
+
if (note) process.stdout.write(`${note}\n`);
|
|
175
|
+
}
|
|
176
|
+
|
|
117
177
|
// --- subcommands ----------------------------------------------------------
|
|
118
178
|
|
|
119
179
|
const GREP_USAGE = 'Usage: ss-grep <regex> [-i|--ignore-case] [-w|--word-regexp] [-F|--fixed-strings] [--in <file>] [-k N]';
|
|
@@ -132,14 +192,27 @@ async function cmdGrep(rawArgs) {
|
|
|
132
192
|
process.stderr.write(GREP_USAGE + '\n');
|
|
133
193
|
process.exit(2);
|
|
134
194
|
}
|
|
135
|
-
const s = await getSweetSearch();
|
|
136
|
-
|
|
137
195
|
if (inFile) {
|
|
138
196
|
// Single-file scope: flat output, depth up to k within that file.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
197
|
+
let result;
|
|
198
|
+
try {
|
|
199
|
+
result = await queryWarmSearch(regex, {
|
|
200
|
+
mode: 'grep', regex, maxMatches: k, contextLines: 0,
|
|
201
|
+
fileFilter: inFile, expand: false, rerank: false, useLateInteraction: false,
|
|
202
|
+
_isAgentFormat: !fixedString,
|
|
203
|
+
});
|
|
204
|
+
} catch {
|
|
205
|
+
const s = await getSweetSearch();
|
|
206
|
+
result = await s.bareGrep(regex, null, {
|
|
207
|
+
regex, maxMatches: k, contextLines: 0, fileFilter: inFile,
|
|
208
|
+
_isAgentFormat: !fixedString,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
142
211
|
const total = result.stats?.totalMatches ?? result.results.length;
|
|
212
|
+
await recordAgentToolCall({
|
|
213
|
+
query: fixedString ? undefined : regex,
|
|
214
|
+
regex: fixedString ? undefined : regex,
|
|
215
|
+
});
|
|
143
216
|
process.stdout.write(`# ss-grep: ${total} total match(es) for /${regex}/ (scope: --in ${inFile})\n`);
|
|
144
217
|
result.results.forEach((r, i) => {
|
|
145
218
|
const text = (r.matchText || '').replace(/\s+/g, ' ').trim().slice(0, 140);
|
|
@@ -148,6 +221,7 @@ async function cmdGrep(rawArgs) {
|
|
|
148
221
|
process.stdout.write(`${r.file}:${r.line}: ${text}${marker}\n`);
|
|
149
222
|
});
|
|
150
223
|
if (result.results.length === 0) process.stdout.write('(no matches)\n');
|
|
224
|
+
writeRegexDialectHint(result.stats);
|
|
151
225
|
process.exit(0);
|
|
152
226
|
}
|
|
153
227
|
|
|
@@ -157,23 +231,47 @@ async function cmdGrep(rawArgs) {
|
|
|
157
231
|
// flooded file can never hide every other matching file (the gradethis-161
|
|
158
232
|
// failure). Rendering stays grouped per file; truncation is marked inline
|
|
159
233
|
// and drillable via --in.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
234
|
+
let result;
|
|
235
|
+
try {
|
|
236
|
+
result = await queryWarmSearch(regex, {
|
|
237
|
+
mode: 'grep', regex, maxMatches: 0, contextLines: 0,
|
|
238
|
+
perFileCap: Math.min(k, 100), maxFiles: k,
|
|
239
|
+
expand: false, rerank: false, useLateInteraction: false,
|
|
240
|
+
_isAgentFormat: !fixedString,
|
|
241
|
+
});
|
|
242
|
+
} catch {
|
|
243
|
+
const s = await getSweetSearch();
|
|
244
|
+
result = await s.bareGrep(regex, null, {
|
|
245
|
+
regex, maxMatches: 0, contextLines: 0,
|
|
246
|
+
perFileCap: Math.min(k, 100), maxFiles: k,
|
|
247
|
+
_isAgentFormat: !fixedString,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
164
250
|
const total = result.stats?.totalMatches ?? result.results.length;
|
|
165
251
|
const fileSummary = result.fileSummary
|
|
166
252
|
|| { files: [], hiddenFileCount: 0, hiddenMatchCount: 0, hiddenSample: [] };
|
|
167
253
|
const body = renderGrepBody(result.results, fileSummary, k);
|
|
254
|
+
const completed = reallocateGrepTailForManifest(body.lines, result.familyManifest);
|
|
255
|
+
await recordAgentToolCall({
|
|
256
|
+
query: fixedString ? undefined : regex,
|
|
257
|
+
regex: fixedString ? undefined : regex,
|
|
258
|
+
});
|
|
168
259
|
|
|
169
|
-
|
|
260
|
+
// Sibling-surface signal (E6, 2026-07-08 trace audit): when a symbol/stem
|
|
261
|
+
// matches in more than one file, say so unconditionally in the header —
|
|
262
|
+
// the file count is the objective "visible siblings" trigger for
|
|
263
|
+
// fix-surface mapping, and it must not depend on truncation having occurred.
|
|
264
|
+
const across = body.matchedFileCount > 1 ? ` across ${body.matchedFileCount} files` : '';
|
|
265
|
+
process.stdout.write(`# ss-grep: ${total} total match(es) for /${regex}/${across}\n`);
|
|
170
266
|
if (body.truncatedFileCount > 0 || body.hiddenLine) {
|
|
171
|
-
process.stdout.write(`#
|
|
267
|
+
process.stdout.write(`# (+N more in this file)=truncated — ` +
|
|
172
268
|
`see the rest: ss-grep "<regex>" --in <file>\n`);
|
|
173
269
|
}
|
|
174
|
-
for (const line of
|
|
270
|
+
for (const line of completed.lines) process.stdout.write(line + '\n');
|
|
271
|
+
if (completed.familyManifest) process.stdout.write(`${completed.familyManifest.rendered}\n`);
|
|
175
272
|
if (body.hiddenLine) process.stdout.write(body.hiddenLine + '\n');
|
|
176
273
|
if (body.shownMatches === 0) process.stdout.write('(no matches)\n');
|
|
274
|
+
writeRegexDialectHint(result.stats);
|
|
177
275
|
process.exit(0);
|
|
178
276
|
}
|
|
179
277
|
|
|
@@ -205,16 +303,33 @@ async function cmdFind(rawArgs) {
|
|
|
205
303
|
const envFindBudget = Number(process.env.SS_SMOKE_FIND_BUDGET || '') || null;
|
|
206
304
|
// Pattern flags apply to the regex candidate generator; the NL query is untouched.
|
|
207
305
|
const effectiveRegex = buildGrepPattern(regex || '', { ignoreCase, wordBound, fixedString });
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
306
|
+
let response;
|
|
307
|
+
try {
|
|
308
|
+
response = await queryWarmSearch(query, {
|
|
309
|
+
mode: 'pattern', regex: effectiveRegex || `\\b\\w+\\b`, topK: k, format,
|
|
310
|
+
_isAgentFormat: !fixedString,
|
|
311
|
+
...(envFindBudget ? { tokenBudget: envFindBudget } : {}),
|
|
312
|
+
});
|
|
313
|
+
} catch {
|
|
314
|
+
const s = await getSweetSearch();
|
|
315
|
+
if (!s.hasLateInteractionIndex) {
|
|
316
|
+
process.stderr.write(`[ss-find] no late-interaction index — falling back to ss-grep\n`);
|
|
317
|
+
return cmdGrep([effectiveRegex || query, '-k', String(k)]);
|
|
318
|
+
}
|
|
319
|
+
response = await s.patternSearch(query, null, {
|
|
320
|
+
regex: effectiveRegex || `\\b\\w+\\b`,
|
|
321
|
+
k,
|
|
322
|
+
format,
|
|
323
|
+
_isAgentFormat: !fixedString,
|
|
324
|
+
...(envFindBudget ? { tokenBudget: envFindBudget } : {}),
|
|
325
|
+
});
|
|
212
326
|
}
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
327
|
+
const shownSpans = SPAN_POLICY_ENABLED
|
|
328
|
+
? collectAgentShownSpans(response.results, { projectRoot: PROJECT_ROOT }) : [];
|
|
329
|
+
await recordAgentToolCall({
|
|
330
|
+
spans: shownSpans,
|
|
331
|
+
query: fixedString ? undefined : query,
|
|
332
|
+
regex: fixedString ? undefined : effectiveRegex,
|
|
218
333
|
});
|
|
219
334
|
|
|
220
335
|
// Header (visible to agent)
|
|
@@ -247,12 +362,22 @@ async function cmdFind(rawArgs) {
|
|
|
247
362
|
if (r.sameFile && r.sameFile.rendered) {
|
|
248
363
|
process.stdout.write(`${r.sameFile.rendered}\n`);
|
|
249
364
|
}
|
|
365
|
+
if (r.continuation?.rendered) {
|
|
366
|
+
process.stdout.write(`${r.continuation.rendered}\n`);
|
|
367
|
+
if (r.continuation.kind === 'symbol' && r.continuation.code) {
|
|
368
|
+
process.stdout.write(`\`\`\`\n${r.continuation.code}\n\`\`\`\n`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (r.familyManifest?.rendered) process.stdout.write(`${r.familyManifest.rendered}\n`);
|
|
250
372
|
}
|
|
251
373
|
if (!response.results || response.results.length === 0) process.stdout.write('(no matches)\n');
|
|
374
|
+
writeRegexDialectHint(response.stats);
|
|
375
|
+
const shownTrailer = SHOWN_SPAN_TRAILER ? renderShownFullTrailer(shownSpans) : '';
|
|
376
|
+
if (shownTrailer) process.stdout.write(`\n${shownTrailer}\n`);
|
|
252
377
|
process.exit(0);
|
|
253
378
|
}
|
|
254
379
|
|
|
255
|
-
// ss-read takes
|
|
380
|
+
// ss-read takes one recovery flag plus positional <file> [start] [end] (or a single
|
|
256
381
|
// "start-end" / "start:end" / "start,end" range token). Unlike ss-grep, a stray
|
|
257
382
|
// flag here can never silently corrupt the result: the line slots are validated
|
|
258
383
|
// as numbers, so a misuse is already a loud error. These hints exist only to
|
|
@@ -263,8 +388,10 @@ const READ_USAGE =
|
|
|
263
388
|
' ss-read <file> <start> # ONE line\n' +
|
|
264
389
|
' ss-read <file> <start> <end>\n' +
|
|
265
390
|
' ss-read <file> 10-20 # range (also 10:20, 10,20)\n' +
|
|
266
|
-
'
|
|
267
|
-
async function cmdRead(
|
|
391
|
+
'Option: --force shows content again after an unchanged-content omission.';
|
|
392
|
+
async function cmdRead(rawArgs) {
|
|
393
|
+
const args = [...rawArgs];
|
|
394
|
+
const force = parseBoolFlag(args, ['--force']);
|
|
268
395
|
const file = args[0];
|
|
269
396
|
if (!file) {
|
|
270
397
|
process.stderr.write(READ_USAGE + '\n');
|
|
@@ -302,19 +429,59 @@ async function cmdRead(args) {
|
|
|
302
429
|
}
|
|
303
430
|
}
|
|
304
431
|
}
|
|
432
|
+
// L4a (2026-07-09) — default read window. An unbounded whole-file read (no range
|
|
433
|
+
// given) delivers the ENTIRE file, which then sits RESIDENT in the agent's context
|
|
434
|
+
// and is re-sent every subsequent turn. P1 measured this read-mass resident tax at
|
|
435
|
+
// ~$11/200 (whole-file/large reads are the tail: p90=180, max=900 lines). So when
|
|
436
|
+
// the agent gives NO range, cap the default to READ_WINDOW lines and let the
|
|
437
|
+
// existing "what remains" trailer advertise the exact continue command — the agent
|
|
438
|
+
// widens on demand instead of paying for the whole file up front. Only the no-range
|
|
439
|
+
// case is capped: an EXPLICIT range is the agent's deliberate choice and capping it
|
|
440
|
+
// would cause widen-thrash (the RETUNE hazard). GCSN-neutral by construction (reads
|
|
441
|
+
// are never ranked). Gated for the standing ON-vs-OFF A/B: SS_NO_READ_WINDOW=1 → OFF
|
|
442
|
+
// (legacy whole-file); SS_READ_WINDOW=<n> retunes the tier. Prod/human ss-read is a
|
|
443
|
+
// separate wrapper and is untouched (byte-identical).
|
|
444
|
+
// PARKED default-OFF (2026-07-09 smoke): the mechanism works (−39% delivered read
|
|
445
|
+
// tokens vs off) and is accuracy-safe (no resolved→unresolved flips), but the ~$11/200
|
|
446
|
+
// pool is too small to show a net idealCost win at n=2 and one read-thrash instance
|
|
447
|
+
// appeared at window=150. Opt-in via SS_READ_WINDOW=<n> pending a larger-n confirmation.
|
|
448
|
+
const READ_WINDOW = Number(process.env.SS_READ_WINDOW) || 0;
|
|
449
|
+
const cappedDefault = (start === null && end === null && READ_WINDOW > 0);
|
|
450
|
+
if (cappedDefault) { start = 1; end = READ_WINDOW; }
|
|
451
|
+
|
|
305
452
|
const { readFile, renderUnreadBelow } = await import(path.join(REPO_ROOT, 'core/search/search-read.js'));
|
|
306
453
|
const r = await readFile({ path: file, projectRoot: PROJECT_ROOT, startLine: start ?? undefined, endLine: end ?? undefined });
|
|
307
454
|
if (!r.ok) {
|
|
308
455
|
process.stderr.write(`[ss-read] error: ${r.error}\n`);
|
|
309
456
|
process.exit(1);
|
|
310
457
|
}
|
|
311
|
-
const
|
|
458
|
+
const readBatch = { files: [r], totalMs: r.timings?.totalMs ?? 0 };
|
|
459
|
+
const shownSpans = EXACT_REREAD_OMISSION
|
|
460
|
+
? collectReadShownSpans(readBatch, { projectRoot: PROJECT_ROOT }) : [];
|
|
461
|
+
const receiptResponse = await recordAgentToolCall({
|
|
462
|
+
operation: 'read',
|
|
463
|
+
spans: shownSpans,
|
|
464
|
+
force,
|
|
465
|
+
});
|
|
466
|
+
if (receiptResponse?.ok && Array.isArray(receiptResponse.decisions)) {
|
|
467
|
+
applyReadOmissionDecisions(readBatch, receiptResponse.decisions);
|
|
468
|
+
}
|
|
469
|
+
// If the window happened to cover the whole file (file ≤ READ_WINDOW, clamped by
|
|
470
|
+
// readFile), present it EXACTLY like an uncapped whole-file read — no synthetic
|
|
471
|
+
// range, no continue trailer — so small-file reads stay byte-identical to legacy.
|
|
472
|
+
const coveredWholeFile = (start === null && end === null) || (cappedDefault && r.range && r.range.endLine >= r.totalLines);
|
|
473
|
+
const range = (r.range && !coveredWholeFile) ? ` (lines ${r.range.startLine}-${r.range.endLine} of ${r.totalLines})` : ` (${r.totalLines} lines)`;
|
|
312
474
|
const fence = r.language ? '```' + r.language : '```';
|
|
313
475
|
// "What remains" trailer: on a range read that stops before EOF, one final
|
|
314
476
|
// line names the symbols in the unread remainder + the exact continue
|
|
315
477
|
// command (last line for recency — the actionable form of truncation).
|
|
316
|
-
const remainder = renderUnreadBelow(r, {
|
|
317
|
-
|
|
478
|
+
const remainder = coveredWholeFile ? '' : renderUnreadBelow(r, {
|
|
479
|
+
command: 'ss-read',
|
|
480
|
+
queryEvidence: receiptResponse?.queryEvidence,
|
|
481
|
+
});
|
|
482
|
+
const omitted = renderReadOmission(r, { surface: 'ss-read' });
|
|
483
|
+
if (omitted) process.stdout.write(`# ss-read ${r.file}${range}\n${omitted}\n`);
|
|
484
|
+
else process.stdout.write(`# ss-read ${r.file}${range}\n${fence}\n${r.text}\n\`\`\`${remainder ? '\n' + remainder : ''}\n`);
|
|
318
485
|
process.exit(0);
|
|
319
486
|
}
|
|
320
487
|
|
|
@@ -331,9 +498,9 @@ async function cmdAgentSearch(rawArgs) {
|
|
|
331
498
|
// ss-search "<query>" --mode hybrid → force a mode (default: auto/CatBoost)
|
|
332
499
|
//
|
|
333
500
|
// Output is agent-readable: a meta header with routed mode + budget,
|
|
334
|
-
// followed by per-result blocks with file/line + fenced code
|
|
335
|
-
//
|
|
336
|
-
//
|
|
501
|
+
// followed by per-result blocks with file/line + fenced code and one compact
|
|
502
|
+
// actionable route trailer. SWEET_SEARCH_ROUTE_META_DEBUG=1 restores the
|
|
503
|
+
// complete JSON trailer for routing diagnostics.
|
|
337
504
|
let format = 'agent';
|
|
338
505
|
if (args.includes('--full')) { format = 'agent_full'; args.splice(args.indexOf('--full'), 1); }
|
|
339
506
|
if (args.includes('--xl')) { format = 'agent_full_xl'; args.splice(args.indexOf('--xl'), 1); }
|
|
@@ -345,7 +512,6 @@ async function cmdAgentSearch(rawArgs) {
|
|
|
345
512
|
process.exit(2);
|
|
346
513
|
}
|
|
347
514
|
|
|
348
|
-
const { queryServer } = await import(path.join(REPO_ROOT, 'core/search/search-server.js'));
|
|
349
515
|
const serverUsed = await ensureWarmServerReady();
|
|
350
516
|
if (!serverUsed) {
|
|
351
517
|
process.stderr.write('[ss-search] warm server is not ready; refusing cold direct search in benchmark wrapper\n');
|
|
@@ -355,7 +521,11 @@ async function cmdAgentSearch(rawArgs) {
|
|
|
355
521
|
// Budget-sweep experiment hook: per-request explicit budget (overrides the
|
|
356
522
|
// auto-tier on the warm server; flows as the `budget` URL param).
|
|
357
523
|
const envSearchBudget = Number(process.env.SS_SMOKE_SEARCH_BUDGET || '') || null;
|
|
358
|
-
const
|
|
524
|
+
const { queryServer } = await import(path.join(REPO_ROOT, 'core/search/search-server.js'));
|
|
525
|
+
const response = await queryServer(query, {
|
|
526
|
+
topK: k, mode, format, projectRoot: PROJECT_ROOT, trackAgentSpans: false,
|
|
527
|
+
...(envSearchBudget ? { tokenBudget: envSearchBudget } : {}),
|
|
528
|
+
});
|
|
359
529
|
if (response?.error) {
|
|
360
530
|
process.stderr.write(`[ss-search] server error: ${response.error}\n`);
|
|
361
531
|
process.exit(1);
|
|
@@ -375,7 +545,7 @@ async function cmdAgentSearch(rawArgs) {
|
|
|
375
545
|
`but server reports serverProjectRoot=${serverProjectRoot ?? '<null>'}. ` +
|
|
376
546
|
`Refusing to surface cross-repo results.\n`
|
|
377
547
|
);
|
|
378
|
-
// Emit a
|
|
548
|
+
// Emit a route trailer so the mismatch remains explicit in agent output.
|
|
379
549
|
const failMeta = {
|
|
380
550
|
query,
|
|
381
551
|
queryHash: shortQueryHash(query),
|
|
@@ -392,9 +562,15 @@ async function cmdAgentSearch(rawArgs) {
|
|
|
392
562
|
repoMatches: false,
|
|
393
563
|
error: 'repo-isolation-mismatch',
|
|
394
564
|
};
|
|
395
|
-
process.stdout.write(`\n
|
|
565
|
+
process.stdout.write(`\n${formatRouteMetadata(failMeta, {
|
|
566
|
+
_isAgentFormat: true,
|
|
567
|
+
debug: process.env.SWEET_SEARCH_ROUTE_META_DEBUG === '1',
|
|
568
|
+
})}\n`);
|
|
396
569
|
process.exit(3);
|
|
397
570
|
}
|
|
571
|
+
const shownSpans = SPAN_POLICY_ENABLED
|
|
572
|
+
? collectAgentShownSpans(response.results, { projectRoot: PROJECT_ROOT }) : [];
|
|
573
|
+
await recordAgentToolCall({ spans: shownSpans, query });
|
|
398
574
|
|
|
399
575
|
// The packaged response shape comes from packageForAgent (or pattern's own
|
|
400
576
|
// packager when CatBoost routes to pattern). Both include:
|
|
@@ -455,16 +631,23 @@ async function cmdAgentSearch(rawArgs) {
|
|
|
455
631
|
if (r.sameFile && r.sameFile.rendered) {
|
|
456
632
|
process.stdout.write(`${r.sameFile.rendered}\n`);
|
|
457
633
|
}
|
|
634
|
+
if (r.continuation?.rendered) {
|
|
635
|
+
process.stdout.write(`${r.continuation.rendered}\n`);
|
|
636
|
+
if (r.continuation.kind === 'symbol' && r.continuation.code) {
|
|
637
|
+
process.stdout.write(`\`\`\`\n${r.continuation.code}\n\`\`\`\n`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (r.familyManifest?.rendered) process.stdout.write(`${r.familyManifest.rendered}\n`);
|
|
458
641
|
}
|
|
459
642
|
|
|
460
643
|
if (!response.results || response.results.length === 0) {
|
|
461
644
|
process.stdout.write('(no matches)\n');
|
|
462
645
|
}
|
|
646
|
+
const shownTrailer = SHOWN_SPAN_TRAILER ? renderShownFullTrailer(shownSpans) : '';
|
|
647
|
+
if (shownTrailer) process.stdout.write(`\n${shownTrailer}\n`);
|
|
463
648
|
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
// let downstream analysis link a routing decision to its query and
|
|
467
|
-
// attribute failures to fast-path vs WASM vs fallback.
|
|
649
|
+
// Keep complete metadata available to the debug serializer, while normal
|
|
650
|
+
// agent output receives only the fields that can change its next action.
|
|
468
651
|
const meta = {
|
|
469
652
|
query, // exact query text (already bounded by SEARCH_SERVER_MAX_QUERY_LENGTH)
|
|
470
653
|
queryHash: shortQueryHash(query),
|
|
@@ -496,7 +679,10 @@ async function cmdAgentSearch(rawArgs) {
|
|
|
496
679
|
sameFileMapTokens: response.results?.[0]?.sameFile?.tokens ?? null,
|
|
497
680
|
sameFileNeighborCount: response.results?.[0]?.sameFile?.neighbors?.length ?? null,
|
|
498
681
|
};
|
|
499
|
-
process.stdout.write(`\n
|
|
682
|
+
process.stdout.write(`\n${formatRouteMetadata(meta, {
|
|
683
|
+
_isAgentFormat: true,
|
|
684
|
+
debug: process.env.SWEET_SEARCH_ROUTE_META_DEBUG === '1',
|
|
685
|
+
})}\n`);
|
|
500
686
|
process.exit(0);
|
|
501
687
|
}
|
|
502
688
|
|
|
@@ -515,21 +701,36 @@ async function cmdSemantic(rawArgs) {
|
|
|
515
701
|
process.stderr.write(SEMANTIC_USAGE + '\n');
|
|
516
702
|
process.exit(2);
|
|
517
703
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
704
|
+
let r;
|
|
705
|
+
try {
|
|
706
|
+
if (!await ensureWarmServerReady({ timeoutMs: 5000 })) throw new Error('warm server is not ready');
|
|
707
|
+
const { queryReadSemanticServer } = await import(path.join(REPO_ROOT, 'core/search/search-server.js'));
|
|
708
|
+
r = await queryReadSemanticServer({
|
|
709
|
+
path: file, query, projectRoot: PROJECT_ROOT, maxChars: maxTokens * 4,
|
|
710
|
+
});
|
|
711
|
+
if (r?.error) throw new Error(r.error);
|
|
712
|
+
} catch {
|
|
713
|
+
const { readSemantic } = await import(path.join(REPO_ROOT, 'core/search/search-read-semantic.js'));
|
|
714
|
+
r = await readSemantic({
|
|
715
|
+
path: file, query, projectRoot: PROJECT_ROOT,
|
|
716
|
+
maxChars: maxTokens * 4, verbose: false,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
523
719
|
if (!r.ok) {
|
|
524
720
|
process.stderr.write(`[ss-semantic] error: ${r.reason || 'unknown'}\n`);
|
|
525
721
|
process.exit(1);
|
|
526
722
|
}
|
|
723
|
+
const shownSpans = SPAN_POLICY_ENABLED
|
|
724
|
+
? collectSemanticShownSpans(r, { projectRoot: PROJECT_ROOT }) : [];
|
|
725
|
+
await recordAgentToolCall({ spans: shownSpans, query });
|
|
527
726
|
process.stdout.write(`# ss-semantic ${r.file} | "${query}" | spans=${r.spans?.length ?? 0} | ~tokens=${r.approxTokensReturned}${r.fellBack ? ' [FALLBACK]' : ''}\n`);
|
|
528
727
|
for (const span of r.spans || []) {
|
|
529
728
|
const fence = r.language ? '```' + r.language : '```';
|
|
530
729
|
const sym = span.symbols?.length ? ` [${span.symbols.join(', ')}]` : '';
|
|
531
730
|
process.stdout.write(`### ${r.file}:${span.startLine}-${span.endLine}${sym}\n${fence}\n${span.text}\n\`\`\`\n`);
|
|
532
731
|
}
|
|
732
|
+
const shownTrailer = SHOWN_SPAN_TRAILER ? renderShownFullTrailer(shownSpans) : '';
|
|
733
|
+
if (shownTrailer) process.stdout.write(`${shownTrailer}\n`);
|
|
533
734
|
process.exit(0);
|
|
534
735
|
}
|
|
535
736
|
|
|
@@ -561,6 +762,9 @@ async function cmdTrace(rawArgs) {
|
|
|
561
762
|
else if (Number(process.env.SS_SMOKE_TRACE_BUDGET || '') > 0) opts.tokenBudget = Number(process.env.SS_SMOKE_TRACE_BUDGET);
|
|
562
763
|
|
|
563
764
|
const response = traceSymbol(symbol, opts);
|
|
765
|
+
await recordAgentToolCall({
|
|
766
|
+
query: json ? undefined : `${symbol} ${queryHint}`.trim(),
|
|
767
|
+
});
|
|
564
768
|
if (json) process.stdout.write(JSON.stringify(response, null, 2) + '\n');
|
|
565
769
|
else process.stdout.write(formatStructuralContext(response) + '\n');
|
|
566
770
|
|
package/mcp/read-tool.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import {
|
|
3
|
+
applyReadOmissionDecisions,
|
|
4
|
+
collectReadShownSpans,
|
|
5
|
+
exactRereadOmissionEnabled,
|
|
6
|
+
} from '../core/search/agent-span-ledger.js';
|
|
7
|
+
import { sendAgentSpanOperation } from '../core/search/agent-span-client.js';
|
|
2
8
|
|
|
3
9
|
const ReadFileResultSchema = z.object({
|
|
4
10
|
file: z.string(),
|
|
@@ -33,6 +39,12 @@ const ReadFileResultSchema = z.object({
|
|
|
33
39
|
})),
|
|
34
40
|
moreCount: z.number().int(),
|
|
35
41
|
}).nullable().optional(),
|
|
42
|
+
omitted: z.object({
|
|
43
|
+
file: z.string(),
|
|
44
|
+
startLine: z.number().int(),
|
|
45
|
+
endLine: z.number().int(),
|
|
46
|
+
callsAgo: z.number().int(),
|
|
47
|
+
}).optional(),
|
|
36
48
|
error: z.string().optional(),
|
|
37
49
|
timings: z.object({ totalMs: z.number() }).optional(),
|
|
38
50
|
});
|
|
@@ -70,8 +82,8 @@ export const ReadSemanticOutputSchema = z.object({
|
|
|
70
82
|
});
|
|
71
83
|
|
|
72
84
|
/**
|
|
73
|
-
* @param {{ files: Array<{path: string, startLine?: number, endLine?: number}>, includeMetadata?: boolean }} args
|
|
74
|
-
* @param {{ PROJECT_ROOT: string }} deps
|
|
85
|
+
* @param {{ files: Array<{path: string, startLine?: number, endLine?: number}>, includeMetadata?: boolean, force?: boolean }} args
|
|
86
|
+
* @param {{ PROJECT_ROOT: string, agentSessionId?: string }} deps
|
|
75
87
|
*/
|
|
76
88
|
export async function handleRead(args, deps) {
|
|
77
89
|
try {
|
|
@@ -80,8 +92,27 @@ export async function handleRead(args, deps) {
|
|
|
80
92
|
projectRoot: deps.PROJECT_ROOT,
|
|
81
93
|
includeMetadata: args.includeMetadata !== false,
|
|
82
94
|
});
|
|
95
|
+
let queryEvidence = null;
|
|
96
|
+
if (exactRereadOmissionEnabled() && deps.agentSessionId) {
|
|
97
|
+
const spans = collectReadShownSpans(result, { projectRoot: deps.PROJECT_ROOT });
|
|
98
|
+
const response = await sendAgentSpanOperation({
|
|
99
|
+
operation: 'read',
|
|
100
|
+
sessionId: deps.agentSessionId,
|
|
101
|
+
spans,
|
|
102
|
+
force: args.force === true,
|
|
103
|
+
});
|
|
104
|
+
if (response?.ok && Array.isArray(response.decisions)) {
|
|
105
|
+
const decisions = Array.from({ length: result.files.length }, () => ({ omit: false }));
|
|
106
|
+
spans.forEach((span, index) => { decisions[span.resultIndex] = response.decisions[index]; });
|
|
107
|
+
applyReadOmissionDecisions(result, decisions);
|
|
108
|
+
}
|
|
109
|
+
queryEvidence = response?.queryEvidence || null;
|
|
110
|
+
}
|
|
83
111
|
return {
|
|
84
|
-
content: [{
|
|
112
|
+
content: [{
|
|
113
|
+
type: 'text',
|
|
114
|
+
text: formatReadResults(result, 'agent', { surface: 'mcp', queryEvidence }),
|
|
115
|
+
}],
|
|
85
116
|
structuredContent: result,
|
|
86
117
|
};
|
|
87
118
|
} catch (err) {
|