Skip to content
04Industries05Work06Resources07About
Start a conversation
← All resources
Agentic AI

Agents that plan, act, and terminate

How to build an autonomous planning agent that decomposes goals into subtasks, calls tools in a reasoning loop, manages its own state, and knows when to stop — with step limits, deduplication, and failure recovery built into the architecture.

Cover image for "Agents that plan, act, and terminate" — the ReAct reasoning loop architecture showing how autonomous agents iterate through observe-think-act cycles and terminate when the goal is met.

Agents that plan, act, and terminate — the ReAct pattern in production

The simplest AI agent is a prompt in a loop. The hardest part isn't making it reason — it's making it stop reasoning. Travel planning is the perfect proving ground: the task is open-ended, the tools are real, the decision space is enormous, and a naive loop will spin forever comparing flights it's already compared.

This post builds a ReAct agent that takes a natural-language trip request, breaks it into subtasks, calls search and booking tools, manages its own state, avoids duplicate work, recovers from failures, and produces a final itinerary. The architecture applies to any multi-step planning problem, not just travel.


01 / What is a ReAct agent?

ReAct stands for Reasoning + Acting — a pattern where the LLM alternates between thinking (reasoning about what to do next) and acting (calling a tool and observing the result). Unlike a pipeline where steps are hardcoded in sequence, a ReAct agent decides its own next step based on what it's learned so far.

Each iteration follows the same shape: observe the current state, think about what's missing, act by calling a tool, observe what came back, and decide whether to continue or stop. The LLM is both the planner and the executor.

This is powerful — and dangerous. A model with no guardrails will happily loop forever, searching for one more flight option, one more hotel comparison, endlessly refining a plan that was good enough three iterations ago.

Why ReAct over a hardcoded pipeline

A fixed sequence — search flights, then hotels, then activities, then combine — works for predictable workflows. It breaks the moment the user says "I'm flexible on dates, find me the cheapest weekend in April." Now the agent needs to search multiple date ranges, compare results across them, and backtrack if the best flight doesn't align with hotel availability. That requires reasoning, not sequencing.

ReAct gives the agent the ability to adapt. It can search, evaluate, decide the results aren't good enough, reformulate its query, and search again — without you anticipating every branching path in advance.


02 / Parsing the trip request

The agent starts with an unstructured request:

"Plan a 5-day trip to Japan for two people in October. We like hiking, street food, and temples. Budget around $3,000 total. Fly from San Francisco."

Before the reasoning loop begins, the agent needs to extract a structured goal from this — destination, origin, dates, budget, interests, constraints. This is a single LLM call with a Pydantic schema (the same pattern from the structured output agent post). The model parses the freeform text into typed fields: destination: "Japan", duration_days: 5, budget_total: 3000, interests: ["hiking", "street food", "temples"].

Anything not mentioned becomes null. The agent doesn't invent constraints the user didn't state — if no airline preference was given, there's no airline preference.


03 / Task decomposition — breaking the goal into subtasks

A trip has natural subtasks: find flights, find accommodation, research activities, build a day-by-day plan. The agent decomposes the goal into these and tracks their status — pending, in_progress, done, or failed.

The critical detail is dependencies. Hotels depend on flights because you need confirmed travel dates before booking accommodation. The final itinerary depends on everything. But activities can be researched in parallel with flights — they don't need dates to search for "hiking near Kyoto."

The agent's loop respects these dependencies: it only picks up a subtask when everything it depends on is already done. This prevents wasted work — no point searching hotels for dates that haven't been confirmed yet.


04 / Tool definitions — what the agent can do

The agent's capabilities are defined entirely by its tools. Each tool has a name, a description so the LLM knows when to use it, and a typed parameter interface. For the travel agent, the toolkit is:

  • search_flights — origin, destination, dates, passengers. Returns options with price, airline, and times.
  • search_hotels — city, check-in/out dates, guests, optional price cap. Returns options with price per night, rating, and location.
  • search_activities — city and interest keywords. Returns attractions, tours, and experiences.
  • get_budget_breakdown — calculates remaining budget after subtracting known costs.
  • finish — signals that the agent has enough information to stop.

The finish tool is the most important one

Without an explicit finish tool, the agent has no way to say "I'm done." It will keep searching for marginal improvements — one more hotel, one more activity. The finish tool gives the model a named action for stopping, which the system prompt reinforces: "Call finish once all subtasks are complete. Do not search for additional options after this point."

Without this, agents don't terminate. They optimise endlessly. The finish tool converts "I should probably stop" into a concrete action the model can take.


05 / The observe-think-act cycle

This is the core loop. Every iteration, the agent receives the full current state — the goal, all subtask statuses, and the complete action history — and produces one of two things: a tool call, or a finish signal.

The prompt on each iteration gives the model everything it needs: the trip goal, the subtasks and their current status, everything the agent has already done, and the rules it must follow. The rules are explicit — only work on subtasks whose dependencies are done, never repeat a tool call with the same parameters, if a tool fails try once with adjusted parameters then mark it failed, and when all subtasks are done call finish.

The model responds with a THINK block (its reasoning) and an ACTION block (which tool to call with what parameters). The system executes the tool, appends the result to the history, and loops.

Why the entire state goes into every prompt

LLMs are stateless. They don't remember what happened three tool calls ago. The full state — goal, subtask statuses, action history — gets serialised into the prompt on every iteration. This is expensive in tokens but non-negotiable. Without it, the agent forgets what it's already done and starts repeating itself.

One important optimisation: the history stores a summary of each result, not the full payload. A flight search might return 20KB of JSON. What goes into the history is: "Found 5 flights SFO→NRT, cheapest $850 on ANA, departing Oct 12." Enough context to reason about, small enough to fit in the prompt window.


