Chapter 34: From DAG to Agent Loop

"Just model it as a graph" — fine, until the next node depends on something you can only learn by running the previous one.


Quick Track (Master the core in 5 minutes)

  1. Graph for control flow you can draw in advance; loop for control flow that depends on observations
  2. The tell you picked wrong: nodes that only ever get added after production surprises you
  3. Production converges on a hybrid — deterministic shell, adaptive core, deterministic exit
  4. A long-lived daemon does not mean a long-lived loop; the loop is rebuilt per turn
  5. Continuity comes from persisted session + an explicit history snapshot, never object lifetime

10-Minute Path: 34.1 -> 34.4-34.5 -> Kocoro OSS Lab


Public-source snapshot: implementation details are examples from public Kocoro origin/main at commit 4ec6772, reviewed 2026-07-27. Treat the invariant as the lesson and re-check constants against current source.

34.1 Starting with a Graph That Would Not Stop Growing

A team ships a competitive research pipeline as a DAG. Version one is five nodes, and it's beautiful:

fetch_sources  extract_claims  cross_check  synthesize  format

Then it meets production.

A source returns a paywall instead of an article — add a node. A PDF turns out to be a scanned image — add an OCR branch. A vendor renames a field — add normalization, with a conditional. Two sources contradict each other and the synthesizer confidently averages them into a claim neither source made — add an arbitration node. Six months in, the graph has forty-odd nodes and roughly two thirds of them exist to handle states nobody could have listed on day one.

The node count isn't the interesting part. When the nodes got added is. Every one of them arrived after production surprised somebody. That is the signature of control flow that depends on observations you cannot have until you look.

The opposite mistake is quieter and costs more. Another team wraps a fixed nightly ETL — same five steps, same order, same success criteria, every night for two years — in an Agent loop, "for flexibility." Now each run spends twelve model calls rediscovering a pipeline that has never once changed, and when the model has an off night it skips a step that used to be guaranteed.

Both teams did the same thing in opposite directions. They picked the execution model before asking whether the control flow was knowable in advance.

34.2 What Each One Is Actually For

A DAG earns its complexity when you can name every node before running anything, draw the edges and have them stay drawn, check each node's success deterministically, and — this is the part people forget — when you need the shape itself. Replay, audit, per-stage cost attribution, provable parallel safety: all of that comes from having declared the graph up front.

That last point is underrated. If a compliance reviewer has to see what will execute before it executes, a loop cannot give them that. A graph can. Chapters 13 and 14 cover this machinery, and none of it is obsolete.

The loop earns its cost in exactly one situation: the next action depends on an observation you can only get by acting. You can't draw the edge out of "read the file," because where it goes depends on what the file said. You can enumerate the kinds of next step but not the sequence and not the count — and encoding that in a graph means one branch per possible observation. Which is precisely how the research pipeline got to forty nodes.

So the loop swaps enumeration for a policy: observe, decide, act, repeat until something terminal is true. The model supplies branching at runtime instead of the author supplying it at design time. That's the whole trade. You buy adaptability at one model call per iteration, and you give up knowing the execution shape in advance.

34.3 The Shape Production Actually Converges On

Almost nothing real is purely one or the other. What survives contact with production is a deterministic shell around an adaptive core:

deterministic shell
   authenticate and identify the source
   resolve the route, load or create the session
   build the tool registry, resolve permissions
   snapshot conversation history
  ┌──────────────────────────────────────────┐
    adaptive core                            
      observe  decide  act  repeat        
  └──────────────────────────────────────────┘
   deterministic completion check
   persist final state, emit the reply

Everything outside the box is knowable in advance, so it should be a graph, a function, a straight line — anything except a model call. Everything inside depends on what the model just saw.

The working rule is blunt: if you can write an if statement that decides it correctly, don't spend a model call on it. Every deterministic check you move out of the box is an iteration you stop paying for, forever, on every run.

34.4 A Long-Lived Daemon Is Not a Long-Lived Loop

Here is where most mental models quietly break, and it matters for the whole rest of Part 10.

A daemon that runs for weeks does not keep one loop object alive for weeks. In the public Kocoro runtime, RunAgent is the per-request entry point, and it builds a fresh loop for every request before calling Run:

// internal/daemon/runner.go — one construction per request
loop := agent.NewAgentLoop(deps.GW, reg, runCfg.ModelTier, deps.ShannonDir, ...)
...
result, usage, runErr = loop.Run(ctx, prompt, resolvedContent, history)

The loop is a per-turn object. The daemon is the long-lived thing. Conflate the two and you will reason wrongly about crash recovery, memory growth, and state isolation — three things you cannot afford to be wrong about.

So if the loop is rebuilt every turn, what makes turn 40 remember turn 1? Two mechanisms, both deliberate.

The session is written to disk before the loop runs, not only after. The reasoning in the source is worth quoting, because it's the entire basis for durable agents:

