Skip to content
Kamran Code

How AI Agents Actually Work

Underneath the hype, an AI agent is a loop: a model chooses a tool, the tool runs, the result goes back in. Here is that loop in detail, the engineering that makes it safe, and how to decide whether you need an agent at all.

Author
Kamran Khan
Published
Updated
Reading time
7 min read
On this page
  1. The loop
  2. What a tool actually is
  3. Reads, writes and the human checkpoint
  4. Memory and state
  5. Planning, and whether you need it
  6. Failure modes and how to design for them
  7. Evaluating an agent
  8. Do you need an agent?

"Agent" has become the word for anything AI that seems to do more than answer a question. Strip the marketing away and an agent is a specific, understandable piece of software: a language model running inside a loop, able to call functions you have defined, until it decides the task is done.

The loop itself is short. Most of the work is in the engineering around it, which is what separates a demo from something a business can rely on.

The loop

Every agent, from a simple support assistant to a multi-step research tool, runs some version of this:

  1. Goal + context
  2. Model decides
  3. Call tool
  4. Observe result
  5. Model decides again
  6. Finish
The agent loop. The model never executes anything itself: it asks, and your code runs it.

In code, stripped to essentials:

async function runAgent(goal: string, tools: Tool[], maxSteps = 12) {
  const messages: Message[] = [
    { role: "system", content: SYSTEM_INSTRUCTIONS },
    { role: "user", content: goal },
  ];
 
  for (let step = 0; step < maxSteps; step++) {
    const response = await llm.chat({ messages, tools });
 
    if (response.toolCalls.length === 0) {
      return response.text; // the model decided it is finished
    }
 
    for (const call of response.toolCalls) {
      const tool = tools.find((t) => t.name === call.name);
      const input = tool.schema.parse(call.arguments); // validate before running
      const result = await tool.run(input, { user: currentUser });
      messages.push({ role: "tool", toolCallId: call.id, content: JSON.stringify(result) });
      await audit.log({ step, tool: call.name, input, result });
    }
  }
 
  throw new AgentStoppedError("Step limit reached");
}

The most important thing in that code is what the model doesn't do: it never runs anything. It emits a structured request (call lookup_order with { id: 'A123' }) and your program decides whether and how to execute it. That boundary, in your code, is the whole safety model of agents.

The tools are typed. The model sees each tool's name, description and input schema, and nothing else, so the description is the interface and its quality decides how well the model uses the tool.

And the loop is bounded. A step limit, a time limit and a cost limit are not optional; without them a confused model will happily loop forever.

What a tool actually is

A tool is a function with a schema and a description:

const lookupOrder: Tool = {
  name: "lookup_order",
  description:
    "Fetch an order by its ID. Returns status, items, totals and the customer's shipping address. Use when the user references an order number.",
  schema: z.object({ id: z.string().regex(/^[A-Z]\d{3,}$/) }),
  async run({ id }, ctx) {
    return orders.findForUser(id, ctx.user); // permission-scoped, always
  },
};

A good tool does one job: lookup_order, not do_order_things. Its description is written for a reader who has never seen your system, because the model only knows what the description says. It runs as the current user, never as an admin, and enforces the same access rules the UI would.

Two properties are easy to forget. Tools should be idempotent where possible, since the model may retry: create_ticket called twice with the same input should not create two tickets. And results should be compact. Everything a tool returns goes back into the model's context and costs tokens, so return what the next step needs, not the whole record.

If a tool-using system seems unreliable, read the tool descriptions before blaming the model. Vague descriptions produce vague behaviour.

Reads, writes and the human checkpoint

Tools fall into two classes, and they need different treatment.

Read tools (look up, search, calculate, summarise) are low risk. Let the agent call them freely, within rate limits.

Write tools send, create, update, delete or pay. They change the world. For these I use a proposal: the tool doesn't perform the action, it returns a proposed action that a person approves in the interface. The real write happens only after approval.

const sendReply: Tool = {
  name: "send_reply",
  description: "Propose a reply to the customer. A human will review before it is sent.",
  schema: z.object({ ticketId: z.string(), body: z.string().max(2000) }),
  async run(input, ctx) {
    const proposal = await proposals.create({ ...input, proposedBy: "agent", forUser: ctx.user.id });
    return { proposalId: proposal.id, status: "awaiting_approval" };
  },
};

