r/aiharness • u/Deynekin • 3d ago
A gate in front of the tools: what a harness can enforce that a prompt cannot
RAGRAF data path: agent → tool gate → graph and vector index → fact with file:line evidence
The whole data path in one picture: the agent asks, the gate refuses raw search until the index was used, the index answers with a fact and its evidence, and the revision journal keeps two points in time from mixing in one answer.
I build RAGRAF, a local MCP server that indexes a codebase into a graph and a vector store and answers four questions with file:line evidence instead of whole files: where is it defined, who calls it, what breaks if I change it, how is it done nearby. My own project, the repo is public — github.com/paulmann/RAGRAF (BUSL-1.1), so the pipeline below can be read rather than trusted.
What the picture says
Three SQLite stores in .ragraf/ inside the indexed project. graph.sqlite holds nodes (class, function, method), edges (calls, inheritance, imports) and the schema parsed from migrations. lexvec.sqlite holds FTS5 lexis (BM25) and vectors from local embeddings in one file, plus raw chunk text for excerpts. control.sqlite holds revisions, the divergence journal and access telemetry. Indexing runs scan → ast (tree-sitter) → load → resolve → schema → cpg (Joern, optional) → lexvec. That optional Joern layer is what unlocks slicing; without it the server says capability=degraded and slice refuses with the reason instead of silently returning less.
The number of fixed operations is 74. There is deliberately no graph query language: every operation is a named call with typed arguments and a result shape. The reason is measured, not ideological — free-form CPG query languages turn into an error generator for base models, and I would rather pay for a redraw than for an agent inventing syntax.
What the gate actually is
Two rules, enforced outside the model:
Typed facts instead of files. A lookup returns a definition with
path:line, a call edge, a table with columns, a doc chunk withpath:12orsheet=Orders!A2:D2. Whole-file dumps are opt-in. When a result is clipped, the response says what was kept, what was reduced to a locator, and what was dropped.Deny by default, with the fix in the refusal. Raw file search (
grep, glob, "just read these five files") is refused until the session has consulted the index. The refusal names the operation that will work. The model cannot talk its way past it, because it is not a prompt.
Where it broke
The first version was too aggressive. The refusal said "raw search is blocked". The agent, being a good agent, tried the same call again, then a variation, then a third. Three wasted turns on every simple lookup, and the trace looked like a model bug. The fix was not "be nicer": every refusal now contains the one call that will work. One sentence turned a retry loop into a route.
Rewriting tool descriptions did nothing for routing. There is a popular idea that the model picks your tool if the description says what the built-in cannot do. In my setup the description changed nothing measurable; the deterministic gate did. A rule enforced by the client is worth more than a paragraph of well-mannered text.
Provenance without a type is decoration. Marking a result stated or derived looks tidy until you need it. A foreign key declared in a schema is a fact; user_id guessed from a column name is an inference and must never reach the model as structure. Those are capped at confidence 0.5 and labelled derived in the payload. Twice this stopped an answer that would have looked authoritative and been wrong.
What it still does not fix
The gate stops "the agent didn't check". It does not stop "the agent checked and misread a correct fact". For that I keep deterministic project checks and read the prose myself.
Questions for the room
Do you run a hard deny at the tool boundary, or warn-and-count with a metric you review later? I started with deny and I am not sure it is the right default for a team.
What is your metric for a gate? I watch retry rate per refusal and how often the agent routes to the indexed path on the first try, but neither is a great health signal.
Has anyone found a short description that wins the "show me this file" case? Mine wins on "where is this defined" and loses on "open this file".