The Living Graph¶
Agent frameworks make you choose between two things you should not have to choose between: a structure you can inspect, and a structure that can change.
This page argues the choice is false, and that it only looks forced because of an unexamined assumption — that the graph describes the future.
The dichotomy¶
Plan-and-execute designs ask a model to decompose a task into steps, then run them. The appeal is obvious: you get a structure before spending a token on execution, and you can show it to someone. The problem is equally obvious once you have watched one run. Step three returns something nobody anticipated — a column that doesn't exist, a service that returns 403, a document that says the opposite of what the plan assumed — and the plan is now fiction. The framework re-plans, discarding what it learned, or it forces the stale plan forward.
ReAct loops give up on planning. Think, act, observe, repeat. They adapt beautifully, because adaptation is all they do. What they leave behind is a transcript: a flat, append-only list of messages. You cannot diff two transcripts meaningfully. You cannot ask a transcript which step caused which other step. You cannot hand it to an auditor and point at the moment a decision was made.
One gives structure without adaptation. The other gives adaptation without structure.
The assumption underneath¶
Both designs treat a graph as a plan — a claim about what will happen.
Plans must be complete before they are useful, which is why static planners have to guess. Plans become invalid when reality disagrees, which is why they are brittle. And because a plan is a prediction, keeping one accurate during execution means continuously rewriting it — which is why frameworks that try end up maintaining two sources of truth: the plan, and what actually ran.
Drop the assumption. Let the graph describe the past instead.
A graph of what has already happened is never wrong. It cannot be invalidated by a surprise, because a surprise is just another node. It does not need to be complete to be useful — a partial history is still a history. And there is nothing to reconcile, because there is only one record.
The apparent cost is that you no longer know the shape of the work up front. That is not a cost. You never knew it. Static planners only appeared to, and the appearance is precisely what made them brittle.
Loops become appends¶
Here is the whole mechanism.
When a step fails, the intuitive repair is to go back and try again — an edge from the failure to the node that produced it. That introduces a cycle, and cycles are what make execution history hard to read: you can no longer distinguish the first attempt from the fourth, or tell which attempt produced the result you kept.
Instead, append. The retry becomes a new node whose parent is the failure.
graph LR
subgraph before["Cycle — the retry erases the failure"]
direction TB
A1([Draft SQL]) --> B1{{Failed}}
B1 -. retry .-> A1
end
subgraph after["Append — the failure is kept as a cause"]
direction TB
A2([Draft SQL v1]) --> B2{{"Failed: no column region"}}
B2 --> C2([Discover: region lives in dim_store])
C2 --> D2([Draft SQL v2 with join])
D2 --> E2[["Validated result"]]
end
Both graphs encode the same events. Only the second can answer why does the final query contain a join.
That is the difference between a retry counter and a causal chain. A counter tells you something failed three times. A chain tells you what each failure taught the run, and which lesson produced the answer you shipped.
What acyclicity buys¶
Because expansion only ever appends, the graph is acyclic by construction. There is no cycle detection, no checker, no invariant enforced at a boundary — the operation that would create a cycle does not exist in the API.
That is worth more than tidiness. It means the graph is a partial order over events, and a partial order over events is exactly what an audit log is. So:
- every node maps to one OpenTelemetry span
- every edge is a causal claim: this happened, and it is why that happened
- the thing that ran and the record of what ran are the same object
Most systems maintain execution state and observability separately, then spend real effort keeping them consistent. Here there is nothing to keep consistent. There is one structure, and tracing is a projection of it.
For anyone working under audit — clinical, financial, regulated in any way — this is the property that matters. "Show me why the system did that" is answered by pointing at a subgraph, not by correlating a trace against a checkpoint against a log.
The frontier¶
If the graph grows from outcomes, something must decide which outcomes deserve growing. That decision is the frontier, and it is the sharpest design question in the system.
A node is eligible for expansion when all three hold:
- it is terminal — it has produced an outcome, success or failure
- it is unexpanded — it has no children yet
- its outcome leaves an open obligation — something the task requires that no node in the graph yet satisfies
The frontier is the set of eligible nodes. Expansion takes one, hands it to a policy along with the graph so far, and receives zero or more children.
Zero is the important case. A policy returning no children is saying this branch is finished — not failing, not erroring, just done. Branches retire themselves.
Note that failure is not special. A failed node is eligible on exactly the same terms as a successful one, which is why "retry" needs no dedicated machinery: a retry is just what expansion happens to produce when the parent's obligation is still open.
Termination, honestly¶
This is where the design is weakest, and where the temptation to hand-wave is strongest.
The graph does have a natural termination condition: the frontier empties when every branch declines to expand. That is real, and most runs reach it.
But nothing guarantees it is reached. The expansion policy consults a language model, and a language model can always propose one more thing to try. A run that never finds its answer can keep finding plausible next steps indefinitely. The natural stopping condition exists; it is simply not enforceable.
So budget and circuit-breaking are not safety features bolted to the side. They are the only termination guarantee the system has, and they belong in the type system — an expansion path that can reach a model call without passing a budget check is a severity-one bug, not a missing config option.
That is the honest trade for everything above. A static plan terminates because it is finite. This design buys adaptability by giving that up, and must pay for termination explicitly.
Why this shape fits serverless¶
Cloud Run caps a request at 60 minutes, and Google's own guidance is to tolerate client reconnection beyond fifteen. Frameworks that model a run as one long-lived process fight that ceiling, and lose at exactly the moment a run gets interesting.
There is no long-lived process here. One expansion is: read the frontier, execute a node, append the result. That is a short request against durable state. A run is many such requests, and a crash resumes by reading the frontier — the same operation the scheduler performs normally, so recovery is not a separate code path that runs only during incidents.
The constraint that breaks other designs is one this design already had to satisfy. That is not luck. It is what falls out when the only mutable state is an append-only structure.
What this costs¶
Three things, plainly:
Graphs grow monotonically within a run. Nothing is ever removed, so long runs accumulate nodes and need a retention story. Append-only is cheap to write and expensive to keep.
Termination is your problem. See above. This is the real one.
You cannot show the user a plan, because there isn't one. A progress bar over unknown total work is a genuine UX problem, and pretending otherwise would be dishonest — though it is worth saying that plan-based progress bars were lying anyway.
Next: Prior Art — where this comes from and who did it first. Very little of the execution model is new. The application of it is.