← all builds

Dual Agent Auditor Pattern

agentsverificationreliability
Download .md

A sanitised reference implementation. Drop it into Claude Code, point it at your own schema, adapt the prompts. Nothing here is tied to any specific business; the schema and field names below are generic on purpose.

The problem

When an automated step writes a record to your database, how do you know it wrote the right one? You can log the action. But the same bug that causes a wrong write logs it just as confidently. You end up with a confident wrong record, and every downstream decision inherits the error.

The pattern

Two agents, separated by capability:

  1. Matcher — proposes a record (a reconciliation, a classification, a link between two rows). It can be cheap and eager.
  2. Auditor — a second, independent agent that only reads. It has no write tools. It looks at the source data and the proposed record and returns a confidence verdict.

A write only happens when the auditor confirms. Otherwise the proposal stays in a pending state for a human to glance at. The auditor fails closed: uncertain is treated exactly like a definite mismatch. Ghost records can never accumulate silently — the worst case is a pending item, never a confirmed but wrong one.

source rows ──> [matcher] ──proposal──> [auditor: read-only] ──┐
                                                               │ confirm?
                                          pending <── no/unsure ┘
                                          write   <── yes (>= threshold)

Minimal reference implementation

Generic schema — rename to fit yours:

CREATE TABLE records   (id INTEGER PRIMARY KEY, key TEXT, payload TEXT, status TEXT DEFAULT 'pending');
CREATE TABLE proposals (id INTEGER PRIMARY KEY, record_id INTEGER, proposed_key TEXT, source_ref TEXT);
CREATE TABLE audit_log (id INTEGER PRIMARY KEY, proposal_id INTEGER, verdict TEXT, confidence REAL, note TEXT, ts TEXT);
# dual_agent_auditor.py  — reference only, swap in your own LLM client + schema.
import sqlite3, json

CONFIDENCE_THRESHOLD = 0.85   # tune per task; higher = more conservative

def matcher_propose(conn, source_ref):
    """Cheap, eager. Produce a candidate record from a source reference.
    Replace this body with your own retrieval / LLM match logic."""
    # ... your matching logic returns a dict like:
    return {"proposed_key": "abc-123", "source_ref": source_ref}

def auditor_verify(source_row, proposal):
    """READ ONLY. No DB handle, no write tools. Returns (verdict, confidence, note).
    In production this is a separate agent/model call whose system prompt says:
    'You verify. Default to refuted when uncertain. Return JSON {match, confidence, note}.'
    The stub below shows the contract; replace with your model call."""
    # result = llm_json(system=AUDITOR_PROMPT, user=render(source_row, proposal))
    result = {"match": False, "confidence": 0.0, "note": "stub"}   # <- replace
    verdict = "confirm" if (result["match"] and result["confidence"] >= CONFIDENCE_THRESHOLD) else "reject"
    return verdict, result["confidence"], result["note"]

def run(conn, source_ref, source_row):
    p = matcher_propose(conn, source_ref)
    verdict, conf, note = auditor_verify(source_row, p)
    cur = conn.cursor()
    cur.execute("INSERT INTO proposals(proposed_key, source_ref) VALUES (?,?)",
                (p["proposed_key"], p["source_ref"]))
    pid = cur.lastrowid
    cur.execute("INSERT INTO audit_log(proposal_id, verdict, confidence, note, ts) "
                "VALUES (?,?,?,?,datetime('now'))", (pid, verdict, conf, note))
    if verdict == "confirm":
        cur.execute("UPDATE records SET key=?, status='confirmed' WHERE id IS NOT NULL AND status='pending'",
                    (p["proposed_key"],))   # scope this to the real record id in your code
    # else: leave status='pending' for a human. Fail closed: do nothing on uncertainty.
    conn.commit()
    return verdict

Why it works

Adapt it

Built for my own multi entity stack; this is the generalised version. Take it, rip it apart, tell me what breaks.