gather: deterministic context-pack tool (aft_gather) - #152
Conversation
There was a problem hiding this comment.
4 issues found across 7 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
f8e7fa9 to
cf09b9b
Compare
|
Follow-up on the two maintainability notes from the Greptile summary (they weren't separate review threads, so noting here) — both addressed in
Matched/rendered text is byte-identical; 23/23 gather tests green. |
1d068c9 to
d2e15e6
Compare
5364eac to
f42c5ec
Compare
f42c5ec to
66e05e2
Compare
66e05e2 to
d20f276
Compare
|
Rebased onto current The rename covers the advertised tool name and the agent-facing error strings only. The wire command stays Rebase conflicts were confined to Verified: Still one commit. Two notes on the full plugin suite, both pre-existing on |
d20f276 to
8d85020
Compare
8d85020 to
0de9d84
Compare
gather: deterministic context-pack tool (aft_gather)
What
One new tool —
aft_gather— assembles a bounded "context pack" (ranked, deduped, budgeted verbatim code evidence) in a single call. It replaces the multi-turnsearch → outline → zoom → callgraphread chain an agent otherwise runs to build context around a question or a symbol.Two modes (mutually exclusive):
question: "how does X work?"— seeds fromhandle_semantic_search(same pipeline asaft_search, all lanes/fallbacks)symbol+filePath— seeds from the callgraph (impactdepth-1 callers +call_treedepth-1 callees)Seeds expand one hop through the callgraph, dedupe by canonicalized (file, symbol) with seeds winning, and render via
render_symbol_within_budgetuntil a hard line budget (default 400, cap 800) is spent. Everything past the cut appears as one-line stubs under## Beyond budget (zoom to expand)— nothing is silently dropped.Why
Agents burn serial turns assembling context: search, then outline the hits, then zoom the symbols, then chase callers. Each turn round-trips through the model. A pack returns evidence (verbatim bodies with file:line headers), not conclusions — the agent reasons over it directly, ready to attach to a subagent dispatch.
Measured on a real config repo (3 questions, one call each vs. the manual chain):
used=283/400. Manual baseline: 5-6 tool calls.budget=200.Independently reproduced on the aft codebase itself:
question: "how does bash output compression dispatch pick a compressor"→seeds=15, used=226/400, one pack assembling the full dispatch chain (compress→ gate →compress_with_registry_exit_code20-compressor array →Compressortrait → install path → subc mirror) that otherwise takes a 4–5-call search→zoom chain.Honest degradation
The pack never lies about its own quality:
file:line (no containing symbol)stubs and flags the header withdegraded=semantic-index-building (partial results — retry when index ready)— detected via the response'ssemantic_statusfield, cleared as soon as one real seed resolves. No blocking or retry inside the tool.{file, line_text, line}, no symbol name) resolve to their containing symbol by line containment — definitions, call sites, and comment hits all upgrade to the enclosing symbol. Hits with no containing symbol stay visible as stubs.(N unresolved external calls omitted)) instead of drowning the stub list; unresolved seeds and callers are never suppressed.Implementation
crates/aft/src/commands/gather.rs— Rust-side composition: callshandle_semantic_search/impact_result/call_tree_result/render_symbol_within_budgetdirectly (shared&AppContext, no bridge round-trips, no parallel reimplementation of search).main.rsdispatch arm,subc_translate.rsmapping, TS factorypackages/opencode-plugin/src/tools/gather.ts+ registration (same tier asaft_callgraph— depends on the callgraph store).protocol.rsResponsedoc-comment):success:false+codefor un-performable calls (e.g.invalid_requeston a bad mode combo),success:truewith a visible degraded/stub pack for partial results — never a bare empty success.Tests
22 unit tests in
gather.rs, including red-checked regressions (each confirmed to fail against pre-fix code): mid-codepoint truncation panic, duplicate-symbol line-anchored resolution, abs/rel path dedupe, containing-symbol resolution via a realTreeSitterProvider, callee-only stub suppression driven through the productionbuild_packpath, and degradation-flag presence/absence/mixed cases.Limitations (deliberate scope)
aft_callgraph.Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Summary by cubic
Adds
aft_gather_context, a single‑call, deterministic context‑pack builder that returns ranked, deduped, verbatim code within a fixed line budget, replacing the multi‑step search→outline→zoom→callgraph chain. Exposed asgatherin Rust andaft_gather_contextin@opencodeon the “all” surface.New Features
question(semantic seeds) orsymbol+filePath(impact callers + call‑tree callees), 1‑hop expansion; dedupe by(file, symbol)with seeds winning.degraded=semantic-index-buildingandneighbors=skipped(callgraph-unavailable); exact used lines computed post‑render.start_line. Tests cover truncation, dedupe, stubs, suppression, and path resolution.@opencodeasaft_gather_context(ALL‑only; inventory updated), with XOR schema validation andbudgetmin/max enforced.Migration
{ question }or{ symbol, filePath }; optionalbudget1–800 (default 400).symbolmode; inquestionmode, neighbors are skipped with a header notice if unavailable.Written for commit 0de9d84. Summary will update on new commits.
Greptile Summary
This PR introduces
aft_gather— a single-call, deterministic context-pack builder that replaces the serialsearch → outline → zoom → callgraphpattern agents previously needed for gathering code context. It is well-designed and thoroughly tested (22 unit tests), with honest degradation flags, callee-only stub suppression, path normalization, and Unicode-safe truncation, all wired correctly through the Rust dispatch, subc-translate, and TypeScript plugin layers.gathercommand (gather.rs): two mutually exclusive modes (question via semantic search, symbol via callgraph impact), 1-hop neighbor expansion, dedup by(file, symbol)with seeds winning, and a hard line budget (default 400, cap 800) with over-budget symbols rendered as visible stubs rather than silently dropped.format!("{}\n{}\n{}", header, body, truncated_note)yields different.lines().count()results depending on whethertruncated_noteis empty (trailing\nignored by Rust's.lines()) versus non-empty (no trailing\n, extra line counted). The+1separator then overcounts by 1 per truncated symbol, causingused=to over-report and the budget guard to be slightly more conservative — directly contradicting the "exact used lines" stated design goal fixed in earlier iterations.Confidence Score: 4/5
render_symbol_section; the rest of the implementation is solid and well-tested.render_symbol_section: theformat!("{}\n{}\n{}", header, body, truncated_note)pattern silently over-countslines_usedby 1 for every symbol that receives a truncation or menu note, causing theused=header to over-report and the budget guard to be slightly more aggressive than intended. Earlier review rounds fixed analogous undercounting bugs in the separator path, and this is the same class of problem on the opposite side. The fix is a one-line restructuring of the format call. All other logic — dedup, path normalization, callee suppression, degradation flags, TypeScript wiring — is correct and comprehensively exercised by the 22 unit tests.render_symbol_sectionfunction and corresponding line-counting inbuild_pack.Important Files Changed
.lines()counts depending on whethertruncated_noteis empty, causingused=to overcount by 1 per such symbol and the budget guard to be slightly more conservative than intended. Core logic (dedup, path normalization, callee suppression, degradation flags) is well-structured and thoroughly tested with 22 unit tests.Sequence Diagram
sequenceDiagram participant Agent participant GatherTS as gather.ts (TS) participant GatherRS as gather.rs (Rust) participant SemanticSearch participant CallgraphStore Agent->>GatherTS: "aft_gather_context({question|symbol+filePath, budget})" GatherTS->>GatherTS: XOR mode validation GatherTS->>GatherRS: tool_call("gather", rawArgs) alt question mode GatherRS->>SemanticSearch: "handle_semantic_search(query, top_k=15)" SemanticSearch-->>GatherRS: results[] + semantic_status GatherRS->>GatherRS: resolve grep-fallback hits via line containment GatherRS->>GatherRS: sort seeds by score GatherRS->>CallgraphStore: impact_result(seed) + call_tree_result(seed) per seed CallgraphStore-->>GatherRS: callers[] + callees[] else symbol mode GatherRS->>GatherRS: validate_path(filePath) → absolute path GatherRS->>CallgraphStore: "impact_result(filePath, symbol, depth=1)" CallgraphStore-->>GatherRS: callers[] GatherRS->>CallgraphStore: "call_tree_result(filePath, symbol, depth=1)" CallgraphStore-->>GatherRS: callees[] end GatherRS->>GatherRS: dedup_by_file_and_name (seeds win) GatherRS->>GatherRS: build_pack: render each candidate within per_symbol_budget Note over GatherRS: Budget guard: lines_used + section_lines + 1 > budget → stub Note over GatherRS: Suppress unresolved external callees, keep seeds/callers visible GatherRS->>GatherRS: "Build header last (used=exact_count)" GatherRS-->>GatherTS: "{text: "## gather pack | ..."}" GatherTS-->>Agent: pack textReviews (13): Last reviewed commit: "gather: deterministic context-pack tool ..." | Re-trigger Greptile