Skip to main content
AI10 min readUpdated

LLM Agent Architecture: Tool Design, Control Loops, State, and Failure Handling

How to build agentic systems that do real work: designing tools an LLM can actually use, bounding the control loop, managing context over long runs, handling partial failure, and deciding where a human belongs.

Summary

An agent is a loop: model proposes a tool call, your code executes it, the result goes back into context, repeat until done. Everything that makes agents hard is in the details around that loop — tool interfaces designed for a reader who cannot ask clarifying questions, hard bounds on iteration and cost, context that gets compacted before it overflows, and failure paths that return actionable errors instead of stack traces. Most agent projects fail on tool design, not on model capability.

TypeScriptClaude APIMCPNode.jsPostgres

The loop, and its bounds

async function run(task: string, limits: Limits) {
  const messages: Message[] = [{ role: "user", content: task }];
  let spent = 0;

  for (let step = 0; step < limits.maxSteps; step++) {
    const res = await client.messages.create({ model, tools, messages });
    messages.push({ role: "assistant", content: res.content });
    spent += cost(res.usage);

    if (spent > limits.maxCostUsd) return halt("budget", messages);
    if (res.stop_reason !== "tool_use") return done(res, messages);

    const results = await Promise.all(
      toolCalls(res).map((call) => execute(call, { timeoutMs: 30_000 }))
    );
    messages.push({ role: "user", content: results });

    if (tokensOf(messages) > limits.compactAt) await compact(messages);
  }
  return halt("step_limit", messages);
}

Every bound in that function exists because of a specific production incident somewhere. Step limits stop loops where the model retries a failing tool forever. Cost ceilings stop a runaway from becoming an invoice. Per-tool timeouts stop one hung HTTP call from consuming the entire run. Add them before launch, not after.

Tool design is API design for a reader who cannot ask questions

The model sees only your tool names, descriptions, and parameter schemas. It cannot read your source, ask a colleague, or check a wiki. Treat each description as documentation for a competent new engineer with no context — including what the tool does not do, and what to do when it fails.

  • Few, capable tools beat many granular ones. Ten tools that each do a real unit of work outperform forty that must be composed in exactly the right order.
  • Name by intent, not implementation: search_customers, not query_pg_users_table.
  • Constrain in the schema — enums, formats, ranges. A constraint in the schema is enforced; the same constraint in prose is a suggestion.
  • Return errors the model can act on. 'Customer not found. Search by email with search_customers first' recovers; 'Error: 500' produces a retry loop.
  • Return structured, compact results. Dumping a 40KB JSON blob into context burns budget and buries the answer — paginate and summarize at the tool boundary.
  • Make read tools obviously safe and write tools obviously consequential, and say so in the description.

If you are exposing the same capabilities to more than one agent or client, define them once behind MCP rather than reimplementing the schema per integration. The tool contract becomes a server you version, test, and reuse.

Context over long runs

Agents fail slowly. Twenty tool calls in, context is full of stale intermediate output, and the model is reasoning over noise. Manage this explicitly rather than hoping the window is large enough.

  • Compact at a threshold: summarize completed subtasks into a short record of what was learned and what remains, and drop the raw transcript behind it.
  • Keep an explicit external state object — the task, its decomposition, findings so far, open questions — and re-inject it after compaction. This is the agent's memory of intent, and it must survive.
  • Write large artifacts to files or a store and pass identifiers, not contents.
  • Cache the stable prefix. System prompt and tool definitions are identical across every step of a run; cached, they cost a fraction of the tokens.

Single agent or many?

Multi-agent architectures are frequently reached for too early. Every additional agent adds a lossy handoff, and coordination overhead grows faster than the parallelism gains. The honest default is one agent with good tools.

SituationUse
Linear task, shared contextSingle agent
Wide independent search across many sourcesParallel subagents, results merged by the lead
Distinct expertise with different tool sets and permissionsSpecialized agents behind a router
Long-running workflow with approval gatesDurable workflow engine, agents as steps

When you do fan out, give each subagent a self-contained brief. A subagent starts cold: it cannot see the parent's reasoning, so anything it needs must be written into its prompt. Vague briefs are the main reason multi-agent systems underperform the single-agent baseline they replaced.

Where humans belong

Agents should be trusted with reversible actions and gated on irreversible ones. That line — not model quality — is what makes an agentic system deployable inside a company that has auditors.

  • Autonomous: reading, searching, drafting, analysis, anything in a sandbox, anything that can be undone with one command.
  • Confirmed: sending communications on someone's behalf, moving money, deleting data, changing production config, publishing.
  • Logged regardless: every tool call with arguments, result, latency, and cost, tied to a trace ID a human can replay end to end.
  • Idempotent by construction: every write tool takes a client-supplied key, so a retried step cannot double-charge, double-send, or double-post.

Frequently asked questions

How do I stop an agent from hallucinating tool calls?
Constrain the schema tightly, validate arguments server-side before executing, and return a specific correcting error when validation fails rather than throwing. Models recover well from a clear message describing what was wrong and what to do instead — most 'hallucinated call' problems are ambiguous schemas.
What does an agent cost to run?
Cost scales with total tokens across all loop iterations, which grows superlinearly with steps because context accumulates. A twenty-step run over a large codebase can be a hundred times a single completion. Prompt caching, compaction, and compact tool results are the three levers that matter, and together they routinely cut cost by an order of magnitude.
Should agent state live in the conversation or in a database?
Both, deliberately. The conversation carries recent reasoning; a database carries durable facts, task decomposition, and results. Anything you would be unwilling to lose to a compaction or a crash belongs outside the context window.
What is MCP and do I need it?
Model Context Protocol is an open standard for exposing tools and data to LLM clients over a defined transport. You need it when the same capability must be reachable from multiple agents or applications; for a single bespoke agent, plain tool definitions in your own codebase are simpler and entirely sufficient.

Building something like this?

I'm Harsh Mittal — I build production systems across Web3, AI, and financial infrastructure: smart contracts and DeFi protocols, RAG pipelines and LLM agents, market data infrastructure, and the interfaces on top of them. If this is the kind of problem you're working on, I can help you ship it.