Inteligencia Artificial

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.

Guía técnica: arquitectando IA con Mastra, Next.js y TypeScript

"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.

TS
// 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

TS
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+.

TS
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.

TS
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:

  1. Agent-as-tool (supervisor estático): sub-agente envuelto en createTool(). Coordinación determinística, flujo previsible.

  2. agent.network() (ruteo dinámico): un Agent con agents, workflows y tools registrados; el LLM decide qué primitiva llamar. Requiere memory (persiste task history y detecta conclusión). Soporta suspensión con agent-execution-approval / tool-execution-approval.

  3. Workflows multi-agent: steps invocando mastra.getAgent(...).

Deprecación importante (2026): la clase AgentNetwork fue deprecada. Usa agent.network() o supervisor explícito.

TS
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:

TS
// (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.

TS
// 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 (mastra dev + @mastra/client-js)

Deploy

Único (vercel deploy)

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 (maxDuration)

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)

TYPESCRIPT
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_URL

Configuración obligatoria:

TS
// 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 serverExternalPackages funciona en dev pero falla en build. 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.

TS
// 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:

TSX
'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().

TS
// 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):

TS
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):

TS
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

TSX
'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:

TS
// 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):

BASH
npx mastra build --dir src/mastra    # gera .mastra/output/ (Hono bundle)node --import=./.mastra/output/instrumentation.mjs .mastra/output/index.mjs
DOCKERFILE
FROM 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):

TS
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

z.infer<typeof schema>

compile-time

Validación runtime

.parse() / .safeParse()

post-LLM

Especificación para el LLM

JSON Schema (vía zodSchema() del AI SDK)

pre-request

Documentación semántica

.describe() leído por el modelo

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):

TS
// ❌ 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):

TS
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 core

baseline

2,3× menor

Importante en edge

z.record()

1 arg

2 args obligatorios

Rompe migración

.optional().default()

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

.describe()/.meta()

cualquier posición

debe ser último chain call

No hereda via .optional()/.extend()

Mastra ≥ beta.16 normaliza ambas via Standard Schema; Zod coexiste via zod/v3 y zod/v4.

3.4 Tools tipadas con createTool

TS
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):

TS
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:

TS
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 tipado

Streaming de objeto parcial:

TS
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 validado

AI SDK puro (forma moderna con Output.object(), ya que generateObject está deprecated):

TS
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:

TS
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.

TS
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:

TS
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

safeParse en Server Action

Tool input

createTool({ inputSchema })

Tool output

createTool({ outputSchema })

⚠️ informativo

Structured output

generate({ structuredOutput })

Workflow step

createStep({ inputSchema, outputSchema })

Runtime context

RuntimeContext<T>

✅ en set; ⚠️ en get

⚠️ opcional

Memory

workingMemory: { schema }


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

Agent con tools; Mastra ejecuta loop automáticamente

Tareas abiertas, N de pasos desconocido

Explícito (workflow)

Código orchestrator

createWorkflow + .dountil() llamando step con agente

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.

TS
// 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:

TS
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).

TS
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

.then()

Pasos fijos, dependencia lineal (ETL, content pipeline)

Fan-out/Fan-in

.parallel([])

N fijo de tasks independientes; output es objeto con claves = step ids

MapReduce

.foreach(step, {concurrency})

N dinámico; procesar lista

Router/Branch

.branch([[cond, step]])

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

agent.network()

LLM decide primitiva a llamar

Evaluator-Optimizer

.dowhile() / .dountil() + scorer

Refinamiento iterativo convergente

Human-in-the-loop

suspend() / resume() / bail()

Aprobaciones, pago >$X, acciones irreversibles

Handoff

workflow + agents con memory compartida

Especialista asume control

Council

.parallel() + synthesis step

Múltiples opiniones para síntesis

Evaluator-Optimizer concreto:

TS
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:

TS
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):

TS
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

{ success: false, error: { code, message } } en union

Retry interno

Falla transitoria

p-retry dentro del execute

Circuit breaker

API inestable

opossum, abre tras N fallas

Timeout

Prevenir agente atascado

AbortSignal.timeout(ms)

TS
// 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):

TS
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:

TS
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.

TS
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:

TS
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:

  1. Empiece simple. Single agent + tools. Workflow/multi-agent sólo cuando los pasos son conocidos o el contexto se vuelve inviable.

  2. Workflows para auditabilidad/SLO; agent.network() para flexibilidad. Workflows = código dicta flujo. Networks = LLM dicta flujo.

  3. Zod everywhere. Todo tool inputSchema/outputSchema, todo step, todo scorer. Es su única defensa contra hallucination en tool args.

  4. Persistencia desde el día 1. Postgres (prod) o LibSQL (dev). Sin esto, suspend/resume no funciona y pierde traces en restart.

  5. serverExternalPackages: ['@mastra/*'] + runtime = 'nodejs'. No negociable en Next.js.

  6. Route Handlers para streaming; Server Actions para síncrono. No intente hacer streaming vía Server Action.

  7. Vercel Fluid Compute + Postgres/Turso remoto para producción serverless. Jamás LibSQL local.

  8. Observabilidad por ambiente vía configSelector: dev→Default, staging→10% Langfuse, prod→1% + Datadog.

  9. Scorers con sampling bajo en prod (5-20%), 100% en toxicity/safety.

  10. MCP antes de reimplementar tools. GitHub, Slack, Notion, filesystem, Playwright ya tienen servers oficiales.

  11. HITL vía suspend() siempre que costo/irreversibilidad > conveniencia. Pago >$X, deletion, envío en masa.

  12. Prefiera agent.network() o supervisor explícito sobre AgentNetwork class (deprecated).

  13. 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 error en outputSchema rompe narrowing del tool.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; createTool del Mastra no sufre de esto.

  • generateObject deprecated → migre para generateText({ output: Output.object(...) }).

  • Zod v4 crea schemas 17× más lento (JIT) — nunca instancie en render/loop caliente.

  • toDataStreamResponse() + output: zodSchema entra en conflicto (mastra-ai/mastra#5544) — usa experimental_output.

  • serverExternalPackages tiene issue en el build (vercel/next.js#74816) — ten fallback de Webpack.

  • AgentNetwork clase → deprecated; usa agent.network().

  • legacy_workflows → reemplazado por createWorkflow/createStep.

Stack de referencia para producción

TYPESCRIPT
┌──────────────────────────────────────────────────────────────┐│  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.