Building AI Agents: Tool Use, Planning and Memory Architecture Patterns

Building an AI agent that reliably handles multi-step tasks in production has very little to do with prompting a large language model to "think step by step." The real work lies in engineering the control plane around the model: how it selects and invokes tools, how it plans across extended horizons, and how it retains and recalls information over time. After shipping several agent systems for code automation and enterprise knowledge work, I've learned that naive ReAct loops demo well but collapse under real tool latency, partial failures, and memory bloat. This article breaks down the architectural patterns that actually hold up, from tool interface design to planning algorithms and memory systems, with concrete implementation details you can adapt.

From Autocomplete to Control Loop: Anatomy of a Production Agent

An LLM on its own is a stateless function: tokens in, tokens out. An agent wraps that function in a persistent loop with state, perception, and action. In my experience, the cleanest mental model is a discrete control loop with four components: state manager, reasoner (the LLM), tool executor, and memory subsystem. The reasoner proposes the next action, the executor carries it out, the state manager records the transition, and the memory subsystem provides relevant context for the next reasoning step.

Many teams start by putting everything into the prompt. That works for 3-5 turns, then context limits and attention dilution create brittle behavior. I prefer to explicitly separate ephemeral working state from long-term memory. Working state is a structured object that lives outside the context window and is selectively rendered into the prompt. It typically contains the original user goal, the current plan step, tool call history with observations, and a scratchpad for intermediate variables.

Keeping the Loop Deterministic

The biggest reliability win I've found is making the loop deterministic and inspectable. Avoid letting the model freely emit text that you parse with regex. Force it to emit a structured decision object: either act with a tool call, plan with a revised plan, ask for user clarification, or finish with a final answer. This constraint simplifies validation, logging, and recovery. For orchestration, frameworks documented in the Hugging Face Documentation provide useful primitives for chat templating and tool integration, but the state machine itself is best owned by your application code, not hidden inside a library.

# Core agent state - persisted externally, rendered into prompt selectively
from pydantic import BaseModel, Field
from typing import List, Literal, Optional
import uuid

class ToolCall(BaseModel):
    id: str = Field(default_factory=lambda: str(uuid.uuid4()))
    name: str
    arguments: dict
    observation: Optional[str] = None
    status: Literal["pending", "success", "error"] = "pending"
    latency_ms: Optional[int] = None

class AgentState(BaseModel):
    goal: str
    plan: List[str] = Field(default_factory=list)
    current_step: int = 0
    history: List[ToolCall] = Field(default_factory=list)
    scratchpad: dict = Field(default_factory=dict)
    
    def render_for_prompt(self, max_history: int = 6) -> str:
        # Only render recent history to control context size
        recent = self.history[-max_history:]
        return f"Goal: {self.goal}\nPlan: {self.plan}\nRecent Actions: {recent}"

Tool Use as Typed Interfaces: Schema Design, Validation and Recovery

Tools are where agents meet the real world, and tool design determines 80% of your agent's reliability. A common anti-pattern is exposing tools as free-form natural language wrappers around APIs. That invites hallucinated arguments and type errors. Instead, treat tools as typed functions with strict JSON Schemas, input validation, and machine-readable error messages.

Each tool needs three things: a precise name and description for the model, a JSON Schema that constrains arguments, and a deterministic execution layer that never leaks raw stack traces to the model. In production, I've found that models perform dramatically better when tool descriptions include when not to use the tool, example arguments, and the shape of successful observations. Keep descriptions under 80 tokens and avoid overlapping functionality between tools.

Handling Tool Failure Without Derailing the Agent

Tools will fail. APIs time out, search returns no results, code execution throws. If you return a raw exception, the agent often loops or hallucinates a workaround. I implement a tool wrapper that normalizes every outcome into a structured observation with a status, a concise error category, and a suggested next action. This gives the reasoner something actionable to plan from. For inference efficiency when the agent issues multiple independent tool calls, patterns like those described in LLM Inference Optimization: Continuous Batching, KV Cache and Speculative Decoding become critical to keep latency acceptable under parallel execution.

from typing import Any, Dict

TOOL_SCHEMAS = [
    {
        "name": "search_codebase",
        "description": "Search codebase for functions/classes. Use when you need to locate implementation, not for general docs.",
        "parameters": {
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "e.g., 'class VectorStore retriever'"},
                "top_k": {"type": "integer", "default": 5, "minimum": 1, "maximum": 20}
            },
            "required": ["query"]
        }
    },
    {
        "name": "execute_python",
        "description": "Execute python in sandbox. Use for calculations or data analysis. Returns stdout/stderr.",
        "parameters": {
            "type": "object",
            "properties": {
                "code": {"type": "string"},
                "timeout_s": {"type": "integer", "default": 10}
            },
            "required": ["code"]
        }
    }
]

