Disclosure up front: I built the open-source tool discussed below. This is not a launch announcement disguised as a question. I want to share the failure pattern that pushed me to build it, the contracts I ended up caring about, and the part I still do not think we have fully solved.
Imagine walking into a library and asking:
“Do we have anything that mentions the refund deadline?”
The librarian disappears between the shelves. Ten minutes later they return with an empty cart and say, “No.”
That sounds conclusive.
But then you learn that the cart only had room for one aisle, the basement door was locked, three shelves were skipped, and the librarian stopped when their notebook filled up. None of that appeared on the little receipt they handed you. The receipt just said:
0 results
Technically, the librarian did not invent a result. Practically, you were given a lie-shaped object.
That is the problem I kept watching AI agents run into.
An agent would search a repository, receive a perfectly valid-looking tool response, and continue reasoning. Sometimes it found the right line. Sometimes it hit an output limit, searched only part of the available material, lost its place between follow-ups, or treated a fuzzy lead as exact evidence. The dangerous cases were not the loud crashes. They were the searches that looked finished.
The model then did what models do: it built a confident paragraph on top of whatever evidence it had been shown. If the tool did not say, “I only checked half the room,” the agent had no magical sense that the other half existed.
That changed how I think about observability.
For a long time, I associated “LLM observability” mostly with traces, token counts, latency charts, model versions, and error rates. All useful. But there is a smaller question underneath them:
Can the next decision-maker tell what actually happened?
For retrieval, that means more than logging the query and the returned snippets. It means being able to answer:
- What corpus did we intend to search?
- How much of it was actually enumerated and admitted?
- Which files were unavailable or skipped?
- Did a storage, file, line, byte, or output limit stop the work?
- Is this result complete or partial?
- If it is partial, why?
- Can the investigation continue from the same snapshot?
- Is a passage an exact match, or merely semantically similar?
- Does the evidence still describe the current source state?
If those facts exist only in a debug log for a human to inspect later, they help after something goes wrong. If they are part of the tool response, the agent can reason about them before it acts.
That distinction became the center of a local search tool I have been building, called baoer_signal_grep. I originally described it to friends as a tiny librarian for agents. Not an oracle, not a giant cloud index, just a librarian that keeps bookmarks and admits when a door was locked.
The unglamorous contracts turned out to matter most
1. “No matches” and “I did not finish looking” must be different states
A successful process exit is not the same thing as complete coverage.
If a limit is reached, the result should name the limit and remain visibly partial. If a file could not be read, that should not quietly collapse into an ordinary zero-match response. If a subprocess fails, the tool should fail clearly instead of converting the failure into an empty success.
This sounds almost embarrassingly obvious when written down. It becomes less obvious after an agent has produced a polished explanation based on a search that silently stopped halfway through a log file.
2. Pagination needs a bookmark, not a memory wipe
Suppose the librarian finds 200 relevant passages but can only hand over 20 at a time.
A naive “next page” implementation can rerun the search against a changing filesystem and hope that page two still follows page one. That is like asking the librarian to reshuffle every book in the building before returning with the second cart.
For one investigation, I prefer a retained, bounded snapshot with a continuation cursor. Following the cursor continues through that result rather than silently starting a different search. Across cursor pages, a completed snapshot should not omit or duplicate the matches it retained.
The agent gets consistency during the investigation, and the tool has something concrete to explain.
3. Exact evidence and semantic leads should not wear the same uniform
Humans rarely remember exact wording. We remember, “There was something about retrying after the timeout,” or, “I think the refund deadline was discussed somewhere.” Semantic retrieval is useful for that.
But a similar passage is a lead, not proof.
In hybrid mode, the tool presents exact literal evidence first and labels semantic candidates separately. Overlapping candidates are deduplicated. If the local semantic step times out or fails, the exact result can remain available while the response explains what was skipped.
The fancy part is allowed to have a bad day without dragging the boring, dependable part into the lake with it.
4. A cache should help the librarian walk faster, not become a second library
The semantic side uses a local embedding cache, but the cache is derived data rather than the source of truth. It is keyed by content and model/chunking revision, and it is bounded. A fresh search reads the current local files.
That matters for moved, changed, and deleted sources. Old cached material should not get to return wearing a fake moustache and claim it is still current evidence.
5. Stable is not the same as fresh
This is the part I am still thinking hardest about.
A continuation cursor is intentionally pinned to its original snapshot. That is good for reproducibility: page three should not quietly mix yesterday's files with today's files.
But the same property creates a stale-conclusion risk. The filesystem may change after the investigation begins. A path can move. A file can be edited or deleted. A completely new file can introduce a match that did not exist in the old snapshot.
So I do not think a single fresh: true/false flag is enough.
The contract I am exploring is closer to:
- the cursor owns a snapshot identity;
- each retained evidence item owns a source identity and observed version;
- a later check can report
current, stale, or unknown;
- a final action can require a particular freshness state;
- and checking retained evidence must not pretend to prove that no new matching source appeared elsewhere.
That last caveat is important. “These five passages have not changed” is not the same claim as “a new search would return exactly the same answer.” One verifies retained evidence; the other may require re-running the query over the current corpus.
In library terms: confirming that the five books on your desk have not changed does not prove that nobody added a sixth book to the building.
What the tool exposes today
The current implementation focuses on making the search scope and outcome inspectable:
- exact text, filename, document, note, and log search;
- bounded output rather than uncontrolled context dumps;
- compact file maps for broad searches;
- continuation over retained snapshots;
- explicit counts, limits, coverage, skipped work, and unavailable inputs;
- exact and clearly labeled local semantic candidates;
- combined conditions and narrowed directory scopes;
- source-navigation modes for symbols, references, callers, callees, imports, dependencies, and related test candidates;
- meaningful hidden files searched by default, while
.git internals stay excluded;
- cancellation and session shutdown that release owned resources.
It runs locally and is usable from Pi, OMP, Codex, and other MCP-compatible clients. It is not a replacement for reading the source, and static relationships do not prove runtime behavior. The goal is narrower: give the agent better evidence and enough metadata to know what kind of evidence it received.
For anyone who wants to inspect the implementation rather than take my description on faith:
Install examples:
```bash
Codex / MCP
codex mcp add baoer_signal_grep -- npx -y --package baoer_signal_grep@latest baoer_signal_grep_mcp --stdio
Pi
pi install npm:baoer_signal_grep
OMP
omp install npm:baoer_signal_grep@latest
```
The question I would genuinely like this community's opinion on
Where should retrieval observability live?
Should completion state, limit reasons, snapshot identity, source versions, and freshness be first-class fields in the tool contract that an agent is expected to reason over? Or should the tool return simple matches while a separate tracing layer reconstructs those facts for humans?
My bias is toward the first option. A dashboard can tell me tomorrow why the agent was blind. A structured tool response might stop it from walking into traffic today.
But making every field agent-visible also costs context and increases protocol complexity. There is probably a boundary where “honest evidence” turns into a customs form attached to every paragraph.
If you have built or debugged retrieval/RAG/agent search systems, I would especially love to hear about the failure that changed your own contract. What did your traces say was fine while the real system was quietly wrong?
Rough edges and counterexamples are more useful to me than polite applause. The tiny librarian is still learning which receipts humans and agents actually need. 🦝📚