How to Build AI Agents in TypeScript

AI Agents Deep Dive
10 parts- 1AI Agents Explained: A TypeScript Developer's Guide
- 2How to Build AI Agents in TypeScriptCurrent
- 3Multi-Agent Systems: How to Orchestrate Multiple AI Agents in TypeScript
- 4The Agentic Development Tech Stack for 2026
- 5How to Debug AI Agent Workflows
- 6AI Agent Memory Patterns
- 7Over-Editing: Why Your AI Coding Agent Rewrites What Isn't Broken
- 8Intent Debt: The AI-Era Debt Nobody Is Tracking
- 97 AI Agent Orchestration Patterns Developers Should Know
- 10The Agent Reliability Cliff: Why Your 10-Step Chain Only Succeeds 20% of the Time
TL;DR
A practical guide to building AI agents with TypeScript using the Vercel AI SDK. Tool use, multi-step reasoning, and real patterns you can ship today.
Last updated: May 20, 2026
Official Sources#
| Resource | Link |
|---|---|
| AI SDK Documentation | sdk.vercel.ai/docs |
| AI SDK Agents Guide | sdk.vercel.ai/docs/ai-sdk-core/agents |
| AI SDK Tool Calling | sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling |
| Anthropic Provider | sdk.vercel.ai/providers/ai-sdk-providers/anthropic |
| Zod Documentation | zod.dev |
| GitHub Repository | github.com/vercel/ai |
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.
What Makes Something an Agent#
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.
Defining Tools with Zod Schemas#
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.
Multi-Step Reasoning#
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.
The Agent Loop Architecture#
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.
Streaming Agents in Next.js#
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.
Tool Design Patterns#
The quality of your tools determines the quality of your agent. Here are patterns that work well in production.
Constrained tools over general tools#
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 }) => { /* ... */ },
});
Return structured data, not strings#
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,
};
}
Confirmation tools for destructive actions#
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 };
},
});
Where AI Coding Tools Fit In#
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.
Frequently Asked Questions#
What is an AI agent?#
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.
Can you build agents with TypeScript?#
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.
What is the best framework for AI agents?#
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.
How do AI agents use tools?#
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.
What is the difference between agents and chatbots?#
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.
What's Next#
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.
Get the next deep dive like this in your inbox
One email a week on AI Agents and the rest of the AI dev stack. Free.
Read next on Claude Code
AI Agents Explained: A TypeScript Developer's Guide
AI agents use LLMs to complete multi-step tasks autonomously. Here is how they work and how to build them in TypeScript.
6 min readVercel AI SDK: Build Streaming AI Apps in TypeScript
The AI SDK is the fastest way to add streaming AI responses to your Next.js app. Here is how to use it with Claude, GPT, and open source models.
5 min readMulti-Agent Systems: How to Orchestrate Multiple AI Agents in TypeScript
From swarms to pipelines - here are the patterns for coordinating multiple AI agents in TypeScript applications.
6 min readTechnical 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.








