Project: the personal chef
Four ingredients, one constructor. The project lessons in the official course are where the pieces stop being independent, and the useful thing about them is not the recipe domain, it is the wiring.
The assembly
agent = create_agent(
model=init_chat_model("gpt-5.5", temperature=0),
tools=[search_recipes, check_pantry],
system_prompt="You are a personal chef. Respect stated dietary restrictions permanently.",
response_format=Recipe,
checkpointer=InMemorySaver(),
)
result = agent.invoke(
{"messages": [HumanMessage(content=[
{"type": "text", "text": "What can I make with this?"},
{"type": "image", "url": "https://example.com/fridge.jpg"},
])]},
{"configurable": {"thread_id": "chef-rai"}},
)
recipe = result["structured_response"]Every line there is a lesson from this module. The model instance is lesson 1, the tools are lesson 2, the checkpointer and thread id are lesson 3, the content blocks are lesson 4, and response_format is the one new thing.
The one new parameter
response_format takes a schema and the parsed object comes back on result["structured_response"].
The bug this project exists to teach
The dietary restriction is stated on turn one and forgotten by turn three. There is a checkpointer. Nothing errors.
The cause, almost every time, is a freshly generated thread id per invocation. Persistence is present and working perfectly; it is persisting each turn into its own private conversation that nobody ever reads again.
This is worth dwelling on because of how it fails. The symptom is identical to having no checkpointer at all, and the code has a visible, correct checkpointer in it, so review slides straight past. When memory looks absent, check the key before you check the store.
Why the project format is worth doing
Reading four lessons gives you four correct mental models that have never been in the same room. Assembly is where you find out that two of them disagreed and you had not noticed.
So do the deliberate-breakage exercise. Predicting the symptom before you cause it is the difference between knowing what a parameter does and knowing what its absence looks like at three in the afternoon, which is the form the exam actually asks in.
Try it yourself
Where the structured response comes back
The agent is created with a response_format. Where does the parsed object appear?
class Recipe(BaseModel):
title: str
minutes: int
agent = create_agent(model=..., tools=tools, response_format=Recipe)
result = agent.invoke({"messages": [...]})Show answer
Correct answer: C — result["structured_response"]
The parsed object lands on result["structured_response"], alongside the messages rather than instead of them. The first option is the tempting one because the final message is where you look for the answer in every other case; response_format adds a key, it does not replace the message list.
Diagnose the assembly
The chef agent forgets the user's dietary restrictions between turns, even though they were stated clearly two messages ago and nothing errors.
agent = create_agent(
model=init_chat_model("gpt-5.5", temperature=0),
tools=[search_recipes, check_pantry],
system_prompt="You are a personal chef.",
checkpointer=InMemorySaver(),
)
agent.invoke({"messages": [...]}, {"configurable": {"thread_id": str(uuid4())}})Show answer
Correct answer: B — A fresh thread id per call means every turn starts a new conversation
The checkpointer is present and working; a new uuid per invocation means every call resumes an empty thread. The first option is the reflex answer because forgetting reads as no-persistence, and it is why this bug survives code review: the persistence is visibly there, and the key selecting it is the broken half.
What each ingredient contributes
The point of a project lesson is that four things you learned separately have to sit in one constructor without contradicting each other.
For a chef agent, state what the model, the tools, the checkpointer and the content blocks each contribute, in one clause each.
Reveal answer
The model decides what to do and phrases the answer. The tools give it the two things it cannot do from its own weights, which are looking up recipes and reading the actual pantry. The checkpointer plus a stable thread id carry the dietary restrictions and the running conversation across turns. Content blocks let the user hand over a photo of what is in the fridge rather than typing an inventory.
Build it in one file
Small enough for a scratch file, offline apart from the model call, and it is the first time all four pieces have to agree with each other.
- Write two tools, search_recipes and check_pantry, each with a docstring good enough that a stranger could tell when to call it.
- Assemble create_agent with an instance model, both tools, a system prompt and InMemorySaver.
- Use one fixed thread id, not a generated one, and hold a three-turn conversation where turn three depends on a fact from turn one.
- Add a response_format with a small Pydantic model and read the result back from the structured_response key.
- Send one HumanMessage whose content is a list containing a text block and an image block.
Break it deliberately
Remove one ingredient at a time from the working agent and predict the symptom before you run it.
You predicted the symptom correctly for at least three of the four removals, and you noticed that removing the checkpointer and randomising the thread id produce the same visible symptom.