Classic RAG retrieves fuzzy chunks and hopes the model does not invent the next three lines. Tool-augmented RAG flips the last mile: use retrieval only as a pointer, then force a read_file(path, start, end) tool against the live tree before any claim ships to the engineer. You keep recall from vectors while answers cite exact bytes that exist on HEAD.
⚡ TL;DR: Treat KB hits as candidates, not evidence. Require a file-range tool call (with SHA) before final answers. Reject completions that lack verified citations. Cap tool depth and bytes. Combine with Citation-required RAG and Bedrock Converse tool choice.
Retrieval proposes; tools dispose
// agent/tool-rag.ts
type Hit = { path: string; start: number; end: number; score: number; blobSha?: string };
export async function answer(question: string, retrieve: (q: string) => Promise<Hit[]>) {
const hits = await retrieve(question);
// ✅ Pass hits as tool hints, not as final context dump
const system = `You MUST call read_range for every claim. Never invent lines.`;
return runAgent({ system, tools: [readRangeTool], hints: hits.slice(0, 8), question });
}
❌ Stuffing top-12 chunk texts into the prompt and skipping verification — that is ordinary RAG with extra latency.
Exact range tool with SHA checks
# tools/read_range.py
from pathlib import Path
import hashlib
MAX_BYTES = 24_000
def read_range(path: str, start: int, end: int, expect_sha: str | None = None) -> dict:
p = Path(path).resolve()
if not str(p).startswith("/workspace/"): # ✅ allowlist root
return {"error": "path_denied"}
text = p.read_text(encoding="utf-8", errors="replace")
sha = hashlib.sha1(text.encode()).hexdigest()
if expect_sha and sha != expect_sha:
return {"error": "stale_blob", "sha": sha}
lines = text.splitlines()
start = max(1, start); end = min(len(lines), end)
if end < start or (end - start) > 400:
return {"error": "range_invalid"}
body = "\n".join(lines[start - 1 : end])
if len(body.encode()) > MAX_BYTES:
return {"error": "too_large"}
return {"path": str(p), "start": start, "end": end, "sha": sha, "text": body}
Force tool choice until evidence exists
With Bedrock Converse, prefer toolChoice: any (or specific read_range) on the first turn when hints are present, then auto once citations are attached. Fail closed if the model tries to answer with zero successful tool results.
{
"toolChoice": { "tool": { "name": "read_range" } },
"comment": "First hop must verify a candidate path from retrieval"
}
Budget depth, bytes, and loops
| Guard | Default | Why |
|---|---|---|
| Max tool calls | 6 | Prevent path thrash |
| Max lines / call | 400 | Keep context sane |
| Max unique paths | 4 | Focus the answer |
| Stale SHA | Re-retrieve or abort | Avoid lying about HEAD |
Log every tool I/O for deterministic replay. Pair with LLM output validators when the agent also patches.
Closing checklist
✅ Dos
– ✅ Use RAG hits as pointers into read_range
– ✅ Verify blob SHA before trusting lines
– ✅ Require citations from tool results in the final answer
– ✅ Allowlist repository roots; cap bytes and depth
– ✅ Fail closed when tools return errors / stale SHAs
❌ Don’ts
– ❌ Don’t paste unverified chunk text as ground truth
– ❌ Don’t allow unbounded recursive reads
– ❌ Don’t skip tool choice on the evidence hop
– ❌ Don’t read secrets paths (.env, keys) even if retrieved
– ❌ Don’t answer from memory when the tool failed
Related reading
- Citation-Required RAG Answers: Force Models to Quote File Paths
- Bedrock Converse API: Tool Choice Modes for Production Coding Agents
- Deterministic Replay: Agent Sessions You Can Debug in Postmortems
- Agent Tool Allowlists: Least Privilege for Filesystem and Shell Access
Last updated on September 11, 2026
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
