Essay

The model is the smallest part of a deployable agent

What makes an agent usable in a regulated credit decision is everything around the model: an ontology that will not load a broken book, a semantic layer that owns the definitions, a compiled procedure that fixes the steps, policy a person can change without rewriting a prompt, an audit packet keyed to one decision id, and a promotion gate written in code.

Six months after a loan is declined, someone asks why. They do not want to know what the model thought. They want to know which rules fired, which facts those rules read, which version of the policy applied, and who confirmed the decision. Brightwater is a loan agent I built to answer that question. Its Python package is about 5,100 lines across 27 modules, and the decision-model code, retraining loop included, is under 900 of them. The rest exists so that question still has an answer six months later.

What makes an agent usable in a regulated credit decision is everything around the model: an ontology that will not load a broken book, a semantic layer that owns the definitions, a compiled procedure that fixes the steps, policy a person can change without rewriting a prompt, an audit packet keyed to one decision id, and a promotion gate written in code.

What Brightwater is

Brightwater is a clean-room implementation of that lifecycle, tagged one chapter at a time. It started from a Neo4j talk and reuses none of the talk's code, data, prompts or text. It builds on neo4j-agent-memory from Neo4j Labs for the memory graph and on AIP, the Agent Instruction Protocol for the compiled procedure. It runs from systems of record to a sealed audit packet and a retraining loop. Tests pin the policy boundaries, data contracts and procedure behavior, and CI runs them against Postgres, Neo4j and Cube without calling or training a model.

The book is fictional. The names, balances and six hand-built applications are invented, and a seeded generator adds 1,200 more. This is a tutorial, not a production bank system, and not evidence of regulatory compliance. What it shows is exactly where the model is allowed to act.

The numbers behind the claim

Policy is seven rules in a pure function. Unit tests pin each boundary: KYC, a 365-day adverse-event lookback, DSCR at 1.25 versus 1.2499, a $1,500,000 exposure cap, a 25% affiliate 60 days past due, a person on any decline for a customer of ten years or more, and a person above $2,000,000 of group exposure. The function reports every rule that holds, not only the one that decided, and it sees identifiers, booleans and numbers, never names, dates of birth or relationship notes.

Once the procedure is compiled, the model does not choose the order of work and does not apply those seven rules. It answers three typed questions, about risk tier, cash-flow stability and a recommendation, and it writes the rationale. A person confirms only when the procedure says so. You can read all of that in the skill graph without running a benchmark.

The benchmark still matters. Chapters 3 and 4 each record 18 runs over the six hand-built applications. The free-form agent took a median of 4 model turns at a median cost of $0.1118 a run. The compiled procedure took a median of 2 turns at a median cost of $0.0355, half the turns at about a third of the cost. Both filed the policy outcome on all 18 runs, and that 18 of 18 comes from the policy script, not from the model being right. The small model Chapter 6 trains agrees with the rubric between 73% and 83% of the time on the held-out split, depending on the question. Those are different columns, and I will not average them into one number.

Try it

git clone https://github.com/tankcdr/brightwater-agent
cd brightwater-agent
git switch -c my-ch5 ch5

Every chapter has an annotated tag, ch0 through ch10. Chapters 0 to 3 run on Python 3.12, and from Chapter 4 on you recreate the environment with uv venv -p 3.14 --clear. The walkthrough is in docs/working-through-the-chapters.md.

A note on the numbers. Tests cover the policy boundaries and the semantic-layer change. The medians, costs, model agreement, Chapter 7 filing counts, policy impact and retraining cycle come from the chapter writeups, and their run logs are not committed, so regenerating them means running the chapters with a paid model API and local model hardware.

Each section below previews a companion post still to come.

Ontology

Northpine’s own payment history isn’t the whole credit picture. Priya Lindqvist owns 40% of Northpine Freight and 35% of Ridgeback Tile, whose loan is 80 days past due. Brightwater’s policy checks that shared ownership when assessing Northpine. Chapter 1 models these relationships and validates the records before loading them into Neo4j. The compiled procedure introduced in Chapter 4 gets the same relationships through SQL instead. There isn’t yet a test proving that the graph and SQL return matching results. Start at Chapter 1 with git switch -c my-ch1 ch1.

Semantic layer

Debt service coverage is one SQL expression in one definition file, and the approve line of 1.25 is not in it. Chapter 2 records 20 of 1,206 applications changing when non-operating inflow is dropped. One is Larkspur Bindery, which falls from an approve at 1.33 to a review at 1.18 while the old hand-written SQL still reports 1.3297. Start at Chapter 2 with git switch -c my-ch2 ch2.

Compiled procedures

The skill graph is fixed: gather facts, apply policy, assess, draft a rationale, file, and stop, or pause for a person when the router says so. The model never applies a policy rule, but it can still keep a decision from being filed. When Chapter 7 moved the rationale writer to a local qwen3:8b model, 15 of 18 recorded runs filed, and only 3 of those 15 passed the linter on the first try. All three failures were Northpine, where the linter rejected figures the rationale had not cited. The model is small in the authority it is given. It is not small in the failure mode of the writing step. Start at Chapter 4 with git switch -c my-ch4 ch4.

Governed policy

A policy change is a new skill revision, not a new prompt. Chapter 5 adds a seventh rule, a person review above $2,000,000 of group exposure, and its recorded impact run touched 51 applications without changing a single outcome. Thirty-two reviews now need a person, and nine declines that used to file automatically now wait for one. The other ten were declines P6 already held for a person, and they now record P7 as a second reason. An outcome-only diff would have reported no change, while 41 decisions started waiting for a person. Start at Chapter 5 with git switch -c my-ch5 ch5.

Audit

Each run mints a decision id first, and that id joins the packet: who acted, records read and cited, the semantic hash, the procedure revision, policy versions, the model calls, and a re-linted rationale. The pgaudit read log joins it only when an operator sets BRIGHTWATER_PGAUDIT_LOG, and the walkthrough's sample packet has that field empty. Each packet is hashed and chained to the one before it. Edit one in the middle and verification fails. Delete the newest ones and it passes, which is why the chain head has to live outside the database. The same post covers identity and private inference, because the packet has to say who acted and where inference ran. Chapter 7's network witness samples connections every 100 milliseconds, and the chapter shows it reporting a brief external connection as clean, so a clean report cannot establish that no data left. Start at Chapter 9 with git switch -c my-ch9 ch9.

Retraining loop

The promotion rule is in code, not in a prompt. A candidate replaces production only if its agreement is no more than 0.005 below the current model's and its calibration error is no more than 0.02 above it. Automatic confirmations never become training labels, and a test enforces that. The recorded Chapter 10 cycle used no answers from a person and promoted the version it started with, which proves the gate accepts an identical model and nothing more. The retraining post will run a cycle on answers recorded at the review console, against a candidate the gate should reject. Start at Chapter 10 with git switch -c my-ch10 ch10.

What I would do differently

  • Keep the audit chain head outside the database from the first run, not as a footnote.
  • Fail a policy sync when parameters change and the version number does not. Today a sync can overwrite the historical row, and nothing stops you.
  • Cross-check the memory graph against the semantic layer's related-facility query in a test.
  • Commit redacted run logs, so a clone can rebuild every median in this post.
  • Back the network witness with the sealed compose network, and add the test Chapter 7 still lacks.

For the proof rather than the argument, clone the repository and start at a tag.