Module 1: Why Operating Is Different From Building
The Agent Already Works in a Notebook, Now What?
Description
agent-fundamentals M8 tested the Reservo agent against one script: five turns, hand-written, rehearsed until they produced exactly the trace that guide wanted to show. That's not a flaw in that guide — it's precisely how you test a system while building it: with a known, controlled case that you yourself designed to exercise a specific capability (in that case, self-correction against an invalid tier). It's the right way to confirm the mechanism works.
But there's a huge difference between "it works against the script I wrote" and "it works with whatever a real user asks it." This lesson doesn't change a single line of run_reservo_agent — the agent stays exactly the same, and it keeps working exactly as well. What changes is the context it runs in, and that change of context is, precisely, the problem this entire guide exists to solve.
Connection to the module
Lesson 01 showed that a single line of response doesn't answer five basic operational questions. This lesson stops one step earlier: why those questions start to matter right at the moment the agent leaves the notebook. In the notebook, you wrote the script, you ran the code, you read the output with your own eyes. In operation, none of those three things is true.
Analogy: opening night, and every night that follows
agent-fundamentals M8 compared its capstone to a restaurant's opening night: the full kitchen, serving a real customer for the first time, with a multi-step order that included a genuine error in the middle. That comparison was accurate for what that guide delivered — but notice a detail that stayed implicit: it was one night, with one order, and the restaurant's owner was standing right there, watching every dish come out of the kitchen.
This guide starts the day after opening night. The restaurant no longer has a single test night — it has to open every day, serve strangers who order whatever occurs to them, without the owner necessarily being there to see every dish. The kitchen — the agent — didn't change: it's still the same, it still cooks just as well. What changed is that nobody can rely anymore on "I was there and I saw it work" as evidence that it's still working. That's the exact line where building ends and operating begins.
Worked example: the same function, three orders nobody scripted in advance
run_reservo_agent doesn't know, and doesn't care, whether you call it once in a notebook or a thousand times from a real service. Check it: call it three times in a row, with three different tasks, none repeated from agent-fundamentals's original script.
import reservo_agent as ra
# "User" 1: the usual task.
script_ana = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "list_rooms", "input": {}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_02", "name": "get_quote",
"input": {"room": "Focus", "tier": "premium", "hours": 3}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_03", "name": "get_quote",
"input": {"room": "Focus", "tier": "pro", "hours": 3}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_04", "name": "book_room",
"input": {"room": "Focus", "tier": "pro", "hours": 3, "member": "Ana"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Reservé Focus pro por 3 horas para Ana. Total $60.00. Confirmación #1."}]},
]
# "User" 2: a different room, a different tier, no trip-ups at all -- nobody wrote this before.
script_luis = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Studio", "tier": "basic", "hours": 2}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_02", "name": "book_room",
"input": {"room": "Studio", "tier": "basic", "hours": 2, "member": "Luis"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Reservé Studio basic por 2 horas para Luis. Total $80.00. Confirmación #2."}]},
]
# "User" 3: doesn't book anything -- cancels something that already existed.
script_cancel = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "cancel_booking", "input": {"id": 1}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Cancelé la reserva #1 de Ana."}]},
]
final_1, _ = ra.run_reservo_agent("Reserva Focus pro 3h para Ana", script_ana)
print("Usuario 1 ->", final_1["content"][0]["text"])
final_2, _ = ra.run_reservo_agent("Reserva Studio basic 2h para Luis", script_luis)
print("Usuario 2 ->", final_2["content"][0]["text"])
final_3, _ = ra.run_reservo_agent("Cancela la reserva #1", script_cancel)
print("Usuario 3 ->", final_3["content"][0]["text"])
What to expect:
Usuario 1 -> Reservé Focus pro por 3 horas para Ana. Total $60.00. Confirmación #1.
Usuario 2 -> Reservé Studio basic por 2 horas para Luis. Total $80.00. Confirmación #2.
Usuario 3 -> Cancelé la reserva #1 de Ana.
Three tasks, three correct answers, zero changes to the agent's code. Notice Confirmación #2: Luis's booking_id is 2, not 1, because BOOKINGS — Reservo's state — is shared within this same Python process, and Ana's booking already took 1. That's correct and expected (agent-fundamentals M8, lesson 07, demonstrated it in depth). And the third "user" didn't book anything — they canceled something the second user never even touched. run_reservo_agent had no trouble with the variety: each call is independent, each script solves a different task, and the agent answers correctly to all three, exactly as it would answer to the 1,000th.
What changed, and what didn't
Nothing about the mechanism changed between this lesson and agent-fundamentals M8. It's worth naming with precision what actually does change when this same pattern — calling run_reservo_agent with a question and a script — goes from being a notebook exercise to being a system in operation:
- Volume. A notebook runs one call, looks at it, moves on. A real system receives hundreds or thousands of calls, without a pause between one and the next.
- Variety you don't control. In the example above, you wrote the three scripts — you knew in advance what would happen in each one. In production, the variety of questions comes from users: you don't decide what they're going to ask, in what order, or whether an invalid
tierwill show up on call number 4 or call number 4,000. - Nobody is reading the
print. Above, every response passed in front of your eyes, in your terminal, the exact moment it happened. In a real system, the response fromfinal["content"][0]["text"]goes straight to a chat, an API, an email — an automated process consumes it, not a human standing in front of the console. - Distance in time. You're not going to be watching when call 4,347 on Tuesday at 3 a.m. has something weird happen to the agent. To know something weird happened, you need a trail left behind — and today, as lesson 01 confirmed, none is left.
None of these four points is a problem with the agent's engineering. run_reservo_agent is still just as reliable as the day agent-fundamentals delivered it. The problem is a visibility problem: the distance between "I know this works because I saw it run" and "the system can prove it's still working, without anyone watching it." Closing that distance is, in one sentence, what this entire guide is about.
Common mistakes
-
Thinking "operating" means "rewriting the agent to be more robust." No — the agent is already robust: it validates, retries, cuts off on timeout, never lets an uncontrolled exception through except for the iteration cap. Operating isn't making it more resistant to a single error (that's already done); it's making it visible, measurable, and gated against what happens when it runs unsupervised.
-
Confusing "I tested several different scripts" with "it's ready for production." This lesson's worked example ran three different scripts — and that still isn't operating. It's still a notebook: you wrote all three, you ran them, you read the output. The real difference appears when nobody does those three things for you.
-
Believing that volume is "just a matter of a
forloop." Mechanically, yes — callingrun_reservo_agenta thousand times instead of three is trivial to code. The problem volume exposes isn't one of performance, it's one of knowledge: with three calls you can read all three responses by hand; with a thousand, you need the system to summarize what happened for you, because you can no longer read them all. -
Thinking concurrency (several questions at the same time) is this guide's topic. It isn't. This guide doesn't build a concurrent server or solve race conditions — that's an infrastructure problem, mentioned but not developed here. The focus is simpler and more fundamental: even if calls arrived one at a time, none of them simultaneous, you'd still be unable to answer lesson 01's five questions.
-
Jumping straight to building the logger, without first understanding what information is missing. It's tempting to go straight to Module 2 and start writing logging code. But a logger that captures the wrong fields is almost as useless as no logger at all — that's why this guide spends two more lessons (03 and 04) precisely naming what exactly is missing before building how to capture it.
Exercises
Exercise 1: Predict the booking_id before running it (Easy)
Without running anything: if you added a fourth "user" to the worked example who books Boardroom, basic, 1 hour, for "Carla", right after the third user (who cancels), what booking_id should they receive? Then run it and confirm.
See solution
3. book_room's itertools.count counter only advances when book_room runs — and up to this point it ran twice: Ana (1) and Luis (2). cancel_booking (the third "user") doesn't touch the counter at all; it only deletes entry 1 from BOOKINGS. The next real booking — Carla's — is the third time book_room runs in this process, so it receives 3, not 4.
script_carla = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "book_room",
"input": {"room": "Boardroom", "tier": "basic", "hours": 1, "member": "Carla"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Reservé Boardroom para Carla."}]},
]
final_4, history_4 = ra.run_reservo_agent("Reserva Boardroom basic 1h para Carla", script_carla)
print("Usuario 4 ->", final_4["content"][0]["text"])
print("booking_id real:", history_4[2]["content"][0]["content"])
Expected output (continuing the same process from the worked example):
Usuario 4 -> Reservé Boardroom para Carla.
booking_id real: {"booking_id": 3, "confirmed": true}
Explanation: the real point of this exercise isn't memorizing the exact number, but confirming something more important: nothing in the agent's code keeps count of "how many distinct users" — the counter only counts bookings created, and canceling one neither subtracts from it nor adds to it. That's exactly the kind of operational question ("how many distinct conversations did I handle today, beyond how many bookings ended up active?") that the agent, as it stands, can't answer on its own.
Exercise 2: Simulate "the next day" (Medium)
Run the complete worked example (the three "users") in a fresh Python process. Then, without looking at reservo_tools.py's code, answer: if this same script ran tomorrow, in a new process, would Ana's booking_id be 1 again? Justify it with what you already know from agent-fundamentals M8, lesson 07.
See solution
Yes, it would be 1 again. BOOKINGS and the _booking_ids counter are module-level variables in reservo_tools.py — they live in the RAM of the Python process that imported them. A new process (for example, running the script "tomorrow," in a new terminal) starts with reservo_tools freshly imported, an empty BOOKINGS = {}, and the counter reset to 1. This is, exactly, the limit agent-fundamentals M8 lesson 07 demonstrated with real execution: no Reservo state survives past the end of the process.
import reservo_tools as rt
print("BOOKINGS al arrancar un proceso nuevo:", rt.BOOKINGS)
Expected output (in a freshly started process):
BOOKINGS al arrancar un proceso nuevo: {}
Explanation: this matters for this guide for a concrete operational reason: a real system doesn't run "a script that executes and finishes" — it runs as a long-lived process (or several processes, behind a load balancer), and that is the scope within which booking_id stays consistent. Knowing where that scope begins and ends is part of understanding what you're operating.
Exercise 3: Design, in one sentence, the question none of the example's three "users" can answer (Hard)
With the worked example already run (three responses, in your terminal), imagine your boss asks you: "how many of the requests that came in today had to self-correct because of an invalid argument?" Without writing code yet — that's lesson 04 — explain in a paragraph why you can't answer that question with what you have right now, even though you technically ran all three calls and saw all three responses with your own eyes.
See solution
You can't answer it because the information you need — whether there was a tool_result with is_error: True somewhere along each run's path — exists only inside the history variable of that specific call, and that variable was never saved anywhere beyond the scope of that one line of code. In the worked example, Ana's call's history did have an is_error (the rejected tier="premium"); Luis's and the cancellation's didn't have any. But since the example only printed final["content"][0]["text"] — the final response, not the full trace — that distinction was lost the moment each call to run_reservo_agent returned. Having "seen all three responses with your own eyes" tells you the three tasks were completed; it tells you nothing about how each one was completed. Answering your boss's question precisely — counting how many out of three, or out of three thousand, had at least one is_error — requires deciding, in advance, to capture that signal on every run and store it somewhere that survives beyond that single call. That is, exactly, what lesson 04 starts to build.
Summary and next step
- The agent didn't change:
run_reservo_agentsolves three different tasks — none scripted inagent-fundamentals— exactly as well as the original task, with zero code changes. - What changes between a notebook and a real operation isn't the agent's mechanism: it's the volume, the variety you don't control, the absence of a human reading every response, and the distance in time between when something happens and when someone could notice.
- We confirmed, with a concrete exercise, that even with three calls — seen with your own eyes — you can't answer an operational question as simple as "how many self-corrected?" without having decided, in advance, to capture that signal.
Next lesson: 03 — What You Cannot See Without Instrumentation. We pick back up the car-without-a-dashboard analogy and put it to the test with real code: which questions can be answered by inspecting history by hand, and which ones simply aren't anywhere.
Additional resources
- Anthropic — Building effective agents — On the difference between an agent that solves a demo and one that's reliable with real, varied traffic.
- Anthropic — Tool use (function calling) overview — The protocol
run_reservo_agentalready implements, unchanged, for any task that comes its way. - Python — Modules: scope and lifecycle — The technical basis for why
BOOKINGSstays consistent within one process and resets in a new one. - Python 3.14 — What's New — The version every line of code in this lesson runs on.