Most TypeScript AI agent tutorials skip the part that actually matters: what type is that tool's output? What does the agent return when it's done? "It returns a string" is not an answer a production TypeScript codebase accepts.
I've been building TypeScript AI agents for eight months. The shift that made them production-ready wasn't a better model or a more powerful framework. It was defining types before writing agent logic, the same pattern that works for TypeScript contracts in LLM prompting.
TL;DR:
- Mastra handles agent orchestration, tools, memory, multi-step reasoning
- Vercel AI SDK handles the generation layer, model calls, streaming, structured outputs
- TypeScript with Zod schemas catches tool argument errors and output mismatches at compile time
- The full stack installs in under 10 minutes; a typed agent runs in under 30
Why These Two SDKs Together
Mastra and Vercel AI SDK solve different problems. Mastra is the agent architecture layer: you define tools, register them with agents, and describe what the agent should do. It handles multi-step reasoning, the agent deciding which tool to call, in what order, with what arguments.
Vercel AI SDK is the model communication layer. It abstracts over OpenAI, Anthropic, Google Gemini, and a dozen other providers. Its generateText, generateObject, and streamText functions have consistent TypeScript signatures regardless of which model you're talking to.
Neither replaces the other. What you get by combining them: Mastra's opinionated agent patterns on top of Vercel AI SDK's polished provider integrations. Here's what installing them looks like:
npm install @mastra/core ai @ai-sdk/openai zod
Four packages. Mastra core, the Vercel AI SDK, the OpenAI provider, and Zod. That's the entire typed agent stack.
Typed Tools Are the Foundation
Tools are where TypeScript earns its place. A tool with any types isn't safer than a raw API call. A tool with Zod schemas is something the compiler can reason about.
import { createTool } from '@mastra/core';
import { z } from 'zod';
const searchFiles = createTool({
id: 'search-files',
description: 'Search TypeScript files for a pattern or function name',
inputSchema: z.object({
pattern: z.string().describe('Search pattern or function name'),
fileExtension: z.enum(['.ts', '.tsx']).optional().default('.ts'),
maxResults: z.number().int().positive().max(20).default(10),
}),
outputSchema: z.object({
matches: z.array(z.object({
file: z.string(),
line: z.number(),
snippet: z.string(),
})),
totalCount: z.number(),
}),
execute: async ({ context }) => {
// context is fully typed: { pattern: string, fileExtension: '.ts' | '.tsx', maxResults: number }
const results = await runSearch(context.pattern, context.fileExtension);
return { matches: results, totalCount: results.length };
},
});
The inputSchema tells the model exactly what arguments to pass. The outputSchema validates what your handler returns and types the result throughout the rest of your code. Wrong argument type? Compiler error before the agent runs. Handler returns the wrong shape? Compiler error at the return statement.
This is what separates TypeScript AI agents from Python AI agents in practice. Python agents fail at runtime with attribute errors. TypeScript agents fail at compile time with specific messages.
Structured Outputs With Zod Schemas
Some agents return free text. Most production agents return structured data. Vercel AI SDK's generateObject handles this with a Zod schema:
import { generateObject } from 'ai';
import { openai } from '@ai-sdk/openai';
const ReviewResult = z.object({
issues: z.array(z.object({
severity: z.enum(['error', 'warning', 'suggestion']),
file: z.string(),
description: z.string(),
suggestedFix: z.string().optional(),
})),
overallAssessment: z.enum(['approve', 'request-changes', 'needs-discussion']),
confidence: z.number().min(0).max(1),
});
type ReviewResult = z.infer<typeof ReviewResult>;
const { object } = await generateObject({
model: openai('gpt-4o'),
schema: ReviewResult,
prompt: `Review this TypeScript PR diff for issues:\n\n${diff}`,
});
// object is typed as ReviewResult, no type assertions, no casting
object.issues.forEach(issue => {
console.log(`[${issue.severity}] ${issue.file}: ${issue.description}`);
});
The schema does two things simultaneously. It generates the structured output prompt that tells the model what format to return. And it validates and types the response so your downstream code has full TypeScript inference. If the model returns something that doesn't match the schema, the SDK throws at the boundary, loud failure close to the source, not a silent type mismatch three function calls deep.
The Complete Agent Pattern
A Mastra agent combines tools and instructions into a unit that reasons across multiple steps:
import { Agent } from '@mastra/core';
const codeReviewAgent = new Agent({
name: 'code-reviewer',
instructions: `You review TypeScript code for correctness, type safety, and style.
Use the searchFiles tool to find relevant context before making judgments.
Return structured findings with severity levels and suggested fixes.`,
model: openai('gpt-4o'),
tools: { searchFiles },
});
const result = await codeReviewAgent.generate(
'Review the authentication module for type safety issues',
{ output: ReviewResult }
);
// result.object is ReviewResult
console.log(`Decision: ${result.object.overallAssessment}`);
console.log(`Issues: ${result.object.issues.length}`);
The agent can call searchFiles multiple times across reasoning steps, searching for the auth module, then for usage sites, then for related types, before returning the final ReviewResult. The TypeScript types flow through the entire chain.
Streaming Long-Running Agents
Some agent runs take 20-40 seconds on complex tasks. Don't make users stare at a spinner. Stream the reasoning:
const stream = await codeReviewAgent.stream(
'Review all API route handlers for input validation gaps'
);
for await (const chunk of stream.textStream) {
process.stdout.write(chunk); // or push to a UI
}
const finalResult = await stream.object; // typed as ReviewResult
stream.object carries the same type as generate's result. The Zod schema validates the final structured output when the stream completes. You get streaming UX without giving up type safety.
The Pattern Applied
The same principles that make TypeScript strict for AI apply to AI agents: define the shape first, let the tools enforce it. An agent with untyped tools and any return values gives you the same class of silent runtime failures that untyped AI code does.
Zod schemas as tool contracts, generateObject for structured outputs, TypeScript inference throughout, these aren't nice-to-haves. They're the difference between an agent that fails loudly at compile time and one that fails silently at 2am in production. The CodeRabbit + GitHub Actions setup catches any any that slips through into your agent code.
Once your agent is running, the next question is how to connect it to external databases, filesystems, and APIs in a standardized way. That's exactly what Model Context Protocol addresses, a JSON-RPC-based standard for wiring AI agents to developer tools without custom integrations for every service.
Define the types. Build the agent. Let the compiler verify the chain.
Related Tutorials
- TypeScript form validation
- SOLID in TypeScript
- TypeScript 7 migration
- TypeScript as AI Contracts: Guide LLM Code Generation