Topic 37

Decomposition

Subtasks

A ticket with three intents is three pieces of work that succeed and fail independently. Decomposition makes that explicit — a list of subtasks, each carrying its own status — and it exists to prevent one specific failure: the agent solves the interesting problem beautifully, writes a confident reply about it, and never mentions the boring one.

That failure is nearly invisible in production. The customer got an answer, the run ended cleanly, no exception was raised, and the ticket closed as resolved. It shows up as a second ticket from the same buyer four days later, filed under a different subject, counted as new work.

Ticket T-40219: one paragraph, three subtasks, and the field that says the run is not finished
Ticket
T-40219 · order SU-88421
three intents, one paragraph
Subtask 1 · resolved
Refund the duplicate delivery charge
evidence: issue_refund 995 cents, turn 6
Subtask 2 · in_progress
Settle the cracked side panel
evidence: seller supplement SUP-ASH-011, turn 4
Subtask 3 · open
Confirm no replacement is sent
evidence: none
Loop exit
An open item means the run is not finished
code reads the field · six of eight failures used to close as resolved

Subtasks as State, Not Prose

The three intents of the canonical ticket can live in two places. They can live in the transcript, as a sentence the model wrote on turn two — "I need to handle the damage, the duplicate charge, and confirm no replacement" — or they can live in a structured object your code owns, alongside the conversation state from Chapter 6.

Subtask state for ticket T-40219, held outside the transcript and updated by the dispatcher
{
  "ticket": "T-40219",
  "order": "SU-88421",
  "subtasks": [
    {"id": 1, "goal": "refund the duplicate delivery charge",
     "status": "resolved",
     "evidence": "issue_refund 995 cents, turn 6"},
    {"id": 2, "goal": "settle the cracked side panel",
     "status": "in_progress",
     "evidence": "seller supplement SUP-ASH-011 retrieved, turn 4"},
    {"id": 3, "goal": "confirm no replacement is being sent",
     "status": "open", "evidence": None}
  ]
}

Three fields do the work. The goal is written in the customer's terms, the status is one of a fixed set your code sets rather than the model, and the evidence names the tool call or document that justifies the status. Nothing here is prose the model has to remember to repeat. It rides in the request as pinned material, it is about 130 tokens, and no summarizer is allowed near it.

The difference is what happens on turn nineteen of a long thread. Prose in the transcript competes with everything else in the buffer and can be compacted away entirely — the failure Chapter 5 measured, where a fluent summary drops the only line with money attached. A field with "status": "open" in it cannot be forgotten by a model, because no model is being asked to remember it. Your code reads it and decides the run is not finished.

Granularity

Set subtasks at the level of a customer-visible outcome: refund the duplicate delivery charge, settle the cracked panel, confirm no replacement. Each one is something the buyer would recognize as done or not done, and each one maps to a sentence in the final reply.

Decomposing finer is the common mistake, and it is worth being concrete about why. A plan that reads "call get_order, read the charges array, compare the two delivery lines, call issue_refund" has turned the model into an interpreter executing a small program badly. Every step is a chance to stop, every step costs a model call, and the plan breaks the moment the order record has a shape the planner did not anticipate. The sequencing inside a subtask is exactly what tools and dispatcher code already do reliably — that argument is Chapter 3's, and decomposition does not get to re-fight it at a higher level.

Ordering and Dependencies

A few subtasks depend on an earlier result and most do not. The duplicate delivery charge can be settled without knowing anything about the crack: the order record shows two identical charges, the policy for a billing error is unconditional, and the amount is $9.95. The damage subtask cannot start before the seller supplement comes back, because whether a return is even possible depends on which seller shipped it.

Put that difference in the structure rather than leaving it in the model's head. A subtask with an explicit depends_on is a thing your loop can reason about: independent items can be worked in any order, or issued as parallel tool calls on the same turn where the API allows it, and a blocked item is skipped rather than attempted and failed. Leave it implicit and you get the version where the agent asks the customer for a photograph before it has established that the seller accepts returns at all.

Plan Drift

The plan says three items. The agent has spent the last two turns chasing a fourth — the carrier's missing scan — that appears nowhere in it. That gap between the plan and the transcript is plan drift, and detecting it is the entire difference between a plan that constrains a run and a plan that decorates one.

The check is cheap and it belongs in the loop, not in the prompt. Every few turns, compare the tools actually called against the tools the open subtasks expect, and compare the number of turns spent against the number of subtasks still open. At Sundry the rule is three turns: three consecutive turns with no subtask changing status is divergence, and divergence forces either a status update or a replan. Neither of those is a model decision — the code notices, and the code chooses what to do about it.

