Project: the email assistant, and the chat UI
Everything from module 3 in one agent. The email assistant is the official course’s closing project because email has all four production pressures at once: long threads, an irreversible action, unattended running, and real data you should not leak.
The stack
agent = create_agent(
model, tools=[read_mail, search_archive, draft_reply, send_mail],
system_prompt="...",
middleware=[
PIIMiddleware("email", strategy="redact"),
SummarizationMiddleware(model="gpt-5.4-mini", trigger=("fraction", 0.8)),
ToolCallLimitMiddleware(run_limit=25),
HumanInTheLoopMiddleware(interrupt_on={
"send_mail": {"allowed_decisions": ["approve", "edit", "reject"]},
}),
],
checkpointer=checkpointer,
)Read that array as a nesting, not a list. Position 0 is outermost, so the PII guard wraps everything and the approval gate sits innermost, nearest the tool call it exists to intercept.
Note the explicit trigger on summarization. Leaving it off would give you a middleware that never fires, which is the silent failure from lesson 3.
Gate on irreversibility, not on risk
Four tools, one interrupt.
The bonus lesson: rendering the interrupt
The official course closes with an Agent Chat UI lesson, and the useful part of it is structural rather than visual.
An interrupt payload carries action_requests, each with a tool name, its arguments and a human-readable description, plus review_configs naming the allowed_decisions per action. Any front end that renders approvals is rendering exactly those four things, and it resumes by sending back a decisions list in the same order as the requests.
I have not verified the current button labels in that interface, so treat the following as structural rather than as a description of specific controls: expect a paused-run view showing the proposed call and its arguments, a control per permitted decision, and an editable form for the arguments where edit is allowed. If edit is not in allowed_decisions, that form should not be reachable.
Which is the last useful thing this course can tell you about the shape of these systems: the UI is downstream of the payload. Get interrupt_on right and the interface has something honest to render. Get it wrong and no amount of front end fixes a gate that was never asked to pause.
Try it yourself
Order the array
Four middlewares for an email assistant. Given that before hooks run first to last and after hooks run in reverse, which array puts the PII guard outermost on the way in and the approval gate nearest the tool call?
PIIMiddleware(...), SummarizationMiddleware(...),
HumanInTheLoopMiddleware(...), ToolCallLimitMiddleware(...)Show answer
Correct answer: D — [PII, Summarization, ToolCallLimit, HumanInTheLoop]
Position 0 is outermost, so PII first puts it around everything, and HumanInTheLoop last puts it innermost, closest to the tool call it gates. The first option is the tempting inversion, because putting the approval gate at the head of the array reads as giving it priority; first in the array means outermost, which is furthest from the call rather than nearest to it.
What module 3 actually added
The agent from module 1 already worked. Naming what changed is the clearest way to see what production-ready means here.
The module 1 agent already answered mail correctly. What did module 3 add, and what failure does each addition prevent?
Reveal answer
Middleware itself, which is where the rest hangs. Summarization or context editing, preventing a long thread from filling the window mid-run. An approval gate, preventing an irreversible send from happening without a human. Call limits, preventing a loop from burning budget unattended. Error and retry handling, preventing one flaky tool call from ending the run. None of them make the agent smarter; every one of them changes what happens when something goes wrong.
What to gate
The assistant can read mail, search an archive, draft a reply, and send. Which get an interrupt?
Show answer
Correct answer: B — Send only, because it is the one irreversible action
Gate on irreversibility, and sending is the only action here that cannot be undone. The first option is the instinct that ruins these systems: gating everything means the human approves twenty harmless reads to reach the one decision that mattered, stops reading carefully by the fifth, and the gate becomes a formality rather than a control.
Specify the stack, then trace one turn
Paper again. The value here is the trace, not the code, and the trace is exactly what a scenario question asks you to do in your head.
- Write the middleware array in order, with one sentence per entry saying why it sits where it does.
- Trace one full turn, naming every hook that fires and in what order, including the reversal on the way out.
- Mark the point in that trace where the interrupt fires, and say what exists at that moment that did not exist a step earlier.
- Write the interrupt_on mapping, and for each gated tool say which decisions you allowed and why you excluded the others.
Describe the approval surface
Without opening it, write down what any interrupt-rendering front end has to show, derived from the payload rather than from a screenshot.
Your list includes the tool name, the proposed arguments, the human-readable description, and the permitted decisions, and you derived all four from action_requests and review_configs.