DEV Community

Stephen Sookra
Stephen Sookra

Posted on

How I built Curtail: a governed agent fleet for water curtailment, where the human always signs

In August 2022, a water association serving about 80 ranchers ran its pumps for eight days against a standing drought curtailment order on the Shasta River. The proposed penalty came to $4,000 in total, which is $500 per day. One rancher said, on the record: "We could have kept going for $500 a day."

The statute has since moved. Water Code 1846(b), as amended effective January 1, 2025, sets the rate at $10,000 per day. But the regulation a person actually reads, 23 CCR 875.9(b), still prints $500. The published rule understates the law by a factor of twenty, and nothing in the state's tooling notices.

That gap is the product. Curtail is my entry to the Google x Devpost All Things Agentic Hackathon: a governed multi-agent system that watches a river in real time, computes who must stop diverting under California prior-appropriation priority law, and drafts the curtailment order for a named human official to sign. Nothing self-executes. This post is about how it was built, and what building it taught me about putting language models anywhere near legal text.

The first decision: what may never be an LLM

The center of the system is not an agent. The Allocation Core, the thing that decides which priority groups a curtailment must reach, is deterministic Python with golden tests and property tests. Priority ordering, decree membership, gage reading against the operative minimum, computed shortfall: these are facts, and facts do not need a temperature parameter.

Just as deliberately, the Core produces a recommendation, never a determination. The regulation (23 CCR 875(b)) assigns the determination to a human official and expressly preserves their discretion to decline, narrow, or suspend. So the codebase uses the word "recommendation" throughout, and "determination" is reserved for the signature. The signed record shows what the official actually considered, including the judgment inputs the system surfaces but refuses to auto-resolve: weather, fisheries information, voluntary contributions, groundwater evidence.

Around that core sits a four-node fleet built on Google's Agent Development Kit, running on Cloud Run: a Gage Sentinel that classifies USGS readings against date-period-bounded minimums, an Order Scribe on gemini-3.5-flash via Vertex AI that drafts the legal document, a Herald that models the four legally distinct service methods under Water Code 1121 and never reports a notification as legal service, and a Season Ledger holding the statutory clocks (reconsideration windows, response deadlines) durably in Cloud Firestore. Durability was proven the only way I trust: a second, independently constructed client read the season back after the writer was gone.

Guards, because models fabricate exactly where it hurts

Before writing any code I had six external models review the concept. Two of them cited case law that does not exist in any database. Invented authorities, with confident citations, in a legal domain. That settled the security design: a system prompt forbidding invention is not a guardrail.

So the Scribe's output passes through a deterministic, server-side citation scrubber after the model call. Any authority not present in a verified allowlist is stripped and flagged before the draft can reach the document generator, and the fabricated citations from that review ship in the repository as a CI test fixture, so the guard can never quietly stop guarding. A second layer, ledger validation, rejects any draft that asserts a right, a priority date, or a tier the deterministic Core did not compute, with one retry and then escalation to the human queue marked UNVERIFIED. Model Armor screens untrusted order text on the way in, chunked to fit its documented prompt-injection window, and an unreachable screen reports UNAVAILABLE rather than clean.

The same discipline applies to my own prose. Every number in the demo video narration, the README, and the Devpost description draws from one generated fact sheet, docs/FACTS.md, computed from the shipped code. CI fails if the file drifts from the code that produces it. A gate reads the narration script and refuses any figure, digits or spelled out, that the fact sheet cannot source. That gate caught real errors before they reached a published, uncorrectable video.

Testing against the Board's own decisions

The credibility artifact is a backtest. I fetched the State Water Board's published curtailment orders and addenda for the Scott and Shasta rivers, 98 PDFs byte-verified, 95 scorable, and replayed the gage record against them. Curtail reproduces the direction of 6 of 6 scored historical curtailment decisions, with 5 documents excluded before scoring for stated reasons (a bound is not a reading; a named-diverter scope is not a basin threshold decision). The exclusions are reported next to the result, because a denominator you cleaned in private is not a metric.

The rights tables are the Board's own attachments, not synthetic data. The Scott table carries 384 rights, and here is the finding that justifies the whole architecture: the Board's attachment states each right's curtailment group in its own column, and inferring those groups from the rights' attributes instead agrees with the Board on 8 of 384. Group 1 is curtailed first and group 8 nearly last, so that gap is the difference between a ranch irrigating and a ranch shutting off. Read the record; never re-derive what the agency already stated.

My favorite fixture is July 2025. Fort Jones read 48.7 cfs on a Sunday night and curtailment was reinstated. Community members disputed the measurement, the watermaster district ran field flows, USGS revised the rating curve, and the same water read 78.4 cfs on Tuesday morning. Curtailment lifted. The river never rose; the measurement moved. An agent system that cannot be corrected by the person responsible for it is not governed, so that sequence, human evidence flowing back into the machine's recommendation, is the demo's centerpiece, and the near-threshold band that flags "field verification recommended" exists because of it.

There is also a scored axis for restraint: five cases where the right answer is not the obvious action, including a reading inside the near-threshold band and rights the Board published without a priority date. The system refuses or withholds rather than extrapolating. Scoring that axis immediately found a real bug: the Sentinel would classify a negative discharge, which on a sensor fault reads as far below minimum and points at curtailment. That is now unrepresentable at the domain object.

Data sovereignty, concretely

One more piece worth naming: document intake runs Gemma (gemma3:4b) locally through Ollama. It reads a published Board order and returns four filing fields, each verified verbatim against the source text before acceptance, and no document leaves the machine. An agency that cannot send landowner records to a third-party inference API can host these weights itself. The model files documents; it is not permitted to read law out of them.

What I learned

Most of my defects were false greens: checks that reported success without checking. A guard scoped to tracked files cannot see the file you are about to commit. A web application firewall serves its block page as HTTP 200, so a corpus fetcher that trusts status codes downloads error pages named as PDFs. A test can assert a figure that no primary document contains, and then defend the error against correction. The pattern underneath all of them: verify the artifact, never the action that claims to have produced it. That principle is also the product thesis, which is the kind of symmetry you only notice after the third incident.

The demo video's agent section is one continuous, uncut take of the production system, including the full length of the real Gemini call, because a rules requirement ("an unedited, live execution") is also just the honest way to show an agent working.

See it

I created this piece of content for the purposes of entering the All Things Agentic Hackathon. #AllThingsAgenticHackathon

Top comments (1)

Collapse
 
reidmarlow profile image
Reid Marlow

This is a good boundary for agents around law. Keeping allocation in deterministic Python is the part that makes the rest usable, because the draft can be wrong without silently changing who loses water. I’d probably add one more artifact beside the human signature, a tiny diff between the cited rule text and the current statute. That mismatch is exactly where the system earned its keep.