def execute_tool_safely(name: str, args: Dict[str, Any], registry: dict) -> Dict[str, Any]:
    try:
        result = registry[name](**args)
        return {"status": "success", "observation": str(result)[:4000]}
    except Exception as e:
        # Categorized error helps model recover instead of retrying blindly
        category = "validation_error" if isinstance(e, ValueError) else "execution_error"
        return {
            "status": "error",
            "error_category": category,
            "observation": f"{category}: {str(e)[:500]}. Try correcting arguments or using a different tool."
        }

Another lesson from deploying to users: enforce idempotency and side-effect gating. Read-only tools (search, read, calculate) can be auto-executed. Write tools (send_email, create_ticket, merge_pr) should require an explicit confirmation step in the plan and should be simulated first where possible. I log every tool call with its full arguments and observation, which is invaluable for debugging loops where the agent repeatedly calls the same tool with slightly different phrasing.

Planning Architectures That Survive Long Horizons: ReAct, Plan-and-Execute and Search

Planning is how the agent avoids greedy, locally-optimal tool calls that never reach the goal. The choice of planning strategy is a tradeoff between latency, cost, and task complexity. I've run all three major families in production and each has a distinct failure profile.

ReAct interleaves reasoning and acting at every step. It's simple and flexible for exploratory tasks where the next step depends heavily on the last observation, but it suffers from myopia. Without an explicit plan, the agent can drift or repeat work. Plan-and-Execute generates a full DAG of steps up front, then executes them, replanning only when a step fails or new information invalidates the plan. This is far more stable for well-scoped tasks like "migrate this database table" or "generate a report from three data sources." Tree Search variants (Tree-of-Thoughts, MCTS over tool calls) explore multiple branches and backtrack. They are powerful for reasoning-heavy tasks but multiply inference cost by 3-10x.

In my current systems, I use a hybrid: plan-and-execute as the default, with a ReAct sub-loop for individual plan steps that require exploration. The planner model can be a smaller, cheaper model focused on decomposition, while the executor uses a more capable model for tool use and synthesis.

Pattern Best For Latency / Cost Common Failure Mode
ReAct (interleaved) Exploratory research, debugging, open-ended Q&A Low per-step, high total turns Looping, goal drift without global plan
Plan-and-Execute Multi-step workflows with known dependencies Medium; one planning call + sequential execution Stale plan when observations invalidate assumptions
Tree Search / MCTS Complex reasoning, code generation with tests High; 3-10x more rollouts Explosion of branches, evaluation bottleneck
Hierarchical Planner-Executor Long-horizon enterprise tasks (10+ steps) Medium-High; specialized models per role Coordination overhead, plan-exec misalignment

Implementing Replanning Triggers

A static plan is only useful if you know when to abandon it. I trigger replanning on three signals: a tool returns an error for two consecutive attempts, an observation semantically contradicts a plan assumption (checked via an LLM critic), or the agent's confidence scorer falls below a threshold. The replanning prompt receives the original goal, the failed plan, and the history, and is instructed to produce a minimal diff rather than starting over.

# Hybrid planner-executor loop with explicit replanning
def run_agent(goal: str, llm, tools, max_steps: int = 15):
    state = AgentState(goal=goal)
    # Initial planning phase
    state.plan = llm.generate_plan(goal)  # e.g., ["search codebase", "write script", "test script"]
    
    for _ in range(max_steps):
        if state.current_step >= len(state.plan):
            break
            
        decision = llm.decide_next_action(state.render_for_prompt(), TOOL_SCHEMAS)
        
        if decision["type"] == "act":
            result = execute_tool_safely(decision["name"], decision["arguments"], tools)
            state.history.append(ToolCall(name=decision["name"], arguments=decision["arguments"], **result))
            
            # Check if replanning is needed
            if result["status"] == "error" and needs_replan(state):
                state.plan = llm.replan(state)
                
        elif decision["type"] == "finish":
            return decision["answer"]
            
        state.current_step += 1
    return state

Memory Systems Beyond the Context Window: Working, Episodic and Semantic Layers

Memory is the most under-engineered part of most agent stacks. Stuffing past conversation into the context window is not a memory architecture. Real memory requires deciding what to store, how to index it, and when to retrieve it. I structure memory into three layers that map to different retrieval needs.

Working memory is the prompt itself: the last N turns, the current plan, and tool observations. Keep it small and relevant. Episodic memory stores past trajectories: goal, plan, tool sequence, and outcome, indexed for case-based retrieval. Semantic memory stores facts, user preferences, and document knowledge in a vector store with structured metadata. The separation matters because retrieval precision for episodic memory benefits from trajectory-level embeddings, while semantic memory benefits from chunk-level embeddings with source provenance.

Retrieval That Helps Instead of Distracting

Indiscriminate retrieval hurts more than it helps. I've seen agents degrade when fed five marginally relevant documents. Implement retrieval with explicit gates: only retrieve when the reasoner's confidence is low or when the decision type is act requiring external knowledge. Use a re-ranker and enforce a similarity threshold. Store observations with metadata like tool name, timestamp, and task ID so you can filter by recency or source type. For long-running agents, summarize and compress history into a rolling summary rather than truncating.

