n4nAI

AI agent planning with tool use: a practical guide

Engineer-focused AI agent planning tool use guide: build task boundaries, tool registries, execution loops, and robust fallback for LLM agents.

n4n Team3 min read662 words

Audio narration

Coming soon — every post will get a voice note here.

Most agent failures trace back to sloppy planning, not weak models. This AI agent planning tool use guide lays out an ordered path for shipping agents that decompose tasks, call tools reliably, and recover from partial failures. We focus on concrete code and the tradeoffs you’ll hit in production.

1. Pin down the task boundary

Write a spec before touching model code. Define the input the agent receives, the final output shape, and which tools are allowed. Ambiguous goals produce agents that loop or hallucinate steps.

{
  "task": "Book a flight and reserve a rental car for the dates",
  "inputs": {
    "origin": "SFO",
    "destination": "JFK",
    "depart_date": "2025-06-01",
    "return_date": "2025-06-08"
  },
  "output_schema": {
    "flight_confirmation": "string",
    "car_confirmation": "string",
    "total_cost_usd": "number"
  },
  "allowed_tools": ["search_flights", "book_flight", "search_cars", "book_car"]
}

If you skip this, the planner will invent tools or emit free-form text that your executor can’t parse.

2. Pick a planning representation

Use a structured step list, not a prose narrative. Each step should name a tool, its arguments, and a success criterion. This keeps the execution loop dumb and testable.

interface PlanStep {
  step_id: number;
  tool: string;
  args: Record<string, unknown>;
  expects: string; // human-readable success condition
}

interface Plan {
  steps: PlanStep[];
  final_answer_key: string;
}

The AI agent planning tool use guide recommends JSON over natural language because you can validate it with a schema and feed it directly to a loop. The tradeoff: the model must support JSON mode or function calling reliably.

Why plain text fails

Free-text plans like “First, search flights. Then book the cheapest.” require a second LLM pass to extract intent. That adds latency and a failure surface. Reserve natural language for the planner’s internal reasoning, not the contract.

3. Build a strict tool registry

Each tool is a function with a JSON schema and a timeout. The registry is the only thing the executor can call.

from dataclasses import dataclass
import asyncio

@dataclass
class Tool:
    name: str
    schema: dict
    fn: callable
    timeout: float = 5.0

registry = {}

def register_tool(name, schema, timeout=5.0):
    def deco(fn):
        registry[name] = Tool(name, schema, fn, timeout)
        return fn
    return deco

@register_tool("search_flights", {
    "type": "object",
    "properties": {"origin": {"type": "string"}, "dest": {"type": "string"}},
    "required": ["origin", "dest"]
})
async def search_flights(origin: str, dest: str):
    # call external API
    return [{"id": "FL123", "price": 300}]

If a tool isn’t in the registry, the executor must reject the step before calling anything. That prevents prompt-injection from spawning arbitrary code.

4. Generate the plan in one shot

Call a strong model with the task spec, tool schemas, and a system prompt demanding a Plan object. Use an OpenAI-compatible client.

from openai import OpenAI

client = OpenAI(base_url="https://api.n4n.ai/v1", api_key="KEY")
# n4n.ai exposes one OpenAI-compatible endpoint covering 240+ models,
# with automatic fallback when a provider is rate-limited or degraded.

resp = client.chat.completions.create(
    model="anthropic/claude-3.5-sonnet",
    messages=[
        {"role": "system", "content": "Produce a Plan JSON per the schema. No extra text."},
        {"role": "user", "content": task_spec_json}
    ],
    response_format={"type": "json_object"}
)
plan = Plan.parse_raw(resp.choices[0].message.content)

This AI agent planning tool use guide assumes you validate plan against your Pydantic/JSON schema immediately. Invalid plans should trigger a replan with a stricter prompt, not a guess.

5. Execute step-by-step with checks

The executor walks plan.steps in order. It calls the tool, enforces the timeout, and stores the result keyed by step_id.

async def execute_plan(plan: Plan):
    results = {}
    for step in plan.steps:
        tool = registry.get(step.tool)
        if not tool:
            raise ValueError(f"Unknown tool {step.tool}")
        try:
            res = await asyncio.wait_for(
                tool.fn(**step.args), timeout=tool.timeout
            )
            results[step.step_id] = res
        except asyncio.TimeoutError:
            results[step.step_id] = {"error": "timeout"}
    return results

Pitfall: swallowing errors and continuing. If a step that later steps depend on fails, you must halt or replan. Track dependencies explicitly if your graph isn’t strictly linear.

6. Replan on tool failure

When a step returns an error, don’t blindly retry. Send the failed step, its error, and prior results back to the planner with a constraint to avoid the same bad args.

replan_prompt = f"Step {failed_id} failed: {err}. Prior results: {results}. Produce a revised Plan."
# same client call as section 4, different model maybe

Tradeoff: replanning costs another model call and adds latency. For transient errors (timeout, 429), one retry with backoff is cheaper. Use replanning for semantic failures—invalid arguments, missing data.

7. Control context growth

Tool outputs can be huge. Truncate or summarize before feeding back to the model. Keep only fields the next steps need.

def trim(result: dict, keep_keys: list[str]):
    return {k: result[k] for k in keep_keys if k in result}

If you skip this, you’ll blow the context window and silently drop earlier steps. The AI agent planning tool use guide treats context as a budget: log token counts per loop iteration.

8. Validate with offline simulations

Replace real tools with fakes that return canned data. Assert the planner produces valid steps and the executor handles happy path and a forced failure.

@register_tool("search_flights", {"type": "object", "properties": {}}, timeout=1)
async def fake_search(**kwargs):
    return [{"id": "TEST", "price": 1}]

Run this in CI. If the model provider changes, your tests catch schema drift before users do.

Common pitfalls and tradeoffs

  • Over-decomposition. Ten tiny steps mean ten model-adjacent calls and more latency. Group independent calls.
  • Strict schema vs flexibility. JSON mode is reliable but the model may refuse impossible tasks instead of explaining. Provide an error step type.
  • Provider downtime. Even with fallback, a planner call can fail. Cache the last good plan for read-only tasks.
  • Cost. A replan per failure multiplies tokens. Set a max replan count (e.g., 2) and surface a clear error.

Building agents is mostly systems engineering around the model, not model tuning. This AI agent planning tool use guide gives you the skeleton; adapt the schemas to your domain and you’ll have something that survives real traffic.

Tagstool-useagent-planningguideai-agents

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All agent planning & task decomposition posts →