JJoeven

Curriculum/Agent Architectures

Plans Are Data, Not Poetry

A plan is a list of typed steps with ids. Free-text paragraphs cannot be skipped, retried, or shown in a UI.

intermediate19 min8 / 24

If the planner writes a paragraph, you cannot:

  • Skip a step that is already done
  • Retry only the failed step
  • Show progress in a UI
  • Guard which tools exist on which step
  • Replan from a step id
  • Freeze one step’s args for a human

So the plan is JSON: a list of objects with id, do, optional tool, optional depends_on, and a status your runtime owns (todo, doing, done, failed).

Poetry cannot be retried. Operations need ids and status.

A plan is a list of steps
iddotoolstatus

Skip, retry, and a progress bar need these fields. A paragraph cannot.

A plan is a list of steps

The planner may draft English. The parser must turn it into this list or reject the plan. A plan missing do is rejected before the loop starts. You do not begin a furnace because the model wrote a nice essay.

Fields that earn their keep

FieldWhy
idStable handle for retry, UI, HITL, traces
doWhat this step means in your runtime
toolOptional; some steps are pure code (check_window)
depends_onOptional; do not run 3 before 1
statusOwned by the runtime, not the model

The model should not set status: done in the draft. You mark done after the executor succeeds. If the planner is allowed to mark done, it will skip work.

do is a small enum you handle in code: lookup, check_window, decide, refund. Open English do (“maybe be nice”) is a paragraph again.

Parse, then walk

parse_plan is the parser costume for plans. Fail closed: missing do, unknown tool, duplicate id, empty list. Do not start execute_plan on an error object.

Default id to the enumeration index only if you must. Prefer ids from the planner so replans can keep them. If you replan and ids change, UI progress lies.

Skip, retry, next

next_todo is the cursor. After a crash, you do not guess the cursor from prose. You look at status. Skip means mark done without running (already have the lookup in notes). Retry means set one failed step back to todo. Restart-all is a last resort when the goal changed.

When a step fails, replan from here, do not restart from step 1 unless the goal changed. Side-effecting tools on done steps must not run again unless they are idempotent (tools track).

Guards on steps

Typed state said: refund is illegal in gather. Plans can carry the same idea: the refund step’s tool is only legal if notes have ok: true. That is a guard on the step, not a hope in the paragraph “then refund if appropriate.”

HITL later freezes one step’s args, not a poem. You need an id to freeze.

UI is done / total

The progress bar is count of done over length of plan. Blocked is “waiting_human on step 3.” Failed is step id plus error. None of that exists for a paragraph. If your UI shows a spinner and a thought, you do not have a plan product. You have ReAct with a planning essay up front.

Replans are versions

Store plan_v1, plan_v2 on the run. Do not overwrite. Operators ask what you intended at step 0 vs after the lookup failed. A single mutating list without history is how you gaslight yourself.

depends_on, failure records, and empty plans

If step 3 needs the order dict from step 1, say so as data: depends_on: [1]. The walker should not run 3 while 1 is todo or failed. Implicit order (list index) is fine for linear refunds. The moment you have two lookups that could run in either order, ids plus depends_on beat a paragraph that says “then, after the lookups.” Parallel reads are an optimization the tools track already allowed. Parallel writes of dependent steps are how you charge before you check the window. The plan object is where you forbid that.

When a step fails, record why on the step: status: failed, error: timeout, maybe obs truncated. The replan prompt (or the code path that rebuilds the list) should see that record, not a thought that “something went wrong.” If you delete the failed step instead of marking it, you cannot retry only that id. If you leave it todo, next_todo will spin on a broken lookup until the budget. Failed is a real status. Skip is a real status (already have the data). Todo / doing / done / failed / skipped is enough. Do not invent “kinda.”

Empty plans and one-step plans are parse errors or special cases you decide in code. An empty list is not “the model wants to finish.” It is bad_plan. A one-step plan that is only finish is observe-before-finish again: illegal unless evidence is already in context. A plan that lists tools not in the registry is the parser’s unknown_tool, applied to the whole list before step 1 runs. Rejecting the plan is cheaper than executing step 1 and dying on step 2’s invented name.

The planner is a model call with a schema. Temperature and poetry belong in a scratch thought you throw away. The object you store is the list. If the model also writes a rationale field, log it like a ReAct thought: operator UI only, never the thing the walker switches on.

Common mistakes

  • Storing the plan as markdown bullets only.
  • Letting the model own status.
  • Open-ended do strings you cannot switch on.
  • Restarting from 1 after a write.
  • No parse of the plan (walking a blob).
  • Changing ids on every replan.
Live PythonOpen full playgroundpython
Output
Run to execute this in your browser. Nothing is sent to a server.

Step 1 is done. Next is refund. A step without do is rejected before the loop starts. mark returns True when the id exists. next_todo walks in list order. That order is your default depends_on. If you need real edges, add them as data; do not encode them in a sentence.

The bad parse never produces a plan you can mark. That is fail closed for planners.

How agents use this

The UI progress bar is done / total on this list. Human-in-the-loop later freezes one step’s args, not a poem.

Checkpoints (later) should save this list, not a 4k “memory” string. Replay means load statuses and continue next_todo. If you only saved the paragraph, replay is “ask the model what we were doing.” That is not a checkpoint.

Check your understanding

Why store a plan as a list of objects instead of a paragraph?