Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions internal/cli/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ const (
// interactive user does not have to add flags to every call. Its value is a comma-separated list
// of `container-map`, `signature-types`, `type-card`, or `all`. See searchReferenceBlocks.
envReferenceBlocks = "ENTIRE_GRAPH_REFERENCE_BLOCKS"
// envPresearch names a file holding the payload for this session, computed BEFORE the agent
// started. When it is set, `search` echoes those bytes instead of querying — see
// echoPresearchPayload for the measurement that motivates it.
//
// envPresearchAlias is the same knob under the prefix the benchmark harness already uses for
// every one of its search knobs (EG_TOPK, EG_DEEP, EG_MAXBYTES, EG_PROFILE), so the harness can
// set it beside them; the ENTIRE_GRAPH_ name is the one this repo documents, and it wins.
envPresearch = "ENTIRE_GRAPH_PRESEARCH"
envPresearchAlias = "EG_PRESEARCH"
// envSearchSession names the file that carries ONE task's search state between calls. The CLI is
// one-shot, so nothing else can tell the second search of a task from the first; setting it is
// what turns the search echo on. See searchSession.
envSearchSession = "EG_SEARCH_SESSION"
// envMaxSearches is how many searches of that session actually run a query (default 1, `0`
// disables the echo). See searchSession for the measurement.
envMaxSearches = "EG_MAX_SEARCHES"
)

// cacheDirName is this provider's directory inside the platform's per-user cache
Expand Down Expand Up @@ -58,14 +74,28 @@ type EntireEnv struct {
PluginDataDir string
// ReferenceBlocks is the session-wide default for the off-by-default search reference blocks.
ReferenceBlocks string
// PresearchPath is the file holding this session's pre-computed search payload, or "" when the
// caller has not pre-delivered one. See envPresearch.
PresearchPath string
// SearchSession is the state file for one task's searches; empty means the echo is off.
SearchSession string
// MaxSearches is how many of that task's searches run a query; empty means the default of 1.
MaxSearches string
}

func EnvFromOS() EntireEnv {
presearch := os.Getenv(envPresearch)
if presearch == "" {
presearch = os.Getenv(envPresearchAlias)
}
return EntireEnv{
CLIVersion: os.Getenv(envCLIVersion),
RepoRoot: os.Getenv(envRepoRoot),
PluginDataDir: os.Getenv(envPluginDataDir),
ReferenceBlocks: os.Getenv(envReferenceBlocks),
PresearchPath: presearch,
SearchSession: os.Getenv(envSearchSession),
MaxSearches: os.Getenv(envMaxSearches),
}
}

Expand Down
131 changes: 125 additions & 6 deletions internal/cli/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"strconv"
"strings"

