Skip to content

Design: gather inside aft_search — the plugin sends the reasoning trace, a decision model walks the call graph from the hits #890

Open

Description

@iceteaSA

1. Summary

The maintainer suggested this change: aft_search receives the reasoning trace that caused the agent to call it. The tool then extracts the call graph near the results and walks outward. At each step, a decision model selects the nodes to keep and decides if the walk continues. The comparison was to "sidekick": it works, but it is not sufficient.

This issue is a design for review before a draft PR. Please correct the shape early. A prototype starts in parallel, on the parts that do not depend on the open questions in section 8.

2. Relation to #192 and #152

#192 and PR #152 closed because a deterministic packer on the tool surface moves pack-composition judgment onto the calling model, and each new tool costs surface. This design keeps the judgment off the calling model: a decision model inside AFT selects what to keep. It also adds no tool and no agent argument; the trace travels next to the arguments, not in them.

3. Sidekick

Sidekick is not in this repository. AFT mentions it only as a helper route class: a sidekick bind does not start a semantic index (semantic_admission.rs:20). The #192 closing note describes a judged reader in the delegation model that selects what goes into a context pack; that may be the same component. Magic Context had a sidekick agent behind /ctx-aug for prompt augmentation and removed it in v0.42.0. Question 1 in section 8 asks which one is meant and where it is not sufficient.

4. Facts that the design uses

Fact Evidence
OpenCode 1 gives a tool sessionID and messageID. @opencode-ai/plugin ToolContext (dist/tool.d.ts:2-4)
The plugin holds an SDK client and already reads session messages. shared/last-assistant-model.ts:123-156
On OpenCode 1, the reasoning and text parts of the current assistant message are in storage when the tool runs. Live probe, 2026-10-25: a running bash call's message had its reasoning part (213 characters) stored 9 ms before the tool part.
OpenCode 2 gives plugins generate.text({prompt, model?}) and session.message.get({sessionID, messageID}). @opencode/client 2.0.11 effect/api/api.d.ts:1550-1712; plugin context members in test/matrix/delta-audit-ga-2.0.11.md:33
Pi gives a tool sessionManager (read-only: getLeafEntry, getBranch), model and modelRegistry. pi-coding-agent extensions/types.d.ts:208-232
OpenAI models frequently store empty reasoning text. 3 days of data on one machine: gpt-6-luna reasoning parts average 28 characters; Claude, MiniMax, GLM and DeepSeek average 800-3,700.
AFT has no chat-completion client. It has a reranker with off, onnx, remote and synapse backends. config.rs:620-660, commands/semantic_search/rerank/
Extra request fields already travel next to the agent arguments. edit_slot_survives, preview: bridge.ts:1084-1105, tool_call.rs:40-119
The call-graph store reports Ready, Building, Off, Unavailable, Suspended or Error, and does not block. context.rs:4865-4887; callers.rs:84-105
aft_search maps to semantic_search. Expansion fits after the ranked response and before the handler attaches metadata. subc_translate.rs:2460-2536; commands/semantic_search/mod.rs:1034-1046

5. Design

5.1 Trace input

The plugin collects the trace. The agent does not write it. The plugin reads
no message parts unless search.gather.enabled is true for that session.
While gather is off, the plugin collects no trace.

  • OpenCode 1: read the current message with messageID. Use the reasoning
    and text parts that come before this tool call.
  • OpenCode 2: the same parts through session.message.get. The prototype must
    confirm that the tool context has messageID.
  • Pi and OMP: the same parts from the leaf assistant entry. The prototype must
    confirm that Pi stores the assistant message before it runs the tools.
  • Add the newest user message text in all hosts.
  • Cap the trace at 6,000 characters. Keep the newest text.
  • Send the trace as an out-of-band field search_context on the tool-call
    request, the same way the plugin sends edit_slot_survives. The tool schema
    does not change.
  • If no trace is available, use the query only. The search does not fail.
  • AFT does not log trace text. Logs show the length only.

