MG12 design
← All articles Essay · AI engineering

The whisper game

Ask a coding agent to build something with several steps and you get a chain of small functions, each handing its output to the next. By the end of the chain the data is a rumour of what arrived. The fix is not a better prompt; it is telling the agent to orchestrate, and to persist one canonical entity through the whole journey.

Give a coding agent something genuinely multi-step. Place an order. Plan a journey. Take a complaint through to resolution. It will decompose the problem sensibly, and it will almost always decompose it the same way: one function or one sub-agent per stage, each taking the previous stage's output as its input.

You end up with something that reads beautifully.

parseRequest to validateItems to checkStock to calculatePricing to createOrder to arrangeDelivery to sendConfirmation. Small units. Single responsibilities. Clean signatures. Every one of them testable in isolation, and every one of them tested. It passes review, because each piece is exactly what you would have asked for if you had asked for that piece.

And then the outcomes are subtly wrong in ways nobody can trace.

Each function is handed what the previous one thought it would need. Ten steps later the system is acting on a rumour of the original request.

Every hop is a lossy projection

The problem is in the shape, not the code. A function signature is a contract, and a contract written at step two can only describe what step two knew to care about. Step two takes the request and returns three fields, because three fields are what step three needs. That is correct, local, defensible design.

Then step seven needs a fourth field. It was on the original request. It was dropped at step two, four hops ago, by a decision that looked right at the time. Now there are three ways out, and the agent will cheerfully take any of them:

  • Re-fetch it. Step seven goes back to the source and reads it again. Now the same fact is loaded twice by two different queries, and there is a window between them in which it can change.
  • Re-derive it. Step seven recalculates it. Now the same value is computed in two places by two pieces of code, and the day one of them changes, they disagree quietly and forever.
  • Widen the contract. The return type of step two grows a field. Then another. Within a week it is called OrderContext and it is a bag, being threaded through nine functions, most of which use two fields from it and pass on the rest untouched.

That third one is the most common, and it is the worst, because it looks like the fix. The bag grows until nobody knows which fields are authoritative, which are stale, and which are there because something in the middle of the chain needed them once.

The shapes the loss actually takes

What happensWhat the customer sees
The dropped field They told you at the start that someone would need step-free access, or that the delivery has to go to the side gate. Step two had no use for it, so nothing downstream ever saw it.
The re-derived value The total on the confirmation email does not match the total on the invoice, because two functions each worked it out and one of them rounds differently.
The stale copy Step two read the address. The customer changed it. Step seven is still holding the copy it was handed, and dispatches to the old one.
Laundered inference Something upstream guessed, and returned the guess as a plain value. Three hops later it is indistinguishable from a fact the customer stated, and it is acted on with full confidence.
The narrowed type A nullable became a default. An enum became a string. "Not known yet" and "no" are now the same value, and the branch that should have asked never fires.

None of these is a bug in a function. Every function does exactly what its signature promises. That is why they survive review, survive the tests, and surface only as a customer telling you something that seems impossible.

Why coding agents are unusually prone to it

This failure predates AI. Any team can build it. But an agent working turn by turn is close to a perfect machine for producing it, for three reasons.

It optimises for the unit in front of it. Asked to write a function that checks stock, the textbook-correct answer takes what it needs and returns what it produces. Nothing about that turn contains the information that a step five turns later will need a field this one is about to discard. The loss is invisible at every individual step and complete by the end.

It mirrors the shape of the request. Describe a process as a series of stages and you will get a series of functions, one per stage, wired in that order. The decomposition is an echo of your sentence, not an architectural decision anybody made.

Its tests confirm the wrong thing. Each function gets tests, and they pass, because they assert the contract the function was given. They cannot fail on information the contract never carried. You end up with a green suite over a chain that loses the customer's accessibility requirement at step two, and nothing anywhere is red.

How to spot it in a review

A growing type with Context, Data or Payload in its name. The same entity loaded at three different points in one journey. Functions with seven parameters. A step whose actual job is to put back something an earlier step dropped, usually called something like enrich. The same value computed in two places. And the clearest tell of all: no single thing you can look at to answer "what does the system currently believe about this order?"

The fix is not a better prompt

You cannot reliably fix this by asking for more care, because the agent is not being careless. It is answering the question it was asked, one turn at a time. You fix it by changing the shape it is asked to produce, and the change is a specific one: separate the two things the chain conflates.

A chain of functions passing payloads is doing two jobs at once. It is deciding what happens next, and it is carrying what is known. Those are different concerns and they have different lifetimes. Pull them apart and the whisper game has nowhere to happen.

  • Orchestration owns the sequence. One component decides what runs next and under what conditions. Steps do not call other steps. A step that calls the next step has taken a decision that belongs somewhere you can see it.
  • A canonical entity owns the state. The journey has one persisted record with an identity: the order, the trip, the complaint. It exists before step one and outlives step ten.
  • Steps take the id, not a payload. Each step loads what it needs from the entity, does its work, and writes what it learned back. Nothing is threaded through a signature, so nothing can be dropped from one.
  • Everything written carries its provenance. The customer said it, a system returned it, a model inferred it at this confidence. A guess stays visibly a guess however far down the journey it travels.
  • Steps are re-runnable. Because state lives in the entity rather than in the call stack, running a step twice is safe, and a journey that fell over at step six resumes at step six instead of starting again with a half-built bag of data.

The immediate practical gain is that adding a step stops being a refactor. In the chain, a new requirement at step seven changes the signature and the tests of steps two through six, because the data has to be threaded down to it. With a canonical entity, the new step reads the field it needs and nothing else moves.

What to actually say to the agent

This is the part worth being blunt about, because agents follow architectural instructions well when they are given as constraints rather than as preferences. Put it in the brief, not in a review comment:

Model this journey's state as one persisted entity with an id. Every step loads it by id and writes its results back to it. No step passes a payload to another step, and no step calls another step: an orchestrator decides the sequence. Record where each value came from. Every step must be safe to run twice.

Say it before the first line is written. This is not a change you make later. Retrofitting it means rewriting every signature, every test and every call site in the chain, which is exactly the work the chain was chosen to avoid; and by then the bag has become the thing the whole codebase agrees on.

It is worth being equally explicit about what you are not asking for. You are not asking for a database table per step, or an event-sourced ledger, or a workflow engine. Those may be right, but they are implementations. The constraint is that there is exactly one place the journey's truth lives, and steps read and write it there rather than handing it to each other.

What you get, beyond correct outcomes

The same structure that stops the data degrading turns out to be the structure everything else wants. A journey you can resume, because the state is not in the call stack. A journey you can audit, because what the system believed and when is a record rather than an inference from logs. And a journey a model can reason about, because there is one assembled view of the customer and their request to give it, which is the whole argument for canonical entities at the edge.

It is also the only version of this that you can measure. A chain of functions can tell you that every step ran. Only a persisted journey can tell you what the customer asked for at the start and what they got at the end, which is the comparison that any improvement loop is built on. Both Aurum and Codamor are built this way for exactly that reason: not because the decomposition is prettier, but because a system that hands its data down a chain cannot tell you what it did, and one that persists it can.

Building something this applies to?

Start a conversation