What building a contract-cancellation agent taught me about precedent, deterministic gates, and auditable autonomy
I created Reaper and wrote this article for the purposes of entering the All Things Agentic Hackathon.
“Have we seen this before?”
It is one of the most useful questions an experienced person can ask.
A lawyer remembers a clause that caused trouble. An accountant remembers a vendor that continued billing after cancellation. An engineer remembers the deployment that failed under eerily similar conditions.
So when I added memory to Reaper—an agent that manages auto-renewal cancellations—I expected retrieval to be the easy part.
Embed the current clause. Search for similar clauses. Show the closest matches.
That took a morning.
Deciding what memory was allowed to do took much longer.
Because an agent that remembers previous decisions can also inherit previous mistakes. Worse, it can turn resemblance into authority:
This clause looks 97% like one we blocked before, so I blocked this one too.
That sounds intelligent. In a system acting on contracts, it is dangerously close to superstition.
The solution was not better prompting. It was architecture.
The expensive difference between “similar” and “the same”
Imagine a contract that costs $540 per month and renews for another year unless notice arrives 60 days before the term ends.
Miss that deadline and a small administrative failure becomes a $6,480 mistake.
Reaper’s job is to prevent that failure. It reads the renewal clause, computes the deadline, waits until the notice window, requests one human signature, sends the notice, and later checks whether billing stopped.
But the model is not permitted to schedule an obligation simply because its answer sounds confident.
Gemini proposes a deadline. A separate deterministic engine—regex and calendar arithmetic, with no language model inside—derives the date independently.
The decision is deliberately boring:
model_deadline = model.read(clause)
engine_deadline = date_engine.derive(clause)
if engine_deadline is ambiguous:
block("The clause cannot be derived safely")
elif model_deadline != engine_deadline:
block("The two readings disagree")
else:
schedule(engine_deadline)
This gate has already caught a real one-day model error. In contract work, “only one day” can mean another year of fees.
It also refuses clauses such as:
written notice no later than sixty (90) days before renewal
A model can invent a plausible interpretation. Reaper cannot. The contradiction becomes the result.
That was the original trust boundary: the model may propose; deterministic code decides whether the proposal is safe enough to act on.
Then I added precedent memory and nearly weakened it.
Why useful memory can make an agent less trustworthy
The new memory system stores clause shapes and what happened to them:
- Did the deterministic gate accept or block the clause?
- Did the vendor stop billing?
- Was a dispute opened?
- Did the contract require email, registered post, certified mail, or a portal?
- Was the record a real obligation or a labelled test fixture?
Each clause is embedded with gemini-embedding-001 and stored as a 768-dimensional vector in BigQuery. At intake, BigQuery’s native VECTOR_SEARCH finds clauses with similar meaning even when their wording differs.
For example, a new clause might resemble:
- a contradictory clause that was blocked;
- a clean cancellation the vendor honoured;
- a valid notice after which the vendor billed anyway; or
- a contract where email was only a courtesy copy because registered post was required.
That is valuable context.
It is also capable of contaminating the current decision.
Suppose the current clause cleanly says “sixty (60) days,” but a highly similar prior clause said “sixty (90) days” and was blocked. If precedent is fed into the model before the gate rules, the old contradiction can anchor the new interpretation.
The system begins deciding by analogy when it should be deciding from the document in front of it.
So Reaper follows a strict ordering rule:
CURRENT CONTRACT
↓
DETERMINISTIC GATE
↓
VERDICT IS FIXED
↓
PRECEDENT LOOKUP
↓
ADVISORY CONTEXT FOR THE REPORT
Memory arrives after the verdict.
A test makes this boundary executable: even a 97%-similar blocked precedent cannot change a clean clause from MATCH to BLOCKED.
The warning may say:
A near-identical clause was blocked before because its words disagreed with its numerals. This is prior history, not a verdict.
But the current clause keeps the result earned from its own text.
That last sentence—“This is prior history, not a verdict”—is more than interface copy. It describes the permission model.
Store outcomes, not just documents
A vector database full of text can answer:
What looks similar?
An institutional memory should answer:
What happened next?
That distinction changed the BigQuery schema.
Reaper stores the clause alongside structured outcome fields: gate verdict, notice method, final status, whether billing stopped, whether a dispute opened, and the terminal receipt hash when one exists.
The embedding finds semantic neighbours. The structured fields explain why those neighbours matter.
This lets the system distinguish two contracts with nearly identical renewal language:
- One was cancelled successfully and billing stopped.
- The other was cancelled correctly, but the vendor charged again and the obligation went to dispute.
The text is similar. The operational lesson is not.
This pattern applies beyond contracts. If you are building memory for a support agent, store whether the proposed resolution actually worked. For a deployment agent, store whether the rollout was rolled back. For a collections agent, store whether the promised payment arrived.
Retrieval without outcomes is recollection without learning.
Provenance has to survive retrieval
Reaper begins with a labelled corpus of synthetic clause shapes so the memory system can be tested reproducibly.
Those rows must never masquerade as customer history.
Every precedent therefore carries a source such as fixture or a real ledger-backed obligation. A seeded example may demonstrate that registered post is required, but the product cannot imply that a real customer previously encountered that vendor.
This sounds like metadata housekeeping. It is actually a trust boundary.
Once retrieved text is condensed into a sentence, provenance disappears easily. “A similar contract was blocked before” means something very different from “a synthetic test clause of this shape is expected to be blocked.”
If your agent mixes examples, simulations, generated data, and production history, provenance must travel with the memory—not live in a forgotten README.
A missing memory is also a result
There is another tempting failure mode:
try:
matches = search_memory(clause)
except Exception:
matches = []
Now “BigQuery was unavailable” and “no similar precedent exists” look identical.
They are not.
Reaper’s lookup fails open. If the table is disabled, expired, unreachable, or the embedding call fails, the contractual verdict remains unchanged. But the evidence chain records that memory was unavailable and why.
The system distinguishes:
- memory was consulted and no match passed the threshold;
- memory was disabled;
- the embedding could not be created; and
- BigQuery could not be reached.
An unavailable advisory system should not stop a valid cancellation. It also should not quietly pretend that history was checked.
Failure transparency is part of memory design.
The evidence should include restraint
Reaper records every material action in an append-only SHA-256 hash chain: extraction, precedent consultation, the gate verdict, autonomous wakes, approval, delivery, invoice verification, and dispute creation.
But the more interesting receipts describe what the agent did not do.
The mailbox pipeline scans headers first. Messages that fail its admission rules remain unopened, and those refusals are recorded. If the system declines to inspect a bank statement from an unrelated sender, the refusal becomes evidence of restraint.
The same principle applies to precedent:
- memory cannot change the gate;
- failed retrieval cannot become “no history”;
- fixture data cannot become customer history; and
- raw vectors do not need to be copied into the operational evidence ledger.
A trustworthy agent should be able to prove not only what it did, but which powers it deliberately refused to exercise.
Long-running agents spend most of their lives doing nothing
Contract cancellation also exposed a less glamorous agent problem: waiting.
A cancellation agent may read a document today and need to act six months later. It should not burn tokens, hold a process open, or depend on someone remembering to restart a chat.
Reaper persists the obligation and sleeps. When the notice window opens, it wakes and writes a receipt showing that calendar time—not a human pressing “continue”—triggered the action.
It then pauses at the one point where authority is genuinely required: signing and sending the notice.
That pause survives a process restart because the invocation and resume pointer live in persistent state rather than RAM. Killing the server does not manufacture a new approval or lose the old one.
This led me to a broader rule:
Human approval should be a durable state transition, not a conversational moment.
If an approval disappears when the process dies, it was never part of the workflow. It was part of the demo.
Five rules I would reuse in any high-consequence agent
After building the date gate, durable approval, evidence chain, and BigQuery precedent memory, these are the rules I would carry into another agentic system:
Separate proposals from permissions.
The model can recommend an action without possessing the authority to execute it.Run deterministic checks before advisory retrieval.
Memory should explain a decision, warn about history, or help a human—not rewrite facts already established from the current input.Store outcomes with memories.
Similar text becomes useful only when paired with what actually happened.Preserve provenance and failure states.
“Fixture,” “production history,” “no match,” and “search unavailable” are four different facts.Test boundaries, not just happy paths.
The most important test is not whether memory returns a match. It is whether an alarming match can improperly change the verdict.
The agent I trust is the one that distrusts itself
We often describe capable agents by listing everything they can do.
I have become more interested in the opposite list:
- The model cannot schedule its own date.
- A contradictory clause cannot be resolved by confidence.
- Similar history cannot overrule the current evidence.
- A missing lookup cannot pretend to be an empty result.
- A process restart cannot erase a pending human decision.
- An unread message cannot quietly become a read one.
That negative space is where trust comes from.
Memory makes an agent more experienced. Boundaries stop that experience from becoming prejudice.
Determinism is not the opposite of agency.
It is what makes agency defensible.
Reaper is my solo entry for the All Things Agentic Hackathon.
Repository: https://github.com/N-45div/Reaper
Live project: https://reaper-sxxs.onrender.com
Top comments (0)