Best on a larger screen — the animation and controls have more room to breathe.
Inputs

Task System

A task system is how an agent survives its own death: the harness decomposes a big goal into small tasks with dependency edges, writes that graph to disk, and re-saves it at every completion. Execution order falls out of the edges — a task runs only when every dependency is done — and a crash costs only the in-flight task.

Compare that to the way an agent "plans" by default: a to-do list living in messages[]. It works, right up until the process dies — and processes die constantly. The terminal closes, the machine sleeps, the API errors out, someone hits Ctrl-C. An in-memory plan doesn't just lose its place; it loses the evidence that anything happened at all. The work was done, tokens were spent, and the next process starts from a blank slate. In this lesson's run the same crash finishes with 5 completed executions on disk and 7 in memory — the two extra are t1 and t2, paid for twice.

The fix is embarrassingly old technology: a file. tasks.json holds the graph — five tasks, their edges, their statuses — and save runs inside the loop, right after each task.status = "done". That placement is the whole design. The plan is persisted once; the progress is persisted continuously; and a restarted agent's first move is load, not decompose.

This lesson builds on the agent loop (the executor here is that loop with a scheduler bolted on) and pairs with subagents — that lesson splits work across contexts, this one splits it across time.

How a Task System Works: Edges Gate, Completions Persist

Two lines of the disk-mode loop carry everything: next_ready(graph) and the save("tasks.json") that follows every completion.

First, ordering. next_ready(graph) returns a task whose dependencies are all done. In this lesson's workload, scaffold the module (t1) unlocks both implement parser (t2) and write tests (t3); run test suite (t4) is a join that needs both t2 and t3; release notes (t5) waits on t4. Nobody sorts this list. The agent doesn't exercise discipline. The order is a property of the data: when t2 finishes but t3 hasn't, t4 simply isn't in the ready set, and the loop can't pick it. Two tasks legal at once (t2 and t3 after the fan-out) is fine — this single-worker loop breaks the tie by lowest id; a fancier harness would run them in parallel.

Second, durability. load("tasks.json") runs before decompose — resuming beats re-planning — and save runs after every completion. Note what save never records: a running status. The file is written at decompose time and at completion time, so it always describes a consistent world of "completion recorded" and "completion not recorded." A task that was mid-execution when the process died is, as far as the file knows, still pending. That's not sloppiness — it's the correct answer, because the checkpoint cannot certify completion. Repeating real side effects requires idempotency or a recovery protocol. The restart re-runs exactly that one task.

The memory variant runs the same task graph without durable load and save boundaries. Nothing else differs. That persistence boundary is the entire lesson.

Task System Step by Step: One Crash, Two Tasks Saved

The default run uses the disk store and kills the process mid-run. Watch the panel on the right — it mirrors the file, not the screen.

  1. Init. The goal card: "Ship the parser feature." One goal, no structure.
  2. Decompose. The goal becomes a graph, level by level: t1 on top, the t2/t3 fan-out, the t4 join, the t5 tail. Five tasks, five edges, all pending.
  3. Persist. save("tasks.json") — the panel fills with five pending rows. The plan has left the process.
  4. Pick t1. The only task with no dependencies. It turns purple: running.
  5. Complete t1. The node flips green and its file row flips done in the same beat — mark done, then save, two adjacent lines.
  6. Unlock. Both edges out of t1 are green: t2 and t3 lift into the ready frontier together.
  7. Pick t2. Two tasks are legal; the tie-breaker takes the lower id.
  8. Complete t2. File now reads t1 ✓ t2 ✓. And note the status line: t4 is still blocked — one green edge into a join isn't enough.
  9. Pick t3. The tests start.
  10. Crash. The process is killed while t3 runs. Everything in memory — the graph, the header readout — collapses under a red process killed stamp. The file panel doesn't flinch: it still reads t1 ✓, t2 ✓, t3 pending. The dying screen said t3 was running; the file never heard about it. Completion, not intention, is what gets persisted.
  11. Restart. A new process. Its first act is load("tasks.json") — and the graph redraws from the file: t1 and t2 come back already green. Nothing was re-derived, re-planned, or re-executed.
  12. Resume. next_ready recomputes the frontier from the loaded state and hands back t3. Zero completed tasks re-run; the in-flight one restarts, because the file (correctly) never recorded it as done.
  13. Complete t3, unlock t4. The join finally has both its edges.
  14. Pick t4, complete t4, unlock t5. Pick t5, complete t5. The rest of the run is ordinary: pick, complete, save, unlock.
  15. Done. The ledger: 5 tasks, 5 completed executions, plus 1 aborted attempt. The crash cost exactly the task it interrupted.