5.2 Seeds and graph

  1. Run the current ranked search. Its results do not change.
  2. Take the top 5 hits that are code. Resolve each hit to the symbol that
    contains it (resolve_containing_symbol from PR gather: deterministic context-pack tool (aft_gather) #152).
  3. Get a pinned read snapshot of the call-graph store. Read the callers and
    callees of each seed: one hop per round.
  4. If the store is not Ready, do not walk. Return the normal results with
    one line that gives the store state.

5.3 Walk and decision model

Each round sends one request to the decision model with the trace, the query,
and the frontier. Each frontier node has an id, the name, the kind,
path:line, the signature, up to 3 doc lines, and its edge (for example
"called by seed parse_config"). The model returns JSON:
{"keep": [ids], "expand": [ids], "stop": true|false}.

Limits (defaults; configurable in user config):

Limit Default
Rounds 3
Nodes kept 12
Frontier nodes per round 40
Deadline for one decision 3 s
Deadline for the full walk 8 s
Output added to the result 12 KB, inside the current 50 KB cap

If the model times out, fails, or returns JSON that is not valid, that round
uses the deterministic order from PR #152: seed order, then hop
distance, then reranker score when the user configures search.rerank. The
result tells the agent that the round used the fallback. A walk failure does
not make the search fail.

5.4 Decision-model channel (open question)

Option Works on Credentials Cost
A. AFT calls an OpenAI-compatible chat endpoint. All hosts, standalone and daemon User config only (endpoint, model, api_key_env), as search.rerank.remote New HTTP client in the engine; the trace goes to that endpoint
B. The host runs the model. OpenCode 2 (generate.text), Pi (model registry). Not OpenCode 1. The host's providers; no new keys The plugin runs the walk loop and the engine runs graph steps; one more request per round
C. A daemon LLM-runner module runs it. Daemon mode only The daemon's credential custody Depends on that module

Recommendation: a DecisionModel trait in the engine with backend A first,
because it works on every host and in the two modes. Add B when AFT stops
support for OpenCode 1. The maintainer selects the option.

5.5 Privacy and credentials

  • Egress. Backend A sends the trace (reasoning, visible text and the newest
    user message) and symbol bodies to an endpoint that the engine calls
    directly. That request does not go through host hooks, so host redaction
    (for example a secret-guard plugin on OpenCode 2) does not see it. Backend B
    uses the host's providers, so host masking stays in the path. The issue
    gives three choices for A: the plugin sends a trace that host redaction has
    already processed; the engine applies a redaction hook before each request;
    or the documentation states the risk and recommends a local endpoint.
  • Credentials in daemon mode. api_key_env reads the environment of the AFT
    process. In daemon mode, that environment comes from the module
    configuration of the daemon. The daemon-mode choice is a custody grant for
    the module (for example a vault grant), the same path that
    search.rerank.remote would need.
  • AFT does not log trace text or decision-model requests. Logs show lengths,
    counts and timings only.

5.6 Caching

  • Neighbor lists: keyed by the call-graph generation.
  • Decisions: keyed by a hash of the trace, the query and the frontier ids,
    for 10 minutes for each session. The same search again does not call the
    model again.

5.7 Output

The ranked results come first and do not change. Then one section:

Related code (gathered for this search, 2 rounds, 7 of 31 nodes kept):
crates/aft/src/config.rs:612 RerankConfig: struct  · called by seed resolve_rerank
  <symbol body, cut to budget>
...
gather: model=<name> rounds=2 considered=31 kept=7 fallback=none 2.4s

5.8 Configuration

"search": {
  "gather": {
    "enabled": false,
    "decision": { "backend": "endpoint", "endpoint": "...", "model": "...", "api_key_env": "..." },
    "max_rounds": 3, "max_nodes": 12, "deadline_ms": 8000
  }
}
  • Off by default.
  • The decision backend, endpoint and key are user-tier only. A project can turn
    gather off but cannot turn it on or change the endpoint.
  • Gather runs only for natural-language queries. A pattern-only search does
    not walk.

6. Reuse from PR #152

Part Use
resolve_containing_symbol Port to current main.
collect_callgraph_neighbors, collect_callees_for_seed Rewrite on the pinned store snapshot (callers_result, call-tree queries).
build_pack ordering and provenance Use as the deterministic fallback order.
Budget-aware rendering Use render_symbol_within_budget (symbol_render.rs:209).
Mode handling (question/symbol) Do not port. aft_search already has the query.

7. Test plan

  • Unit: trace capping, JSON parsing of decisions, each fallback path, limits.
  • Integration: a fixture repository with a known call chain and a fake
    decision model. The test checks that the walk keeps the chain and stops.
  • A test that the store states other than Ready return the plain search.
  • Mutation: remove the fallback and the deadline, and the tests fail.
  • Host tests: each plugin sends search_context and does not put it in the
    agent arguments.

8. Questions for the maintainer

  1. Which sidekick, and where is it not sufficient?
  2. Decision-model channel: A, B or C (5.4)? For A: which redaction choice
    (5.5)?
  3. Default: off, or on when the user configures a decision backend?
  4. Gather on each natural-language search, or only when a trace is available?

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions