Agent-era architecture · Practical technical guide · 23 October 2026
How to let an agent write a desired state and leave the step-by-step execution to a deterministic reconciler: a level-triggered loop that is idempotent and restartable, why this beats an agent that calls each API itself, the migration path, and when a plain workflow is enough.
Two ways to let an agent change a system
There are two ways to wire an agent into a system that has real side effects. In the first, the agent calls each mutating API itself: it creates the record, then updates the index, then sends the notification, deciding after each call what to do next. In the second, the agent writes down a desired end state, and a separate deterministic component, a reconciler, compares that desired state with the current state and makes whatever changes close the gap. The agent proposes; the reconciler converges.
The first style is edge-triggered: each decision reacts to the event in front of it, and a crash between two calls leaves the system in a state no one described. The second style is level-triggered: the reconciler looks at the whole desired state and the whole current state every time it runs, so running it again after a crash is safe and simply resumes the work. Kubernetes is built on exactly this idea. Its controllers are control loops that watch the desired state of a resource and work to make the current state match it, and the documentation is explicit that a controller tracks at least one resource type and drives current state toward desired state. The agent, in this pattern, is a controller of a different kind: it produces the desired-state object that the ordinary reconciler then enforces.
What stays deterministic
The agent decides what the end state should be. Nothing else needs a model. The table divides the work so that the only non-deterministic step is the one that genuinely needs judgement.
| Concern | Owner | Deterministic? |
|---|---|---|
| What the end state should be | Agent | No; this is the judgement |
| Is the proposed state well formed | Schema validator | Yes |
| Is the proposed state allowed | Policy check | Yes |
| What changes close the gap | Reconciler diff | Yes |
| In what order, and how to retry | Reconciler | Yes |
| Whether the system reached the goal | Reconciler status | Yes |
Because the reconciler is deterministic, you can test chosen invariants systematically with synthetic desired states, and you can run it without an agent at all: a human, a form, or a rules engine can write the same desired-state object and the reconciler will not know the difference.
A worked example: access grants
A support agent handles requests like grant this contractor read access to these three projects until Friday. The edge-triggered design would have the agent call the grant API three times and set a reminder. Instead the agent emits a desired-state document, and a reconciler enforces it. The desired state is declarative and carries an expiry, so the same object also describes the removal.
Illustrative python, checked for syntax; adapt names and storage to your system.
# The agent's only output: a desired-state object, not a sequence of API calls.
desired = {
"principal": "contractor:erika",
"grants": [
{"project": "atlas", "role": "reader"},
{"project": "orion", "role": "reader"},
{"project": "vega", "role": "reader"},
],
"expires_at": "2026-10-30T00:00:00Z",
}
def reconcile(desired, now, read_current, apply_change):
"""Level-triggered: compare, then make only the changes that close the gap."""
active = {} if now >= iso(desired["expires_at"]) else {
(g["project"], g["role"]) for g in desired["grants"]
}
current = read_current(desired["principal"]) # set of (project, role)
for key in active - current: # missing -> add
apply_change("add", desired["principal"], key)
for key in current - active: # extra or expired -> remove
apply_change("remove", desired["principal"], key)
return {"converged": True, "active": sorted(active)}Run reconcile once and three grants appear. Run it again a second later and it makes no changes, because the desired set already equals the current set; the loop is idempotent. Run it after Friday and it removes all three, because active is empty once the object has expired, with no separate reminder to lose. In this fixture, a later pass recovers after a crash using keyed idempotent changes and refreshed state. Real APIs need those guarantees and concurrency controls; a crash is not automatically harmless. The runnable example drives this loop through partial failures and expiry, and the lab below lets you inject a crash between any two changes and watch the next pass finish the work.
Why this beats an agent that calls each API
An agent that executes steps one at a time has to remember where it was, because its progress lives only in the sequence of calls it has made so far. If it stops after the second grant, nothing in the system says that a third was intended, and re-running the task risks granting the first two again. The reconciler keeps intent and progress in separate places: intent is the desired-state object, progress is the current state read fresh each pass. That separation is what makes the loop restartable. It also makes the agent cheaper and safer, because the model runs once to produce a small document instead of staying in the loop for every retry, and a retry never calls the model at all.
Migration steps from an edge-triggered agent
- Name the desired state. Write a schema for the end state the agent currently reaches through a sequence of calls, with any time bounds expressed as fields rather than reminders.
- Build the reconciler against that schema and test it with synthetic desired states, including the empty one, before any agent is involved.
- Run the reconciler in shadow: have it compute the diff it would apply and log it next to what the edge-triggered agent actually did. Differences are bugs in one side or the other.
- Switch writes to the reconciler while the agent keeps producing the same desired-state object, now as its only output.
- Schedule the reconciler to run on a timer as well as on each new proposal, so expiries and external drift are corrected without a new agent call.
- Delete the per-step tool calls from the agent once the reconciler owns all writes.
When a reconciler is unnecessary
This pattern earns its keep when an action has several steps that must agree, when it has a time dimension such as an expiry, or when external drift can undo it. If the agent makes a single idempotent call with a natural key, that call is already its own tiny reconciler and a second layer adds nothing. If the effect is a one-shot message with no later state to maintain, a durable queue with an idempotency key is simpler. Reach for a reconciler when there is a state to keep true over time, not merely an action to take once.
Failure modes
- A desired state that encodes steps instead of an end state, so the reconciler inherits the ordering problem it was meant to remove.
- A reconciler that is not idempotent, so a second pass re-applies changes; every apply must be keyed and safe to repeat.
- Reading current state through a cache that lags the writes, so the diff oscillates and the loop never settles.
- No policy check between the agent and the reconciler, so a bad proposal is enforced faithfully and quickly.
- A timer that never runs, so expiries and drift are corrected only when a new proposal happens to arrive.
Verification
Test the reconciler as a pure function: given a desired state and a current state, assert the exact set of changes. Add a test that runs it twice and asserts the second pass makes no change, and a test that applies changes until a chosen failure point, then runs again and asserts the result is the full desired state. Separately, confirm the agent output validates against the schema for a sample of real requests. Keep a dashboard metric for passes that still found a diff after the system was supposed to have converged; a persistent non-zero value means something outside the reconciler is changing the state.
Sources and scope
Checked against these sources on 23 October 2026. Examples and figures are illustrative, not measurements.
Have the agent write a desired-state object and leave execution to a deterministic, idempotent, level-triggered reconciler, keep intent and progress in separate places, run the loop on a timer as well as on each proposal, and reach for it only when there is state to keep true over time.