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
|
@@ -10,6 +10,16 @@ import { CodebaseRepository } from '../infrastructure/codebase-repository.js';
|
|
|
10
10
|
import { DB_PATHS, PROJECT_ROOT } from '../infrastructure/config/index.js';
|
|
11
11
|
import { withPinnedRead } from './search-reader-pin.js';
|
|
12
12
|
import { emitToolIdentityAuto } from './cli-decoration.js';
|
|
13
|
+
import { resolveProjectRoot } from './server-identity.js';
|
|
14
|
+
import {
|
|
15
|
+
applyReadOmissionDecisions,
|
|
16
|
+
collectReadShownSpans,
|
|
17
|
+
exactRereadOmissionEnabled,
|
|
18
|
+
renderReadOmission,
|
|
19
|
+
resolveAgentSessionId,
|
|
20
|
+
} from './agent-span-ledger.js';
|
|
21
|
+
import { sendAgentSpanOperation } from './agent-span-client.js';
|
|
22
|
+
import { selectUnreadSymbols } from './unread-symbol-ranking.js';
|
|
13
23
|
|
|
14
24
|
const CACHE_MAX_ENTRIES = 64;
|
|
15
25
|
const CACHE_LARGE_FILE_BYTES = 4 * 1024 * 1024; // 4MB — switch to range-read mode
|
|
@@ -250,6 +260,7 @@ const SNIFF_MAX_LINES = 4000;
|
|
|
250
260
|
const UNREAD_SYMBOLS_MAX = 5; // hard cap on named symbols in the trailer
|
|
251
261
|
const UNREAD_SYMBOLS_MIN_LINES = 20; // smaller remainders get the short form
|
|
252
262
|
const C_FAMILY_EXTS = new Set(['.c', '.h', '.cc', '.cpp', '.cxx', '.hpp', '.hh', '.hxx', '.java', '.cs', '.m', '.mm']);
|
|
263
|
+
const _unreadSymbolCandidates = new WeakMap();
|
|
253
264
|
|
|
254
265
|
// Keyword-introduced definitions (Python/Ruby/JS/TS/Go/Rust/Kotlin/PHP/...).
|
|
255
266
|
const KEYWORD_DEF_RE = /^\s*(?:export\s+|default\s+|pub(?:\([^)]*\))?\s+|static\s+|async\s+|abstract\s+|final\s+|public\s+|private\s+|protected\s+|inline\s+|constexpr\s+|unsafe\s+|override\s+|open\s+|sealed\s+)*(?:def|fn|func|function\*?|class|struct|enum|trait|interface|impl|object|module|proc)\s+(?:\([^)]*\)\s*)?([A-Za-z_][\w]*(?:(?:::|\.)[A-Za-z_][\w]*)*)/;
|
|
@@ -374,6 +385,7 @@ async function _readFileUnpinned(req) {
|
|
|
374
385
|
symbols: symbols.slice(0, UNREAD_SYMBOLS_MAX),
|
|
375
386
|
moreCount: Math.max(0, symbols.length - UNREAD_SYMBOLS_MAX),
|
|
376
387
|
};
|
|
388
|
+
_unreadSymbolCandidates.set(unreadBelow, symbols);
|
|
377
389
|
}
|
|
378
390
|
|
|
379
391
|
// If a line range was requested, narrow attached chunks to the overlap.
|
|
@@ -454,24 +466,35 @@ export async function readFiles(files, opts = {}) {
|
|
|
454
466
|
* covered the whole file / reached EOF.
|
|
455
467
|
*
|
|
456
468
|
* @param {Object} result - readFile() result
|
|
457
|
-
* @param {{ command?: 'read'|'ss-read' }} [opts]
|
|
469
|
+
* @param {{ command?: 'read'|'ss-read', queryEvidence?: {anchors?: string[], subtokens?: string[]} }} [opts]
|
|
470
|
+
* continue-command surface plus agent-session query evidence
|
|
458
471
|
* @returns {string} one line without trailing newline, or ''
|
|
459
472
|
*/
|
|
460
|
-
export function renderUnreadBelow(result, { command = 'read' } = {}) {
|
|
473
|
+
export function renderUnreadBelow(result, { command = 'read', queryEvidence = null } = {}) {
|
|
461
474
|
const u = result?.unreadBelow;
|
|
462
475
|
if (!u) return '';
|
|
463
|
-
|
|
464
|
-
|
|
476
|
+
let symbols = u.symbols || [];
|
|
477
|
+
let moreCount = u.moreCount || 0;
|
|
478
|
+
if (queryEvidence) {
|
|
479
|
+
const candidates = _unreadSymbolCandidates.get(u) || u.symbols || [];
|
|
480
|
+
const selected = selectUnreadSymbols(candidates, queryEvidence, UNREAD_SYMBOLS_MAX);
|
|
481
|
+
symbols = selected.symbols;
|
|
482
|
+
moreCount = selected.moreCount;
|
|
483
|
+
}
|
|
484
|
+
const names = symbols.map(s => s.symbol).join(', ');
|
|
485
|
+
const more = moreCount > 0 ? ` +${moreCount} more` : '';
|
|
465
486
|
const cont = command === 'ss-read'
|
|
466
487
|
? `ss-read ${result.file} ${u.startLine} ${u.endLine}`
|
|
467
488
|
: `read ${result.file} ${u.startLine}-${u.endLine}`;
|
|
468
489
|
return `# unread below (${u.startLine}-${u.endLine})${names ? ': ' + names + more : ''} — continue: ${cont}`;
|
|
469
490
|
}
|
|
470
491
|
|
|
471
|
-
function _formatAgent(result) {
|
|
492
|
+
function _formatAgent(result, opts = {}) {
|
|
472
493
|
if (!result.ok) {
|
|
473
494
|
return `### ${result.file}\n[error] ${result.error}\n`;
|
|
474
495
|
}
|
|
496
|
+
const omitted = renderReadOmission(result, opts);
|
|
497
|
+
if (omitted) return `### ${result.file}\n${omitted}\n`;
|
|
475
498
|
const fence = result.language ? '```' + result.language : '```';
|
|
476
499
|
const range = result.range
|
|
477
500
|
? ` (lines ${result.range.startLine}-${result.range.endLine} of ${result.totalLines})`
|
|
@@ -483,18 +506,18 @@ function _formatAgent(result) {
|
|
|
483
506
|
.filter(Boolean);
|
|
484
507
|
if (names.length) symbolHint = `\nsymbols: ${names.join(', ')}`;
|
|
485
508
|
}
|
|
486
|
-
const remainder = renderUnreadBelow(result);
|
|
509
|
+
const remainder = renderUnreadBelow(result, opts);
|
|
487
510
|
return `### ${result.file}${range}${symbolHint}\n${fence}\n${result.text}\n\`\`\`${remainder ? '\n' + remainder : ''}\n`;
|
|
488
511
|
}
|
|
489
512
|
|
|
490
|
-
export function formatReadResults(results, format = 'agent') {
|
|
513
|
+
export function formatReadResults(results, format = 'agent', opts = {}) {
|
|
491
514
|
if (format === 'json') {
|
|
492
515
|
return JSON.stringify({ files: results.files, totalMs: results.totalMs }, null, 2);
|
|
493
516
|
}
|
|
494
517
|
if (format === 'raw') {
|
|
495
518
|
return results.files.map(r => r.ok ? r.text : `[error: ${r.file}] ${r.error}`).join('\n\n');
|
|
496
519
|
}
|
|
497
|
-
return results.files.map(_formatAgent).join('\n');
|
|
520
|
+
return results.files.map((result) => _formatAgent(result, opts)).join('\n');
|
|
498
521
|
}
|
|
499
522
|
|
|
500
523
|
// ---------------------------------------------------------------------------
|
|
@@ -519,6 +542,7 @@ function _parseArgs(args) {
|
|
|
519
542
|
let includeMetadata = true;
|
|
520
543
|
let plain = false;
|
|
521
544
|
let noBanner = false;
|
|
545
|
+
let force = false;
|
|
522
546
|
for (let i = 0; i < args.length; i++) {
|
|
523
547
|
const a = args[i];
|
|
524
548
|
if (a === '--json') format = 'json';
|
|
@@ -526,6 +550,7 @@ function _parseArgs(args) {
|
|
|
526
550
|
else if (a === '--agent') format = 'agent';
|
|
527
551
|
else if (a === '--no-metadata') includeMetadata = false;
|
|
528
552
|
else if (a === '--no-banner') noBanner = true;
|
|
553
|
+
else if (a === '--force') force = true;
|
|
529
554
|
else if (a === '--format' || a.startsWith('--format=')) {
|
|
530
555
|
const v = a === '--format' ? args[++i] : a.slice('--format='.length);
|
|
531
556
|
if (v === 'json' || v === 'raw' || v === 'agent') format = v;
|
|
@@ -543,7 +568,7 @@ function _parseArgs(args) {
|
|
|
543
568
|
positional.push(a);
|
|
544
569
|
}
|
|
545
570
|
}
|
|
546
|
-
return { positional, format, startLine, endLine, includeMetadata, plain, noBanner };
|
|
571
|
+
return { positional, format, startLine, endLine, includeMetadata, plain, noBanner, force };
|
|
547
572
|
}
|
|
548
573
|
|
|
549
574
|
function _printHelp() {
|
|
@@ -562,6 +587,7 @@ function _printHelp() {
|
|
|
562
587
|
' --format <fmt> json | raw | agent | plain (plain = no identity line)',
|
|
563
588
|
' --no-banner Suppress the identity line',
|
|
564
589
|
' --no-metadata Skip index metadata attachment',
|
|
590
|
+
' --force Retry the exact read named by an omission',
|
|
565
591
|
'',
|
|
566
592
|
].join('\n'));
|
|
567
593
|
}
|
|
@@ -585,11 +611,32 @@ export async function handleReadCli(args) {
|
|
|
585
611
|
endLine: wantsRange ? parsed.endLine : undefined,
|
|
586
612
|
}));
|
|
587
613
|
const out = await readFiles(files, { includeMetadata: parsed.includeMetadata });
|
|
614
|
+
let queryEvidence = null;
|
|
615
|
+
if (parsed.format === 'agent' && exactRereadOmissionEnabled()) {
|
|
616
|
+
const agentSessionId = resolveAgentSessionId();
|
|
617
|
+
const spans = collectReadShownSpans(out, { projectRoot: resolveProjectRoot() });
|
|
618
|
+
const response = await sendAgentSpanOperation({
|
|
619
|
+
operation: 'read',
|
|
620
|
+
sessionId: agentSessionId,
|
|
621
|
+
spans,
|
|
622
|
+
force: parsed.force,
|
|
623
|
+
});
|
|
624
|
+
if (response?.ok && Array.isArray(response.decisions)) {
|
|
625
|
+
const decisions = Array.from({ length: out.files.length }, () => ({ omit: false }));
|
|
626
|
+
spans.forEach((span, index) => { decisions[span.resultIndex] = response.decisions[index]; });
|
|
627
|
+
applyReadOmissionDecisions(out, decisions);
|
|
628
|
+
}
|
|
629
|
+
queryEvidence = response?.queryEvidence || null;
|
|
630
|
+
}
|
|
588
631
|
if (parsed.format !== 'json') {
|
|
589
632
|
const detail = files.length === 1 ? files[0].path : `${files.length} files`;
|
|
590
633
|
emitToolIdentityAuto('read', detail, { plain: parsed.plain, noBanner: parsed.noBanner });
|
|
591
634
|
}
|
|
592
|
-
process.stdout.write(formatReadResults(out, parsed.format
|
|
635
|
+
process.stdout.write(formatReadResults(out, parsed.format, {
|
|
636
|
+
surface: 'cli',
|
|
637
|
+
force: parsed.force,
|
|
638
|
+
queryEvidence,
|
|
639
|
+
}));
|
|
593
640
|
if (parsed.format !== 'json') process.stdout.write('\n');
|
|
594
641
|
// Non-zero exit if every file failed (so shell pipelines see the error).
|
|
595
642
|
const allFailed = out.files.length > 0 && out.files.every(f => !f.ok);
|