latentSource

How a ReAct Agent Loop Actually Works

A walkthrough of one real ReAct agent run — the query was "Houston, we have a problem" and the agent returned the exact YouTube timestamp where Tom Hanks delivers the line. You can replay the recorded run right inside this article, thinking tokens and all.

·11 min read
Share
How a ReAct Agent Loop Actually Works

Worked through with real recorded runs you can replay below.

Why ReAct exists

You have an LLM. It can reason and write, but it has two limits worth taking seriously: its knowledge ends at training cutoff, and it will hallucinate facts confidently when it doesn't know them. For tasks that need fresh data or verifiable answers — looking up a song, finding a film clip, querying an API — the model on its own isn't enough. It needs tools. ReAct (Reasoning + Acting) is a pattern for orchestrating that.

The idea is simple. Instead of asking the model for an answer in one shot, you ask it to think and act in cycles. Each cycle, the model reasons about what it knows so far, picks a tool, and the system runs the tool and feeds the result back. The model thinks again with the new information. Repeat until the model decides it has enough to answer.

Reading about a loop is one thing. Watching it run is better. The widget below replays real recorded runs of our movie-clip agent — the actual captured interaction, with its real timing, thinking tokens, tool calls, and tool results. Nothing is simulated and no LLM is called when you press play; you're watching a recording of the agent doing the work. Pick a run:

Recorded runs of the ReAct movie-clip agent. Each replay shows the full trace: the model's internal thinking (expand the blocks), every tool call with its arguments and results, and the final answer — exactly as it streamed during the original capture.

The run I'll dissect through the rest of this article is the first one: the query was "Houston, we have a problem." The agent ended up returning the exact YouTube timestamp where Tom Hanks delivers that line in Apollo 13. A handful of loop iterations, three tool invocations, one verified URL. Every snippet of data quoted below comes from a real run of this agent.

The three steps: thought, action, observation

Every iteration of a ReAct loop has three logical phases. Two of them cycle; the loop only exits when the model decides it's done:

Thought. The model reasons about what it knows and what it still needs. Part of this happens in thinking tokens — an internal scratchpad the user normally never sees. We capture those tokens during recording, which is why the replay above has expandable thinking blocks. This is the part I find most people have never actually looked at, and it's the part worth looking at.

In the Houston run, the early thinking goes roughly like this:

Verifying famous film scene

I've confirmed the film is indeed Apollo 13 (1995), directed by Ron Howard. The user specifically wants the "Houston, we have a problem" line, which I know is from that movie. My next step is to find the relevant video clip for reference.

That's the model deciding what to do: verify the quote once, then go straight to YouTube.

Action. The model picks ONE tool from the available set and supplies its arguments. This agent has five tools — web_search, youtube_search, get_timestamp_from_text, process_video, and finish. The tools are declared to Gemini as function schemas, and the model emits a native function call:

{"name": "youtube_search", "args": {"query": "Apollo 13 Houston we have a problem scene"}}

One tool, one set of args, no commentary. Because the call comes through the API's function-calling channel rather than free-form text, there's no JSON to parse out of prose and no regex to maintain. The API only lets the model call tools that were declared, with arguments matching the declared schema.

Observation. The system runs the chosen tool and captures the result. The result goes back into the conversation as a function response — from the model's point of view, new information just arrived. The system writes the observation; the model never writes one itself.

The conversation IS the memory

This is the most important detail to understand. The model has no memory between API calls. What it knows about the loop has to be IN the conversation it sees on the next call. The agent holds one chat session and every round appends to it:

  • The original user query is the first user turn.
  • The model's visible reasoning and each tool call are model turns.
  • Each tool result is fed back as a function response.
  • The final answer arrives through the finish call.

By the last round of the Houston run, the conversation the model sees contains every search result and every timestamp it has gathered. There's no hidden session state on some server. The transcript is the memory. This shape is also what lets implicit prefix caching work: the previous call's prefix is byte-identical to the current call's prefix, so most of the conversation isn't re-billed at full price.

The tool contract: why schemas matter

ReAct tutorials sometimes show the model "calling tools" in free-form text — Action 1: web_search("Apollo 13"). That works in a demo. It breaks in production: you need a regex to parse the tool name, quoting rules vary, the model drifts into prose mid-action. We learned this the hard way while recording these demos — with the loop protocol described only as text examples, the model would occasionally write out an entire fake transcript, inventing video IDs and tool outputs it never received, in five seconds flat. It looked plausible. Every fact in it was fabricated.

