AI Agents Explained

What AI agents are, how the agent loop works, and why they're different from chatbots.

Trevor I. Lasn Trevor I. Lasn
· 7 min read

You tell ChatGPT to “fix the bug” and it gives you a code snippet. You copy it, paste it, realize it’s wrong, go back, paste the error, get another snippet. Repeat.

You tell Claude Code to “fix the bug” and it does everything itself. It opens your files, finds the problem, writes a fix, runs the tests. If a test still fails, it goes back, tries a different approach, and runs the tests again. When everything passes, it commits.

Same model underneath. The difference is agency.

An agent is just a program that can use tools, look at what happened, and decide what to do next — in a loop. A chatbot gives you one response and stops. An agent keeps going until the job is done.

Here’s the difference visually. A chatbot is one turn — you ask, it answers, you’re on your own.

Chatbot
You: "Fix the bug in auth.ts"
┌─────────┐
│ Model │
└────┬─────┘
"Here's the fix:
Change line 42..."
Done. You go copy-paste it.

An agent loops. It thinks, does something, checks if it worked, and keeps going.

Agent
You: "Fix the bug in auth.ts"
┌─────────┐
│ Think │ ◄──────────────┐
└────┬─────┘ │
│ │
▼ │
┌─────────┐ │
│ Act │ call a tool │
└────┬─────┘ │
│ │
▼ │
┌─────────┐ not done │
│ Observe │ ───────────────┘
└────┬─────┘
│ done
"Fixed. Tests pass.
Committed to main."

One gives you instructions. The other does the work.

Every agent you’ve heard of — Claude Code, Codex, Cursor, Devin — runs this same loop. Think, act, observe, repeat. That’s it. People call it the ReAct pattern (Reason + Act), but it’s really just a while loop.

Say you ask an agent: “Find all TypeScript files with unused imports and remove them.” Here’s what actually happens under the hood:

Think
"I need to find .ts files first."
Act
glob("src/**/*.ts")
Observe
Found 47 files.
Think
"Now I'll check each file for unused imports.
Let me start with src/utils/auth.ts."
Act
read("src/utils/auth.ts")
Observe
Line 2: import { hash } from 'bcrypt' ← never used
Act
edit("src/utils/auth.ts", remove line 2)
Think
"Removed. Let me verify it still compiles."
Act
run("npx tsc --noEmit")
Observe
No errors. Move to next file.
... repeat for 46 more files ...
Think
"All files checked. 12 unused imports removed.
Everything compiles. Done."

Notice it’s not running all these steps in advance. It decides each step based on what it saw in the previous one. That’s the key difference between an agent and a script. A script follows a fixed path. An agent adapts.

Now here’s the thing — an agent is only as useful as its tools. Without tools, it’s just a chatbot with extra steps. The model itself doesn’t actually touch your filesystem. It outputs a structured tool call — basically saying “hey, I want to read this file” — and the system around it does the actual work and passes the result back.

┌──────────────────────────────────┐
│ AI MODEL │
│ │
│ "I need to read a file" │
│ │ │
│ ▼ │
│ tool_call: read_file │
│ args: { path: "src/auth.ts" } │
└──────────┬───────────────────────┘
┌──────────────────────────────────┐
│ TOOL EXECUTOR │
│ │
│ read_file("src/auth.ts") │
│ │ │
│ ▼ │
│ returns file contents │
└──────────┬───────────────────────┘
┌──────────────────────────────────┐
│ AI MODEL │
│ │
│ "Now I can see the bug on │
│ line 42. Let me fix it." │
└──────────────────────────────────┘

Common tools agents use:

ToolWhat it does
read_fileRead a file from disk
write_fileWrite or edit a file
run_commandExecute a shell command
web_searchSearch the internet
list_filesList directory contents

Different agents give the model different amounts of freedom. Some ask permission before every action. Some run fully autonomously and just show you the result. The loop is the same — the leash is what changes.

You can build one yourself. It’s simpler than you’d think. Here’s a working agent in TypeScript — it takes a goal, calls Claude, and loops until the model says it’s done.

agent.ts
import Anthropic from "@anthropic-ai/sdk";
import { execSync } from "child_process";
const client = new Anthropic();
const tools: Anthropic.Tool[] = [
{
name: "run_command",
description: "Run a shell command and return the output",
input_schema: {
type: "object" as const,
properties: {
command: { type: "string", description: "The shell command to run" },
},
required: ["command"],
},
},
];
async function agent(goal: string) {
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: goal },
];
while (true) {
const response = await client.messages.create({
model: "claude-sonnet-4-5-20250929",
max_tokens: 1024,
tools,
messages,
});
// If the model responds with text and no tool calls, it's done
if (response.stop_reason === "end_turn") {
const text = response.content.find((b) => b.type === "text");
return text?.text;
}
// Otherwise, execute each tool call
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type === "tool_use") {
const input = block.input as { command: string };
const result = execSync(input.command, { encoding: "utf-8" });
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: result,
});
}
}
// Feed results back into the conversation
messages.push({ role: "assistant", content: response.content });
messages.push({ role: "user", content: toolResults });
}
}
agent("What's the latest version of @anthropic-ai/sdk on npm? Check using curl.");

The whole thing is a while(true) loop. Call the model, check if it wants to use a tool, execute the tool, feed the result back. That’s an agent.

I ran this on my machine. npx tsx agent.ts and watched it work:

npx tsx agent.ts
🎯 Goal: What's the latest version of @anthropic-ai/sdk on npm? Check using curl.
Step 1: Think
"I'll check the latest version of @anthropic-ai/sdk on npm using curl."
Step 2: Act
$ curl -s https://registry.npmjs.org/@anthropic-ai/sdk/latest | grep -o '"version":"[^"]*"' | head -1
Step 3: Observe
"version":"0.72.0"
✅ Done: The latest version of @anthropic-ai/sdk on npm is 0.72.0.
agent("Count .ts files")
┌─────────────┐
│ Call model │ ◄──────────────────┐
└──────┬──────┘ │
│ │
▼ │
┌──────────────┐ tool_use? │
│Check response│───── yes ──► Execute tool
└──────┬───────┘ │
│ no │
│ (end_turn) Feed result back
Return final answer

In production you’d add error handling, timeouts, and permission checks. But the core is always this loop.

Agents aren’t magic though. They fail in ways you’ll recognize pretty quickly.

They loop forever. The agent edits a file, runs the test, it fails, edits it back, runs the test again. Over and over. Good agents have a retry limit.

They pick the wrong tool confidently. The model decides to delete a file when it should have edited it. This is why every serious agent has a permission system.

They forget what they were doing. Long tasks generate tons of tool results. Eventually the conversation gets so long the model loses track of the original goal. The best agents summarize as they go to keep context manageable.

They compound small mistakes. Step 3 is slightly off. Step 7 builds on it. By step 15, the agent has built an elaborate wrong solution and feels great about it. Shorter feedback loops help — verify after each step, not after 50.

Agents work best when the task is clear and there’s a way to check the result. “Fix the failing test” is perfect — the agent can run the test to verify. “Make the UI look better” is terrible — it can’t see what the UI looks like.


Found this article helpful? You might enjoy my free newsletter. I share dev tips and insights to help you grow your coding skills and advance your tech career.



This article was originally published on https://www.trevorlasn.com/blog/ai-agents-explained. It was written by a human and polished using grammar tools for clarity.