Persist session to disk before loop.Run() so there's a record even if the daemon crashes mid-execution. The final save after completion is still needed to capture the assistant's reply.

Crash mid-turn and you're left with a record instead of a hole. Chapter 40 builds resumable turns directly on that property.

History is passed in as a snapshot, not held as loop state. And it's snapshotted before the new user message is appended — otherwise the model receives that message twice, once as the prompt and once inside history. The snapshot also strips guardrail nudges the previous run injected, so last turn's "you appear to be repeating yourself" can't leak into this turn's conversation.

That second detail is easy to miss and expensive to get wrong. Loop-injected system messages belong to the loop that injected them. Persist them into history and every future turn inherits corrections that stopped applying twenty turns ago.

Continuity, then, is durable session plus explicit snapshot. Never object lifetime.

34.5 Snapshot Evidence

ObservationSource at 4ec6772
RunAgent is the per-request entry pointrunner.go L1898
A fresh loop is constructed per requestrunner.go L2732
History is passed into Run, not held by itrunner.go L3176
Session persists before execution, not only after itrunner.go L2576
Delegation is a tool call, not a graph edgecloud_delegate.go L59
Iteration cap is a configurable backstopconfig.go L421

These describe one dated implementation, not a universal contract.

34.6 When to Keep the Graph

Reach for a graph — or for plain code — when the control flow is fixed, because a nightly ETL does not need to rediscover itself every night. When you must declare execution in advance, because regulated pipelines need a shape before the run, not a trace after it. When you need provable parallel safety, because a graph lets you prove two branches never touch the same resource while a loop can only decide that at runtime — which is exactly why Chapter 44 has to classify concurrency safety per call.

And when the work is cheap but the model call isn't. A step that runs deterministically in a millisecond, replaced by a model call costing a second and a fraction of a cent, needs to justify a three-order-of-magnitude latency premium. Usually it can't.

There's also an operational reason people underweight: "node 7 failed" is a page you can act on. "The agent gave up on iteration 23" is a research project. If failure has to be attributable to a stage, the graph is giving you something the loop structurally cannot.

34.7 Common Pitfalls

Treating the iteration cap as control flow. The public default is 40, raised from 25 because real tasks — "refactor 12 files," "batch-process 20 attachments" — routinely need more. It's a runaway backstop, not a plan. If your loop reliably terminates because it hits the cap, you don't have a terminal condition. You have a timeout in a costume.

Putting deterministic validation inside the loop. Schema checks, permission resolution, path normalization. None of it needs a model, all of it gets paid for per iteration.

Assuming the loop object carries state across turns. It doesn't. Attach a cache to the loop expecting it to survive the next message and it will be silently empty every time. Put it on the session.

Letting the model declare completion. "I have finished the task" is a claim, not a postcondition. Where a deterministic check exists — file written, tests green, row count matched — check it and exit on the check, not on the sentence.

Rebuilding the shell inside the loop. Resolve the tool registry, permission set, and working directory per turn. Do it per iteration and you pay repeatedly while inviting drift between iterations of the same turn.

Kocoro OSS Lab (10 minutes)

  1. Open runner.go and find every line that executes between RunAgent's entry and loop.Run. That list is the deterministic shell — note how much of it never touches a model.
  2. Find the comment explaining why history is snapshotted before the user message is appended. Write down what breaks if you snapshot after.
  3. Take one pipeline you own and mark each step: knowable in advance, or observation-dependent? If nothing is observation-dependent, you don't need a loop.

Key Points

  1. Graph for known control flow, loop for observation-dependent control flow. Picking before you've asked which one you have is the root error.
  2. Watch when nodes get added. Nodes that only appear after production surprises you are the signal you needed a loop.
  3. Deterministic shell, adaptive core. If an if decides it correctly, it doesn't get a model call.
  4. The daemon is long-lived; the loop is per-turn. Everything else in Part 10 depends on getting this right.
  5. Continuity is state you persisted, not an object you kept alive. Session written before execution, history passed in as a snapshot.

Read Part 10 as one system: compaction, tool surface, durability, steering, time discipline, loop detection, and concurrency all constrain one another.

Cite this article
Zhang, Wayland (2026). Chapter 34: From DAG to Agent Loop. In AI Agent Architecture: From Single Agent to Enterprise Multi-Agent Systems. https://waylandz.com/ai-agent-book-en/chapter-34-from-dag-to-agent-loop/
@incollection{zhang2026aiagent_en_chapter-34-from-dag-to-agent-loop,
  author = {Zhang, Wayland},
  title = {Chapter 34: From DAG to Agent Loop},
  booktitle = {AI Agent Architecture: From Single Agent to Enterprise Multi-Agent Systems},
  year = {2026},
  url = {https://waylandz.com/ai-agent-book-en/chapter-34-from-dag-to-agent-loop/}
}