The eval set makes the case for the whole mechanism. Nineteen of Sundry's 120 tickets carry more than one intent. Before subtasks were held in state, the agent fully resolved eleven of them, and six of the eight failures closed as resolved with an intent silently dropped — the worst possible ending, because the metrics were reporting a success. With subtask state and a drift check, seventeen of nineteen resolve, and the two that do not escalate explicitly with the open item named. Silent drops went to zero, which matters more than the eleven-to-seventeen move.

Replanning

New information sometimes invalidates the plan rather than advancing it. The carrier scan comes back showing the parcel was never marked delivered, which means the case is not a damage return at all — it is a non-delivery claim against the carrier, with a different policy, a different remedy, and one of the three subtasks now meaningless.

Replanning must be a deliberate step with its own record: the old plan, the fact that triggered the change, the new plan, and the turn it happened on. Sundry writes that as a replan event next to the subtask state. A silent rewrite costs two things — the audit trail loses the reason a refund was justified under one policy and issued under another, and the customer gets a confident answer to a question they did not ask, with no trace of the moment the agent changed its mind.

Closing the Loop

Every subtask ends in exactly one of three states: resolved, escalated, or explicitly deferred with a reason. There is no fourth state and no default. A run whose subtask list still contains open when the loop stops is not a finished run, and the code that ends the loop is where that is checked — never the model, which will happily write a closing paragraph over an open item.

The final message then reports all of it, not the flattering part. "I've refunded the duplicate $9.95 delivery charge, arranged a collection for the cracked unit, and confirmed no replacement is on its way" is three sentences for three subtasks. When one is unresolved the reply says so in the same plain terms, and the metrics record a partial resolution rather than a success — which is the ending Topic 39 has to make a first-class outcome rather than an edge case.

Common Mistakes
  • Keeping the plan only in the transcript — compaction replaces it with a fluent summary, and the agent finishes the subtasks it can still see while closing the ticket as resolved (Chapter 5).
  • Decomposing into tool-sized steps — the model becomes an interpreter for a small program, pays a model call per instruction, and the plan breaks on the first record shaped differently than expected.
  • Never comparing progress against the plan — half-finished tickets close as successes, which is invisible in transcripts and obvious in the eval set, where six of eight multi-intent failures looked clean.
  • Replanning silently — the audit trail loses why the remedy changed, and the customer receives a well-written answer to a question they never asked.
Best Practices
  • Hold subtasks in structured state with a status field your dispatcher sets, and pin that object into every request the way Chapter 5 pins irreversible actions.
  • Set granularity at the customer-visible outcome, so every subtask maps to one sentence of the final reply.
  • Compare plan against progress every few turns and act on divergence in code — three turns with no status change is a replan, not a nudge in the prompt.
  • Report unresolved subtasks explicitly in the customer message and record them as partial resolutions in the metrics.
Comparable toolsAgentic coding tools the same subtask list, shown to the userTemporal per-activity status that survives a crashLangGraph plan state carried between nodesAn ordinary Postgres table the version that outlives the process

Knowledge Check

Why do subtasks belong in structured state rather than in a sentence the model wrote on turn two?

  • Prose in the transcript competes for attention and can be compacted away; a status field cannot
  • A structured object costs no tokens, so the plan stops consuming the context budget entirely
  • Models follow instructions written as JSON more reliably than instructions written as prose
  • Structured plans stay correct as new tool results arrive, so an explicit replanning step is never needed

A team writes plans as sequences like "call get_order, read the charges array, compare the delivery lines, call issue_refund". What goes wrong?

  • The model becomes an interpreter executing a program, paying a call per step and able to stop at any of them
  • The plan copes better with unexpected record shapes, at the cost of being harder to read back in a trace
  • The customer receives a reply written in tool names, because the plan text is reused in the final message
  • Dependencies between subtasks can no longer be expressed, since each step now has exactly one predecessor

What does plan drift look like from inside the loop, and what should the code do about it?

  • Turns pass with no subtask changing status, and the code forces either an update or an explicit replan
  • The model reports that it has lost the plan, and the loop responds by re-sending the plan in the prompt
  • A tool call fails with an error the dispatcher can catch, and the loop retries the subtask that raised it
  • The context budget is exceeded, and the loop compacts the transcript to make room for the remaining subtasks

A run ends with two subtasks resolved and one still open. What is the correct ending?

  • Name the open item in the reply and record the run as a partial resolution in the metrics
  • Close the ticket on the two resolved items, since the customer's main problem was actually handled
  • Ask the model whether the remaining item still matters, and end the run if it says the reply is complete
  • Keep looping until every subtask reaches resolved, since a partial ending is not a valid outcome

You got correct