Curriculum/Agent Architectures
Long-Running Agents
A job with a store and a wakeup, not a request that holds a socket for an hour.
If a loop may wait on a human, a batch job, or tomorrow’s email, it is a job:
run_id- serialized state
wake_when(event, cron, or approval)- a worker that loads, steps, and saves
The worker loads, takes a slice, saves, and sleeps. The store is the truth.
A job, not a held socketDo not hold an HTTP request open for an hour. Do not keep a chat socket as your only store. Cloud functions time out. Laptops sleep. The browser tab closes. HITL “waiting_human” is overnight by nature. An agent that emails you in three days is this pattern, not a longer prompt.
The loop you already wrote becomes step_job(state) -> new_state. The platform retries the worker, not the user’s browser tab. Anatomy still holds: each wakeup, you assemble (maybe), or you apply an event, then save. You do not restart the furnace from step 1 unless the checkpoint says so.
Requests die, jobs resume
A request is a live connection. It has a timeout. It has a client that can hang up. It is the wrong container for “wait until the invoice email arrives.” A job record is a row (or document) that outlives the process. The worker is a short function: load, maybe step, save, exit. A scheduler or queue calls it again when wake_when matches.
If you only store the loop in RAM, a deploy kills the agent. If you only store the transcript in the model’s context window, you cannot wait at all. If you store in the user’s clipboard, you are joking — and yet people try “copy this JSON and paste tomorrow.” Use a store.
The four fields
run_id — unique. Resume, traces, HITL links, freeze hashes all key off it.
serialized state — typed state plus memory pointers plus pending freeze. JSON. Version it if you can (v1). Pickle is how you cannot load after a class rename.
wake_when — why this job is asleep: mail:invoice, cron:tomorrow, approval:tix_9, tool:timeout-retry. Null means runnable now or done.
worker — step_job. Idempotent enough that a double wakeup does not double-write unless you intend it. Tools track owns idempotency keys. The worker should not call refund twice because the queue delivered twice — checkpoint and keys.
step_job is the loop in slices
Each call does a little:
- phase start → park at wait_email, set wake_when
- phase wait_email without email → return unchanged (still waiting)
- phase wait_email with email on state → done, answer, clear wake_when
The email arrives on a different event: some inbound handler writes state["email"] and saves, then the worker runs. The socket that received the mail is not the agent loop. The store is the truth.
HITL pause is a wakeup: approval event sets a decision, worker resumes freeze check, maybe executes. Tool timeouts are a wakeup: retry once later, not while holding the request.
Do not step forever in one worker
A worker that internally while True until done reintroduces wall-time death. Slice: one phase, or a small step cap, then save. The next wakeup continues. Stop budgets still apply across slices: store steps_used on state. An overnight job with no step cap is still a furnace; it just bills slowly.
What the user sees
The user sees “working” or “waiting on email / waiting on a person.” They do not see a spinner tied to one HTTP call for an hour. Operators see run_id and wake_when. If wake_when is stuck, the job is stuck — not the model.
What this is not
This is not a production course in queues and brokers. Dicts and a STORE in the live box are the shape. When you go to a real queue, keep the same names: run_id, state, wake_when, step. If the cloud diagram cannot map onto that, you bought a maze. Prod-as-a-track comes later. Here you need the loop to survive sleep.
Multi-agent does not replace jobs. Five specialists in one request still die when the request dies.
Events, crons, and who is allowed to write state
Wakeups are events you name: mail received, approval clicked, cron tick, tool retry due. Each event handler should write a small, typed update (email id, decision, clock) then call the worker — or enqueue the run_id. Do not let the mail handler execute refund itself. It does not own the loop. It owns “this mail arrived.” The worker owns phase transitions.
Cron is a wakeup that says “it is tomorrow,” not a second brain. If nothing is due, step_job returns unchanged. That is cheap. A cron that starts a new agent every tick without run_id is a furnace factory.
Idempotency of the worker: two deliveries of the same mail should not file twice unless you want that. Store processed event ids on state, or use tool keys. step_job on wait_email when email is already filed should see phase done and return. The live box does not show that guard; add if phase == done: return state in your head. Missing it is how double wakeup double-files.
Serialization must round-trip every field the next step reads: phase, wake_when, email, pending freeze, steps_used, memory pointers. If JSON drops None vs missing, normalize on load. If you store only the transcript, you cannot wake. If you store only phase and forget wake_when, the scheduler cannot find you.
The user-facing “waiting on email” is a projection of phase. Operators get run_id. Support should not need to keep a tab open. That is the whole point of a job.
Common mistakes
- Open HTTP for an hour.
- RAM-only loop.
- Worker while-True until dawn.
- No run_id.
- Pickle.
- Double wakeup double refund.
- Prompt: “wait three days then continue” with no store.
Run to execute this in your browser. Nothing is sent to a server.
Start parks the job. Wakeup with an email finishes it. The store is the truth, not the socket. After start, phase is wait_email and wake is mail:invoice. After the mail is written onto state and stepped, phase is done and the answer cites m9. A step in the middle without email would have returned the same wait.
JSON dump/load is the serialization. If you added a nested freeze dict, it would survive the round trip. A live Python object in a global would not survive a process restart — this STORE is a stand-in for a database.
How agents use this
Queue + DB + worker. HITL pause is a wakeup. Tool timeouts are a wakeup. “Agent that emails you in three days” is this pattern, not a longer prompt.
Checkpoints (next) are the snapshots inside the job. Jobs without checkpoints restart blindly. Checkpoints without jobs still die when the process dies. You want both.
Check your understanding