Three Trucks After a Flood (Creatura, Part 4)

Three trucks, four villages, one road that lies, and a judge that only believes the world.
So far every Creatura agent lived alone: one agent, one world, one set of tools that might lie to it. Real work is usually done by several machines that share one world and have to agree on who does what over a link that drops messages.
So I gave the agents bodies.
The convoy
The code for everything here is public on GitHub: github.com/egarim/creatura, and the full results are in its design note.
A flood has hit a valley. There is one depot, four villages (Alba, Brisa, Ceiba and Dalia) and twelve roads between them. Three trucks must carry 30 units of supplies from the depot to the villages.
The trucks differ: Truck 1 carries 6 units with a 10-unit tank, Truck 2 carries 4 with 8, Truck 3 carries 5 with 12. Every truck needs at least two trips.
Each truck is a creatura: an ordinary Creatura agent loop with its own regulator, its own cell (an identity with a signing key) and its own ledger (a signed log of who is working on what). The trucks coordinate by writing to their ledgers, "Truck 2 takes Brisa", "delivered 4 units", "I hand Ceiba back", and the entries travel between them over an in-memory mesh. In some arms that mesh loses 30% of its messages, the way a convoy's radio would.
The vessel, which already says which model an agent runs and on what computer, can now also describe a machine: a truck with this capacity, this tank and these sensors. That is only the specification. Where the truck really is and how much fuel it really has belongs to the world. The truck only believes it, from its sensors.
And the sensors can lie.
A world that lies, and a judge that does not listen to the trucks
The world does three bad things in every run:
- Two roads close somewhere between ticks 2 and 10. A tick is one step of the simulation; every truck takes one decision per tick.
- The first closed road keeps reporting itself open. The sensor says it is clear, the truck drives onto it, the radio reports arrival, and the truck has not moved.
- One truck's fuel gauge reads three units high. That truck believes it has fuel it does not have.
The lying road is the fault from Worlds that lie: a tool that reports success and changes nothing. Here the tool is "drive this road".
The judge reads only the world's own facts, which no truck can write: supplies that actually reached each village, trips to a village already served, trucks stranded, lies acted on, ticks used. A delivery a truck reports but the world did not receive counts as claimed, never delivered.
Everything random (closures, which road and which gauge lie, lost messages) comes from one seed, and the same seed gives the same run byte for byte. When a hypothesis fails, I can replay the exact run where it failed and watch it.
The first battery said no, four times
A battery runs every arm (one configuration of the trucks) over the same 50 seeds and scores hypotheses that were written down before the code. With 30 units per run, the best any arm can do is 1,500 units.
I covered that first battery in Part 3. The short version: of seven hypotheses, four failed. The plain loop with a shared ledger delivered 1,406 units. Trucks working alone, with no ledger at all, delivered more: 1,430. In 15 of 50 runs a village was left unserved while a working truck sat idle. And the trucks kept driving onto the lying road: 210 second tries on it in arm A alone.
Those failures were the most useful result, because each had a cause I could read in the replays. There were three, and each became a fix.
Fix one: a stuck truck still holds its claim
The original takeover rule said: if a truck's claim goes quiet for 5 ticks, with no progress and no heartbeat, another truck may take it over. That covers a truck that dies. It does not cover a truck that is alive and stuck. A truck that holds Ceiba's claim and cannot reach Ceiba keeps trying and keeps talking, so its claim never goes quiet, and everyone else politely waits for it until the run ends.
Without a ledger nobody waits for anybody: more trips are wasted, but more gets delivered.
The fix is a stalled-claim takeover. A claim whose truck has delivered nothing to its village for 12 ticks may be taken over, as a silent one can. Twelve ticks is enough for the longest round trip, depot to the farthest village and back, with a load and a refuel. Delivery reports carry a marker, so a truck that is only driving around does not count as making progress.
Fix two: distrust the road, not the tool
Creatura's control layer already catches a tool that lies: when a tool's promised effects did not land, it sets the tool aside for a while, then trusts it again. Each direction of a road is a different tool, and a flooded road does not get better after a cooldown. The control layer was distrusting "drive from the North crossing to Alba" when what it had learned was "the road to Alba is flooded".
The fix is an extension, not a change to the control layer. When the layer judges a drive a lie (the truck reported arrival and nothing landed), the truck stops trusting that road in both directions for the rest of the run, and tells its planner to avoid it:
if (verdict != ActionVerdict.Lied || !outcome.Succeeded || !tool.Id.StartsWith("drive:", StringComparison.Ordinal)) return;
var road = tool.Id.Split(':')[1];
if (!_roads.Add(road)) return;
A drive that failed honestly, such as running out of fuel, is not a lie about the road and does not count.
Fix three: a stranded truck hands its claim back
The truck with the lying gauge believes it has fuel and runs dry somewhere on the map. Before, it kept its claim, and its village waited.
The fix: a truck whose drive fails for lack of fuel hands back the claim it holds, on the ledger, and claims nothing more. Another truck can pick the village up immediately.
What the fixes did
Each fix became its own arm, tested on the same 50 seeds and the same floods: G for the takeover alone, H for distrust by road alone, I for the hand-off alone, and J for all three.
| arm | what changed | delivered | lies acted on | second tries on the lying road |
|---|---|---|---|---|
| A | the plain loop, shared ledger | 1,406 | 262 | 210 |
| G | stalled-claim takeover | 1,454 | 211 | 140 |
| H | distrust by road | 1,473 | 52 | 0 |
| I | hand off when stranded | 1,423 | 280 | 226 |
| J | all three | 1,500 | 54 | 0 |
Every pre-registered claim about a single fix held. Together they deliver every unit in every run, with no wasted trips and no second try on the lying road. Arm K, which adds a breakdown at tick 8, also delivered all 1,500, and never left a village unserved while a working truck stood idle.
Distrust by road does most of the work. On its own it cut lies acted on from 262 to 52.
A takeover spreads a lie before it stops it. With the takeover or the hand-off alone, a village changes hands and the new truck tries the same lying road, which is why arm I acted on even more lies than the plain loop. Moving work between agents moves the risk too, unless what the first agent learned moves with it.
The ledger still is not faster. Trucks working alone finish sooner, 18.1 ticks on average against 25.5 for arm J, because nobody waits to claim anything. The ledger wins on every other measure.
And one thing did not change: the truck with the lying gauge still runs dry, 6 times in 50 runs, in every arm. The fixes make the convoy survive it, not avoid it.
Shared distrust: one truck's warning
Arm J's distrust was private. A truck that caught the lying road avoided it from then on, but the other two kept trusting the road until they were caught too. Counting over 500 separate training runs (seeds 100 to 599, never the battery's), 106 of 490 road lies in arm J came after another truck had already caught the same road.
No cleverness inside one truck can avoid those, because a truck's view does not include another truck's distrust. This needed a design change, not a smarter planner.
The ledger got a new kind of entry: a Warning. It is one cell telling the others something it saw in the world, with a subject and the evidence. It is signed, verified and merged like every other entry, and it refers to no claim. A truck that catches a road lying now writes, beside its own distrust:
shared?.Warn(quot;road:{road}", quot;drove {tool.Id}: reported arrival, and nothing landed");
A truck that reads another's warning treats it as its own distrust: the road's drives disappear from what its planner can choose. The warning arrives the tick after it is written, like any entry on the mesh, and the planner itself did not change. That is arm W, with a new measure: road lies acted on after another truck had already acted on the same lying road.
| arm W (shared) | arm J (private) | |
|---|---|---|
| delivered | 1,500 / 1,500 | 1,500 / 1,500 |
| road lies acted on | 39 | 48 |
| of them, on a road another truck had already caught | 0 | 9 |
| lies acted on, all | 45 | 54 |
| stranded trucks | 6 | 6 |
Every road lie a warning could have prevented was prevented: the 9 in arm J became 0. Road lies fell by 19%, close to the one in five the training runs had predicted. Nothing else moved.
The 39 that remain are first discoveries: the first truck onto the lying road after it floods. Nothing in any truck's view tells that road apart from an honest open one, so nothing can avoid those. What this does not test yet is a warning that is wrong or forged; here only the control layer writes one, after a lie that really happened.
Watching a run
Numbers tell you whether something worked, not why. For the why, I needed to watch.
Every run can write a replay, a file of JSON lines: a header with the map, trucks and faults; one line per tick with each truck's true and believed state, what each village still needs, the closed roads, and every event with a sentence explaining it; and a last line with the judge's reading. Anything that reads JSON lines can show a run.
The viewer is a three.js page with no server and no build: open it and drop a replay on it. Flooded roads sink and turn red. The lying road stays the color of an open road, striped, because that is what it claims to be. Claims and reports travel as pulses between trucks, and a lost message fades halfway. A timeline lets you scrub through the run while the story is told tick by tick beside it. Each truck's fuel meter shows the real tank, with a mark where the gauge thinks it is.
Arm J, seed 0, tick 19: the road to Ceiba has flooded but its sensor still says open, and Truck 1 has run dry at South crossing while its gauge still read 3. Every village was served anyway.
Each truck also has a definition panel showing how its agent is built, including the C# that builds it, written out by the same code. The viewer is in the repository at tools/convoy-viewer; run the convoy sample with --replay and drop the file on it.
What the convoy taught me
A shared ledger is not coordination. It is a place to write things down. In the first battery the trucks with a ledger delivered less than the trucks without one, because a claim is a promise, and a promise from a stuck truck is worse than no promise.
The three fixes are all about the same idea: what one agent learns has to change what the group does. A stalled claim has to be freed. A stranded truck has to say so. A road that lied has to be distrusted, first by the truck that saw it, then by everyone.
And none of it would have been visible without a judge that reads only the world. If the score had come from what the trucks reported, the lying road would have been a perfect road.
This is Part 4 of a series about Creatura. Part 1 is how it began, Part 2 the architecture, Part 3 the hypotheses that failed. Next: who is working on what, the cells and the ledger.