Build AI Agents in TypeScript: A Practical Guide
How to build AI agents in TypeScript from scratch: the tool-use loop, memory, error handling, and a working agent in under 200 lines.
Who this is for: backend or full-stack developers who already ship TypeScript in production and want to build AI agents in TypeScript instead of duct-taping together a no-code agent builder. If you've never written an async function, start with prompt engineering for business first.
An agent is not a chatbot with extra steps. It's a loop: the model decides what to do, a piece of code executes that decision against the real world, the result goes back into the model's context, and the loop repeats until the task is done or a limit is hit. Everything else - frameworks, orchestration layers, memory stores - is scaffolding around that loop. This guide builds the loop by hand in TypeScript, then adds the scaffolding one piece at a time so you understand what each piece actually buys you.
Why build AI agents in TypeScript instead of using a framework
Frameworks like LangChain.js and the Vercel AI SDK exist because the loop above, once you add retries, streaming, and multi-tool orchestration, gets verbose fast. But most teams reach for a framework before they've built the loop once by hand, and end up debugging someone else's abstraction instead of their own logic.
My rule after building a dozen of these for client projects: write the raw loop yourself first, even if it's a throwaway prototype. It takes about an hour and teaches you exactly what a framework is doing under the hood - which matters the first time a tool call silently fails in production and you need to know where to look.
TypeScript specifically earns its keep here because:
- Tool schemas are just types.
zodgives you runtime validation and a TypeScript type from one definition - critical when the model hallucinates a malformed argument. - Most teams already have a Node/TypeScript backend, so the agent lives next to the code it needs to call (your database client, your Stripe SDK, your internal APIs).
- Streaming responses to a browser client is a solved problem in the Node ecosystem (SSE, ReadableStream) in a way it isn't in, say, a Python Flask app bolted onto a JS frontend.
The core loop: tool-use in ~40 lines
Every agent, regardless of framework, reduces to this shape. Here's it built directly against the Anthropic SDK:
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const tools: Anthropic.Tool[] = [
{
name: "get_order_status",
description: "Look up an order's shipping status by order ID",
input_schema: {
type: "object",
properties: { orderId: { type: "string" } },
required: ["orderId"],
},
},
];
async function runAgent(userMessage: string) {
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: userMessage },
];
for (let step = 0; step < 8; step++) {
const response = await client.messages.create({
model: "claude-opus-4-6",
max_tokens: 1024,
tools,
messages,
});
messages.push({ role: "assistant", content: response.content });
if (response.stop_reason !== "tool_use") {
return response.content; // model is done, no more tools needed
}
const toolResults = [];
for (const block of response.content) {
if (block.type === "tool_use") {
const result = await executeTool(block.name, block.input);
toolResults.push({
type: "tool_result" as const,
tool_use_id: block.id,
content: JSON.stringify(result),
});
}
}
messages.push({ role: "user", content: toolResults });
}
throw new Error("Agent exceeded step budget");
}
That for loop with a hard cap (8 steps here) is not optional. Ungoverned agent loops are the single most common cause of runaway API bills - see prompt caching for how to keep the repeated context in that loop cheap, since every iteration re-sends the growing message history.
Warning: never let an agent loop run unbounded on user-triggered input. A model that gets confused can call the same tool in a cycle 40 times before you notice, and each call is a billed request.
Planning: single-shot vs. explicit plan-then-execute
There are two dominant patterns for how an agent decides its next move, and picking wrong for your use case wastes both latency and money.
Reactive (ReAct-style)
The model sees the current state and decides one tool call at a time, like the loop above. No upfront plan - it improvises step by step. This works well for open-ended tasks where the right sequence of steps genuinely depends on what earlier tool calls return (debugging, research, customer support triage).
Plan-then-execute
The model first emits a full plan as structured output (a numbered list of steps, each with the tool it intends to call), then a separate execution phase runs through the plan, only replanning if a step fails. This is more predictable and easier to show to a user for approval before anything happens - and it's dramatically cheaper for multi-step workflows because you avoid re-reasoning from scratch at every single step.
| Pattern | Best for | Latency | Predictability | Typical cost per task |
|---|---|---|---|---|
| Reactive (step-by-step) | Debugging, open-ended research, support triage | Higher (one model call per step) | Lower - path can wander | Higher, scales with steps |
| Plan-then-execute | Data pipelines, approvals, known workflows | Lower (one planning call, then fast execution) | Higher - plan is inspectable | Lower, fixed per plan |
| Hybrid (plan + reactive fallback) | Production agents with occasional edge cases | Medium | High | Medium |
In practice, production agents I've shipped use the hybrid: plan first, execute deterministically, but let any individual step fall back to reactive tool-use if its output doesn't match the expected shape. For the concepts behind these patterns in more depth, see agentic workflow patterns and, if you're deciding whether you need more than one agent at all, multi-agent systems.
Memory: what an agent actually needs to remember
"Memory" gets used loosely. In a working TypeScript agent it splits into three distinct, differently-implemented layers:
- Conversation memory - the
messagesarray in the loop above. This is just context window management. Once it gets long, you either summarize older turns or drop them; see context windows explained for the actual token math. - Working memory / scratchpad - intermediate results the agent needs across steps but that shouldn't go back to the user (a fetched record, a partial calculation). Store this in a plain in-memory object keyed by a run ID, not in the LLM context, unless the model genuinely needs to re-reason over it.
- Long-term memory - facts that should persist across sessions (a user's preferences, past orders, prior decisions). This is where a vector store or a plain Postgres table with full-text search comes in, depending on whether you need semantic retrieval or exact lookups.
type AgentMemory = {
runId: string;
scratchpad: Record<string, unknown>;
turnsSummarized: number;
};
function trimHistory(messages: Anthropic.MessageParam[], keepLast = 10) {
if (messages.length <= keepLast) return messages;
const [system, ...rest] = messages;
return [system, ...rest.slice(-keepLast)];
}
Don't reach for a vector database on day one. Most agents I've built for SMB clients need long-term memory that's really just "the last 20 support tickets for this customer" - a SQL query, not embeddings. If you do need semantic recall, vector databases compared and embeddings explained cover the actual decision, and RAG for SMB covers the more common "answer from our docs" case that people mistake for needing an agent at all.
Tool design: the part everyone gets wrong first
The quality of your agent is bounded by the quality of your tool descriptions, not your prompt. A model can only call a tool correctly if it can guess, from the name, description, and schema alone, exactly what it does and what it needs.
Three rules that fixed more agent bugs for me than any prompt tweak:
- One tool, one job. A
manage_ordertool that takes anactionenum (get,cancel,refund) is a trap - the model conflates arguments across actions. Split it intoget_order,cancel_order,refund_order. - Return errors as data, not exceptions. If a tool throws, catch it inside
executeTooland return{ error: "order not found" }as the tool result. The model can react to a structured error; it cannot react to your process crashing. - Validate every argument with zod before executing. The model will occasionally send a string where you expect a number, or omit a required field despite the schema. Fail loud, into the tool_result, not into your database.
import { z } from "zod";
const GetOrderInput = z.object({ orderId: z.string().min(1) });
async function executeTool(name: string, input: unknown) {
try {
if (name === "get_order_status") {
const { orderId } = GetOrderInput.parse(input);
const order = await db.orders.findById(orderId);
if (!order) return { error: `no order found for id ${orderId}` };
return { status: order.status, eta: order.eta };
}
return { error: `unknown tool: ${name}` };
} catch (err) {
return { error: err instanceof Error ? err.message : "tool execution failed" };
}
}
For the deeper theory on this - parallel tool calls, tool choice forcing, streaming partial arguments - see LLM tool calling and structured outputs.
Error handling and guardrails
Three failure modes account for nearly all agent incidents in production:
Infinite or near-infinite loops
Covered above - hard step caps are non-negotiable. Add a wall-clock timeout too (60-90 seconds is reasonable for most business workflows), because a step cap doesn't protect you if each step is slow.
Destructive actions without confirmation
Never let an agent directly call a tool that deletes, refunds, or sends money without a human-in-the-loop check for anything above a threshold you define. A common pattern: tools tagged requiresApproval: true in your schema get executed only after the plan is shown to a human or a secondary "reviewer" LLM call approves it. See LLM guardrails and safety for a fuller checklist, and handling hallucinations for how to catch confidently-wrong tool arguments before they execute.
Cost blowouts from re-sent context
Every loop iteration resends the full message history unless you're using prompt caching. On Claude models, mark the system prompt and tool definitions with cache_control so you're only paying full price for the new turn each time - this alone cut agent costs by roughly 60-70% on a multi-step workflow I ran cost comparisons on. Full mechanics in prompt caching, and token cost control for the broader budgeting mindset even though that guide is framed around Claude Code specifically.
Tip: log every tool call, its input, its output, and the token count of that step. When an agent misbehaves in production, this log is the only way to reconstruct what it "saw" at each decision point.
Testing and evaluating your agent
You cannot unit-test an agent the way you unit-test a pure function, because the model's exact wording varies between runs. What you can and should do is build an eval suite: a set of representative inputs with pass/fail assertions on the outcome (did it call the right tool? did the final answer contain the right fact?), run against the real model on a schedule.
- Start with 15-20 real examples pulled from actual user queries, not hypotheticals.
- Assert on tool-call sequence and final structured output, not exact prose - the wording will drift, the facts shouldn't.
- Re-run the suite whenever you change a tool description, the system prompt, or the model version. A one-word change to a tool description regularly flips 2-3 eval cases in my experience.
This is its own discipline - see eval-driven AI development and evaluating LLM apps for the full setup, including how to wire this into CI.
Where a full agent framework earns its cost
Once your agent needs parallel sub-agents, durable execution across server restarts, or built-in observability dashboards, hand-rolling stops being the efficient choice. At that point, the Vercel AI SDK (good DX, first-class streaming to React) or the Claude Agent SDK (if you're committed to Claude and want built-in memory/context management) are worth the dependency. I cover the SDK specifically in building agents with the Claude Agent SDK, and if you're choosing a model provider for the agent's brain, Claude Opus 4 guide and GPT vs Claude for coding cover the trade-offs that actually affect agent reliability, not just benchmark scores.
FAQ
What's the difference between an AI agent and a chatbot with function calling?
A chatbot with function calling makes one tool call per user turn and hands control back to the user. An agent runs a loop autonomously across multiple tool calls to complete a task before returning control - the user gives one instruction and the agent decides how many steps it needs, without a human in between each one.
Do I need LangChain to build AI agents in TypeScript?
No. The raw tool-use loop is under 50 lines against any major provider's SDK. Reach for LangChain.js or the Vercel AI SDK once you need multi-provider abstraction, built-in streaming to a frontend, or a large library of pre-built integrations - not to write the loop itself.
How do I stop an agent from calling the same tool repeatedly?
Cap the loop at a fixed number of steps (8-12 is typical for business workflows), add a wall-clock timeout, and log every tool call so you can spot a cycling pattern. If a specific tool keeps getting called with the same failing input, return a more specific error message from that tool - vague errors are the most common cause of repeated identical calls.
What model should I use for building AI agents in TypeScript?
Claude Opus 4 and GPT-5-class models both handle multi-step tool use reliably as of 2026; the practical difference is usually cost and latency for your specific tool count, not raw capability. Test with your actual tool schemas rather than trusting a generic benchmark - agent reliability is driven heavily by how well your tool descriptions match the model's training, which varies by provider.
Can I build an agent without a framework in production, or is that reckless?
It's reasonable for a bounded number of tools (under ~10) and a single execution flow. Once you need durable state across server restarts, parallel sub-agents, or a team of non-experts maintaining it, a framework's guardrails start paying for themselves in fewer 2am incidents.
Get a working agent shipped, not just prototyped
If you've got the loop working locally and need it hardened, deployed, and wired into your actual business systems, that's exactly the kind of project I take on. Book a free 30-minute call through the contact form or message me directly on WhatsApp.