Before changing extraction, resolution, graph queries, context output, or MCP retrieval behavior, read the relevant sections; adding a language/framework requires running the validation methodology. The notes below were migrated from the former root AGENTS.md, code paths are relative to the repository root, and the case data is historical upstream measurement, not a measured result of this fork.
CodeGraph's core value is letting an agent answer structural/flow questions ("how does X reach Y", trace, impact, callers) with a few fast codegraph calls and zero Read/Grep. The optimization target is wall-clock latency + tool-call count — don't optimize for token cost. (Cost is lower, not "flat" as earlier framing claimed: a current-build with-vs-without A/B across the 7 README repos, median of 4, saved on average 35% cost · 57% tokens · 46% time · 71% tool calls — reproducing the published README. The mechanism is far fewer turns over a much smaller accumulated context — NOT cache-ability: the without-arm's huge token volume is mostly cheap cache-reads, which is why token-count savings (57%) look bigger than cost savings (35%). Measure tokens by summing per-turn assistant usage, not result.usage (last-turn only in current Claude Code). See docs/benchmarks/call-sequence-analysis.md.) The mechanism that drives everything here: an agent falls back to Read/Grep the instant a codegraph answer is insufficient. So every change is judged by one question — is codegraph's answer sufficient enough to stop the agent from reading?
Target behavior: a flow question resolves in 1 codegraph call on small repos, scaling to 3–5 on large, with Read/Grep = 0. When reviewing a PR or trying something new, do not regress this.
The lever that decides whether a retrieval change lands. Test before building anything here: does this make a tool the agent already calls do more with the input it already gives? If it instead needs the agent to behave differently — pick a different tool, query differently, learn from examples — it hits the low-salience wall and won't land.
CodeGraph's only channels to influence the agent are low-salience: the MCP initialize instructions (server-instructions.ts) and the tool descriptions. Changing them does not reliably move the agent's tool choice or query style — validated: trace-first steering ported into the server-instructions + tool descriptions (3 wording variants) never reproduced what a CLI --append-system-prompt achieved, and regressed wall-clock vs baseline. New tools fare worse (rarely chosen — the agent under-picks even trace); "better examples" is the same steering. The agent's tool-choice does improve on its own as host models get better at tool use — but that is not ours to force.
What works is meeting the agent where it already is:
- explore-flow —
codegraph_exploreis the PRIMARY tool the agent reliably calls; its query is a precise bag of symbol names (incl. qualifiedClass.method) spanning the flow the agent is after; explore finds the call path among those named symbols (riding synthesized edges) and leads its output with it. (buildFlowFromNamedSymbols: segment/co-naming disambiguation; ≤1 unnamed bridge so it never wanders a god-function's fan-out. Overload-aware: a PascalCase type token in the query biases an overloaded name to that type's own def —DataRequest task→ DataRequest'stask, not the abstract base; named-symbol files sort first.) - Sufficiency — make the tool's output complete enough that the agent stops.
codegraph_nodereturns the full body + the caller/callee trail, and for an AMBIGUOUS name returns every overload's body in one call (so the agent never Reads a file to find the right overload — validated on Alamofire/gin). This is the after-explore depth tool (labeled SECONDARY). - Errors teach abandonment — one or two
isError: trueresponses early in a session and the agent stops calling codegraph entirely (maintainer-observed, repeatedly).isErroris reserved for genuine "stop trying" cases: security refusals (PathRefusalError) and real malfunctions (which carry a retry-once note). Every expected/recoverable condition — project not indexed, symbol not found, file not in the index — returns a SUCCESS-shaped response carrying the guidance (NotIndexedError→textResult, seeToolHandler.execute's catch). The same principle is why the tool surface is always exposed, even at an un-indexed root (the old empty-tools/listgate was removed in #964 — it broke monorepos where only sub-projects carry a.codegraph/, and hid the tools from a session that started beforecodegraph init): safety comes from the response SHAPE (success-shaped guidance, neverisError), not from hiding tools. An un-indexed root'sinitializesends a per-project variant (SERVER_INSTRUCTIONS_NO_ROOT_INDEX— "passprojectPathto a project that has a.codegraph/"), not an "inactive" note; indexing is still deliberately the user's call, never the agent's.
What fails is the inverse — folding a precise answer into a fuzzy-input tool: the now-removed codegraph_context took a description, not symbols, so it couldn't disambiguate a flow's endpoints and surfaced the wrong feature (which is why it was cut). Precise output needs precise input — explore takes a symbol bag for exactly this reason. (codegraph_trace was likewise removed: explore-flow does its job and the agent under-picked it.)
The remaining lever under this axis is coverage: every flow made to connect statically (a new dynamic-dispatch synthesizer, or extracting symbols static parsing skipped — e.g. object-literal store actions in create((set,get)=>({...}))) is then surfaced automatically by explore-flow, no agent change needed. Reactive/reconciler runtimes (Halo's ReactiveExtensionClient, MediatR, Vue Proxy) are the frontier — flows there have no static edges, so nothing surfaces (correctly — silent beats wrong). Full investigation + A/B record: docs/benchmarks/call-sequence-analysis.md + auto-memory project_codegraph_read_displacement.
Two functions in src/mcp/tools.ts scale explore with indexed file count. This is the expected resolution (a regression here silently forces agents back to Read):
| Repo | files | explore calls | chars/call | per-file |
|---|---|---|---|---|
| express (small) | 147 | 1 | 18K | 3800 |
| excalidraw/django (medium) | 643–3043 | 2 | 24K | 6500 |
| vscode (large) | 10446 | 3 | 24K | 7000 |
| ~20k / ~40k | — | 4 / 5 | 24K | 7000 |
Every tier at 500+ indexed files now caps maxOutputChars at 24,000 — the ~25K inline tool-result limit above which the host externalizes the result and the agent Reads it back — so more files means more calls, not a fatter single response. The 28K / 35K / 38K values in earlier revisions of this table were the pre-#185 shape and are historical, not the current contract.
getExploreBudget(fileCount)→ call budget:<500→1, <5000→2, <15000→3, <25000→4, ≥25000→5(max 5).getExploreOutputBudget(fileCount)→ per-call output (chars / files / per-file). Invariant: a larger tier must never get a smallermaxCharsPerFilethan a smaller tier. (Regression that motivated this doc: the<5000tier's 2500 was below the<500tier's 3800, so on a god-file repo — excalidraw's 415 KBApp.tsx— one explore returned <1% of the file and forced a Read.)- 这个分档也是
codegraph_explore.maxFiles未指定时的唯一事实来源:schema 刻意不再声明default,实际默认值随档位取 4 / 5 / 8 / 8 / 8,写死任何一个数字都会与运行时不一致。 - Explore output must never tell the agent to "use Read" — steer to another
codegraph_exploreand "treat returned source as already Read." When a section stops short of the end of a file the query NAMED, the response must say so inline: the uncovered line range plus themode:"source"+startLine+limitcall that fetches it. A silent stop is the insufficiency that sends the agent to Read.
Static tree-sitter extraction misses computed/indirect calls, so flows break at dynamic dispatch and the agent reads to reconstruct them. Synthesizers/resolvers bridge these so codegraph_explore connects them end-to-end (src/resolution/callback-synthesizer.ts, src/resolution/frameworks/). Channels today: callback/observer, EventEmitter, React re-render (setState→render), JSX child (render→child component), React Native native→JS events (sendEvent(withName:) / JVM emit → the addListener handler, named or inline, rn-event-channel), django ORM descriptor. The JS→native direction is a resolver (frameworks/react-native.ts: RCT_EXPORT_METHOD, RCT_EXTERN_MODULE Swift shims, TurboModules), which trusts receiver evidence — an alias bound to NativeModules.X — over the import resolver. All synthesized edges are provenance:'heuristic' with metadata.synthesizedBy + registeredAt (the wiring site), surfaced inline in codegraph_explore's Flow section and the codegraph_node trail.
Principle: partial coverage is WORSE than none. Bridging one boundary but not the next reveals a hop the agent then drills + reads to finish. Measured on excalidraw: react-render alone raised reads to 5–7; only completing the flow (adding the jsx-child hop) dropped it to 0–1. Always close the flow end-to-end and re-measure — never ship a half-bridged flow.
Before adding or extending WHEN rules for routes, frameworks, or languages, read Framework coverage and update the corresponding coverage status. Dynamic calls need end-to-end path and inference-precision validation; see Dynamic-dispatch coverage and Callback-edge synthesis.