Guía técnica: arquitectando IA con Mastra, Next.js y TypeScript
Aprende a arquitectar IA con Mastra y Next.js y descubre cómo reemplazar el enfoque LangChain/LangGraph.js en entornos de producción.

"Escrito en abril/2026, referenciando
@mastra/core@1.25.0. Verifique el changelog oficial antes de implementar."
Mastra + Next.js + TypeScript forman hoy el stack más maduro y cohesivo para construir agentes de IA type-safe en JavaScript, reemplazando con ventaja el enfoque LangChain/LangGraph.js en ambientes de producción. Mastra 1.0 (GA desde ene/2026, versión actual @mastra/core@1.25.0, ~23,1k stars en GitHub) consolidó una arquitectura de registry central con inyección de dependencia, workflows durables con suspend/resume, memory-as-first-class, MCP nativo (cliente y servidor) e integración transparente con el Vercel AI SDK v5/v6. La combinación con Next.js App Router entrega streaming nativo, Server Actions type-safe y deploy tanto en Vercel serverless (con Fluid Compute, hasta 800s) como en VPS/Docker vía mastra build.
Esta guía consolida cuatro pilares — arquitectura del framework, integración con Next.js, type-safety con Zod, y patrones de diseño — en un blueprint accionable para arquitecto sénior. Todos los snippets son idiomáticos y funcionales contra Mastra 1.x, AI SDK v5 y Next.js 14/15.

