Most agent systems use a todo list to manage tasks — but engineering tasks are rarely linear.
Pattern Layer
10.1 Work Graph vs Todo List
A todo list is the most intuitive way to manage tasks. You list what needs to be done, sort them in order, and complete them one by one. It is simple, easy to understand, and suitable for daily work.
But engineering tasks are rarely linear. A typical refactoring task might involve dependencies like this:
Understand module A's structure → Extract interface
|
Understand module B's structure → Extract interface → Merge interfaces → Test → Commit
|
Understand module C's structure → Extract interface
Task D (merge interfaces) requires both A and B to be complete, while C can run in parallel with A. In a todo list, you can only sort — put D after A and B, and C after A — but this loses the information that "C and A can run in parallel."
A Work Graph uses a DAG (Directed Acyclic Graph) to express dependencies. Each node is a milestone, each edge is a dependency. The agent navigates the graph to understand "what can be done" (all dependencies complete), "what is waiting" (dependencies not yet complete), and "what is blocking" (multiple milestones depend on it).
The Work Graph is not an "upgrade" to the todo list — they are two different data structures. A todo list is linear, suitable for tasks with a clear sequence. A Work Graph is a DAG, suitable for tasks with dependencies and multiple paths. For a simple three-step task (compile -> test -> deploy), a todo list suffices. For multi-module, multi-branch refactoring tasks, a Work Graph is the right tool.
10.2 Graph Before Execution vs Tree After Execution
The separation between Work Graph and Session (the subject of Thought Volume, Essay 5) is revisited here from an engineering implementation perspective:
The Work Graph is a "graph constructed before execution" — the planning vehicle. Its structure is determined before execution begins. Adding milestones, adjusting dependencies, modifying acceptance criteria — all happen during the planning phase. During execution, the graph's structure cannot be modified; only milestone states can be changed (pending -> in_progress -> done / needs_fix).
The Session is a "tree recorded after execution" — the factual record. Its structure grows continuously during execution. Every tool call, every message, every sub-agent result — all are appended to the Session tree during execution. The Session is not written during planning; it is written during execution.
The engineering rationale for separating them:
- Different lifecycles: The Work Graph has a terminal state (all milestones done -> archived), while the Session has no terminal state (as long as the agent is running, the Session grows)
- Different access patterns: The Work Graph is frequently read and written by the milestone tool (state changes), while the Session is written once and rarely modified afterward (except during compaction)
- Different compaction strategies: The Work Graph does not need compaction (limited number of nodes), while the Session requires two-tier compaction (tier-1 + tier-2)
10.3 Milestone Granularity
What size makes a "good milestone"? Both too coarse and too fine have problems.
Milestones that are too coarse: A milestone contains "refactor module A" — but refactoring module A might involve modifying 10 files, interface changes, and test updates. If the milestone is too coarse, it is impossible to accurately judge at acceptance time whether "this milestone is actually done" — under what conditions is "refactor module A" considered complete? If only the main files were changed but tests were missed — is it done or needs_fix?
Milestones that are too fine: Every small step gets its own milestone — "modify file A", "modify file B", "modify file C" — each milestone's acceptance criterion is nothing more than "file has been modified." At this granularity, management overhead (creating milestones, updating status, acceptance) exceeds execution effort.
Granularity criterion: Can a milestone be independently accepted?
If, after a milestone is completed, its output can be independently verified against expectations — without needing to check the completion status of other milestones — then the milestone's granularity is appropriate. If acceptance requires "looking at this milestone together with another milestone's completion to make a judgment" — it means the granularity is too fine and they should be merged.
Practical rule of thumb: A milestone should correspond to the scale of a single git commit. If the work an agent produces after completing a milestone is enough for an independent commit (with a clear scope of changes, accompanying tests, and a meaningful commit message), then the milestone's granularity is appropriate.
Case Layer
10.4 Six Operations of the Milestone Tool
The milestone tool supports six operations:
enum MilestoneAction {
Add {
name: String,
deps: Vec<String>, // Names of dependent milestones
acceptance: String, // Acceptance criteria description
command: Option<String>, // Acceptance command gate (optional)
},
Start { name: String },
Done {
name: String,
verdict: Option<String>, // Agent's self-assessment
},
NeedsFix {
name: String,
reason: String, // Reason for needing a fix
},
Next,
List,
}
- add: Add a milestone, declaring its dependencies and acceptance criteria. Dependencies must be specified at creation time and cannot be modified afterward
- start: Mark a milestone as "in progress" — indicating the agent is working on this milestone
- done: Mark as "done", optionally with an agent self-assessment. If the milestone has a
commandacceptance gate configured,donedoes not take effect immediately — it waits for the gate to pass - needs_fix: Mark as "needs fix" — the only state entered when acceptance fails or the user rejects
- next: Query "what can be done next" — returns the lowest-id pending milestone with all dependencies done
- list: List all milestones and their current states
10.5 The next_ready() Scheduling Logic
next_ready() is the core scheduling function of the Work Graph:
fn next_ready(graph: &WorkGraph) -> Option<&Milestone> {
graph.nodes
.iter()
.filter(|m| matches!(m.status, MilestoneStatus::Pending))
.filter(|m| m.deps.iter().all(|dep| {
graph.nodes.iter().any(|n| n.name == *dep && matches!(n.status, MilestoneStatus::Done))
}))
.min_by_key(|m| m.id) // Lowest id first
}
The scheduling logic in order:
- Only consider milestones with
Pendingstatus - Check dependencies: all dependencies must be in
Donestate - From the eligible milestones, select the one with the smallest
id(id reflects creation order; the smallest was created first)
The choice of min_by_key(|m| m.id) is not random — it ensures that "milestones created first are executed first." If two milestones both have their dependencies satisfied, the one created earlier executes first. This prevents "milestones created later but with simpler dependencies cutting in line."
10.6 drive_workgraph Automatic Advancement
drive_workgraph combines next_ready() scheduling with milestone state changes to form an automatic advancement loop:
fn drive_workgraph(graph: &mut WorkGraph, context: &Context) -> Result<()> {
loop {
// 1. Find the next ready milestone
let next = next_ready(graph);
// 2. If no more milestones, done
let milestone = match next {
Some(m) => m.clone(),
None => break,
};
// 3. Mark as in progress
milestone.start(context);
// 4. Execute the milestone's command (if any)
if let Some(cmd) = &milestone.command {
let result = context.execute_command(cmd)?;
if !result.success {
milestone.needs_fix("command failed", context);
continue;
}
}
// 5. Mark as done
milestone.done(context);
// 6. Loop — find the next ready milestone
}
}
Key design points of drive_workgraph:
- Advances one milestone at a time: Find the next ready milestone -> execute -> mark done -> find the next. Does not advance multiple milestones in parallel — even if their dependencies are all satisfied
- Command failure -> needs_fix -> continue looping: After a command failure, the current milestone enters the needs_fix state and the loop continues searching for the next ready milestone. Does not block the entire graph's progress
- Loop termination condition: No more ready milestones, or all milestones are either done or needs_fix
10.7 NodeStatus State Machine
NodeStatus defines the milestone state transitions:
enum NodeStatus {
Pending, // Initial state, waiting for execution
InProgress, // Currently executing
Done, // Complete (acceptance passed)
NeedsFix, // Acceptance failed, needs repair
// Diagnostic extension reservations:
Hypothesis, // Hypothetical milestone (not confirmed whether to proceed)
Locked, // Locked (waiting for external condition)
}
State transition rules:
Pending -> InProgress : start operation
InProgress -> Done : done operation (acceptance passed)
InProgress -> NeedsFix : needs_fix operation (acceptance failed)
NeedsFix -> InProgress : start operation (re-execution)
Done -> NeedsFix : needs_fix operation (issue discovered by user or secondary verification)
The complete milestone state machine flow is as follows:
stateDiagram-v2
[*] --> Pending: add milestone
Pending --> InProgress: start
Pending --> InProgress: next_ready() schedule
InProgress --> Done: command passes acceptance
InProgress --> NeedsFix: command acceptance failed
NeedsFix --> InProgress: self-recovery loop<br/>(bounded retry)
NeedsFix --> InProgress: user manual reset
Done --> NeedsFix: secondary verification found issue
NeedsFix --> Stuck: retry budget exhausted
Stuck --> [*]: human intervention
Pending --> Blocked: dependency not met
Blocked --> Pending: dependency completed
state Pending {
[*] --> Ready: all dependencies Done
Ready --> Wait: preceding milestone not completed
}
state NeedsFix {
[*] --> Readying: inject fix prompt
Readying --> Retrying: re-execute
Retrying --> Verify: re-acceptance
Verify --> Pass: acceptance passed
Verify --> Fail: acceptance failed
Pass --> [*]: done
Fail --> [*]: retry count
}
note right of NeedsFix: Check gate and review gate<br/>failures enter this state
The Hypothesis and Locked states are reservations for diagnostic extensions. Hypothesis is for milestones that are "not yet certain but marked for consideration" — the agent can create hypothetical milestones and convert them to formal milestones via add or start after subsequent validation. Locked is for milestones "waiting for external conditions" — such as waiting for another service to finish deploying.
10.8 Complete Milestone Example
Here is a typical milestone definition:
{
"id": 3,
"name": "extract-interface",
"deps": ["analyze-structure", "identify-dependencies"],
"acceptance": "Module A's public interface has been extracted into an independent trait; the original module depends only on the trait, not the implementation",
"command": "cargo check --lib && cargo test --lib"
}
name:extract-interface— short, describes what to dodeps: Depends on two prerequisite milestones —analyze-structureandidentify-dependenciesacceptance: Acceptance criteria — describes "what conditions should be met when done"command: Acceptance command gate —cargo check --libensures compilation passes,cargo test --libensures tests pass
The distinct roles of command and acceptance: command is a compile-time check (deterministic), while acceptance is a description for human review reference (non-deterministic). They are not redundant — command checks "does it compile," while acceptance judges "was it done correctly."
ADR Deep Reading
From Flat Todo to Work Graph
CodeCoder's original task management was a simple todos.json file:
{
"tasks": [
{ "id": 1, "title": "Analyze module A", "done": false },
{ "id": 2, "title": "Extract interface", "done": false },
{ "id": 3, "title": "Test", "done": false }
]
}
The problem: todos.json can only express order, not dependencies. Extracting the interface requires analysis of module A to be complete — but the agent might go do "test" first (because it was listed third, but the agent might think "checking the test environment first" is also reasonable).
A more severe problem: todos.json has no concept of "acceptance." Once the agent marks a task as done, there is no mechanism to verify whether it was actually completed. The user has to check manually — but the user might not be online.
The introduction of the Work Graph solved both problems:
- Dependencies are expressed explicitly via the
depsfield — the agent cannot skip incomplete dependencies - Acceptance criteria are implicitly attached via the
acceptanceandcommandfields — verification is required before marking done
After introducing the Work Graph, todos.json was deprecated. But the migration was not simply a file format change — the flat structure of todos.json had caused both the agent and users to overlook the dimension of "task interdependencies." After migrating to the Work Graph, the agent encountered for the first time a blocking state of "dependency not satisfied" — something it had never experienced before. Going from "there is always something to do next" to "some things must wait for others to complete" — this was a qualitative leap in the agent's planning capability.
Next chapter: The three-tier progression of acceptance gates: command gate, check gate, review gate — and how to build trust with them.