Whetstone.
Production-Ready AgentProject: the email assistant, and the chat UI
Module 3, Lesson 614 min

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

The production array
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.

Practice

Try it yourself

Quiz

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?

The four, unordered
PIIMiddleware(...), SummarizationMiddleware(...),
HumanInTheLoopMiddleware(...), ToolCallLimitMiddleware(...)
  1. A[HumanInTheLoop, PII, Summarization, ToolCallLimit]
  2. B[ToolCallLimit, Summarization, PII, HumanInTheLoop]
  3. C[Summarization, PII, ToolCallLimit, HumanInTheLoop]
  4. D[PII, Summarization, ToolCallLimit, HumanInTheLoop]
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.

Recall

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.

Quiz

What to gate

The assistant can read mail, search an archive, draft a reply, and send. Which get an interrupt?

  1. AEvery tool, since any of the four could be wrong
  2. BSend only, because it is the one irreversible action
  3. CSend and draft, since a draft is visible to the user
  4. DNone; the system prompt already instructs it not to send
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.

Do

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.
Done whenYour trace has the after hooks reversed, and you can name the thing that exists at interrupt time, which is the proposed tool call with its arguments and nothing executed yet.
Check

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.

You should see

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.

Sign in to track your progress →