* 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>)
| 2617 | * tax on small projects while earning its keep on large ones. |
| 2618 | */ |
| 2619 | private async handleExplore(args: Record<string, unknown>): Promise<ToolResult> { |
| 2620 | const rawQuery = this.validateString(args.query, 'query'); |
| 2621 | if (typeof rawQuery !== 'string') return rawQuery; |
| 2622 | // One normalization point so the flow-builder, relevance search, and |
| 2623 | // ranking all see the same canonical spelling (Erlang `mod:fn/arity`). |
| 2624 | const query = normalizeQuerySpelling(rawQuery); |
| 2625 | |
| 2626 | const cg = this.getCodeGraph(args.projectPath as string | undefined); |
| 2627 | const projectRoot = cg.getProjectRoot(); |
| 2628 | |
| 2629 | // Resolve adaptive output budget from project size. Falls back to the |
| 2630 | // largest-tier defaults if stats aren't available, which preserves |
| 2631 | // pre-#185 behavior for callers that hit the rare stats failure. |
| 2632 | let budget: ExploreOutputBudget; |
| 2633 | try { |
| 2634 | budget = getExploreOutputBudget(cg.getStats().fileCount); |
| 2635 | } catch { |
| 2636 | budget = getExploreOutputBudget(Infinity); |
| 2637 | } |
| 2638 | const maxFiles = clamp((args.maxFiles as number) || budget.defaultMaxFiles, 1, 20); |
| 2639 | |
| 2640 | // Step 1: Find relevant context with generous parameters. |
| 2641 | // Use a large maxNodes budget — explore has its own 35k char output limit |
| 2642 | // that prevents context bloat, so more nodes just means better coverage |
| 2643 | // across entry points (especially for large files like Svelte components). |
| 2644 | const subgraph = await cg.findRelevantContext(query, { |
| 2645 | searchLimit: 8, |
| 2646 | traversalDepth: 3, |
| 2647 | maxNodes: 200, |
| 2648 | minScore: 0.2, |
| 2649 | }); |
| 2650 | |
| 2651 | if (subgraph.nodes.size === 0) { |
| 2652 | return this.textResult(`No relevant code found for "${query}"`); |
| 2653 | } |
| 2654 | |
| 2655 | // Graph-aware glue: findRelevantContext builds the subgraph from name/text |
| 2656 | // search, so a method that BRIDGES named symbols — e.g. App.tsx's |
| 2657 | // triggerRender, which calls the named triggerUpdate — is never a search hit |
| 2658 | // and gets missed, forcing the agent to Read the file to trace it. Pull in |
| 2659 | // the callers/callees of the entry (root) nodes, but ONLY those that live in |
| 2660 | // files the subgraph already surfaces (where the agent reads to fill gaps), |
| 2661 | // so we add wiring without dragging in unrelated files. These get an |
| 2662 | // importance boost below so they survive the per-file cluster budget. |
| 2663 | const glueNodeIds = new Set<string>(); |
| 2664 | const subgraphFiles = new Set<string>(); |
| 2665 | for (const n of subgraph.nodes.values()) subgraphFiles.add(n.filePath); |
| 2666 | const GLUE_NODE_CAP = 60; |
| 2667 | for (const rootId of subgraph.roots) { |
| 2668 | if (glueNodeIds.size >= GLUE_NODE_CAP) break; |
| 2669 | let neighbors: Node[] = []; |
| 2670 | try { |
| 2671 | neighbors = [ |
| 2672 | ...cg.getCallers(rootId).map(c => c.node), |
| 2673 | ...cg.getCallees(rootId).map(c => c.node), |
| 2674 | ]; |
| 2675 | } catch { |
| 2676 | continue; |
no test coverage detected