Most "AI agent" tutorials give you a chatbot with a tool and call it a day. That is not an agent. An agent receives an objective, breaks it into steps, calls tools, evaluates results, and keeps looping until the job is done. The difference is autonomy - the model decides the control flow at runtime, not you.
This guide shows you how to build real agents in TypeScript. Not wrappers around a single API call, but systems that reason across multiple steps, use tools to interact with the outside world, and produce structured output you can trust in production. We will use the Vercel AI SDK as the foundation because it handles streaming, tool execution, and multi-step loops with minimal boilerplate.
An agent is a loop. The model looks at the current state, decides what to do next, takes an action, observes the result, and repeats. This is the ReAct pattern (Reason + Act), and it is the backbone of every agent framework.
The critical ingredient is maxSteps. Without it, you get a single model call that might request a tool. With it, you get an autonomous loop that can chain multiple tool calls together, react to intermediate results, and converge on an answer.
import { streamText, tool } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
const result = streamText({
model: anthropic("claude-sonnet-4-20250514"),
system: "You are a research agent. Use tools to gather information, then synthesize a final answer.",
prompt: "What are the top 3 most-starred TypeScript AI libraries on GitHub right now?",
tools: {
searchGitHub: tool({
description: "Search GitHub repositories by query",
parameters: z.object({
query: z.string().describe("Search query"),
sort: z.enum(["stars", "updated", "forks"]).describe("Sort criteria"),
}),
execute: async ({ query, sort }) => {
const res = await fetch(
`https://api.github.com/search/repositories?q=${encodeURIComponent(query)}&sort=${sort}&per_page=10`
);
const data = await res.json();
return data.items.map((r: any) => ({
name: r.full_name,
stars: r.stargazers_count,
description: r.description,
}));
},
}),
getRepoDetails: tool({
description: "Get detailed information about a specific GitHub repository",
parameters: z.object({
owner: z.string(),
repo: z.string(),
}),
execute: async ({ owner, repo }) => {
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`);
return await res.json();
},
}),
},
maxSteps: 8,
});
With maxSteps: 8, the model can search GitHub, inspect individual repos, compare results, and then write a synthesis. Each step feeds back into the context window. The model sees its own previous tool calls and their results, which lets it make increasingly informed decisions.
Tools are where agents get their power. A tool is a function the model can call, with a typed schema that defines its inputs. The AI SDK uses Zod for this, which means your tool parameters are validated at runtime and fully typed at compile time.
Here is a tool definition pattern that scales well:
import { tool } from "ai";
import { z } from "zod";
const databaseQuery = tool({
description: "Execute a read-only SQL query against the application database",
parameters: z.object({
query: z.string().describe("SQL SELECT query to execute"),
params: z.array(z.string()).optional().describe("Parameterized values"),
}),
execute: async ({ query, params }) => {
if (!query.trim().toUpperCase().startsWith("SELECT")) {
return { error: "Only SELECT queries are allowed" };
}
const result = await db.query(query, params);
return { rows: result.rows, rowCount: result.rowCount };
},
});
const readFile = tool({
description: "Read the contents of a file from the project directory",
parameters: z.object({
path: z.string().describe("Relative file path from project root"),
}),
execute: async ({ path }) => {
const resolved = resolve(PROJECT_ROOT, path);
if (!resolved.startsWith(PROJECT_ROOT)) {
return { error: "Path traversal not allowed" };
}
const content = await readFile(resolved, "utf-8");
return { content, path };
},
});
const writeFile = tool({
description: "Write content to a file in the project directory",
parameters: z.object({
path: z.string().describe("Relative file path from project root"),
content: z.string().describe("File content to write"),
}),
execute: async ({ path, content }) => {
const resolved = resolve(PROJECT_ROOT, path);
if (!resolved.startsWith(PROJECT_ROOT)) {
return { error: "Path traversal not allowed" };
}
await writeFileSync(resolved, content, "utf-8");
return { success: true, path };
},
});
A few things to notice. Every tool has a clear description - this is what the model reads to decide when to use it. The Zod schemas include .describe() annotations on each field, which give the model context about what values to provide. And the execute function includes safety checks before doing anything destructive.
If you are working with complex schemas, the JSON to TypeScript converter on this site can generate Zod schemas from sample JSON payloads. Useful when you are wrapping an existing API and need the schema fast.
The real power of agents shows up when tasks require multiple steps. Consider a code review agent that needs to read files, understand the project structure, check for issues, and produce a structured report.
import { generateObject, tool } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
import { readdir, readFile } from "fs/promises";
import { join } from "path";
const reviewSchema = z.object({
summary: z.string(),
issues: z.array(
z.object({
file: z.string(),
line: z.number().optional(),
severity: z.enum(["error", "warning", "info"]),
message: z.string(),
suggestion: z.string(),
})
),
score: z.number().min(0).max(100),
});
type CodeReview = z.infer<typeof reviewSchema>;
async function reviewCode(projectPath: string): Promise<CodeReview> {
const { object } = await generateObject({
model: anthropic("claude-sonnet-4-20250514"),
schema: reviewSchema,
system: `You are a senior TypeScript engineer performing a code review.
Read the project files using the available tools, then produce a structured review.
Focus on type safety, error handling, and architectural concerns.`,
prompt: `Review the TypeScript project at: ${projectPath}`,
tools: {
listFiles: tool({
description: "List files in a directory",
parameters: z.object({ dir: z.string() }),
execute: async ({ dir }) => {
const entries = await readdir(join(projectPath, dir), {
withFileTypes: true,
});
return entries.map((e) => ({
name: e.name,
isDirectory: e.isDirectory(),
}));
},
}),
readFile: tool({
description: "Read a file's contents",
parameters: z.object({ path: z.string() }),
execute: async ({ path }) => {
const content = await readFile(join(projectPath, path), "utf-8");
return { path, content };
},
}),
},
maxSteps: 15,
});
return object;
}
The agent will list directories to understand the project structure, read key files like tsconfig.json and package.json, then dive into source files. It chains tool calls across multiple steps, building context as it goes. The output conforms to the Zod schema - fully typed, validated, ready to consume in your application.
This is the generateObject approach. The model is forced to return data matching your schema. No parsing strings. No hoping the JSON is valid. The SDK handles retries if the output does not match.
Get the weekly deep dive
Tutorials on Claude Code, AI agents, and dev tools - delivered free every week.
For more complex agents that need custom control flow, you can build the loop yourself. This gives you control over retry logic, context window management, and early termination conditions.
import { generateText, tool } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
interface AgentState {
messages: Array<{ role: string; content: string }>;
steps: number;
maxSteps: number;
done: boolean;
}
async function runAgent(goal: string, tools: Record<string, any>) {
const state: AgentState = {
messages: [
{
role: "system",
content: `You are an autonomous agent. Complete the given goal using available tools.
When you have enough information to provide a final answer, respond with plain text (no tool calls).`,
},
{ role: "user", content: goal },
],
steps: 0,
maxSteps: 20,
done: false,
};
while (!state.done && state.steps < state.maxSteps) {
const { text, toolCalls, toolResults } = await generateText({
model: anthropic("claude-sonnet-4-20250514"),
messages: state.messages as any,
tools,
maxSteps: 1, // One step at a time for manual control
});
state.steps++;
if (toolCalls.length === 0) {
// Model responded with text - it is done
state.done = true;
return { result: text, steps: state.steps };
}
// Add tool interactions to message history
state.messages.push({
role: "assistant",
content: JSON.stringify({ toolCalls }),
});
for (const result of toolResults) {
state.messages.push({
role: "tool",
content: JSON.stringify(result),
});
}
console.log(`Step ${state.steps}: called ${toolCalls.map((t) => t.toolName).join(", ")}`);
}
return { result: "Max steps reached", steps: state.steps };
}
This pattern gives you hooks into every step of the agent's execution. You can log each tool call, implement circuit breakers, manage token budgets, or add human-in-the-loop approval for destructive actions.
For web applications, you want the agent's reasoning and tool calls to stream to the UI in real time. The AI SDK makes this straightforward with streamText and the useChat hook.
Server-side route handler:
// app/api/agent/route.ts
import { streamText, tool } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: anthropic("claude-sonnet-4-20250514"),
system: `You are a developer productivity agent. You can search documentation,
analyze code patterns, and suggest improvements. Use tools to gather information
before providing your answer.`,
messages,
tools: {
searchDocs: tool({
description: "Search documentation for a library or framework",
parameters: z.object({
library: z.string().describe("Library name (e.g., 'nextjs', 'react')"),
query: z.string().describe("What to search for"),
}),
execute: async ({ library, query }) => {
// Your documentation search implementation
return { results: [`${library}: ${query} - relevant docs found`] };
},
}),
analyzeCode: tool({
description: "Analyze a code snippet for issues and improvements",
parameters: z.object({
code: z.string().describe("The code to analyze"),
language: z.string().describe("Programming language"),
}),
execute: async ({ code, language }) => {
return {
language,
lineCount: code.split("\n").length,
analysis: "Analysis complete",
};
},
}),
},
maxSteps: 10,
});
return result.toDataStreamResponse();
}
Client-side component:
"use client";
import { useChat } from "@ai-sdk/react";
export default function AgentChat() {
const { messages, input, handleInputChange, handleSubmit, isLoading } =
useChat({ api: "/api/agent" });
return (
<div className="max-w-2xl mx-auto p-4">
<div className="space-y-4">
{messages.map((m) => (
<div key={m.id} className="p-3 rounded-lg">
<div className="font-medium text-sm mb-1">
{m.role === "user" ? "You" : "Agent"}
</div>
<div>{m.content}</div>
{/* Show tool invocations */}
{m.toolInvocations?.map((tool, i) => (
<div key={i} className="mt-2 p-2 bg-gray-50 rounded text-sm">
<span className="font-mono">{tool.toolName}</span>
{tool.state === "result" && (
<pre className="mt-1 text-xs overflow-auto">
{JSON.stringify(tool.result, null, 2)}
</pre>
)}
</div>
))}
</div>
))}
</div>
<form onSubmit={handleSubmit} className="mt-4">
<input
value={input}
onChange={handleInputChange}
placeholder="Give the agent a task..."
disabled={isLoading}
className="w-full p-3 border rounded-lg"
/>
</form>
</div>
);
}
The useChat hook handles the streaming protocol automatically. Tool invocations appear on each message object, so you can render the agent's reasoning process as it happens. Users see which tools the agent calls and what results come back, giving full transparency into the agent's decision-making.
The quality of your tools determines the quality of your agent. Here are patterns that work well in production.
Do not give the agent a single "do anything" tool. Give it specific, well-scoped tools with clear descriptions.
// Bad: too general
const execute = tool({
description: "Execute any operation",
parameters: z.object({ operation: z.string(), data: z.any() }),
execute: async ({ operation, data }) => { /* ... */ },
});
// Good: specific and well-described
const createUser = tool({
description: "Create a new user account with email and name",
parameters: z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
role: z.enum(["admin", "member", "viewer"]).default("member"),
}),
execute: async ({ email, name, role }) => { /* ... */ },
});
Tools should return structured objects that the model can reason about, not formatted strings.
// Bad: string output
execute: async ({ query }) => {
const results = await db.query(query);
return `Found ${results.length} results: ${results.map(r => r.name).join(", ")}`;
}
// Good: structured output
execute: async ({ query }) => {
const results = await db.query(query);
return {
count: results.length,
results: results.map(r => ({ id: r.id, name: r.name, status: r.status })),
hasMore: results.length === LIMIT,
};
}
For agents that can modify data, add a confirmation step.
const deleteRecords = tool({
description: "Delete records matching a filter. Returns a preview first - call confirmDelete to execute.",
parameters: z.object({
table: z.string(),
filter: z.record(z.string()),
}),
execute: async ({ table, filter }) => {
const preview = await db.query(
`SELECT id, name FROM ${table} WHERE ${buildWhere(filter)} LIMIT 10`
);
return {
willDelete: preview.length,
preview: preview,
confirmationToken: generateToken({ table, filter }),
};
},
});
const confirmDelete = tool({
description: "Confirm and execute a previously previewed delete operation",
parameters: z.object({
confirmationToken: z.string(),
}),
execute: async ({ confirmationToken }) => {
const { table, filter } = verifyToken(confirmationToken);
const result = await db.query(`DELETE FROM ${table} WHERE ${buildWhere(filter)}`);
return { deleted: result.rowCount };
},
});
Building agents is one of the strongest use cases for AI coding tools. Claude Code can scaffold an entire agent system from a natural language description - it reads your existing code, generates typed tool definitions, and wires up the streaming pipeline. Cursor gives you the same capability inside an IDE with inline completions that understand the AI SDK's patterns.
The workflow for most teams looks like this: describe the agent's purpose and tools in your CLAUDE.md file, then use Claude Code to generate the implementation. The model understands the AI SDK deeply, so it produces idiomatic code with proper Zod schemas, streaming handlers, and error boundaries.
For a full breakdown of the AI SDK's streaming and tool use capabilities, see the Vercel AI SDK guide. And if you want to see how agents fit into a broader application stack, the developer toolkit page covers the full set of tools that integrate well with agent architectures.
An AI agent is a program that uses a large language model to autonomously complete multi-step tasks. Unlike a chatbot that responds to a single prompt and stops, an agent receives a goal, breaks it into steps, calls tools to interact with the outside world, evaluates results, and keeps looping until the objective is met. The model decides the control flow at runtime. For a conceptual overview, see AI Agents Explained.
Yes. TypeScript is one of the strongest languages for building AI agents thanks to the Vercel AI SDK and the Claude Agent SDK. Both provide typed tool definitions using Zod schemas, streaming support, and multi-step reasoning loops. TypeScript's type system ensures your tool inputs and outputs are validated at compile time, which reduces runtime errors in production agent systems.
The Vercel AI SDK is the best choice for TypeScript developers building agents that integrate with web applications. It handles streaming, tool execution, and structured output with minimal boilerplate. The Claude Agent SDK is better suited for standalone agent systems with delegation and multi-agent patterns. LangChain.js provides more pre-built abstractions for complex workflows. The right choice depends on whether your agent lives inside a web app or runs independently.
Agents use tools by calling functions you define with typed parameter schemas. When the model encounters a task that requires external data or actions, it generates a tool call with the appropriate arguments. The framework executes the function and feeds the result back into the model's context. The model then reasons about the result and decides the next step. This reason-act-observe loop continues until the goal is complete.
A chatbot processes a single user message and returns a single response. An agent operates in a loop, making multiple LLM calls and tool invocations to accomplish a goal. Chatbots follow a request-response pattern. Agents follow a goal-directed pattern where the model decides what actions to take, observes outcomes, and adjusts its approach. Agents can chain dozens of operations together without human input between steps.
You have the building blocks: tool definitions, multi-step loops, streaming to the UI, and patterns for production safety. The next step is building agents that solve real problems in your domain.
Start with a narrow scope. A code review agent. A data analysis agent. A customer support agent that can look up orders and process refunds. Constrain the tools, test the edge cases, and expand from there.
For more on the concepts behind agents, read AI Agents Explained. To see how agents connect to external services, check out the MCP guide. And for the full application stack these agents run inside, see Next.js AI App Stack 2026.
Technical content at the intersection of AI and development. Building with AI agents, Claude Code, and modern dev tools - then showing you exactly how it works.
The TypeScript toolkit for building AI apps. Unified API across OpenAI, Anthropic, Google. Streaming, tool calling, stru...
View ToolAnthropic's agentic coding CLI. Runs in your terminal, edits files autonomously, spawns sub-agents, and maintains memory...
View ToolNew tutorials, open-source projects, and deep dives on coding agents - delivered weekly.
Anthropic's Python SDK for building production agent systems. Tool use, guardrails, agent handoffs, and orchestration. R...

Getting Started with OpenAI's New TypeScript Agents SDK: A Comprehensive Guide OpenAI has recently unveiled their Agents SDK within TypeScript, and this video provides a detailed walkthrough...

In this video, I demonstrate how to use VectorShift to build AI applications and workflows. By applying ideas from Anthropic's blog post 'Building Effective Agents,' I show you how to create...

No-Code AI Automation with VectorShift: Integrations, Pipelines, and Chatbots In this video, I introduce VectorShift, a no-code AI automation platform that enables you to create AI solutions...

AI agents use LLMs to complete multi-step tasks autonomously. Here is how they work and how to build them in TypeScript.

Aider is open source and works with any model. Claude Code is Anthropic's commercial agent. Here is how they compare for...

Claude Code runs in your terminal. Cursor runs in an IDE. Both write TypeScript. Here is how to pick the right one.