* 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>)
| 2500 | * tax on small projects while earning its keep on large ones. |
| 2501 | */ |
| 2502 | private async handleExplore(args: Record<string, unknown>): Promise<ToolResult> { |
| 2503 | const rawQuery = this.validateString(args.query, 'query'); |
| 2504 | if (typeof rawQuery !== 'string') return rawQuery; |
| 2505 | // One normalization point so the flow-builder, relevance search, and |
| 2506 | // ranking all see the same canonical spelling (Erlang `mod:fn/arity`). |
| 2507 | const query = normalizeQuerySpelling(rawQuery); |
| 2508 | |
| 2509 | const cg = this.getCodeGraph(args.projectPath as string | undefined); |
| 2510 | const projectRoot = cg.getProjectRoot(); |
| 2511 | |
| 2512 | // Resolve adaptive output budget from project size. Falls back to the |
| 2513 | // largest-tier defaults if stats aren't available, which preserves |
| 2514 | // pre-#185 behavior for callers that hit the rare stats failure. |
| 2515 | let budget: ExploreOutputBudget; |
| 2516 | try { |
| 2517 | budget = getExploreOutputBudget(cg.getStats().fileCount); |
| 2518 | } catch { |
| 2519 | budget = getExploreOutputBudget(Infinity); |
| 2520 | } |
| 2521 | const maxFiles = clamp((args.maxFiles as number) || budget.defaultMaxFiles, 1, 20); |
| 2522 | |
| 2523 | // Step 1: Find relevant context with generous parameters. |
| 2524 | // Use a large maxNodes budget — explore has its own 35k char output limit |
| 2525 | // that prevents context bloat, so more nodes just means better coverage |
| 2526 | // across entry points (especially for large files like Svelte components). |
| 2527 | const subgraph = await cg.findRelevantContext(query, { |
| 2528 | searchLimit: 8, |
| 2529 | traversalDepth: 3, |
| 2530 | maxNodes: 200, |
| 2531 | minScore: 0.2, |
| 2532 | }); |
| 2533 | |
| 2534 | if (subgraph.nodes.size === 0) { |
| 2535 | return this.textResult(`No relevant code found for "${query}"`); |
| 2536 | } |
| 2537 | |
| 2538 | // Graph-aware glue: findRelevantContext builds the subgraph from name/text |
| 2539 | // search, so a method that BRIDGES named symbols — e.g. App.tsx's |
| 2540 | // triggerRender, which calls the named triggerUpdate — is never a search hit |
| 2541 | // and gets missed, forcing the agent to Read the file to trace it. Pull in |
| 2542 | // the callers/callees of the entry (root) nodes, but ONLY those that live in |
| 2543 | // files the subgraph already surfaces (where the agent reads to fill gaps), |
| 2544 | // so we add wiring without dragging in unrelated files. These get an |
| 2545 | // importance boost below so they survive the per-file cluster budget. |
| 2546 | const glueNodeIds = new Set<string>(); |
| 2547 | const subgraphFiles = new Set<string>(); |
| 2548 | for (const n of subgraph.nodes.values()) subgraphFiles.add(n.filePath); |
| 2549 | const GLUE_NODE_CAP = 60; |
| 2550 | for (const rootId of subgraph.roots) { |
| 2551 | if (glueNodeIds.size >= GLUE_NODE_CAP) break; |
| 2552 | let neighbors: Node[] = []; |
| 2553 | try { |
| 2554 | neighbors = [ |
| 2555 | ...cg.getCallers(rootId).map(c => c.node), |
| 2556 | ...cg.getCallees(rootId).map(c => c.node), |
| 2557 | ]; |
| 2558 | } catch { |
| 2559 | continue; |
no test coverage detected