* 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>)
| 2458 | * tax on small projects while earning its keep on large ones. |
| 2459 | */ |
| 2460 | private async handleExplore(args: Record<string, unknown>): Promise<ToolResult> { |
| 2461 | const query = this.validateString(args.query, 'query'); |
| 2462 | if (typeof query !== 'string') return query; |
| 2463 | |
| 2464 | const cg = this.getCodeGraph(args.projectPath as string | undefined); |
| 2465 | const projectRoot = cg.getProjectRoot(); |
| 2466 | |
| 2467 | // Resolve adaptive output budget from project size. Falls back to the |
| 2468 | // largest-tier defaults if stats aren't available, which preserves |
| 2469 | // pre-#185 behavior for callers that hit the rare stats failure. |
| 2470 | let budget: ExploreOutputBudget; |
| 2471 | try { |
| 2472 | budget = getExploreOutputBudget(cg.getStats().fileCount); |
| 2473 | } catch { |
| 2474 | budget = getExploreOutputBudget(Infinity); |
| 2475 | } |
| 2476 | const maxFiles = clamp((args.maxFiles as number) || budget.defaultMaxFiles, 1, 20); |
| 2477 | |
| 2478 | // Step 1: Find relevant context with generous parameters. |
| 2479 | // Use a large maxNodes budget — explore has its own 35k char output limit |
| 2480 | // that prevents context bloat, so more nodes just means better coverage |
| 2481 | // across entry points (especially for large files like Svelte components). |
| 2482 | const subgraph = await cg.findRelevantContext(query, { |
| 2483 | searchLimit: 8, |
| 2484 | traversalDepth: 3, |
| 2485 | maxNodes: 200, |
| 2486 | minScore: 0.2, |
| 2487 | }); |
| 2488 | |
| 2489 | if (subgraph.nodes.size === 0) { |
| 2490 | return this.textResult(`No relevant code found for "${query}"`); |
| 2491 | } |
| 2492 | |
| 2493 | // Graph-aware glue: findRelevantContext builds the subgraph from name/text |
| 2494 | // search, so a method that BRIDGES named symbols — e.g. App.tsx's |
| 2495 | // triggerRender, which calls the named triggerUpdate — is never a search hit |
| 2496 | // and gets missed, forcing the agent to Read the file to trace it. Pull in |
| 2497 | // the callers/callees of the entry (root) nodes, but ONLY those that live in |
| 2498 | // files the subgraph already surfaces (where the agent reads to fill gaps), |
| 2499 | // so we add wiring without dragging in unrelated files. These get an |
| 2500 | // importance boost below so they survive the per-file cluster budget. |
| 2501 | const glueNodeIds = new Set<string>(); |
| 2502 | const subgraphFiles = new Set<string>(); |
| 2503 | for (const n of subgraph.nodes.values()) subgraphFiles.add(n.filePath); |
| 2504 | const GLUE_NODE_CAP = 60; |
| 2505 | for (const rootId of subgraph.roots) { |
| 2506 | if (glueNodeIds.size >= GLUE_NODE_CAP) break; |
| 2507 | let neighbors: Node[] = []; |
| 2508 | try { |
| 2509 | neighbors = [ |
| 2510 | ...cg.getCallers(rootId).map(c => c.node), |
| 2511 | ...cg.getCallees(rootId).map(c => c.node), |
| 2512 | ]; |
| 2513 | } catch { |
| 2514 | continue; |
| 2515 | } |
| 2516 | for (const nb of neighbors) { |
| 2517 | if (glueNodeIds.size >= GLUE_NODE_CAP) break; |
no test coverage detected