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
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?
  9. Summary

"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, with the ability to call functions you have defined, until it decides the task is done.

This article explains that loop precisely, then covers the engineering that 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, 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");
}

Three things to notice:

  1. The model never runs code. It emits a structured request — "call lookup_order with { id: 'A123' }" — and your program decides whether and how to execute it. This is the whole safety model of agents: the boundary is in your code.
  2. Tools are typed. The model is given each tool's name, description and input schema. The description is the interface, and its quality determines how well the model uses the tool.
  3. 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
  },
};

Good tools share some properties:

  • Narrow. One job. lookup_order, not do_order_things.
  • Described for a reader who has never seen your system. The model only knows what the description says.
  • Permission-scoped. The tool runs as the current user, never as an admin, and enforces the same access rules the UI would.
  • Idempotent where possible. The model may retry. create_ticket should not create two tickets if called twice with the same input.
  • Returns compact, structured results. The output goes back into the model's context and costs tokens. Return what is needed, not the whole record.

If tool-using systems seem unreliable, look at the tool descriptions before blaming the model. Vague descriptions produce vague behaviour.

Reads, writes and the human checkpoint

Tools fall into two classes and should be treated differently.

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, pay — change the world. For these, the pattern I use is a proposal: the tool does not perform the action, it returns a proposed action that a person approves in the interface. Only after approval does the real write happen.

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 completes its task ("I have drafted a reply and queued it for approval"), the human sees a one-click approval, and the system never sends something nobody read. As trust is earned — measured by how often humans approve without editing — specific low-risk writes can be promoted to automatic.

Memory and state

The loop above holds everything in the messages array for one run. Real systems need more:

  • Task state — where the agent is in a multi-step job, persisted so a crash or a deploy does not lose work. Store it in your database like any other workflow state.
  • Short-term memory — the conversation so far, trimmed or summarised when it grows past a budget.
  • Long-term memory — facts worth keeping across runs (a customer's preferences, a resolved issue). This is just retrieval: store facts with metadata and search them at the start of the next run. It is the same machinery as a RAG system.

Resist the temptation to build 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 step's result changes the picture. The plan becomes visible state that a person can inspect — "it is about to do X, Y, then Z" — which matters for trust.

Multi-agent setups (a planner delegating to specialists) are sometimes useful and often over-engineered. Start with one agent and good tools. Split only when a single context genuinely cannot hold the job.

Failure modes and how to design for them

Infinite loops. Step, time and cost limits. Always.

Hallucinated tool arguments. Validate every input against the schema before running anything; return the validation error to the model so it can correct itself.

Confident wrong answers. Require citations for factual claims (from retrieval) and prefer "I could not find that" over guessing. Test with questions the tools cannot answer.

Runaway cost. Meter tokens per run and per user. Set a budget per task and stop with a clear message when it is exceeded.

Silent side effects. Every tool call logged with user, inputs, outputs and timestamp. If you cannot reconstruct what an agent did, you cannot operate it.

Prompt injection. Content the agent reads — emails, web pages, documents — 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 calls because there are many valid paths to a result. What works:

  • A set of task scenarios with a definition of success (the right ticket created, the correct figure reported).
  • Trajectory review — sample real runs and read the tool-call sequence. Wrong tool choice, unnecessary calls and near-misses show up quickly.
  • Approval rate on proposed writes as the running quality metric in production.

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 for the steps that need language understanding — classification, extraction, drafting. That is a workflow with AI steps, and it is more predictable, cheaper and easier to test than an agent.

Reach for an agent when the path genuinely varies with the input: a support request that might need one lookup or five, a research task where each result changes the next question, an operations assistant that handles many small, different jobs. There, the loop's flexibility earns its cost.

Summary

An agent is a bounded loop in which a model requests typed tool calls and your code decides whether to run them. The engineering that matters is: precise tool definitions, permission-scoped execution, human approval on writes, persistent state, hard limits, full audit logs and a way to evaluate runs. None of that is magic. All of it is necessary.

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

Kamran Khan

Software Engineer and AI Application Developer working across full-stack development, business systems, automation and production AI.

About Kamran →

Keep reading

  • AI Engineering8 min read

    How to Build an AI-Powered Business Application

    A practical architecture for AI applications that do real work inside a business: where the model sits, how it reaches your data and tools, and what has to be true before it goes to production.

    • #ai-applications
    • #llm
    • #architecture
    • #rag
  • LLM Applications7 min read

    How to Integrate AI Into an Existing Web Application

    You do not need a rewrite to add AI to a Laravel or Next.js application. A step-by-step approach: find the right feature, isolate the model behind a service, validate outputs, handle cost and failure, and ship behind a flag.

    • #ai-integration
    • #laravel
    • #nextjs
    • #llm
  • SaaS7 min read

    How to Design a Scalable SaaS Architecture

    Tenancy, billing, background work, environments and the boundaries that let a SaaS product grow without a rewrite. A practical architecture for a first version that is built to last, with Next.js, Laravel and PostgreSQL as the reference stack.

    • #saas
    • #architecture
    • #multi-tenancy
    • #nextjs

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.