Now flip store to in-memory and keep the crash. Same graph, same tie-breaks, same crash point — but the restart finds nothing to load. The graph is re-decomposed with all five tasks pending, and the loop grinds through t1 and t2 again, each wearing a red "again" chip. Final ledger: 7 completed executions for 5 tasks. The two extra are the completed work that existed only in a dead process's memory. And run either store with scenario set to smooth for the honest control: no crash, and the two stores run the same tasks in the same order at the same cost. Persistence is invisible until the day it's everything. Finally, sweep crash after from 0 to 4: on disk the bill stays flat — whatever dies, the restart re-runs exactly one in-flight task — while in memory it climbs by one execution for every task the crash erases, from nothing lost at 0 (nothing was done yet) to four tasks paid for twice at 4. That line, flat versus climbing, is the whole argument for the file.

Why Save Inside the Loop

The tempting shortcut is to save once — the graph is right there after decompose, write it and move on. Then the crash comes, the restart loads a file that says five pending tasks, and the agent redoes everything. The plan survived; the progress didn't. Persisting a task graph means persisting its statuses, which is why the second save call sits immediately after task.status = "done" — the file must never lag reality by more than the one task currently in flight.

The same reasoning explains what's not saved: this simple restart policy does not need a persisted running marker. Production systems can persist it together with ownership, leases, and recovery rules. If the file said running and the process died, the restart couldn't distinguish "died mid-task" from "task is somehow still going." By writing only completions, the file stays trustworthy at the price of re-running one interrupted task — the cheapest insurance in the whole design.

One more thing falls out for free: a scheduled job can add a timestamp to its readiness conditions, alongside any dependency edges. Recurrence, overlap, and missed runs need additional policy. next_ready checks "deps all done"; a cron-style system checks "time has come." Same graph, same loop, same file.

The judgment call runs the other way too. When every task is seconds long and free to re-run, the file buys nothing worth its bookkeeping — persistence pays in proportion to what a crash would erase, which is why your shell scripts don't keep a tasks.json and your data pipelines do.

Edge Cases

  • The crash lands between a completion and its save. Mark-done and save are two adjacent lines, not an atomic unit, so the window is tiny but real: die exactly there and the restart re-runs one completed task. Atomic writes (write-to-temp, then rename) protect checkpoint-file integrity; they do not atomically commit an external task effect with its completion record. That gap requires idempotency, transactional coordination, or an explicit recovery protocol. A journal helps only within the guarantees of its design.
  • Two tasks ready, one worker. After t1, both t2 and t3 are legal. The graph doesn't order them — any tie-breaker is correct, and a multi-worker harness could take both. What the graph does guarantee is that t4 waits for both, whoever finishes last.
  • A cycle in the dependencies. t2 needs t3 and t3 needs t2: next_ready returns nothing, forever, with every task behind the cycle pending. A real decomposer validates the graph is acyclic before saving it.
  • The goal changes mid-run. The file describes the old decomposition. A restart resumes the old plan faithfully — durability cuts both ways. Re-planning requires deliberately deleting or versioning the file.

Common Mistakes

  • Persisting the plan but not the progress. Saving once after decompose feels done and isn't — the restart loads a pristine plan with zero completions and redoes the run. Save belongs inside the loop.
  • Trusting the screen over the file. At crash time the screen said t3 was running; the file said pending. The file is right: "running" is an intention, and intentions don't survive processes. Resume logic should read persisted state only.
  • Treating the order as the agent's job. If the loop "knows" to run tests before the suite, that knowledge dies with the loop. Encode it as an edge (t4.deps = [t2, t3]) and the ordering survives restarts, re-plans, and other workers, because it's data.
  • Marking a task done when it was merely started. The whole resume guarantee rests on done meaning finished. Persist optimistically and a crash leaves a half-executed task the restart will skip forever.
  • Rebuilding the graph on every start. load before decompose isn't an optimization, it's the point — a task system that re-plans on restart is an in-memory system with extra steps.

A Note on Simplification

The task-system mechanics on screen — decomposition into a DAG, dependency gating, the save-per-completion discipline, the crash, the resume — are real and deterministic; that's what this lesson exists to teach. The file is also simulated by a separate in-memory checkpoint, not written to your disk. The task names and their "execution" are scripted for clarity: nothing actually scaffolds a module, and the decomposition itself would, in a real agent, come from a model call — tool-calling and react-loop show recorded runs of that machinery. That last point cuts into the memory-mode story too: this demo's restart re-plans the identical graph because its decompose is deterministic, while a real model's re-plan may produce a different decomposition entirely — so the fixed redo count here does not bound a real model’s recovery cost. Real task systems add what this one omits: atomic file writes and journaling (the completion-save gap above), retry policies and per-task timeouts, priorities beyond lowest-id, parallel workers claiming tasks from the same file, and cycle validation at decompose time. And a naming note, because you already use this pattern: this loop is the core of a workflow engine (Airflow, Temporal), of a CI pipeline resuming a half-finished build, and of the todo file an agent harness keeps on disk. None of that changes the shape: put the plan and its progress on disk, gate execution on the edges, and a process becomes something you can afford to lose.