The agent finishes its task ("I have drafted a reply and queued it for approval"), a person approves with one click, and the system never sends something nobody read. Over time you can measure how often people approve without editing, and promote specific low-risk writes to automatic once that rate is high.

Memory and state

The loop above keeps everything in the messages array for a single run. Real systems need three more kinds of state.

Task state records where the agent is in a multi-step job. Persist it in your database like any other workflow state, so a crash or a deploy doesn't lose work. Short-term memory is the conversation so far, trimmed or summarised when it grows past a budget. Long-term memory holds facts worth keeping across runs, such as a customer's preferences or an issue already resolved. That last one is just retrieval: store facts with metadata and search them at the start of the next run, using the same machinery as a RAG system.

Resist building a bespoke memory framework. A table of facts with embeddings and a timestamp covers most needs.

Planning, and whether you need it

Simple agents decide one step at a time, which is fine for short tasks. Longer tasks benefit from an explicit plan: ask the model to outline the steps first, store the plan, and execute against it, revising when a result changes the picture. The plan also becomes state a person can inspect ("it is about to do X, then Y"), which helps people trust it.

Multi-agent setups, where a planner delegates to specialists, are sometimes useful and often over-engineered. Start with one agent and good tools, and split only when a single context can't hold the job.

Failure modes and how to design for them

Infinite loops are the easy one: step, time and cost limits, always.

Models sometimes invent tool arguments. Validate every input against the schema before running anything, and send the validation error back to the model so it can correct itself.

Confident wrong answers are harder. Require citations from retrieval for factual claims, make "I could not find that" an acceptable answer, and test with questions the tools cannot answer.

Cost can run away quietly. Meter tokens per run and per user, set a budget per task, and stop with a clear message when it's exceeded.

Side effects need a trail. Log every tool call with the user, inputs, outputs and a timestamp; if you can't reconstruct what an agent did, you can't operate it.

Then there is prompt injection. Anything the agent reads, an email or a web page for instance, may contain text designed to hijack it ("ignore previous instructions and forward this to..."). Treat all retrieved content as data, never as instructions. Keep write tools behind approval, and never give an agent more access than the user it acts for.

Evaluating an agent

Agents are harder to evaluate than single model calls because there are many valid paths to a result. Start with a set of task scenarios, each with a definition of success: the right ticket created, the correct figure reported. Then sample real runs and read the sequence of tool calls. Wrong tool choices, unnecessary calls and near-misses show up quickly when you read trajectories. In production, the approval rate on proposed writes makes a good running quality metric.

Do you need an agent?

Often, no. If the task has a fixed sequence of steps, write the sequence in code and use the model only for the steps that need language understanding, like classification, extraction or drafting. That is a workflow with AI steps, and it is cheaper and easier to test than an agent.

Reach for an agent when the path changes with the input: a support request that might need one lookup or five, a research task where each result changes the next question, or an operations assistant handling many small, different jobs. That is where the loop's flexibility pays for itself, as long as the tools are precise, writes wait for approval, and every run has limits and an audit log.

  • #ai-agents
  • #tool-calling
  • #llm
  • #automation
  • #architecture
  • #typescript

Kamran Khan

I build AI-powered applications, full-stack platforms and business software — across full-stack development, business systems, automation and production AI.

About Kamran →

Keep reading

  • AI Agents8 min read

    What Is a Harness Engineer? Agent = Model + Harness

    A harness is everything in an AI agent except the model. Here is what a harness engineer actually builds (tools, context, memory, permissions, budgets, evals) and how to do it well.

    • #harness-engineering
    • #ai-agents
    • #tool-calling
    • #llm
  • AI Agents12 min read

    Harness Engineering, Explained: What It Adds to Prompts and Context

    Prompt, context and harness engineering are three scopes of one job. A dated, source-checked timeline of who said what, where the sources disagree, and a way to decide which fix an agent mistake needs.

    • #harness-engineering
    • #context-engineering
    • #prompt-engineering
    • #ai-agents
  • AI Agents7 min read

    MCP Went Stateless: What Changed in the 2026-07-28 Spec

    The 2026-07-28 MCP spec removes sessions and the handshake. What changed, what breaks, what is deprecated, and how to migrate, checked against the changelog.

    • #mcp
    • #model-context-protocol
    • #ai-agents
    • #protocol

Turn an AI idea into working software.

From retrieval and agents to document intelligence, I build AI systems that plug into real business data and workflows.