Skip to content
AI Agents

How to Build a Simple AI Agent

Build a working agent loop — tool definitions, a system prompt, execution, and the guardrails that keep it from doing something destructive.

Advanced14 min read

What actually makes something an "agent"

A single call to a language model that returns text isn't an agent — it's a chat completion. An agent is a loop: the model decides which tool to call, your code executes that tool and feeds the result back, and the model decides what to do next, repeating until the task is done. See /ai-agents for real-world use cases before you build one from scratch.

Step 1: Define the tools the agent can use

A tool definition is a schema, not code — it tells the model what the tool does and what arguments it takes. The model never executes anything itself; it only ever asks your code to run something on its behalf.

tools.ts
export const tools = [
  {
    name: "search_docs",
    description: "Search internal documentation for a given query and return matching snippets",
    input_schema: {
      type: "object",
      properties: {
        query: { type: "string", description: "The search query" },
      },
      required: ["query"],
    },
  },
  {
    name: "create_ticket",
    description: "Create a support ticket. Only call this after confirming the details with the user.",
    input_schema: {
      type: "object",
      properties: {
        title: { type: "string" },
        description: { type: "string" },
        priority: { type: "string", enum: ["low", "medium", "high"] },
      },
      required: ["title", "description", "priority"],
    },
  },
]

Step 2: Write a specific system prompt

The system prompt is where you set boundaries, not just personality. Say explicitly what the agent should never do without confirmation — this is the cheapest guardrail you have, even though it isn't a hard technical guarantee on its own.

System prompt
You are a support assistant. You can search internal documentation and
create support tickets.

Rules:
- Always search the docs before answering a question you're not certain about
- Never create a ticket without first summarizing the details back to the
  user and getting explicit confirmation
- If you're not confident in an answer, say so instead of guessing
- Keep responses under 150 words unless the user asks for more detail

Step 3: Implement the agent loop

This loop sends the conversation to the model, checks whether it asked to use a tool, executes that tool if so, feeds the result back in, and repeats — with a hard cap on iterations so a confused agent can't loop forever.

agent.ts
import Anthropic from "@anthropic-ai/sdk"
import { tools } from "./tools"
import { executeTool } from "./execute-tool"

const client = new Anthropic()
const MAX_STEPS = 8

export async function runAgent(userTask: string) {
  const messages: Anthropic.MessageParam[] = [{ role: "user", content: userTask }]

  for (let step = 0; step < MAX_STEPS; step++) {
    const response = await client.messages.create({
      model: "claude-sonnet-4-5",
      max_tokens: 1024,
      system: SYSTEM_PROMPT,
      tools,
      messages,
    })

    const toolUse = response.content.find((block) => block.type === "tool_use")
    messages.push({ role: "assistant", content: response.content })

    if (!toolUse) {
      return response.content // agent is done, no more tools requested
    }

    const result = await executeTool(toolUse.name, toolUse.input)
    messages.push({
      role: "user",
      content: [{ type: "tool_result", tool_use_id: toolUse.id, content: result }],
    })
  }

  throw new Error("Agent exceeded max steps without finishing")
}

Step 4: Implement tool execution with guardrails, not blind trust

This is where real safety lives — not in the prompt, but in code the model can't override. A destructive tool should require an explicit confirmation flag before it runs, regardless of how confident the model sounds.

execute-tool.ts
export async function executeTool(name: string, input: Record<string, unknown>) {
  switch (name) {
    case "search_docs":
      return searchDocs(input.query as string)

    case "create_ticket":
      // A real safeguard: this still requires a human-reviewed confirmation
      // step in the product UI before the ticket is actually created.
      return queueTicketForConfirmation(input)

    default:
      return { error: `Unknown tool: ${name}` }
  }
}

Step 5: Add guardrails before you trust it with anything real

  • A hard maximum step count, so a confused loop can't run indefinitely or burn unlimited API usage
  • Explicit human confirmation before any destructive or irreversible tool call — sending an email, charging a card, deleting data
  • Logging every tool call and its arguments so you can audit what the agent actually did after the fact
  • A allowlist of tools per context — don't give a customer-facing agent the same tool access as an internal one

Where agents fail in practice

Agents are confidently wrong more often than people expect — they can call the wrong tool, misinterpret an ambiguous result, or loop on a task that's actually impossible with the tools they have. Test with deliberately messy, ambiguous inputs, not just the happy path, before you connect an agent to anything with real consequences.

Ready to build the next one?

Browse the full tutorial library or grab a ready-made prompt for your next step.