* 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>)
| 4062 | * tax on small projects while earning its keep on large ones. |
| 4063 | */ |
| 4064 | private async handleExplore(args: Record<string, unknown>): Promise<ToolResult> { |
| 4065 | const rawQuery = this.validateString(args.query, 'query'); |
| 4066 | if (typeof rawQuery !== 'string') return rawQuery; |
| 4067 | // One normalization point so the flow-builder, relevance search, and |
| 4068 | // ranking all see the same canonical spelling (Erlang `mod:fn/arity`). |
| 4069 | const query = normalizeQuerySpelling(rawQuery); |
| 4070 | |
| 4071 | const cg = this.getCodeGraph(args.projectPath as string | undefined); |
| 4072 | const projectRoot = cg.getProjectRoot(); |
| 4073 | |
| 4074 | // Resolve adaptive output budget from project size. Falls back to the |
| 4075 | // largest-tier defaults if stats aren't available, which preserves |
| 4076 | // pre-#185 behavior for callers that hit the rare stats failure. |
| 4077 | let budget: ExploreOutputBudget; |
| 4078 | let indexedFileCount = -1; |
| 4079 | let indexedNodeCount = -1; |
| 4080 | try { |
| 4081 | const stats = cg.getStats(); |
| 4082 | indexedFileCount = stats.fileCount; |
| 4083 | indexedNodeCount = stats.nodeCount; |
| 4084 | budget = getExploreOutputBudget(indexedFileCount); |
| 4085 | } catch { |
| 4086 | budget = getExploreOutputBudget(Infinity); |
| 4087 | } |
| 4088 | const maxFiles = clamp((args.maxFiles as number) || budget.defaultMaxFiles, 1, 20); |
| 4089 | |
| 4090 | // File paths named in the query become PINNED files: guaranteed admission, |
| 4091 | // top of the rank order, funded first — and their span is REMOVED from the |
| 4092 | // matching query. Runs on the RAW query (normalizeQuerySpelling strips |
| 4093 | // `/digits` tails, which would mangle numeric path segments). Without this, |
| 4094 | // a SvelteKit path like `runs/[runId]/+page.svelte` was shredded by the |
| 4095 | // seeding tokenizer (splits on brackets → `runId` seeded as a "named |
| 4096 | // symbol") and by FTS (`page`/`runs` fragments admitted every sibling |
| 4097 | // `+page.svelte`), starving the very files the agent asked for. |
| 4098 | let pinnedFiles: string[] = []; |
| 4099 | let unresolvedPathSpans: string[] = []; |
| 4100 | let lineAnchors: QueryLineAnchor[] = []; |
| 4101 | let setAsideMatches: QuerySetAsideMatch[] = []; |
| 4102 | let matchQuery = query; |
| 4103 | if (queryMightContainPaths(rawQuery)) { |
| 4104 | try { |
| 4105 | const extraction = extractQueryPaths( |
| 4106 | rawQuery, |
| 4107 | cg.getFiles().map((f) => f.path), |
| 4108 | { |
| 4109 | maxPins: maxFiles, |
| 4110 | existsOnDisk: (rel) => pathIsProjectFile(projectRoot, rel), |
| 4111 | symbolFiles: (symbol) => filesDefiningSymbol(cg, symbol), |
| 4112 | }, |
| 4113 | ); |
| 4114 | if (extraction.pinnedFiles.length > 0 || extraction.unresolvedPathSpans.length > 0) { |
| 4115 | pinnedFiles = extraction.pinnedFiles; |
| 4116 | unresolvedPathSpans = extraction.unresolvedPathSpans; |
| 4117 | lineAnchors = extraction.lineAnchors; |
| 4118 | setAsideMatches = extraction.setAsideMatches; |
| 4119 | matchQuery = normalizeQuerySpelling(extraction.strippedQuery); |
| 4120 | } |
| 4121 | } catch { /* path pinning must never fail an explore call */ } |
no test coverage detected