Expand Down Expand Up @@ -117,6 +119,27 @@ func runSearch(ctx context.Context, opts Options, args []string) error {
if strings.TrimSpace(flags.Query) == "" {
return errors.New("search requires --query")
}
// ZERO-TOLL DELIVERY. When the caller has already computed this session's payload and handed it
// to the agent inside context the session pays for anyway, this call must cost nothing: echo
// those bytes and return, before the profile, the repo, the cache and the index are touched.
if path := strings.TrimSpace(opts.Env.PresearchPath); path != "" {
return echoPresearchPayload(opts.Stdout, path)
}
// The echo is decided before any work: a replayed payload must not pay for a repo resolution or
// an index build either. See searchSession for what the cap is worth and why it is an echo.
session, err := newSearchSession(opts.Env, opts.Stderr)
if err != nil {
return err
}
if session != nil {
if state, ok := session.echo(); ok {
if _, err := io.WriteString(opts.Stdout, searchEchoHeader(flags.Query, state.Query)); err != nil {
return err
}
_, err := io.WriteString(opts.Stdout, state.Payload)
return err
}
}
profile, err := parseProfile(flags.Profile)
if err != nil {
return err
Expand Down Expand Up @@ -163,22 +186,80 @@ func runSearch(ctx context.Context, opts Options, args []string) error {
if err := response.Validate(); err != nil {
return err
}
switch flags.Format {
// With a session active the payload is teed, so the echo replays the exact bytes this call
// handed the agent — the stored answer is the rendered output, not a re-render of the response.
out := opts.Stdout
var payload bytes.Buffer
if session != nil {
out = io.MultiWriter(opts.Stdout, &payload)
}
if err := writeSearchResponse(out, response, flags.Format, contextBudget); err != nil {
return err
}
if session != nil {
session.record(flags.Query, payload.Bytes())
}
return nil
}

func writeSearchResponse(out io.Writer, response sem.SearchResponse, format string, contextBudget int) error {
switch format {
case "json":
encoder := json.NewEncoder(opts.Stdout)
encoder := json.NewEncoder(out)
encoder.SetEscapeHTML(false)
return encoder.Encode(response)
case "ndjson":
return writeNdjsonSearch(opts.Stdout, response)
return writeNdjsonSearch(out, response)
case "text":
return writeTextSearch(opts.Stdout, response)
return writeTextSearch(out, response)
case "agent":
return writeAgentSearch(opts.Stdout, response, contextBudget)
return writeAgentSearch(out, response, contextBudget)
default:
return fmt.Errorf("search --format must be json, ndjson, text, or agent, got %q", flags.Format)
return fmt.Errorf("search --format must be json, ndjson, text, or agent, got %q", format)
}
}

// echoPresearchPayload returns a payload that was computed before the agent started, verbatim.
//
// The toll this removes is the MESSAGE, not the bytes. Measured over 178 gated benchmark pairs,
// the number of graph calls a session makes is invariant — 1.00 / 1.03 / 1.00 per session across
// baseline-exploration bands <10 / 10-19 / >=20 turns — while what the call buys back is not:
// greps displaced go 1.35 / 2.16 / 3.74 over the same bands, and the cost ratio with it (1.140 CI
// [1.063,1.224] n=51 / 0.950 n=108 / 0.841 CI [0.704,0.998] n=19). Split by outcome instead of by
// band, the two cohorts separate cleanly: sessions where the call displaced no grep at all (n=43)
// cost 1.165 CI [1.073,1.264] and ran +1.21 turns, sessions where it displaced greps (n=135) cost
// 0.938 CI [0.889,0.990] and ran -2.07 turns. corr(turn delta, log cost ratio) = +0.735 CI
// [0.661,0.800] pair-level, +0.802 CI [0.662,0.885] repo-level — the strongest correlate in the
// set. Independently, a regression holding call count fixed still prices a +7.0% CI [1.9,13.2]
// tax on merely having made the call, and prices one call at 1.61 messages rather than 1.
//
// The toll is also unpredictable: no static feature of a payload predicts that its call will turn
// out to be a no-op (AUC 0.553 top score, 0.560 spread, 0.563 gap, 0.690 entropy, at a 28% base
// rate). Selective skipping is therefore not implementable — pre-delivery is the only form the
// fix can take, and it must be universal.
//
// Echoing rather than re-querying is what makes the delivery free: a call the agent still makes
// costs one message and adds zero information, so it cannot re-open the phase the pre-delivery
// closed. Retrieval is unaffected because the agent's own query adds almost nothing over the
// issue text the payload was derived from: of 188 pre-edit greps not covered by the payload, 2
// (1.1%) named a literal that was in the agent's query and not in the issue.
//
// A caller that names a payload it cannot produce gets an error, not a live query. Falling back
// silently would leave the arm measuring whichever mode each session happened to land in — the
// same reason an unknown --reference-blocks name is an error rather than a no-op.
//
// Note for whoever wires the caller: the payload must be computed with the SAME binary, flags and
// cached graph the session would have used, and the instruction telling the agent to search first
// has to go with it — the tool cannot be left un-called while that directive stands.
func echoPresearchPayload(out interface{ Write([]byte) (int, error) }, path string) error {
payload, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("%s: %w", envPresearch, err)
}
_, err = out.Write(payload)
return err
}

// writeNdjsonSearch streams a payload as one record per line: a header, the blocks that are their own
// records, every ranked result, and a summary that carries the rest.
func writeNdjsonSearch(out interface{ Write([]byte) (int, error) }, response sem.SearchResponse) error {
Expand Down Expand Up @@ -413,6 +494,14 @@ func writeTextSearchResult(out interface{ Write([]byte) (int, error) }, result s
// span the snippet does not contain sends the reader to the wrong lines.
start, end := searchResultPrintedRange(result)
fmt.Fprintf(out, "%d. %s:%d-%d", result.Rank, result.FilePath, start, end)
// A merged span says so, and says the one thing that decides whether the reader re-reads the
// region anyway: that the range is contiguous and nothing between the hits it absorbed has
// been left out. Without that, a wide range reads exactly like a stitched-together excerpt,
// which is what an agent opens the file to check — the measured turn-2 behaviour this merge
// exists to remove (fluentd-3328: `sed -n '330,470p'` over a superset of what it already had).
if len(result.MergedRanks) > 0 {
fmt.Fprintf(out, " [contains ranks %s - contiguous, nothing elided]", joinSearchRanks(result.MergedRanks))
}
// A block that carries no relevance score must not print one. The covering test is not a
// ranked answer — it is the statement of what the fix has to achieve — and `score=0.0000`
// beside it reads as "worthless" rather than "not applicable".
Expand All @@ -429,6 +518,25 @@ func writeTextSearchResult(out interface{ Write([]byte) (int, error) }, result s
if result.Kind != "" {
fmt.Fprintf(out, " kind=%s", result.Kind)
}
// THE LINE THE QUERY ACTUALLY MATCHED, not just the range that contains it.
//
// FocusLine is computed for every hit and has always been in the JSON. The text payload — which
// is what an agent reads — printed only the snippet's range, so a hit on a 27-line method said
// "the answer is somewhere in 135-161" and made the reader find the line itself. Asked what one
// change to the returned payload would let it finish in the fewest calls, a Sonnet agent that had
// just fixed apache/lucene-13170 from this tool named exactly this: its only non-essential call
// was a Read of 135-161 to "pin down line 151 specifically before editing", and it wanted the
// matched line marked so "the payload doubles as both 'here's the function' and 'here's the
// precise line to change'". Re-reading a file the payload already printed is 10.1% of all
// post-payload tool calls in the measured Sonnet sessions.
//
// It goes in the HEADER rather than as an inline `>>> 151:` marker on the snippet line, which is
// what the agent literally asked for. Agents copy snippet text verbatim as the `old_string` anchor
// of an edit, so decorating a body line would make that anchor fail to match the file and turn a
// navigation aid into a broken patch.
if result.FocusLine >= start && result.FocusLine <= end && end > start {
fmt.Fprintf(out, " focus=%d", result.FocusLine)
}
// A section entry may carry no source at all — the covering test degrades to a locator when its
// byte allowance cannot hold one line of body. Printing the empty string as if it were source
// costs two blank lines and reads as a truncation bug.
Expand Down Expand Up @@ -492,6 +600,17 @@ func writeTextSearchTypeCard(out interface{ Write([]byte) (int, error) }, card [
}
}

// joinSearchRanks renders the pre-merge ranks a contiguous span absorbed. They are the ranking's
// own numbers from BEFORE the merge renumbered it, which is what makes a rank that no longer
// prints on its own account for itself instead of just going missing.
func joinSearchRanks(ranks []int) string {
parts := make([]string, 0, len(ranks))
for _, rank := range ranks {
parts = append(parts, strconv.Itoa(rank))
}
return strings.Join(parts, ",")
}

func joinSearchUseLines(lines []int) string {
parts := make([]string, 0, len(lines))
for _, line := range lines {
Expand Down
98 changes: 98 additions & 0 deletions internal/cli/search_presearch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package cli

import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)

// presearchPayload is deliberately NOT anything the renderer could produce: if a byte of it is
// missing, added, or reordered, the call re-queried instead of echoing.
const presearchPayload = "PRE-DELIVERED PAYLOAD\nsrc/auth.py:1 validate_token\n"

// TestSearchEchoesPreDeliveredPayload pins zero-toll delivery: when the payload was computed before
// the agent started, `search` returns it byte-for-byte and does no work — not the repo, not the
// index, not the ranking. The nonexistent-repo case is the proof that no work happens: a live query
// cannot resolve that repo, so an echo that survives it cannot have run one.
func TestSearchEchoesPreDeliveredPayload(t *testing.T) {
repo := t.TempDir()
write(t, repo, "src/auth.py", "def validate_token(token):\n return bool(token)\n")
path := filepath.Join(t.TempDir(), "presearch.txt")
if err := os.WriteFile(path, []byte(presearchPayload), 0o600); err != nil {
t.Fatal(err)
}

for _, testCase := range []struct {
name string
envName string
repo string
}{
{name: "documented name", envName: envPresearch, repo: repo},
{name: "harness alias", envName: envPresearchAlias, repo: repo},
{name: "repo never resolved", envName: envPresearch, repo: filepath.Join(repo, "absent")},
} {
t.Run(testCase.name, func(t *testing.T) {
t.Setenv(testCase.envName, path)
var out bytes.Buffer
err := Run(t.Context(), Options{
Version: "test",
Env: EnvFromOS(),
Stdout: &out,
Stderr: &out,
}, []string{"search", "--repo", testCase.repo, "--query", "validate the token", "--format", "text"})
if err != nil {
t.Fatal(err)
}
if out.String() != presearchPayload {
t.Fatalf("payload is not byte-identical:\n got %q\nwant %q", out.String(), presearchPayload)
}
// Named separately because it is the failure that matters: the call went to the graph.
if strings.Contains(out.String(), "def validate_token") {
t.Fatalf("search re-queried the repo instead of echoing:\n%s", out.String())
}
})
}

// Both names set: the documented one wins, so a harness that leaves the alias behind cannot
// silently serve a stale payload.
t.Run("documented name wins", func(t *testing.T) {
stale := filepath.Join(t.TempDir(), "stale.txt")
if err := os.WriteFile(stale, []byte("STALE"), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv(envPresearch, path)
t.Setenv(envPresearchAlias, stale)
var out bytes.Buffer
if err := Run(t.Context(), Options{Version: "test", Env: EnvFromOS(), Stdout: &out, Stderr: &out},
[]string{"search", "--repo", repo, "--query", "validate the token"}); err != nil {
t.Fatal(err)
}
if out.String() != presearchPayload {
t.Fatalf("alias overrode the documented name: %q", out.String())
}
})
}

// TestSearchRefusesUnreadablePresearchPayload pins that a payload the caller promised but cannot
// produce is an error, not a silent live query: a run that half-echoes and half-queries measures
// neither mode.
func TestSearchRefusesUnreadablePresearchPayload(t *testing.T) {
repo := t.TempDir()
write(t, repo, "src/auth.py", "def validate_token(token):\n return bool(token)\n")
t.Setenv(envPresearch, filepath.Join(t.TempDir(), "never-written.txt"))

var out bytes.Buffer
err := Run(t.Context(), Options{Version: "test", Env: EnvFromOS(), Stdout: &out, Stderr: &out},
[]string{"search", "--repo", repo, "--query", "validate the token", "--format", "text"})
if err == nil {
t.Fatalf("a missing payload fell back to a live query:\n%s", out.String())
}
if !strings.Contains(err.Error(), envPresearch) {
t.Fatalf("error does not name the variable that caused it: %v", err)
}
if out.Len() != 0 {
t.Fatalf("stdout is not empty after a refused echo:\n%s", out.String())
}
}
Loading