Understand observe, decide, act, and update as the spine of tool-using agents.
You'll see why a single prompt-and-reply isn't enough for real tasks, and you'll build a clear mental picture of the four-step cycle using a concrete running example: an agent that answers 'What is the weather in Paris, and should I bring an umbrella?'
Explains why a single model call isn't enough and introduces the four-step agent loop — Reason, Act, Observe, Update & Decide — using a concrete weather-query example.
Why this matters: Every agentic system you build runs on this loop; understanding its four steps lets you reason about what your agent is doing at any moment and debug it when it goes wrong.
Imagine you ask: "What is the weather in Paris, and should I bring an umbrella?" A plain language model (an — a text-in, text-out AI) can only guess the answer from memory.
It has no way to check a live weather service, so it either makes something up or says it doesn't know. Neither answer is useful when you're packing a bag.
The real answer: the model replies with a confident-sounding but stale or invented forecast — it has no live data. To do the job properly, the model needs to act (call a weather tool), read the result, and then answer. That requires a loop, not a single reply.
An is an LLM wired into a repeating cycle so it can take actions and use what it learns. Each turn of the has exactly four steps.
If the task isn't done, the loop runs again from step 1. When a is met — the goal is answered, or a limit is hit — the agent exits and returns its final reply.
Here's what a single-prompt approach looks like — and exactly how it fails.
You send the model one message: "What is the weather in Paris right now, and should I bring an umbrella?"
The model has no tool access, so it replies from training data — something like:
This sounds reasonable, but it's useless: it's a generic guess, not today's forecast. The model can't call a weather service, so it fills the gap with plausible-sounding text. This is called hallucination — the model invents a confident answer it cannot actually know.
The loop fixes this by giving the model a way to act first and answer only after it has real data.
Read each moment in the Paris agent's run below. Before checking the label, name the step yourself — Reason, Act, Observe, or Update & Decide.
Answers: 1 → Reason, 2 → Act, 3 → Observe, 4 → Update & Decide. If you got all four, you can already map any agent moment to its step.
In the next module — Observe — you'll zoom into step 3 and trace exactly what information lands in the agent's at the start of each cycle: the goal, the available tools, and any prior results.
Follow the agent through one full cycle for the Paris umbrella question.
Three things go wrong most often — and two of them are silent.
You'll trace exactly what information lands in the agent's context at the start of each cycle — the goal, the available tools, and any prior results — using the Paris weather example to see a real observation payload.
This module explains what information an agent reads at the start of each loop cycle — the goal, available tools, and prior results — and what goes wrong when any part is missing.
Why this matters: Understanding the observation step lets you build agents that always have the right context, and debug them when they repeat work or give wrong answers.
The four steps are: Observe, Decide, Act, and Update repeats these steps until the task is done.
This module zooms in on the very first step — Observe. The driving question: what exactly does the agent see at the start of each cycle, and what happens when something is missing?
.
This snapshot has three parts.
— its working memory.
The Paris weather agent assembles its observation for cycle one.
"What is the weather in Paris right now, and should I bring an umbrella?"
: get_weather(city: string).
says: call this with a city name to get current conditions.
This is cycle 1, so prior results are empty.
The agent knows it must call a tool before answering.
Click each query to see which observation parts are closest to it. Points that cluster together belong to the same role in the observation.
When any part is wrong or missing, the agent makes a bad decision.
The Paris weather agent starts cycle 2 with this observation:
get_weather(city: string)Hint 1: compare this to the correct cycle-2 observation — what should be in prior results? Hint 2: what will the agent do when it sees no prior results?
The step-1 tool result (Paris weather data) is missing from prior results. Because the agent sees an empty prior-results section, it thinks it hasn't called the tool yet. It will call get_weather("Paris") again — wasting a step and potentially returning a slightly different result the second time. No error is raised; the agent just repeats itself.
Before trusting AI-generated observation code, check three things.
.
You know what breaks when one is missing.
Reading the observation is only half the story.
Once the agent has that full picture, it must decide what to do.
The next module follows the Paris weather agent as it reads its observation, weighs options, and chooses which tool to call.
You'll follow the agent's internal reasoning chain as it reads the Paris weather observation, weighs its options, and chooses to call the weather tool — seeing exactly how the model produces a structured tool-call request rather than a plain text answer.
This module shows how an agent reasons through its options and decides whether to call a tool or answer directly.
Why this matters: Understanding the decide step lets you read any agent transcript and know exactly why the agent did what it did — a skill you need to debug and improve agents you build.
Decision this forces: Call a tool vs. answer directly — the agent must choose one on every cycle.
Module 2 showed that the agent's always contains three things: the goal, available with schemas, and prior step results.
Now the agent has that full picture loaded. What does it do next?
After loading its , the produces a : a short chain of thoughts about what to do.
That trace always ends in one of two choices: call a tool for more information, or answer directly if the agent knows enough.
The reasoning trace is the agent's internal scratchpad. It comes before the action, every single cycle.
This two-part structure — think, then act — is the core of the ReAct pattern (Reason + Act).
Here is the full decide step for the Paris weather agent, written out as it actually happens inside the model.
get_weather(city) — returns current conditions for a city.get_weather that can fetch it."The model does not write a reply to the user. Instead it emits a structured : get_weather(city="Paris").
Notice the order: reason first, act second. The trace is what justifies the choice — without it, the model would be guessing.
Drag to see how the number of missing facts shifts the agent's decision. Zero missing facts → answer directly. Any missing facts → call a tool first.
Three things go wrong most often at the decide step — and each one looks different in a transcript.
The model answers directly even though it lacks the real data. You see a confident reply — "Paris is 18 °C and sunny" — with no in the trace. The number is invented.
The model calls a tool, but picks the wrong one — for example, calling get_forecast (5-day outlook) instead of get_weather (current conditions). The result comes back but answers a different question.
The trace says "I should call the weather tool" but the emitted action is a direct reply. This is a silent mismatch — the output looks fine until you check the trace. Always verify that the action in the transcript matches what the trace concluded.
The user asks: "Is it raining in Paris right now?"
get_weather(city) available. I will call it."get_weather(city="Paris")get_weather(city="Paris"). The trace identified missing data and matched the action correctly. If step 3 had said "answer from memory", the agent would hallucinate (failure mode 1). If it had called get_forecast instead, it would return a 5-day outlook, not current conditions (failure mode 2).Next module: what happens after the tool call fires — the request sent, the result returned, and what can go wrong.
You'll see the Paris weather tool call executed step by step — the request the agent sends, the result that comes back, and the failure modes (bad tool name, malformed arguments, tool error) that break the loop if not handled.
The Act step: how the agent sends a tool call, what comes back, and what breaks the loop when it fails.
Why this matters: Every agent you build will call tools — knowing the three-part structure and the three failure modes lets you write Act code that doesn't silently break.
Decision this forces: What to do when a tool call fails — retry, use a fallback, or stop.
Answer: the tool's name (e.g. get_weather) and its arguments (e.g. {"city": "Paris"}). That structured request is called a , and it is exactly what the Act step now has to execute.
Module 3 ended with the agent holding that request. This module is about what happens next: the request leaves the model, a real function runs, and a result comes back — or something goes wrong.
Every has exactly three parts: a name (which function to run), arguments (the inputs that function needs), and an expected return (the shape of data the agent will get back).
The agent doesn't run the function itself. It only writes the order slip. Your application code reads that slip, calls the real function, and hands the result back.
get_weather.{"city": "Paris"}. Here is the exact sequence when the Paris weather agent reaches the Act step.
name = get_weather, arguments = {"city": "Paris"}.{"temp_c": 18, "condition": "cloudy"}.Notice that the model never touches the weather API directly. It only writes the order slip; your code does the actual work.
The five beats of the Act step — the model proposes, your code executes, the result returns as an observation.
A tool call can fail at three different points, and each failure looks different.
The model outputs a name that doesn't match any tool in the schema — for example, fetch_weather instead of get_weather. Your code can't find a matching function and throws: ToolNotFoundError: 'fetch_weather' is not a registered tool. The loop stops because there is nothing to execute.
The tool name is correct but the arguments don't match the schema — a required field is missing or has the wrong type. Example: the schema requires city (a string) but the model sends {"location": "Paris"}. Validation fails with: ArgumentError: required field 'city' missing. The function never runs.
The call is valid but the external service fails — the weather API is down or returns a timeout. The function raises: ToolExecutionError: weather API returned 503 Service Unavailable. The agent receives no observation, so it cannot reason about the result.
# Running example: Paris weather agent — Act step # Stage: execute the call and handle the result def act(tool_call, tools): name = tool_call["name"] args = tool_call["arguments"] if name not in tools: raise ToolNotFoundError(f"'{name}' is not a registered tool") # TODO: call the tool and return its result as an observation # Hint 1: use tools[name](**args) to run the matching function # Hint 2: wrap the return value as {"role": "tool", "content": result}
tool_call["name"]tool_call["arguments"]if name not in toolstools[name](**args){"role": "tool", "content": result}This is the core of the Act step: validate the name, look up the function, run it with the given arguments, and wrap the result as an the agent can read next cycle.
The guard on line 7 handles failure mode 1 (wrong tool name) before anything runs. Your TODO handles the happy path — and the wrapping step is what connects Act to Update.
result = tools[name](**args)
return {"role": "tool", "content": result}
# Changed lines vs. the worked example:
# — tools[name](**args) is the crux: it looks up the function by name
# and unpacks the arguments dict as keyword arguments.
# — Wrapping in {"role": "tool", "content": ...} is what makes it a
# valid observation the agent can read on the next cycle.
# If name is wrong, the 'not in tools' guard above fires first —
# the function never reaches this line.
If an AI tool writes your Act step for you, check these four things before you trust it.
Once your Act step reliably returns a valid observation, the next module, Update, shows how the agent absorbs that result and decides whether to loop again or stop.
You'll watch the agent absorb the weather result, update its working context, and face the key question: loop again or stop? — revisiting the observation concept from Module 2 to show how the updated context becomes the next cycle's starting observation.
This module shows how an agent absorbs a tool result, updates its working context, and decides whether to loop again or stop.
Why this matters: Understanding the update and stop logic is what separates a loop that finishes cleanly from one that runs forever or returns a wrong answer.
Answer: the runtime validated the call and then executed it, returning a result like { "temp_c": 18, "condition": "partly cloudy" }. That result is now sitting outside the agent — it hasn't been absorbed yet.
This module is about what happens next: the agent reads that result, folds it into its working memory, and then asks itself one critical question — am I done, or do I go around again?
The step is the moment the agent absorbs the tool result into its — the model's working memory for this task.
The runtime appends the result to the running record of the conversation: goal → reasoning → tool call → tool result. That full record becomes the new for the next cycle.
This is the direct link back to Module 2: every new observation is just the previous plus whatever the last tool returned.
After the update, the agent faces the loop's most important decision: continue or stop?
A is a rule the agent checks on every update. If any condition is true, the loop ends and the agent delivers its final answer.
The agent evaluates these conditions in order after every update — not just at the end.
Here is the complete Paris weather run, every step labeled so you can see the full loop close.
get_weather. No prior results yet.get_weather(city='Paris')."{ "temp_c": 18, "condition": "partly cloudy" }.Notice that the update in step 4 directly becomes the observation for any hypothetical cycle 2 — the loop is self-feeding.
def run_agent(goal, tools, max_steps=10): context = [{"role": "user", "content": goal}] for step in range(max_steps): action = llm_decide(context, tools) # Observe + Decide if action["type"] == "finish": return action["answer"] # stop: success result = execute_tool(action) # Act context.append({ # Update "role": "tool", "content": result }) return "Max steps reached — no answer." # stop: iteration cap
context.append({...})action["type"] == "finish"for step in range(max_steps)llm_decide(context, tools)This shows the update step as a single context.append() call — the tool result is folded into the context, which becomes the next observation automatically.
Two stop conditions are visible: the agent signals "finish" (success), or the for loop exhausts max_steps (iteration cap).
It returns the string "Max steps reached — no answer." — the iteration cap stop condition fires, and the loop exits without a successful answer. The agent never got enough information to call 'finish'.
def run_agent(goal, tools, max_steps=10): context = [{"role": "user", "content": goal}] for step in range(max_steps): action = llm_decide(context, tools) if action["type"] == "finish": return action["answer"] result = execute_tool(action) if result.get("error"): # NEW: check for tool error # TODO: return an informative failure message here pass context.append({"role": "tool", "content": result}) return "Max steps reached — no answer."
result.get("error")passThis is a small variation of Stage 1 — one new requirement has been added: the agent must stop immediately if the tool returns an error, instead of looping forever on bad data.
Replace the TODO with the line that stops the loop and returns a clear failure message to the caller.
return f"Tool error — stopping: {result['error']}"
Changed lines vs Stage 1:
pass with a return that surfaces the error message, mirroring how the success condition returns action["answer"].Why it matters: without this check, the agent appends the error to context and keeps looping — burning steps and budget — before eventually hitting the iteration cap with no useful answer.
Drag to see how the iteration cap changes the tradeoff between runaway cost and task completeness.
Three ways this step goes wrong — and what you'd actually see:
"Max steps reached — no answer." Fix: always define at least success + iteration cap.result.get("error") before appending (as in Stage 2).context_length_exceeded error and the loop crashes mid-run. Fix: summarize or trim old messages before they hit the model's limit.When you review AI-generated loop code, check for all three: a success condition, an error branch, and a context-length guard.
Before you look at the summary, try to reconstruct the loop from memory: starting from a blank slate, what four things does the agent do in order, and what does each step hand to the next? Sketch it out, then check your answer against the buildOrder below.
Apply what you learned to Agent Loop.
Why does an agent need a loop instead of a single model call?
The loop exists because a task often can't be solved in one shot — the agent needs to take an action, observe the result, and then reason about what to do next. Speed and parallelism are unrelated to why the loop exists. The model does not forget instructions between calls; memory management is a separate concern. Tool APIs do not mandate a loop; that structure comes from the agent design.
An agent is helping a user find the weather in Paris. At the start of the first cycle its observation includes the user's message and the list of available tools. What critical third ingredient must also be present for the observation to be complete?
An observation is made up of three things: the current input (the user's message), the available tools, and the memory or history of what has happened so far. Without history the agent lacks context and can make decisions that contradict earlier steps. A previous final answer is part of history, not a separate ingredient. Source code and confidence scores are not part of the observation structure taught in this lesson.
Look at this short agent reasoning trace:
Thought: I already know Paris is in France and it is a major city.
Action: answer_directly("Paris is the capital of France.")
The user's original question was "What is the weather in Paris right now?" What is wrong with the agent's decision step here?
The question asks for current weather, which requires live data the model cannot know from training. The correct decision is to call a weather tool. Answering directly is only appropriate when the agent already has everything it needs; here it does not. Trace length is irrelevant to correctness. The tool name issue is a distractor — the core flaw is the wrong branch of the call-vs-answer decision, not a naming error.
An agent calls get_weather(city="Paris") and receives this result: Error 503 — service temporarily unavailable. Why can the agent NOT move straight to the update step?
The update step's job is to fold a tool result into the agent's memory so the next observation is richer. A 503 error returns no usable weather data, so there is nothing meaningful to fold in — proceeding would give the agent false confidence that the step succeeded. The update step has no special formatting requirement that blocks error messages. The 30-second wait is invented; the lesson teaches retry, fallback, or stop as options. A 503 is a temporary availability error, not a signal that the tool doesn't exist.
Name the four conditions that can legitimately stop the agent loop.
These four conditions cover the main legitimate exits from the loop taught in the Update module: task completion, step/cycle limit reached, unrecoverable tool failure, and external cancellation. Any answer that correctly names all four in plain terms is acceptable. Partial credit applies if three of four are named accurately.