Vector stores are not the only option. For structured data like user settings or conversation state, a key-value store with exact lookup outperforms similarity search. I use a hybrid retriever: BM25 for keyword-sensitive codebase search and dense embeddings for semantic questions, then fused with reciprocal rank. Recent work cataloged on arXiv Machine Learning consistently shows hybrid retrieval improves recall for agents that switch between code and natural language contexts.

Forgetting and Consolidation

Memory without forgetting becomes a liability. I run a nightly consolidation job that clusters episodic trajectories, extracts reusable patterns, and promotes them to semantic memory as skills or templates. Failed trajectories are retained but tagged as anti-patterns to avoid repeating the same tool sequence. TTLs on working memory and careful eviction of low-utility vectors keep retrieval latency predictable. This is especially important when agents run for days or weeks on behalf of a user.

State Persistence, Observability and Failure Handling in Long-Running Agents

An agent that runs for more than a few seconds needs durable state and clear observability. If your agent state lives only in memory, a process restart or timeout loses everything. Persist AgentState to a database after every tool observation, with an append-only log of transitions. This gives you replay, time-travel debugging, and the ability to resume from any step. Use optimistic concurrency control if multiple workers can advance the same agent.

Observability for agents is different from LLM tracing. Log not just token usage and latency, but decision type distribution, tool success rates, plan revision frequency, and memory retrieval hit rate. I've found that a spike in replan decisions is an early warning that tool schemas or planner prompts are misaligned with the environment. Trace each loop iteration as a span with the rendered prompt, model output, and tool observation, but redact sensitive tool outputs before storage.

Failure handling should be explicit. Implement circuit breakers on tools that repeatedly fail, timeouts with graceful degradation, and a human-in-the-loop escalation path when the agent's uncertainty is high. For agents that send messages or modify systems, always require a dry-run observation. Safety and policy enforcement should sit as a separate layer that inspects both tool arguments and final answers, as outlined in Responsible AI: Building Guardrails, Content Filters and Safety Layers, rather than being embedded only in the system prompt.

Measuring What Matters: Reliability, Task Completion and Cost-Aware Evaluation

Single-turn benchmarks do not predict agent performance. An agent can score well on MMLU and still fail to complete a five-step workflow because it cannot recover from a single tool error. Evaluation needs to measure trajectory-level success: did the agent achieve the goal within the allowed tool budget, without side effects, and with a verifiable artifact?

I evaluate agents on three tiers. First, unit tests for tool use: given a state and tool schemas, does the model produce valid JSON that conforms to the schema? Second, scenario simulations with mocked tools that deterministically succeed or fail, to test planning and recovery logic without LLM variance. Third, end-to-end task suites on real environments with human-rated success criteria. Metrics include task success rate, mean steps to success, tool error recovery rate, and cost per successful task.

Automated metrics like trajectory exact match are too brittle. I use an LLM-as-judge for intermediate reasoning quality, but always calibrate it against human labels and track its agreement rate. For deeper guidance on designing these suites, see Evaluating LLM Applications: Benchmarks, Human Evaluation and Automated Metrics. Log every evaluation trajectory so you can diff agent versions and attribute regressions to specific prompt, model, or tool changes. Without this, agent development becomes prompt whack-a-mole.

Frequently Asked Questions

How many tools should an agent have access to at once?

Fewer than you think. I've seen performance drop sharply beyond 8-10 tools in a single decision context due to choice overload and description truncation. Group related tools into a toolkit and expose only the relevant toolkit per plan step. If you need 30+ tools, implement a two-stage selection where a retriever first narrows to the top 5 candidates, or use hierarchical tool namespaces. Always measure tool selection accuracy on a held-out set before adding a new tool.

Should planning and execution use the same model?

Not necessarily. Planning benefits from strong decomposition and dependency reasoning, while execution needs precise tool argument generation and observation grounding. In my setups, a mid-size model (e.g., 8-13B) handles planning well with few-shot examples, while a larger instruction-tuned model handles execution. Splitting them reduces cost and lets you optimize prompts and sampling parameters separately. For critical tasks, use the same capable model for both but with distinct system prompts.

When should I use vector memory versus just extending the context window?

Extending context helps for short-horizon coherence but does not scale for long histories or cross-session recall. Use vector memory when you need to retrieve facts from days or weeks ago, or when the relevant context is a small subset of a large corpus. Keep working memory within 4-6k tokens and retrieve 1-3 high-precision chunks rather than dumping 100k tokens of history. Monitor retrieval precision; if your top-k relevance falls below ~0.7, the memory is adding noise.

How do you prevent agents from looping on tool calls?

Enforce three guards: deduplicate consecutive identical tool calls with identical arguments, track visited states and abort if a loop is detected, and cap retries per tool to two with exponential backoff. More importantly, return categorized errors that guide the model to a different strategy, and trigger replanning after consecutive failures. Logging plan revision frequency and looping trajectories will show you which tool descriptions or error messages need refinement.

Related Articles

References & Standards: arXiv Machine Learning · PyTorch Documentation · Hugging Face Documentation