Ask an assistant to find a movie scene from a half-remembered line, and the first search may return the wrong film. Now it has a decision to make: change the wording, investigate another candidate, or tell you it can't establish the match. A beautifully written answer about the wrong movie is still the wrong answer.
That decision explains when an agent loop becomes useful. The next action depends on information that arrives during the task. We can write software to handle many of those branches ourselves. Giving the model some control becomes worthwhile when interpreting the result requires more judgment than our fixed rules can comfortably express.
Who chooses the next step?
Consider the movie-clip agent embedded below. Its tools can search the web, search YouTube, locate dialogue within a video, and construct a link that starts at the returned timestamp. The last operation builds a timestamped URL; it doesn't need to download or edit the video.
A fixed workflow could call those tools in a predefined sequence. If the film is already known, it could skip attribution and search YouTube immediately. If the timestamp lookup fails, it could try the next candidate. Fixed workflows can branch, retry, and include LLM calls. "Fixed" means that the program defines the paths.
A model-directed loop gives the model a different responsibility: after reading a tool result, choose which permitted action to take next. Suppose, illustratively, that a search result identifies a different film than the user remembers. The agent could search the newly identified title, compare another source, or ask the user for more detail. That choice uses what the result means.
This is the useful distinction between workflows and agents in Anthropic's architecture guide. Both coordinate work across calls. They differ in who directs the sequence. A task with several steps doesn't automatically need a model making every routing decision.
The loop gives results somewhere to go
A prompt can describe a complete task. It cannot contain a search result that hasn't arrived yet. The surrounding application executes tools, returns their results to the model, and maintains the task's state. In simplified pseudocode, the loop looks like this:
while not task_complete:
# 1. Observe: gather current state and available information
observation = get_current_state()
# 2. Reason: LLM decides what to do next
action = llm.decide(
task=original_task,
history=action_history,
observation=observation,
available_tools=tools
)
# 3. Act: execute the chosen action
result = execute(action)
# 4. Update: record what happened
action_history.append((action, result))
# 5. Check: is the task done?
task_complete = llm.evaluate_completion(action_history)For the movie task, state includes the user's request, candidate films, returned video IDs, and any timestamp evidence. The model receives that information before deciding again. Keeping the observation attached to the action matters: a timestamp from one upload cannot safely be reused for another.
The completion check in this sketch is deliberately incomplete. An application should enforce concrete requirements alongside the model's judgment. If the requested output is a clip link, the video ID and start time should come from the relevant tool results. "The model says it's finished" is a poor substitute for checking that those fields exist and belong together.
ReAct: act, read the result, decide again
ReAct combines reasoning with actions that return new observations. This illustrative revenue task makes the dependency easy to see:
Thought: I need to find the quarterly revenue for Acme Corp. Let me search the database.
Action: search_database(query="Acme Corp quarterly revenue 2025")
Observation: [Results: Q1: $12M, Q2: $14M, Q3: $13M, Q4: $16M]
Thought: I have all four quarters. Total annual revenue is $55M. Now I need to compare
with the previous year.
Action: search_database(query="Acme Corp quarterly revenue 2024")
Observation: [Results: Q1: $10M, Q2: $11M, Q3: $11M, Q4: $13M]
Thought: 2024 total was $45M. Year-over-year growth is 22.2%. I can now write the summary.
Action: write_report(content="...")
The example's arithmetic is $55 million against $45 million, or approximately 22.2% growth. Those are invented company figures for explaining the sequence, not a financial finding. The second lookup supplies the comparison the first lookup couldn't provide.
The Thought lines are explanatory text in this toy example. In a working application, tool calls must execute and observations must come from their responses. A convincing transcript written entirely by the model has accomplished no lookup at all. Likewise, a model's explanation of a decision is useful context, but it doesn't establish that the decision was correct.
The movie-clip replay makes those calls and results inspectable:
Recorded movie-clip interactions expose the tool calls and returned observations behind the answer. Follow which result supplies the next call's arguments, especially the video ID and timestamp used to build the final link.
In the stored "I'll be back" run, YouTube search returns candidate tYc2jQaM8gM. The timestamp tool reports a match at 40 seconds, and process_video receives that same video ID and start_time="40". The final URL carries t=40s. That small chain is what to inspect: the link's inputs have an identifiable source in the preceding results. The tool's confidence score is its own assessment, so playback review still matters when evaluating whether it found the intended scene.
The scene's configured playbooks cover a known film, an unattributed quote, a failed lookup, and an out-of-scope music request. These cases need different amounts of work. Identifying a quote may finish after attribution; producing a clip requires locating it within a video. The configuration caps the loop at eight rounds. That is a limit on work, not evidence that every request succeeds within it.
Plans help when much of the route is already known
Our movie task has a recognizable outline: identify the film if necessary, find a candidate video, locate the scene, then return the link. A plan makes those dependencies explicit. The agent can revise the remaining steps when a result changes what it knows.
The same idea applies to a report pipeline. This pseudocode keeps completed results while replacing the unfinished plan when needed:
# Illustrative pseudocode: helpers supply the model and tool integrations.
plan = llm.generate_plan(task)
# Example: ["Fetch data", "Clean data", "Analyze", "Chart", "Write report"]
pending_steps = list(plan)
previous_results = []
completed_steps = []
while pending_steps:
step = pending_steps.pop(0)
result = execute_step(step, context=previous_results)
previous_results.append(result)
if needs_replanning(result):
pending_steps = list(llm.replan(task, completed_steps, result))
else:
completed_steps.append(step)Notice that replanning replaces the pending steps. Assigning a new list to plan inside a Python for step in plan loop would leave that loop iterating over the original list. A plan that changes on paper but not in execution is an especially frustrating kind of autonomy.
Tool chaining is the simpler case: one tool supplies the input for the next. This illustrative news-summary pipeline has a sequence that could be implemented directly:
Task: "Summarize the top 3 Hacker News stories about AI"
1. web_search("Hacker News top stories AI") → URLs
2. fetch_page(url_1) → article text
3. fetch_page(url_2) → article text
4. fetch_page(url_3) → article text
5. summarize(articles) → final summary
Nothing about fetching three pages requires a model to choose the order. A model-directed branch becomes more useful if one page is inaccessible, the results concern the wrong subject, or several links describe the same story. Let ordinary code handle predictable dependencies; spend another model call where interpretation can change the outcome.
Reflection needs something specific to check
A reflection step asks a model to critique an output and feed that critique into a revision:
draft = llm.generate(task)
for i in range(max_iterations):
critique = llm.critique(draft, criteria=quality_criteria)
if critique.score >= threshold:
break
draft = llm.revise(draft, critique.feedback)For a movie answer, useful criteria include whether the title matches the source, whether the link uses the returned video ID, and whether the timestamp has supporting evidence. Some checks can be deterministic. Others, such as whether the located dialogue matches a loosely described scene, require judgment.
A second pass can catch an omission. It can also repeat the first pass's mistake, especially if both see the same incomplete evidence. I would add reflection where a defined check finds errors worth correcting, then compare the revised results with the single-pass baseline. Extra calls need to improve the answer enough to justify their time and cost.
A failed lookup needs an exit
Suppose the timestamp tool cannot find the requested dialogue. Repeating the same call with identical inputs gives the system little new information. Trying another candidate video may help. Returning a verified film attribution without a clip may be the appropriate fallback. Those are different outcomes, and the application should preserve the distinction.
The following sketch shows iteration and cost limits around a tool loop. Its twenty iterations and $5 threshold are illustrative settings, not measured requirements for this agent:
# Illustrative pseudocode; helpers provide accounting and tool execution.
# Practical error handling in an agent loop
MAX_ITERATIONS = 20
MAX_COST = 5.00 # dollars
for i in range(MAX_ITERATIONS):
if total_cost > MAX_COST:
return fallback_response("Budget exceeded")
try:
action = llm.decide(state)
result = execute(action, timeout=30)
except ToolError as e:
state.add_observation(f"Tool failed: {e}. Try an alternative approach.")
continue
except TimeoutError:
state.add_observation("Action timed out. Simplify the approach.")
continue
state.update(action, result)
if is_complete(state):
return state.final_output
return fallback_response("Iteration limit reached")The helpers stand for application code, including cost accounting and tool-specific timeouts. Checking accumulated spending before a call doesn't impose an exact $5 ceiling: the next call can cross it. A stricter allowance requires accounting for the work about to start and enforcing provider or application limits where available.
Errors also need interpretation. A temporary timeout may justify a bounded retry. An invalid video ID requires correcting the input. A permission failure should not trigger repeated attempts to get around the restriction. The failure message should give the agent enough information to choose a valid next step without exposing secrets.
Longer runs create another problem: history grows. Summarizing earlier observations or retaining a smaller working context can keep the loop manageable, but preserve the evidence needed for the final answer. If compression loses which upload supplied a timestamp, the saved tokens have bought an incorrect link.
Check the decision, then check the result
A successful-looking answer tells us little about whether the orchestration helped. For the movie agent, I would inspect whether each returned link points to the intended scene, whether an unsuccessful lookup leads to a sensible fallback, and whether an out-of-scope request stops without unnecessary searches.
That requires a trace of model calls, tool inputs and outputs, timestamps, and usage. Keep enough information to connect the final answer to the observations that support it, with sensitive data redacted. Compare task completion, latency, and cost against the simpler fixed workflow on the same requests. Include failures and ambiguous requests in the test set, then rerun it when prompts, tools, or routing change.
The same evaluation approach travels well. A research workflow needs sources that support its claims; the number of searches alone says little about its quality. A code reviewer needs to connect findings to the diff and relevant test results. A support workflow needs to use the correct account information and route cases according to the organization's rules. These are possible applications of the patterns, each with its own definition of a successful outcome.
Framework references
Once the control flow is clear, a framework may save implementation work. These references cover the options discussed here:
- LangGraph provides graph-based orchestration and checkpointing for stateful applications. Persistence needs an appropriate checkpoint implementation.
- CrewAI represents agents with roles, goals, tools, and delegation settings. Those abstractions can organize a researcher/writer/editor workflow, although role names alone don't establish useful collaboration.
- AutoGen supports applications built around communicating agents. Its repository now identifies it as being in maintenance mode and directs new projects toward Microsoft Agent Framework.
- The Claude Agent SDK exposes Claude Code's agent loop, tools, and context management in Python and TypeScript, including controls for permissions and hooks.
Choose around the needs you can identify, such as resumable state, human approval, or tool execution. A small application may only need an explicit loop and a few well-defined functions.
When the extra calls earn their cost
For the movie task, a loop earns its place when reading an unexpected result helps the system find the right scene or stop with an honest partial answer. A fixed workflow remains a strong choice when the inputs and recovery paths are predictable. If the user supplies all the relevant text and asks for a summary, a single call may finish the job.
The replay makes the sequence visible; it doesn't establish that dynamic routing is cheaper or more reliable than every alternative. That decision requires a comparison on representative requests. My starting point would be to write the predictable steps in code, identify the decisions that depend on interpreting new evidence, and give the model control over those decisions with explicit stopping rules. Then keep the extra calls that demonstrably improve the result.