06 / Tool-result interpretation

After a tool returns data, the agent needs to make sense of it — not just log it. This is where most naive implementations fail. They call a tool, get a response, and immediately move to the next action without evaluating whether the response was actually useful.

The better pattern is a two-step cycle: act, then interpret. After each tool call, a separate reasoning step asks the model: Is this useful for the current subtask? Should I pick one of these options, or search again with different parameters? If picking, which one and why?

This separation matters because each step gets the model's full attention. Asking it to call a tool and evaluate the result and decide the next action in one shot leads to shallow reasoning. Breaking it into two steps produces better decisions — especially when the results are ambiguous (three hotels at similar prices, or flights with different tradeoffs between cost and connection time).


07 / State management — the agent's memory

The agent needs a single source of truth: the goal, the subtasks and their status, the full action history, the options it's selected, and budget tracking.

State management is the unsexy backbone of the entire system. Every tool call updates the state. Every subtask completion updates the state. Every budget allocation updates the state. The agent's "intelligence" is just an LLM reading a well-maintained state object and deciding what to do next.

Two things to get right:

Budget tracking is continuous, not end-of-trip. After selecting a $1,700 flight, the remaining budget ($1,300) gets passed into every subsequent prompt. The agent needs to know it can't book a $200/night hotel for 5 nights — that's $1,000, leaving only $300 for activities. This constraint should shape its searches, not just validate its final answer.

History summaries, not history dumps. As mentioned — compress tool results into one-line summaries for the history. The full data lives in the selected options; the history is for reasoning context only.


08 / Step limits — preventing infinite loops

This is the single most important safety mechanism. Without it, a ReAct agent will loop indefinitely.

Two limits, not one. A global limit (say, 25 steps) prevents runaway loops. A per-subtask limit (say, 8 steps) prevents the agent from burning all its steps on flights and never reaching hotels. If a subtask exceeds its limit, it's marked failed and the agent moves on. The final itinerary notes what couldn't be completed.

Why per-subtask limits matter: without them, an agent given 25 total steps might spend 20 of them comparing Tokyo hotels at different price points, leaving only 5 steps for flights, activities, and itinerary synthesis. The per-subtask limit forces the agent to satisfice — pick something good enough and move on — which is exactly what a human travel planner does.

The agent should know its limits

The step count and remaining steps should be visible in the prompt. "You have used 14 of 25 steps. 3 subtasks remaining." This creates natural urgency — the model starts making faster decisions as it approaches the limit, which is the correct behaviour. Unlimited agents over-research. Deadline-aware agents ship.


09 / Failure recovery

Tools fail. APIs time out. Searches return empty results. The agent needs to handle this without crashing, and more importantly, without pretending the failure didn't happen.

The recovery strategy depends on the failure type:

Empty results — the search found nothing. The agent should adjust parameters and try once: broaden the date range by a few days, raise the price cap, try a different neighbourhood. If the second attempt is also empty, mark the subtask as failed.

Timeout — the API didn't respond. Retry once with the same parameters. If it times out again, mark failed. Don't retry indefinitely — that burns steps.

Invalid parameters — the agent sent a malformed request (wrong date format, invalid airport code). The error message goes into the history so the model can read it and self-correct on the next attempt. This is surprisingly effective — LLMs are good at fixing their own mistakes when they can see the error.

The key principle: fail explicitly, not silently. A failed subtask shows up in the final itinerary as a warning: "We couldn't find direct flights under $1,000 — the options shown have one stop." That's more useful than an agent that silently picks an over-budget flight to avoid admitting failure.


10 / Duplicate-action prevention

Without deduplication, the agent will search for the same flights three times. It saw interesting results and "wanted to check again." This wastes steps and tokens.

The fix is simple: hash each action as a signature — tool name plus sorted parameters. Before executing any tool call, check if that exact signature has been seen before. If it has, skip it and log "duplicate action — already executed." The agent sees this in its history and moves on.

Near-duplicates are harder. search_flights with October 12th and October 13th are legitimately different searches. But search_hotels with a $200 max and then $201 max is the agent wasting a step. You can add fuzzy deduplication for numeric parameters — round prices to the nearest 25, for instance — but exact deduplication catches the worst offenders and is worth implementing first.


11 / Final itinerary synthesis

Once all subtasks are done or failed, the agent exits the ReAct loop and produces the final output. This is a separate LLM call — not part of the loop — because it's a fundamentally different task: summarise and structure, not reason and act.

The synthesis prompt receives all the selected options (flights, hotel, activities) and the original goal, and produces a structured day-by-day itinerary with a budget breakdown. It's also where the agent surfaces warnings — subtasks that failed, budget overruns, date conflicts, or anything else that needs the user's attention.

The output includes agent metadata: how many steps were taken, how many subtasks completed versus failed, and total tool calls made. This metadata is useful for debugging and for monitoring agent efficiency over time — if your agent routinely uses 24 of 25 steps, the step limit is too low or the task decomposition needs refinement.


12 / What transfers beyond travel

The pattern — parse goal → decompose → ReAct loop with guards → synthesise — applies to any open-ended planning task.

A research agent decomposes a question into sub-questions, searches academic databases, evaluates sources, and synthesises a report. A procurement agent breaks a purchasing request into vendor search, quote comparison, and compliance checks. An event planning agent finds venues, caterers, and entertainment. The tools change, the subtask definitions change, but the loop structure, state management, deduplication, and step limits are universal.

The hardest lesson in building agents isn't making them smart — it's making them stop. An LLM will always find one more thing to research, one more option to evaluate, one more comparison to make. The architecture's job is to channel that energy into a bounded, productive path and then cut it off cleanly.

Build for termination first. Intelligence second.