Our implementation leans on Gemini's native function calling instead. Each tool is a FunctionDeclaration with a JSON Schema for its parameters. The model streams its reasoning, then emits structured function-call parts the SDK hands over as objects, not strings. Dispatch is a lookup in a table, not a parse of prose. And the system prompt says it plainly: never write a tool call as text, never invent an observation — real calls only.

Compare two calls from the Houston run. Same shape, different tool:

{"name": "get_timestamp_from_text", "args": {"video_id": "YwG4F-16Tno", "snippet": "Houston, we have a problem"}}

The agent reads the name, picks the matching skill from a dispatch table, passes the args, runs it. No string matching, no ambiguity.

The finish tool deserves a note. It's declared like any other tool, but the executor treats it as the loop terminator: its response argument carries the final answer, and calling it ends the run. Making "I'm done" a tool call instead of a text convention means the stop condition goes through the same schema-enforced channel as everything else.

The tool dependency chain

The point of ReAct is that each tool call is informed by the previous one. The model's reasoning compounds across iterations.

Look at how the Houston run unfolds — you can watch this exact sequence in the replay:

  • web_search. One verification search on the quote, as the hard rules require.
  • youtube_search. The model knows the movie and needs a video ID. The tool returns several candidates.
  • get_timestamp_from_text. The model now has video IDs. It picks the top result (Universal Pictures' official upload) and asks the timestamp tool to watch the video for the line.
  • process_video. The model now has a video ID plus a verified timestamp. It calls the URL builder.
  • finish. The model has the playable URL and composes the final answer.

Each step depends on the output of the previous one. The model literally cannot call process_video without a video ID and a timestamp; it cannot get a timestamp without a video ID; it cannot get a video ID without a search. That dependency chain is what makes the multi-step approach necessary — no single "answer this" prompt could substitute.

The actual data flowing through the chain is worth seeing raw. Here's a youtube_search result from the recorded run:

{ "results": [ {"video_id":"YwG4F-16Tno","title":"Apollo 13 | \"Houston, We Have a Problem\"","channel":"Universal Pictures","view_count":7698237}, {"video_id":"C3J1AO9z0tA","title":"Apollo 13 (1995) - Houston, We Have a Problem Scene (4/11) | Movieclips","channel":"Movieclips","view_count":2803140}, {"video_id":"jMmo0AaPMn4","title":"Houston, We Have A Problem (Full Scene) | Apollo 13","channel":"Critic Picks","view_count":988838} ] }

The timestamp tool then returned:

{ "start_seconds": 44, "end_seconds": 46, "confidence": 1.0, "matched_text": "Houston, we have a problem", "error": null }

confidence: 1.0 means a verbatim match. matched_text echoes back exactly what the tool found, which is how the agent would catch a near-miss — if the model had asked for "Huston, we have a problem here", the matched_text would show the closest phrase actually present, with a lower confidence telling it not to trust the result.

The URL builder took the video_id and the timestamp and returned:

{ "status": "success", "video_id": "YwG4F-16Tno", "start_seconds": 44, "clip_url": "https://www.youtube.com/watch?v=YwG4F-16Tno&t=44s", "message": "Clip ready → https://www.youtube.com/watch?v=YwG4F-16Tno&t=44s (starts at 44s)" }

Each step's output becomes the next step's input. The chain is short here — three lookups and a URL build — but the same pattern scales: a music query might take five iterations because the song needs identifying before the video can be found before the lyric can be timed.

When does the loop stop?

The model decides. When it calls finish, the loop ends and the value of finish.response is what the user sees.

There's also a hard ceiling — the scene config caps the loop at eight rounds — and a soft pressure. The convergence rules in the system prompt:

Most queries resolve in 3–4 steps. As soon as you have the key facts, call finish — do not chase further confirmation. By round 7 you MUST call finish unless a critical tool call is genuinely required.

The Houston run finished well inside the budget. Its finish call:

{"name": "finish", "args": {"response": "This iconic line comes from the 1995 film **Apollo 13**, directed by **Ron Howard**.\n\nThe line is spoken by Commander **Jim Lovell** (played by Tom Hanks) after an oxygen tank explodes on the spacecraft. While the actual historical quote from the Apollo 13 mission was \"Houston, we've had a problem,\" the film's version became one of the most famous quotes in cinema history.\n\n[Apollo 13 - \"Houston, we have a problem\"](https://www.youtube.com/watch?v=YwG4F-16Tno&t=44s)"}}

Look at the structure of that response. The film metadata came from the model's training: title, year, director, character, actor — even the trivia that the real mission said "we've had a problem" while the film changed the tense. One verified anchor came from the tools: the URL with a timestamp. The model wraps the verified result in context it already knows. That mix of known facts plus verified anchors is the practical shape of useful agent output.

What guards against bad behavior

The model could, in principle, do worse things than these runs show. It could hallucinate a video ID and call process_video with garbage. It could skip the timestamp tool and guess a number. It could loop forever calling the same tool. It could make up an answer without calling any tool at all — we watched it try exactly that before the prompt was hardened.

The agent prevents each of those by stacking constraints:

  • Function declarations. The model can't call a tool that wasn't declared or pass args that don't match the schema — the API rejects it before our code ever sees it.
  • Hard rules in the system prompt. "Always call get_timestamp_from_text before process_video." "Never extract a video clip without a confirmed timestamp." "You are a strictly grounded assistant. Do not assert any fact you have not verified via a tool call this iteration or a prior one in this conversation."
  • Real calls only. The prompt forbids writing tool calls or observations as text — the fabricated-transcript failure mode gets named and banned explicitly.
  • Iteration budget. The loop is capped in config. After that, the agent has to conclude with whatever it found.
  • Convergence pressure. The system prompt tells the model when it MUST call finish.
  • Malformed-call retry. Occasionally the model botches a function call badly enough that the API drops it. The loop detects that and nudges the model to reissue the call instead of silently ending the run.

The Houston run respects all of this. Every claim in the final answer is either training knowledge (uncontroversial movie metadata) or backed by a tool call (the URL).

What the model didn't do

Worth calling out the negatives explicitly. In this run, the model:

  • Did not skip verification — one web_search on the quote, as the rules demand, and no more.
  • Did not pick a random YouTube candidate — it took the first result, which was both the highest-viewed AND the official Universal Pictures upload.
  • Did not invent a timestamp — it used the dedicated get_timestamp_from_text tool, which makes its own Gemini call against the actual video.
  • Did not burn the iteration budget.
  • Did not make up the actor or director — those facts came from training, but the timestamp verification step proved the scene actually exists at the second the model claimed.

A note on the replay itself

The widget at the top isn't a mock-up and it isn't calling a model when you press play. We run the agent for real once, in an admin capture tool, and record every event with its timestamp: user message, each thinking token, each tool call and its result, each answer token. The article replays that recording — long tool waits compressed so you don't sit through a 20-second video-analysis call, everything else at its real pace. It's the same philosophy as flight-recorder debugging: record the run once, replay it forever without spending another token. That's also why you can compare two runs of the same workflow above — same agent, same tools, different query, different path through the loop.

Conclusion

ReAct's value isn't being clever. It's being predictable. Each tool call is narrow. Each tool's output has a specific schema. The model is constrained to act through a schema-enforced channel, and free-form reasoning stays in the thinking stream where it belongs. That contract is what makes dispatch reliable.

The full Houston run, end to end:

  1. User: "Houston, we have a problem"
  2. web_search → confirms the quote is Apollo 13 (1995), Ron Howard
  3. youtube_search → candidate videos, top one is Universal Pictures' official upload
  4. get_timestamp_from_text(YwG4F-16Tno, "Houston, we have a problem") → 44s, confidence 1.0
  5. process_video(YwG4F-16Tno, 44)https://www.youtube.com/watch?v=YwG4F-16Tno&t=44s
  6. finish → final answer assembled from training facts plus the verified URL

The same loop shape runs for movie queries, misremembered quotes, and refusals — when the user asks for something out of scope, the model calls finish on the first round with a polite decline and the loop terminates immediately. The pattern doesn't change; only the tool calls in between do. Scroll back up and replay the second run to see a different path through the same machinery.

Final clip from the Houston run: https://www.youtube.com/watch?v=YwG4F-16Tno&t=44s