CONCEPT · 45 minutes
Learn how to build stateful triage sessions, checkpoint agent state to disk, and write pure replayable functions — grounded in real support-automation work.
In Module 01, the triage agent extracted a structured JSON object from a single support ticket. It worked — for one turn. The moment a customer sent a follow-up, the problem surfaced.
The first time it happened in the support-automation work, a customer opened a ticket about a
failed data export. The agent triaged it correctly: {"category": "data", "priority": "high"}.
Then the customer replied: "Actually, it started last Tuesday, not Thursday." The agent
re-triaged from scratch. It had no memory of the first turn. Every follow-up was treated as a
brand-new ticket, losing all prior context.
Think of session state like a patient chart. Without one, every visit starts from zero — the doctor asks the same intake questions, misses the history, and makes decisions that contradict last week's plan. The chart is not just helpful; it is the mechanism that makes continuity possible.
The pattern that breaks: A stateless agent called on each turn re-classifies the same ticket repeatedly, loses customer context, and contradicts its own prior responses. This is not a model problem — it is a state management problem.
Here is the single-turn stateless version that Module 01 ended with:
Every call to triage_ticket starts cold. The messages list is rebuilt from nothing. There
is no way for the model to reference what it said one turn ago.
What actually breaks in production: The customer re-states context the agent already heard. The agent contradicts earlier advice. The conversation loops. Support staff intervene because the agent cannot hold a thread.
The fix is straightforward: maintain a session dict that accumulates the conversation history and pass it into every API call. Each turn adds to the history; each call receives the full history.
Two functions do the work. The first builds the messages list from history:
The second calls the API with the full history and records the response:
Now a session is just a dict with a history list. Starting a session looks like this:
The model sees both turns. It can reference the first classification when responding to the follow-up. The conversation holds together.
Watch the context window. A long session with dozens of turns will eventually overflow. Module 03 addresses context compression in depth. For now, keep sessions short or summarize old turns before they accumulate.
Before moving on: if a customer sends 30 follow-ups in one session, what happens to the
messages list passed to the API? How would you decide when to stop including older turns?
Write down your answer — Module 03 will test it.
A stateful session in memory is fragile. A server restart, a process timeout, or a nightly deployment wipes it. In the support-automation work, sessions could span hours — a customer might open a ticket in the morning and reply after lunch. Keeping everything in memory was not an option.
The solution is checkpointing: serialize the session to disk after every turn. If the process restarts, load the checkpoint and continue from where things left off.
When the customer replies hours later — in a new process — resume from disk and continue:
The session is serializable because it contains only plain Python values: strings, lists, dicts. No client objects, no open file handles, no threads. This is deliberate — everything the session needs to reconstruct itself must fit in JSON.
Checkpoint after every turn, not at the end. If you only checkpoint on completion and the process crashes mid-session, the customer starts over. Checkpoint early and often.
Reflective prompt: What would you serialize to disk versus keep in memory only, and why? Consider: the Anthropic client object, the raw API response object, a database connection, the customer's email address, and the list of ticket categories. Which of these belong in the session dict?
The checkpoint approach works because triage_turn is a pure function: given the same session
dict and the same customer message, it always produces the same API call. It does not hold
internal state. The state lives in the session dict — outside the function, owned explicitly
by the caller.
This matters most when something goes wrong. In the de-identified clinical-AI work, any uncertain AI decision had to wait for a clinician review before it reached a patient. The agent could be interrupted at any point and had to resume from exactly where it left off. That was only possible because each processing function was pure — the clinician could inspect the session dict, see every prior decision, and either approve or override before the agent continued. No hidden state, no internal counters, nothing that lived only inside a running object.
This is Factor 5 (Own Your Control Flow) and Factor 6 (Stateless Functions, Explicit State) in
practice. The control flow — when to call classify_ticket, when to checkpoint, when to escalate
— lives in the caller. The function itself is dumb and replayable.
The test for "stateless enough": Can you replay the function by re-loading the session dict from disk and calling it again? If yes, the function is stateless enough. If you need to reconstruct internal object state first, it is not.
Apply what you have learned about session state, checkpointing, and stateless functions.
1. A customer sends three follow-up messages to a stateless triage agent. What does the agent see on the third call?
Correct Answer: B
A stateless agent builds a fresh messages list on every call. Without a session dict accumulating history, the third call receives only the third message.
2. In build_messages, why is the agent reply appended with role 'assistant' rather than role 'user'?
Correct Answer: A
The Anthropic messages API requires turns to alternate between 'user' and 'assistant'. Placing the prior agent reply as 'assistant' tells the model this is what it previously said — not another customer message.
3. You checkpoint after every turn. The process crashes after saving turn 3 but before processing turn 4. When you resume, where does the session pick up?
Correct Answer: C
Checkpointing after every turn means the last good save includes turn 3. On resume, the session picks up with history containing turns 1 through 3, and processes turn 4 next.
4. Which of these objects should NOT be included in a serialized session dict?
Correct Answer: C
The Anthropic() client holds network connections and is not JSON-serializable. It must be re-created when the session is resumed. Session dicts should contain only plain values: strings, lists, dicts, numbers.
5. What makes classify_ticket(ticket_text, system_prompt) a 'pure' function in this context?
Correct Answer: C
A pure function's output depends only on its inputs. Because classify_ticket holds no internal counters or state, you can re-call it with the same arguments and get the same API call — which makes it testable and replayable from a saved session.
6. A 50-turn session keeps the full history in the session dict and passes all 50 turns to every API call. What breaks first?
Correct Answer: B
Every API call with all 50 turns eventually exceeds the model's context window. The API returns an error. Module 03 addresses context compression to prevent this.
The triage session below handles two turns but does not checkpoint. Add checkpoint-and-resume so that the session survives a process restart between turns.
What you are building: wrap triage_turn so that every call saves the session to disk and
can be resumed by loading from disk in a fresh Python process.
Hint 1: json.dumps(session) serializes a plain dict. Use pathlib.Path(path).write_text()
to save it. Use json.loads(pathlib.Path(path).read_text()) to restore it.
Hint 2: Call checkpoint(session, path) immediately after each triage_turn call — before
any other work. That way, a crash after the turn but before the checkpoint only loses the
checkpoint write, not the turn result.
Hint 3: To test resume, run the first turn in one Python script, then start a second script
that calls resume(path) and runs the second turn. If the history shows both turns, it works.
In the next module you will add context compression so that long sessions do not overflow the context window — and you will see how the stateful session structure you built here makes that straightforward to add.