* Handle codegraph_explore — deep exploration in a single call * * Strategy: find relevant symbols via graph traversal, group by file, * then read contiguous file sections covering all symbols per file. * This replaces multiple codegraph_node + Read calls. * * Output size is adapti
(args: Record<string, unknown>)
| 3252 | * tax on small projects while earning its keep on large ones. |
| 3253 | */ |
| 3254 | private async handleExplore(args: Record<string, unknown>): Promise<ToolResult> { |
| 3255 | const rawQuery = this.validateString(args.query, 'query'); |
| 3256 | if (typeof rawQuery !== 'string') return rawQuery; |
| 3257 | // One normalization point so the flow-builder, relevance search, and |
| 3258 | // ranking all see the same canonical spelling (Erlang `mod:fn/arity`). |
| 3259 | const query = normalizeQuerySpelling(rawQuery); |
| 3260 | |
| 3261 | const cg = this.getCodeGraph(args.projectPath as string | undefined); |
| 3262 | const projectRoot = cg.getProjectRoot(); |
| 3263 | |
| 3264 | // Resolve adaptive output budget from project size. Falls back to the |
| 3265 | // largest-tier defaults if stats aren't available, which preserves |
| 3266 | // pre-#185 behavior for callers that hit the rare stats failure. |
| 3267 | let budget: ExploreOutputBudget; |
| 3268 | let indexedFileCount = -1; |
| 3269 | try { |
| 3270 | indexedFileCount = cg.getStats().fileCount; |
| 3271 | budget = getExploreOutputBudget(indexedFileCount); |
| 3272 | } catch { |
| 3273 | budget = getExploreOutputBudget(Infinity); |
| 3274 | } |
| 3275 | const maxFiles = clamp((args.maxFiles as number) || budget.defaultMaxFiles, 1, 20); |
| 3276 | |
| 3277 | // File paths named in the query become PINNED files: guaranteed admission, |
| 3278 | // top of the rank order, funded first — and their span is REMOVED from the |
| 3279 | // matching query. Runs on the RAW query (normalizeQuerySpelling strips |
| 3280 | // `/digits` tails, which would mangle numeric path segments). Without this, |
| 3281 | // a SvelteKit path like `runs/[runId]/+page.svelte` was shredded by the |
| 3282 | // seeding tokenizer (splits on brackets → `runId` seeded as a "named |
| 3283 | // symbol") and by FTS (`page`/`runs` fragments admitted every sibling |
| 3284 | // `+page.svelte`), starving the very files the agent asked for. |
| 3285 | let pinnedFiles: string[] = []; |
| 3286 | let unresolvedPathSpans: string[] = []; |
| 3287 | let matchQuery = query; |
| 3288 | if (queryMightContainPaths(rawQuery)) { |
| 3289 | try { |
| 3290 | const extraction = extractQueryPaths( |
| 3291 | rawQuery, |
| 3292 | cg.getFiles().map((f) => f.path), |
| 3293 | { maxPins: maxFiles }, |
| 3294 | ); |
| 3295 | if (extraction.pinnedFiles.length > 0 || extraction.unresolvedPathSpans.length > 0) { |
| 3296 | pinnedFiles = extraction.pinnedFiles; |
| 3297 | unresolvedPathSpans = extraction.unresolvedPathSpans; |
| 3298 | matchQuery = normalizeQuerySpelling(extraction.strippedQuery); |
| 3299 | } |
| 3300 | } catch { /* path pinning must never fail an explore call */ } |
| 3301 | } |
| 3302 | const pinnedSet = new Set(pinnedFiles); |
| 3303 | const pinnedOrder = new Map(pinnedFiles.map((p, i) => [p, i])); |
| 3304 | |
| 3305 | // Per-file allocation diagnostic (CG-4). `null` unless CODEGRAPH_EXPLORE_DEBUG |
| 3306 | // is set — every `diag?.` below is then a no-op and the response is |
| 3307 | // byte-identical. It only OBSERVES: it must never feed back into rendering. |
| 3308 | const diag = ExploreDiagnostics.start(query, projectRoot, budget, maxFiles, indexedFileCount); |
| 3309 | |
| 3310 | // What this session has already been served for THIS project (CG-17), and |
| 3311 | // whether this call may act on it (CG-18). Dedup is off on the session's |
no test coverage detected