1. Arquitectura y capacidades de Mastra
1.1 El objeto Mastra como registry central
Mastra es una registry con DI orquestando Agents, Workflows, Tools, Memory, Storage, Vector, Observability, MCP Servers y Gateways. El servidor HTTP es generado sobre Hono (con adapters para Express/Fastify/Koa a partir de v1.0). En producción, almacenamiento por dominios (memory/workflows/scores/traces) vía MastraCompositeStore es el estándar.
// src/mastra/index.tsimport { Mastra } from '@mastra/core';import { PinoLogger } from '@mastra/loggers';import { MastraCompositeStore } from '@mastra/core/storage';import { WorkflowsPG, ScoresPG, PgVector } from '@mastra/pg';import { MemoryLibSQL } from '@mastra/libsql';import { weatherAgent } from './agents/weather-agent';import { weatherWorkflow } from './workflows/weather-workflow';const storage = new MastraCompositeStore({ id: 'composite', domains: { memory: new MemoryLibSQL({ url: 'file:./local.db' }), workflows: new WorkflowsPG({ connectionString: process.env.DATABASE_URL! }), scores: new ScoresPG({ connectionString: process.env.DATABASE_URL! }), },});export const mastra = new Mastra({ agents: { weatherAgent }, workflows: { weatherWorkflow }, storage, vectors: { pg: new PgVector({ connectionString: process.env.DATABASE_URL! }) }, logger: new PinoLogger({ name: 'Mastra', level: 'info' }), server: { port: 4111, host: '0.0.0.0', timeout: 30_000 }, // mcpServers, observability, scorers, processors, gateways, bundler...});Paquetes principales: @mastra/core (Mastra, Agent, Workflow, Tool, Memory, Storage interfaces, Processors), @mastra/memory, @mastra/libsql, @mastra/pg, @mastra/mcp, @mastra/ai-sdk, @mastra/loggers, @mastra/observability, @mastra/client-js, mastra (CLI). A partir de v1, imports por subpath son obligatorios (@mastra/core/agent, @mastra/core/workflows, etc.), excepto Mastra y type Config.
Status actual (abr/2026): @mastra/core@1.25.0 GA, licencia Apache-2.0 (con áreas ee/ bajo Mastra Enterprise License), Mantenido por el equipo de Gatsby (Sam Bhagwat, Shane Thomas). Posicionado contra LangGraph.js; usa Vercel AI SDK para ruteo de modelos (40+ providers, 3000+ modelos vía Mastra Model Router).
1.2 Ciclo de vida del Agent
import { Agent } from '@mastra/core/agent';import { Memory } from '@mastra/memory';import { LibSQLStore } from '@mastra/libsql';import { weatherTool } from '../tools/weather';export const weatherAgent = new Agent({ id: 'weather-agent', name: 'Weather Agent', description: 'Responde sobre clima.', instructions: 'Você é um assistente de clima. Use weatherTool quando preciso.', model: 'openai/gpt-5.1', // Mastra Model Router — "provider/model" tools: { weatherTool }, memory: new Memory({ storage: new LibSQLStore({ url: 'file:./agent.db' }), options: { lastMessages: 10, workingMemory: { enabled: true } }, }),});// .generate() — resposta completa, retorna { text, toolCalls, toolResults, steps, usage }const res = await weatherAgent.generate('Clima em Tóquio?', { memory: { resource: 'user-123', thread: 'conv-42' },});// .stream() — token-a-token via MastraModelOutputconst stream = await weatherAgent.stream('Planeje meu dia');for await (const chunk of stream.textStream) process.stdout.write(chunk);1.3 Memoria: threads, resources, storage y vector
La clase Memory combina storage (historial persistente), vector (recuperación semántica) y embedder. Thread aísla conversaciones; Resource es un agrupador estable (usuario/proyecto) que permite que múltiples agentes compartan working memory y embeddings cruzando threads. El scope por defecto cambió a 'resource' en Mastra 0.10+.
import { Memory } from '@mastra/memory';import { PgStore, PgVector } from '@mastra/pg';import { OpenAIEmbedder } from '@mastra/openai';const memoryPg = new Memory({ storage: new PgStore({ connectionString: process.env.DATABASE_URL! }), vector: new PgVector({ connectionString: process.env.DATABASE_URL! }), embedder: new OpenAIEmbedder({ model: 'text-embedding-3-small' }), options: { lastMessages: 20, semanticRecall: { topK: 5, messageRange: { before: 2, after: 1 }, scope: 'resource', indexConfig: { type: 'hnsw', metric: 'dotproduct', m: 16, efConstruction: 64 }, }, workingMemory: { enabled: true, template: '# User\n- First Name:\n- Last Name:', scope: 'resource', }, generateTitle: true, },});Vector stores soportados: LibSQLVector, PgVector (HNSW/IVFFlat, bit, sparsevec), Pinecone, Upstash, Qdrant, Chroma, MongoDB, Astra, OpenSearch, S3Vectors, TurboPuffer, Lance, Cloudflare, Couchbase.
1.4 Sistema de Workflows
createWorkflow() / createStep() entregan ejecución durable: snapshots automáticos a cada suspend(), estado serializado en JSON en el storage, resume cross-process por el runId. Las tablas mastra_workflow_snapshot, mastra_traces, mastra_messages se crean automáticamente.
Primitivas de control de flujo: .then() (secuencial), .parallel([]) (fan-out/fan-in), .branch([[cond, step]]) (router), .foreach(step, {concurrency}) (MapReduce), .dountil()/.dowhile() (loops), .map() (transform). Retry configurable a nivel workflow y step.
import { createWorkflow, createStep } from '@mastra/core/workflows';import { z } from 'zod';const approvalStep = createStep({ id: 'approval', inputSchema: z.object({ amount: z.number(), needsApproval: z.boolean() }), outputSchema: z.object({ approved: z.boolean(), message: z.string() }), suspendSchema: z.object({ reason: z.string(), amount: z.number() }), resumeSchema: z.object({ approved: z.boolean(), approver: z.string() }), execute: async ({ inputData, resumeData, suspend, bail }) => { if (!inputData.needsApproval) return { approved: true, message: 'Auto' }; if (resumeData?.approved === false) { return bail({ approved: false, message: 'Rejected' }); } if (resumeData?.approved === undefined) { return await suspend({ reason: 'Human approval required', amount: inputData.amount }); } return { approved: true, message: `Approved by ${resumeData.approver}` }; },});export const paymentWorkflow = createWorkflow({ id: 'payment-workflow', inputSchema: z.object({ amount: z.number(), userId: z.string() }), outputSchema: z.object({ approved: z.boolean(), message: z.string() }), retryConfig: { attempts: 5, delay: 2000 },}) .then(analyzePurchase) .then(approvalStep) .then(executePayment) .commit();// Suspensão transparenteconst run = await paymentWorkflow.createRunAsync();const result = await run.start({ inputData: { amount: 5000, userId: 'u1' } });if (result.status === 'suspended') { // runId salvo em fila, notifique aprovador}// Retomada (mesmo ou outro processo, pelo runId)const resumed = await paymentWorkflow.createRunAsync({ runId });await resumed.resume({ resumeData: { approved: true, approver: 'mgr@acme' } });Discriminated union en el retorno de run.start() ('success' | 'failed' | 'suspended' | 'tripwire') garantiza narrowing tipado.
1.5 Orquestación multi-agente
Tres abordajes disponibles:
Agent-as-tool (supervisor estático): sub-agente envuelto en
createTool(). Coordinación determinística, flujo previsible.agent.network()(ruteo dinámico): un Agent conagents,workflowsytoolsregistrados; el LLM decide qué primitiva llamar. Requierememory(persiste task history y detecta conclusión). Soporta suspensión conagent-execution-approval/tool-execution-approval.Workflows multi-agent: steps invocando
mastra.getAgent(...).
Deprecación importante (2026): la clase
AgentNetworkfue deprecada. Usaagent.network()o supervisor explícito.
export const routingAgent = new Agent({ id: 'routing-agent', instructions: 'Rede de pesquisadores e escritores.', model: 'openai/gpt-5.4', agents: { researchAgent, writingAgent }, workflows: { cityWorkflow }, tools: { weatherTool }, memory: new Memory({ storage: new LibSQLStore({ url: 'file:./mastra.db' }) }),});const result = await routingAgent.network('Clima em Tóquio e atividade sugerida.');for await (const chunk of result) { if (chunk.type === 'network-execution-event-step-finish') console.log(chunk.payload.result);}1.6 LLMs via Vercel AI SDK
Mastra v1 delegó ruteo al Vercel AI SDK (v1/v2/v3 compatibles). Dos formas de especificar modelos:
// (a) String Model Router (recomendado)model: 'openai/gpt-5.4'model: 'anthropic/claude-4-5-sonnet'model: 'google/gemini-2.5-pro'// (b) Instância SDK direta (quando precisa de tipagem rigorosa)import { openai } from '@ai-sdk/openai';model: openai('gpt-4o')// (c) Fallbacks automáticos cross-providermodel: [ { model: 'openai/gpt-5', maxRetries: 3 }, { model: 'anthropic/claude-4-5-sonnet', maxRetries: 2 }, { model: 'google/gemini-2.5-pro', maxRetries: 2 },]// (d) Dinâmico por requestmodel: ({ requestContext }) => requestContext.task === 'complex' ? 'anthropic/claude-4-5-sonnet' : 'openai/gpt-5-mini'1.7 MCP (Model Context Protocol)
@mastra/mcp implementa cliente y servidor. Transports stdio, SSE y Streamable HTTP.
// Consumir MCPs externosimport { MCPClient } from '@mastra/mcp';export const mcp = new MCPClient({ id: 'main-mcp', servers: { filesystem: { command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem', '/tmp'] }, github: { url: new URL('https://api.githubcopilot.com/mcp/'), requestInit: { headers: { Authorization: `Bearer ${process.env.GH_PAT}` } } }, },});export const researchAgent = new Agent({ id: 'research', model: 'openai/gpt-4o', tools: await mcp.getTools(), // estático // ou dinâmico por request: const toolsets = await mcp.listToolsets();});// Expor Mastra como MCPimport { MCPServer } from '@mastra/mcp';const server = new MCPServer({ id: 'my-mcp-server', name: 'My MCP Server', version: '1.0.0', description: 'Expõe Mastra via MCP.', tools: { weatherTool }, agents: { weatherAgent }, // gera tool ask_weatherAgent workflows: { cityWorkflow }, // gera tool run_cityWorkflow});server.startStdio();2. Integración con Next.js App Router
2.1 Monorepo vs servicio separado
Criterio | Monorepo (Mastra embebido) | Servicio separado ( |
|---|---|---|
Deploy | Único ( | Dos dominios, CORS, auth cross-origin |
Latencia agente↔UI | Zero red interna | +1 salto HTTP |
Escala AI vs SSR | Acoplada | Independiente |
Workflows >5 min | Difícil ( | Natural (VM/container) |
Múltiples clientes (web + mobile) | Frontend-centric | Backend reutilizable |
Vercel Hobby | Viable con cautela | No recomendado |
MVP/prototipo | Recomendado | Overkill |
2.2 Estructura de directorios (monorepo)
my-nextjs-agent/├── src/│ ├── app/│ │ ├── api/chat/route.ts # Route Handler streaming│ │ ├── chat/page.tsx # UI client│ │ ├── actions/weather.ts # Server Actions│ │ └── layout.tsx│ ├── mastra/│ │ ├── index.ts # new Mastra({...})│ │ ├── agents/weather-agent.ts│ │ ├── tools/weather-tool.ts│ │ ├── workflows/│ │ └── memory.ts│ └── lib/schemas.ts # Zod compartilhado├── next.config.ts # serverExternalPackages: ['@mastra/*']└── .env.local # OPENAI_API_KEY, DATABASE_URLConfiguración obligatoria:
// next.config.tsimport type { NextConfig } from 'next';const nextConfig: NextConfig = { serverExternalPackages: ['@mastra/*'], // impede o bundler de empacotar binários nativos};export default nextConfig;Gotcha conocido (vercel/next.js#74816): en algunas versiones
serverExternalPackagesfunciona endevpero falla enbuild. Fallback via Webpack:config.externals.push('@mastra/core', '@mastra/libsql').
2.3 Server Actions invocando agentes
Ideal para operaciones síncronas no-streaming (form submit, generación única). Mantiene API keys en el servidor, integra con cache/revalidación de Next.
// src/app/actions/weather.ts'use server';import { z } from 'zod';import { mastra } from '@/mastra';import { revalidatePath } from 'next/cache';const WeatherInput = z.object({ city: z.string().min(1).max(100), units: z.enum(['metric', 'imperial']).default('metric'),});export type WeatherState = | { status: 'idle' } | { status: 'success'; text: string; toolCalls: unknown[] } | { status: 'error'; message: string; fieldErrors?: Record<string, string[]> };export async function getWeather(_prev: WeatherState, formData: FormData): Promise<WeatherState> { const parsed = WeatherInput.safeParse({ city: formData.get('city'), units: formData.get('units') ?? 'metric', }); if (!parsed.success) { return { status: 'error', message: 'Entrada inválida', fieldErrors: parsed.error.flatten().fieldErrors }; } try { const result = await mastra.getAgent('weatherAgent').generate( `Weather in ${parsed.data.city}? Units: ${parsed.data.units}.`, { memory: { thread: 'weather-thread', resource: 'public' } }, ); revalidatePath('/weather'); return { status: 'success', text: result.text, toolCalls: result.toolCalls ?? [] }; } catch (err) { console.error('[getWeather]', err); return { status: 'error', message: 'Falha ao consultar o agente.' }; }}Consumo en el client con useActionState:
'use client';import { useActionState } from 'react';import { getWeather, type WeatherState } from '@/app/actions/weather';export default function WeatherPage() { const [state, formAction, pending] = useActionState(getWeather, { status: 'idle' } as WeatherState); return ( <form action={formAction}> <input name="city" required /> <select name="units"><option value="metric">°C</option><option value="imperial">°F</option></select> <button disabled={pending}>{pending ? 'Consultando...' : 'Ver clima'}</button> {state.status === 'success' && <pre>{state.text}</pre>} {state.status === 'error' && <p>{state.message}</p>} </form> );}Limitaciones importantes: Server Actions no hacen streaming — el cliente espera respuesta completa. Sujetas a maxDuration de la plataforma. Para streaming, usa Route Handler + useChat.
2.4 Route Handlers con streaming
Patrón moderno Mastra 1.0: @mastra/ai-sdk + handleChatStream().
// src/app/api/chat/route.tsimport { handleChatStream } from '@mastra/ai-sdk';import { toAISdkV5Messages } from '@mastra/ai-sdk/ui';import { createUIMessageStreamResponse } from 'ai';import { NextResponse } from 'next/server';import { mastra } from '@/mastra';export const maxDuration = 60; // 300 default com Fluid; até 800 em Proexport const runtime = 'nodejs'; // OBRIGATÓRIO — Mastra não suporta Edgeexport async function POST(req: Request) { const params = await req.json(); const stream = await handleChatStream({ mastra, agentId: 'weatherAgent', params: { ...params, memory: { thread: params.threadId ?? 'default', resource: params.resourceId ?? 'anon' }, }, }); return createUIMessageStreamResponse({ stream });}// Hidrata histórico no mountexport async function GET() { const memory = await mastra.getAgentById('weatherAgent').getMemory(); const res = await memory?.recall({ threadId: 'default', resourceId: 'anon' }); return NextResponse.json(toAISdkV5Messages(res?.messages ?? []));}Alternativa low-level (control total):
export async function POST(req: Request) { const { messages } = await req.json(); const stream = await mastra.getAgent('weatherAgent').stream(messages, { format: 'aisdk', // AI SDK v5 compat memory: { thread: 'demo', resource: 'user-1' }, abortSignal: req.signal, // propaga cancelamento até o LLM }); return stream.toUIMessageStreamResponse(); // ou .toDataStreamResponse() (v4), .toTextStreamResponse()}Servicio separado (Next.js proxy para Mastra standalone en :4111):
import { MastraClient } from '@mastra/client-js';const client = new MastraClient({ baseUrl: process.env.MASTRA_API_URL ?? 'http://localhost:4111', retries: 3, backoffMs: 300,});export async function POST(req: Request) { const { messages } = await req.json(); const response = await client.getAgent('weatherAgent').stream({ messages, threadId: 'demo', resourceId: 'user-1', }); return new Response(response.body, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache, no-transform', 'X-Accel-Buffering': 'no' }, });}2.5 Chat UI con useChat y AI Elements
'use client';import { useEffect, useState } from 'react';import { useChat } from '@ai-sdk/react';import { DefaultChatTransport, type ToolUIPart } from 'ai';export default function ChatPage() { const [input, setInput] = useState(''); const { messages, setMessages, sendMessage, stop, status } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat', // Com Mastra Memory: envie APENAS a última mensagem + identifiers prepareSendMessagesRequest({ messages, body }) { return { body: { ...body, messages: [messages[messages.length - 1]], threadId: 'default-thread', resourceId: 'user-1', }}; }, }), }); useEffect(() => { fetch('/api/chat').then(r => r.json()).then(setMessages).catch(() => {}); }, [setMessages]); return ( <div> {messages.map(m => ( <div key={m.id}> {m.parts?.map((part, i) => { if (part.type === 'text') return <p key={i}>{part.text}</p>; if (part.type === 'reasoning') return <details key={i}><summary>Thinking</summary>{part.text}</details>; if (part.type?.startsWith('tool-')) { const p = part as ToolUIPart; // Estados: 'input-streaming' → 'input-available' → 'output-available' | 'output-error' switch (p.state) { case 'input-available': return <Skeleton key={i} />; case 'output-available': return <ToolCard key={i} output={p.output} />; case 'output-error': return <ErrorCard key={i} text={p.errorText} />; } } return null; })} </div> ))} <input value={input} onChange={e => setInput(e.target.value)} /> <button onClick={() => { sendMessage({ text: input }); setInput(''); }}>Send</button> {status === 'streaming' && <button onClick={stop}>⏹ Stop</button>} </div> );}2.6 Deploy: límites, storages y runtimes
Vercel maxDuration (abr/2026):
Plan | Default | Máx. con Fluid | Máx. sin Fluid |
|---|---|---|---|
Hobby | 300s | 300s | 60s |
Pro | 300s | 800s | 300s |
Enterprise | 300s | 900s | 900s |
Fluid Compute (enabled by default desde abr/2025) permite concurrencia en la misma instancia, active CPU pricing, y los streams continúan después de 300s si el primer byte sale en ~25s.
Storage crítico en serverless: LibSQLStore con file:./mastra.db NO funciona en FS efímero (Vercel/Lambda). Usa:
// Turso (LibSQL remoto)new LibSQLStore({ url: process.env.TURSO_URL!, authToken: process.env.TURSO_TOKEN })// Postgres (Neon, Supabase, Vercel Postgres)new PostgresStore({ connectionString: process.env.DATABASE_URL! })// Upstash Redisnew UpstashStore({ url: process.env.UPSTASH_URL!, token: process.env.UPSTASH_TOKEN! })Runtime: siempre export const runtime = 'nodejs' en rutas que importan Mastra. Edge runtime falla por dependencias nativas (libsql, better-sqlite3, bindings fs/crypto).
VPS/Docker (mastra build):
npx mastra build --dir src/mastra # gera .mastra/output/ (Hono bundle)node --import=./.mastra/output/instrumentation.mjs .mastra/output/index.mjsFROM node:22-alpine AS builderWORKDIR /appCOPY package*.json ./ && RUN npm ciCOPY src ./src && COPY tsconfig.json ./RUN npx mastra buildFROM node:22-alpine AS runnerWORKDIR /appRUN addgroup -g 1001 -S nodejs && adduser -S mastra -u 1001COPY --from=builder --chown=mastra:nodejs /app/.mastra/output ./.mastra/outputUSER mastraEXPOSE 4111HEALTHCHECK --interval=30s CMD wget -qO- http://localhost:4111/api/health || exit 1CMD ["node", "--import=./.mastra/output/instrumentation.mjs", ".mastra/output/index.mjs"]VercelDeployer publica Mastra standalone como función Vercel (sin Next al frente):
import { VercelDeployer } from '@mastra/deployer-vercel';export const mastra = new Mastra({ deployer: new VercelDeployer({ studio: true, maxDuration: 600, memory: 1536, regions: ['gru1', 'iad1'] }),});3. Type-safety end-to-end con Zod
3.1 Zod como contrato cuádruple
Un schema Zod cumple cuatro roles simultáneos:
Rol | Mecanismo | Momento |
|---|---|---|
Contrato estático |
| compile-time |
Validación runtime |
| post-LLM |
Especificación para el LLM | JSON Schema (vía | pre-request |
Documentación semántica |
| pre-request |
Regla crítica: .describe() impacta directamente la calidad de la salida estructurada — es "prompt engineering vía tipos". Siempre describa campos ambiguos.
3.2 Patrones idiomáticos para LLMs
Use .nullable() en vez de .optional() — OpenAI strict mode y GPT-5 rechazan optional() en structured output (mastra-ai/mastra#7234):
// ❌ Quebra em GPT-5 strict modeconst bad = z.object({ details: z.string().optional() });// ✅ Corretoconst good = z.object({ details: z.string().nullable().describe('null se ausente') });Discriminated unions son el patrón para acciones de agente (ReAct, tool-routing):
export const AgentActionSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal('search'), query: z.string() }), z.object({ type: z.literal('answer'), text: z.string(), confidence: z.number().min(0).max(1) }), z.object({ type: z.literal('escalate'), reason: z.string(), severity: z.enum(['low','medium','high']) }),]);3.3 Zod v3 vs v4 — impactos en pipelines de IA
Aspecto | v3 | v4 | Impacto |
|---|---|---|---|
Parse strings/arrays | baseline | 14×/7× más rápido (JIT) | Validación streaming casi gratis |
Compile TS | baseline | ~10× más rápido | Monorepos con muchos schemas |
Bundle | baseline | 2,3× menor | Importante en edge |
| 1 arg | 2 args obligatorios | Rompe migración |
| default ignorado si ausente | siempre retorna default | Cuidado en working memory |
Creación de schema | rápida | 17× más lenta (JIT) | No instancies en loops calientes |
| cualquier posición | debe ser último chain call | No hereda via |
Mastra ≥ beta.16 normaliza ambas via Standard Schema; Zod coexiste via zod/v3 y zod/v4.
3.4 Tools tipadas con createTool
import { createTool } from '@mastra/core/tools';import { z } from 'zod';export const githubRepoTool = createTool({ id: 'get-github-repo-info', description: 'Fetch basic insights for a public GitHub repository', inputSchema: z.object({ owner: z.string().describe('GitHub username or organization'), repo: z.string().describe('Repository name'), }), outputSchema: z.object({ stars: z.number(), forks: z.number(), issues: z.number(), license: z.string().nullable(), lastPush: z.string(), description: z.string().nullable(), }), execute: async ({ context, runtimeContext }) => { // ^ { owner: string; repo: string } inferido const res = await fetch(`https://api.github.com/repos/${context.owner}/${context.repo}`); if (res.status === 404) throw new Error(`Not found`); const d = await res.json(); return { stars: d.stargazers_count, forks: d.forks_count, issues: d.open_issues_count, license: d.license?.name ?? null, lastPush: d.pushed_at, description: d.description, }; // Incompatibilidade com outputSchema é erro em compile-time E runtime },});Retorno de tool.execute() es discriminated union que incluye camino de error — narrowing via if ('error' in result && result.error). Evita el nombre error como campo en el outputSchema (colide con discriminator).
RuntimeContext tipado (⚠️ bug conocido — .get() no infiere; usa cast):
export type SupportCtx = { 'user-tier': 'free'|'pro'|'enterprise'; language: 'en'|'pt-BR' };execute: async ({ runtimeContext }) => { const tier = runtimeContext.get('user-tier') as SupportCtx['user-tier']; const limit = tier === 'enterprise' ? 100 : tier === 'pro' ? 25 : 5; ...}3.5 Structured Outputs
API Mastra v1:
const result = await agent.generate('Who won 2012?', { structuredOutput: { schema: ElectionResultSchema, errorStrategy: 'fallback', // 'strict' | 'warn' | 'fallback' fallbackValue: { winner: 'Unknown', year: 0, party: 'Other' }, jsonPromptInjection: true, // obrigatório: Gemini 2.5 + tools },});result.object.winner; // string, totalmente tipadoStreaming de objeto parcial:
const stream = await agent.stream('...', { structuredOutput: { schema } });for await (const partial of stream.objectStream) { // DeepPartial<T> — campos chegando incrementalmente}const final = await stream.object; // T validadoAI SDK puro (forma moderna con Output.object(), ya que generateObject está deprecated):
import { generateText, Output, tool, stepCountIs } from 'ai';const { output } = await generateText({ model: 'openai/gpt-5.2', tools: { weather: tool({ inputSchema: z.object({ location: z.string() }), execute: async () => ({...}) }) }, output: Output.object({ schema: RecipeSchema }), stopWhen: stepCountIs(5), prompt: '...',});Error handling:
import { AI_NoObjectGeneratedError } from 'ai';try { const { object } = await generateObject({ ... }); return object; }catch (err) { if (AI_NoObjectGeneratedError.isInstance(err)) console.error('No object', err.text, err.cause); if (err instanceof z.ZodError) console.error('Zod failed', err.issues); throw err;}3.6 Workflows con steps tipados
.then(step) solo compila si step.inputSchema es compatible con el outputSchema del step anterior — el compiler sostiene la forma del pipeline.
const scrapeStep = createStep({ id: 'scrape', inputSchema: z.object({ url: z.string().url() }), outputSchema: z.object({ url: z.string().url(), markdown: z.string() }), execute: async ({ inputData }) => ({ url: inputData.url, markdown: await fetch(inputData.url).then(r=>r.text()) }),});const summarizeStep = createStep({ id: 'summarize', inputSchema: scrapeStep.outputSchema, outputSchema: z.object({ library: z.string(), latestVersion: z.string(), breakingChanges: z.array(z.string()) }), execute: async ({ inputData, mastra }) => { const res = await mastra.getAgent('summarizer').generate(inputData.markdown, { structuredOutput: { schema: z.object({ library: z.string(), latestVersion: z.string(), breakingChanges: z.array(z.string()) }) }, }); return res.object; },});export const changelogWorkflow = createWorkflow({ id: 'changelog', inputSchema: z.object({ url: z.string().url() }), outputSchema: summarizeStep.outputSchema,}).then(scrapeStep).then(summarizeStep).commit();Validación de runtime context vía requestContextSchema:
const workflow = createWorkflow({ id: 'tiered', inputSchema, outputSchema, requestContextSchema: z.object({ userTier: z.enum(['free','pro','enterprise']), locale: z.string() }),});3.7 Dónde vive la type-safety
Capa | Herramienta | Compile | Runtime | Enviado al LLM |
|---|---|---|---|---|
Entrada HTTP/Form |
| ✅ | ✅ | — |
Tool input |
| ✅ | ✅ | ✅ |
Tool output |
| ✅ | ⚠️ informativo | ✅ |
Structured output |
| ✅ | ✅ | ✅ |
Workflow step |
| ✅ | ✅ | — |
Runtime context |
| ✅ en | ⚠️ opcional | ❌ |
Memory |
| ✅ | ✅ | ✅ |
4. Patrones de diseño para agentes y workflows
4.1 ReAct: implícito vs explícito
Enfoque | Quién decide | Implementación | Cuándo usar |
|---|---|---|---|
Implícito (agent loop) | LLM, vía tool calling nativo |
| Tareas abiertas, N de pasos desconocido |
Explícito (workflow) | Código orchestrator |
| Auditabilidad, SLA, límites rígidos, HITL en el loop |
Recomendación: empiece siempre por el ReAct implícito. Solo migre a explícito cuando necesite trace granular, budget de pasos, aprobación humana en el medio o A/B testing por tipo de acción.
// Implícito — o agent loop já implementa ReActexport const researchAgent = new Agent({ instructions: `Follow ReAct loop: THOUGHT → ACTION (call one tool) → OBSERVATION. Repeat until confident. Never invent facts.`, model: 'openai/gpt-4o', tools: { searchDocsTool },});await researchAgent.generate('Qual a diferença entre suspend() e bail()?', { maxSteps: 8 });Schema ReAct reutilizable:
export const ReActStepSchema = z.object({ thought: z.string().describe('Reasoning about next step'), action: z.discriminatedUnion('type', [ z.object({ type: z.literal('tool_call'), toolName: z.string(), args: z.record(z.string(), z.unknown()) }), z.object({ type: z.literal('final_answer'), answer: z.string(), confidence: z.number().min(0).max(1) }), ]),});4.2 Plan-and-Execute
Separa Planner (LLM potente, ej.: Claude Sonnet/GPT-5) que produce plano upfront, de Executor (LLMs baratos especializados en tool use). ~30% menos tokens que ReAct en tareas multi-step complejas (benchmark LangChain 2026).
const plannerAgent = new Agent({ id: 'planner', instructions: 'Break goal into 3-7 concrete, tool-executable steps.', model: 'anthropic/claude-sonnet-4',});const executorAgent = new Agent({ id: 'executor', instructions: 'Execute a single plan step. Use tools. Return terse result.', model: 'openai/gpt-4o-mini', tools: { /* ... */ },});const planStep = createStep({ id: 'plan', inputSchema: z.object({ goal: z.string() }), outputSchema: z.object({ goal: z.string(), plan: z.array(z.object({ id: z.string(), description: z.string(), dependsOn: z.array(z.string()).default([]), })) }), execute: async ({ inputData, mastra }) => { const res = await mastra.getAgent('plannerAgent').generate(`Goal: ${inputData.goal}`, { output: z.object({ plan: z.array(/* ... */) }), }); return { goal: inputData.goal, plan: res.object.plan }; },});export const planAndExecuteWorkflow = createWorkflow({ id: 'plan-exec', inputSchema: z.object({ goal: z.string() }), outputSchema: z.object({ results: z.array(z.any()) }),}) .then(planStep) .map(async ({ inputData }) => inputData.plan) .foreach(executeStep, { concurrency: 3 }) .commit();4.3 Matriz de patrones de orquestación
Patrón | API Mastra | Cuándo usar |
|---|---|---|
Pipeline |
| Pasos fijos, dependencia lineal (ETL, content pipeline) |
Fan-out/Fan-in |
| N fijo de tasks independientes; output es objeto con claves = step ids |
MapReduce |
| N dinámico; procesar lista |
Router/Branch |
| Ruteo por clasificación; todos los branches comparten schemas |
Supervisor estático | Agent con sub-agents como tools | Coordinación determinística |
Supervisor dinámico |
| LLM decide primitiva a llamar |
Evaluator-Optimizer |
| Refinamiento iterativo convergente |
Human-in-the-loop |
| Aprobaciones, pago >$X, acciones irreversibles |
Handoff | workflow + agents con memory compartida | Especialista asume control |
Council |
| Múltiples opiniones para síntesis |
Evaluator-Optimizer concreto:
workflow .then(generateDraft) .dowhile( createStep({ id: 'eval-refine', execute: async ({ inputData, state }) => { const scorer = createAnswerRelevancyScorer({ model: 'openai/gpt-4o-mini' }); const { score } = await scorer.run({ input: state.prompt, output: inputData.draft }); if (score >= 0.85) return { ...inputData, done: true, score }; const refined = await refinerAgent.generate(/* with feedback */); return { ...inputData, draft: refined.text, done: false, score }; }, }), async ({ inputData }) => !inputData.done, ) .commit();4.4 Integración de herramientas externas
REST API:
export const githubIssueTool = createTool({ id: 'github-create-issue', inputSchema: z.object({ repo: z.string().regex(/^[\w-]+\/[\w-]+$/), title: z.string().min(1).max(256), body: z.string().max(65_536).optional(), labels: z.array(z.string()).max(100).default([]), }), outputSchema: z.object({ number: z.number(), url: z.string().url() }), execute: async ({ context, tracingContext }) => { const span = tracingContext?.currentSpan?.startSpan('github.api.call'); try { const res = await fetch(`https://api.github.com/repos/${context.repo}/issues`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ title: context.title, body: context.body, labels: context.labels }), signal: AbortSignal.timeout(10_000), }); if (!res.ok) throw new Error(`GitHub ${res.status}: ${await res.text()}`); const data = await res.json(); return { number: data.number, url: data.html_url }; } finally { span?.end(); } },});Database (Drizzle):
export const getUserTool = createTool({ id: 'get-user', inputSchema: z.object({ userId: z.string().uuid() }), outputSchema: z.object({ id: z.string(), email: z.string(), plan: z.enum(['free','pro','enterprise']) }).nullable(), execute: async ({ context }) => { const db = drizzle(process.env.DATABASE_URL!); const [row] = await db.select().from(users).where(eq(users.id, context.userId)).limit(1); return row ?? null; },});Error handling en tools:
Estrategia | Cuándo | Ejemplo |
|---|---|---|
Throw | Error irrecuperable | Auth failure, timeout tras retries |
Return estructurado | LLM debe reaccionar/reintentar |
|
Retry interno | Falla transitoria |
|
Circuit breaker | API inestable |
|
Timeout | Prevenir agente atascado |
|
// Padrão "return estruturado" — melhor para o loop ReActoutputSchema: z.union([ z.object({ success: z.literal(true), data: z.object({ /* ... */ }) }), z.object({ success: z.literal(false), error: z.object({ code: z.string(), message: z.string() }) }),]),4.5 Observabilidad
Logging estructurado (PinoLogger):
import { PinoLogger } from '@mastra/loggers';export const mastra = new Mastra({ logger: new PinoLogger({ name: 'Mastra', level: process.env.LOG_LEVEL ?? 'info', mixin() { return { traceId: getCurrentTraceId(), service: 'ai-api', env: process.env.NODE_ENV }; }, }),});AI Tracing nativo con OTel + múltiples exporters:
import { DefaultExporter } from '@mastra/observability';import { LangfuseExporter } from '@mastra/langfuse';export const mastra = new Mastra({ observability: { default: { enabled: true }, configs: { langfuse: { serviceName: 'prod-agents', exporters: [new LangfuseExporter({ publicKey: process.env.LANGFUSE_PUBLIC_KEY!, secretKey: process.env.LANGFUSE_SECRET_KEY!, baseUrl: process.env.LANGFUSE_BASE_URL, })], sampling: { type: 'ratio', probability: 0.1 }, // 10% em prod }, debug: { exporters: [new DefaultExporter()], sampling: { type: 'always' } }, }, configSelector: (ctx) => ctx.runtimeContext?.get('supportMode') ? 'debug' : 'langfuse', },});Comparativo de plataformas:
Plataforma | Fuerte en | Débil en | Cuándo elegir |
|---|---|---|---|
Langfuse | LLM-native (prompts, cost, evals). Self-host. | Tracing infra genérico | Prompt engineering, costo por feature, evals |
Braintrust | Evals en producción, A/B side-by-side | Tracing menos rico | Teams con énfasis en regression testing |
LangSmith | LangChain integration, datasets | Vendor lock-in | Stack ya LangChain/LangGraph |
SigNoz/Datadog (OTel) | APM full-stack | No es LLM-first | APM unificado (no solo IA) |
Mastra Studio + DuckDB | Built-in, zero setup, costo/latencia | Local/single-node | Dev local, teams pequeñas |
Evals / Scorers (Mastra 2026 — reemplaza evals legacy):
Scorers corren asíncronos después de la respuesta, con pipeline preprocess → analyze → generateScore → generateReason. Built-in: answer-relevancy, answer-similarity, faithfulness, hallucination, completeness, tool-call-accuracy, trajectory-accuracy, bias, toxicity, prompt-alignment.
import { createAnswerRelevancyScorer, createToxicityScorer, createHallucinationScorer } from '@mastra/evals/scorers/llm';export const supportAgent = new Agent({ id: 'support', model: 'openai/gpt-4o', scorers: { relevancy: { scorer: createAnswerRelevancyScorer({ model: 'openai/gpt-4o-mini' }), sampling: { type: 'ratio', rate: 0.2 } }, hallucination: { scorer: createHallucinationScorer({ model: 'openai/gpt-4o-mini' }), sampling: { type: 'ratio', rate: 1.0 } }, toxicity: { scorer: createToxicityScorer({ model: 'openai/gpt-4o-mini' }), sampling: { type: 'ratio', rate: 1.0 } }, },});CI/CD con Vitest:
import { runEvals } from '@mastra/core/evals';describe('Support Agent', () => { it('meets quality thresholds', async () => { const result = await runEvals({ target: supportAgent, data: [{ input: 'How to cancel?', groundTruth: 'cancellation policy' }], scorers: [relevancyScorer, hasSourcesScorer], concurrency: 3, }); expect(result.scores['answer-relevancy']).toBeGreaterThanOrEqual(0.8); });});5. Blueprint arquitectónico consolidado
TL;DR para arquitectos:
Empiece simple. Single agent + tools. Workflow/multi-agent sólo cuando los pasos son conocidos o el contexto se vuelve inviable.
Workflows para auditabilidad/SLO;
agent.network()para flexibilidad. Workflows = código dicta flujo. Networks = LLM dicta flujo.Zod everywhere. Todo tool
inputSchema/outputSchema, todo step, todo scorer. Es su única defensa contra hallucination en tool args.Persistencia desde el día 1. Postgres (prod) o LibSQL (dev). Sin esto,
suspend/resumeno funciona y pierde traces en restart.serverExternalPackages: ['@mastra/*']+runtime = 'nodejs'. No negociable en Next.js.Route Handlers para streaming; Server Actions para síncrono. No intente hacer streaming vía Server Action.
Vercel Fluid Compute + Postgres/Turso remoto para producción serverless. Jamás LibSQL local.
Observabilidad por ambiente vía
configSelector: dev→Default, staging→10% Langfuse, prod→1% + Datadog.Scorers con sampling bajo en prod (5-20%), 100% en toxicity/safety.
MCP antes de reimplementar tools. GitHub, Slack, Notion, filesystem, Playwright ya tienen servers oficiales.
HITL vía
suspend()siempre que costo/irreversibilidad > conveniencia. Pago >$X, deletion, envío en masa.Prefiera
agent.network()o supervisor explícito sobreAgentNetworkclass (deprecated).Plan-and-Execute > ReAct para tareas >5 pasos. Planner potente + executors baratos ahorra 20-30% de tokens.
Trampas críticas conocidas
optional()rompe strict mode OpenAI/GPT-5 → use.nullable()con.describe()(mastra-ai/mastra#7234).Gemini 2.5 + tools + structured output → siempre
jsonPromptInjection: true.z.record()en Zod v4 necesita 2 args obligatorios.Campo llamado
errorenoutputSchemarompe narrowing deltool.execute().RuntimeContext.get()no infiere — cast manual necesario..describe()/.meta()debe ser última chain call (no hereda vía.optional()/.extend()).tool()helper del AI SDK es obligatorio para inferencia;createTooldel Mastra no sufre de esto.generateObjectdeprecated → migre paragenerateText({ output: Output.object(...) }).Zod v4 crea schemas 17× más lento (JIT) — nunca instancie en render/loop caliente.
toDataStreamResponse()+output: zodSchemaentra en conflicto (mastra-ai/mastra#5544) — usaexperimental_output.serverExternalPackagestiene issue en el build (vercel/next.js#74816) — ten fallback de Webpack.AgentNetworkclase → deprecated; usaagent.network().legacy_workflows→ reemplazado porcreateWorkflow/createStep.
Stack de referencia para producción
┌──────────────────────────────────────────────────────────────┐│ Next.js App Router (Node runtime) ││ ├─ Route Handlers (streaming, useChat) ││ └─ Server Actions (síncrono, forms) │├──────────────────────────────────────────────────────────────┤│ Mastra (embedded ou standalone) ││ ├─ Agents (ReAct implícito) + agent.network() ││ ├─ Workflows (.then/.parallel/.branch/.foreach/.dountil) ││ ├─ Tools (createTool + Zod) + MCP (client + server) ││ └─ Memory (threads/resources, semanticRecall, workingMemory)│├──────────────────────────────────────────────────────────────┤│ Storage: MastraCompositeStore ││ ├─ memory: LibSQL (dev) / Postgres (prod) ││ ├─ workflows: Postgres (snapshots persistentes) ││ ├─ scores: Postgres ││ └─ vectors: pgvector / Pinecone / Upstash │├──────────────────────────────────────────────────────────────┤│ LLM: Vercel AI SDK v5/v6 ││ ├─ openai/gpt-5.x, anthropic/claude-4-5-sonnet, ││ │ google/gemini-2.5-pro ││ └─ Fallbacks automáticos cross-provider │├──────────────────────────────────────────────────────────────┤│ Observabilidade ││ ├─ PinoLogger (structured) ││ ├─ OTel tracing → Langfuse/Braintrust/SigNoz ││ └─ Scorers async (relevancy, hallucination, toxicity) │├──────────────────────────────────────────────────────────────┤│ Deploy ││ ├─ Vercel (Fluid Compute, maxDuration: 800s Pro) ││ ├─ VPS (mastra build → node + PM2) ││ └─ Docker (multi-stage, Alpine, healthcheck) │└──────────────────────────────────────────────────────────────┘Conclusión
El ecosistema Mastra 1.x + Next.js 15 + AI SDK v5/v6 es hoy, en abril de 2026, la aproximación más cohesionada y type-safe para construir agentes de IA en TypeScript — superando a LangChain/LangGraph.js en ergonomía, DX e integración nativa con el runtime JavaScript. Las tres decisiones arquitectónicas que más impactan escala y mantenibilidad son: (1) elegir entre Mastra embebido en Next.js (MVP, frontend único) vs. standalone (escala independiente, múltiples clientes), (2) migrar de LibSQL local a Postgres remoto en el día cero en serverless (sin esto suspend/resume y traces son ilusiones), y (3) invertir en Zod como contrato cuádruple (compile-time, runtime, prompt al LLM, documentación semántica) desde la primera tool.
El insight contra-intuitivo aquí es que el mayor ganancia de calidad no viene del modelo más potente, sino de la granularidad de los schemas Zod: .describe() bien escritos en campos de outputSchema son prompt engineering disfrazado, y .nullable() en vez de .optional() elimina clases enteras de fallas en strict mode de OpenAI. Combinado con workflows durables (suspend/resume/bail), agent.network() para routing dinámico, MCP para interop sin reimplementación, y scorers continuos con sampling estratificado, el stack entrega agentes auditables, resilientes y observables — requisitos no-negociables en producción.
El roadmap inmediato del framework (post-1.25) se concentra en AI SDK v3 (ToolLoopAgent nativo), consolidación de MastraCompositeStore, expansión de providers en el Model Router y maduración de Agent Networks como reemplazo definitivo de la clase deprecated. Para arquitectos decidiendo hoy: adoptar Mastra 1.x es seguro para producción, con la salvedad de monitorear deprecaciones semanales en el changelog oficial (ritmo alto de evolución) y mantener snippets canónicos referenciados contra node_modules/@mastra/*/dist/docs/ o https://mastra.ai/llms.txt en vez de blog posts antiguados.


