Guia técnico: arquitetando IA com Mastra, Next.js e TypeScript
Aprenda a arquitetar IA com Mastra e Next.js e descubra como substituir a abordagem LangChain/LangGraph.js em ambientes de produção.

"Escrito em abril/2026, referenciando
@mastra/core@1.25.0. Verifique o changelog oficial antes de implementar."
Mastra + Next.js + TypeScript formam hoje o stack mais maduro e coeso para construir agentes de IA type-safe em JavaScript, substituindo com vantagem a abordagem LangChain/LangGraph.js em ambientes de produção. Mastra 1.0 (GA desde jan/2026, versão atual @mastra/core@1.25.0, ~23,1k stars no GitHub) consolidou uma arquitetura de registry central com injeção de dependência, workflows durables com suspend/resume, memory-as-first-class, MCP nativo (cliente e servidor) e integração transparente com o Vercel AI SDK v5/v6. A combinação com Next.js App Router entrega streaming nativo, Server Actions type-safe e deploy tanto em Vercel serverless (com Fluid Compute, até 800s) quanto em VPS/Docker via mastra build.
Este guia consolida quatro pilares — arquitetura do framework, integração com Next.js, type-safety com Zod, e padrões de design — em um blueprint acionável para arquiteto sênior. Todos os snippets são idiomáticos e funcionais contra Mastra 1.x, AI SDK v5 e Next.js 14/15.

1. Arquitetura e capacidades do Mastra
1.1 O objeto Mastra como registry central
Mastra é uma registry com DI orquestrando Agents, Workflows, Tools, Memory, Storage, Vector, Observability, MCP Servers e Gateways. O servidor HTTP é gerado sobre Hono (com adapters para Express/Fastify/Koa a partir de v1.0). Em produção, armazenamento por domínios (memory/workflows/scores/traces) via MastraCompositeStore é o padrão.
// 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...});Pacotes principais: @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 são obrigatórios (@mastra/core/agent, @mastra/core/workflows, etc.), exceto Mastra e type Config.
Status atual (abr/2026): @mastra/core@1.25.0 GA, licença Apache-2.0 (com áreas ee/ sob Mastra Enterprise License), Mantido pela equipe do Gatsby (Sam Bhagwat, Shane Thomas). Posicionado contra LangGraph.js; usa Vercel AI SDK para roteamento de modelos (40+ providers, 3000+ modelos via Mastra Model Router).
1.2 Ciclo de vida do 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 Memória: threads, resources, storage e vector
A classe Memory combina storage (histórico persistente), vector (semantic recall) e embedder. Thread isola conversas; Resource é um agrupador estável (usuário/projeto) permitindo que múltiplos agentes compartilhem working memory e embeddings cruzando threads. Default scope mudou para 'resource' em 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 suportados: 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() entregam execução durable: snapshots automáticos a cada suspend(), estado serializado em JSON no storage, resume cross-process pelo runId. Tabelas mastra_workflow_snapshot, mastra_traces, mastra_messages são criadas automaticamente.
Primitivas de controle de fluxo: .then() (sequencial), .parallel([]) (fan-out/fan-in), .branch([[cond, step]]) (router), .foreach(step, {concurrency}) (MapReduce), .dountil()/.dowhile() (loops), .map() (transform). Retry configurável em nível workflow e 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 no retorno de run.start() ('success' | 'failed' | 'suspended' | 'tripwire') garante narrowing tipado.
1.5 Orquestração multi-agente
Três abordagens disponíveis:
Agent-as-tool (supervisor estático): sub-agente envolvido em
createTool(). Coordenação determinística, fluxo previsível.agent.network()(roteamento dinâmico): um Agent comagents,workflowsetoolsregistrados; o LLM decide qual primitiva chamar. Requermemory(persiste task history e detecta conclusão). Suporta suspensão comagent-execution-approval/tool-execution-approval.Workflows multi-agent: steps invocando
mastra.getAgent(...).
Deprecação importante (2026): a classe
AgentNetworkfoi deprecada. Useagent.network()ou 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 delegou roteamento ao Vercel AI SDK (v1/v2/v3 compatíveis). Duas 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 e servidor. Transports stdio, SSE e 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. Integração com Next.js App Router
2.1 Monorepo vs serviço separado
Critério | Monorepo (Mastra embutido) | Serviço separado ( |
|---|---|---|
Deploy | Único ( | Dois domínios, CORS, auth cross-origin |
Latência agente↔UI | Zero rede interna | +1 hop HTTP |
Escala AI vs SSR | Acoplada | Independente |
Workflows >5 min | Difícil ( | Natural (VM/container) |
Múltiplos clientes (web + mobile) | Frontend-centric | Backend reutilizável |
Vercel Hobby | Viável com cautela | Não recomendado |
MVP/protótipo | Recomendado | Overkill |
2.2 Estrutura de diretórios (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_URLConfiguração obrigatória:
// next.config.tsimport type { NextConfig } from 'next';const nextConfig: NextConfig = { serverExternalPackages: ['@mastra/*'], // impede o bundler de empacotar binários nativos};export default nextConfig;Gotcha conhecido (vercel/next.js#74816): em algumas versões
serverExternalPackagesfunciona emdevmas falha nobuild. Fallback via Webpack:config.externals.push('@mastra/core', '@mastra/libsql').
2.3 Server Actions invocando agentes
Ideal para operações síncronas não-streaming (form submit, geração única). Mantém API keys no servidor, integra com cache/revalidação do 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 no client com 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> );}Limitações importantes: Server Actions não fazem streaming — o cliente aguarda resposta completa. Sujeitas a maxDuration da plataforma. Para streaming, use Route Handler + useChat.
2.4 Route Handlers com streaming
Padrão 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 ?? []));}Low-level alternativo (controle 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()}Serviço separado (Next.js proxy para Mastra standalone em :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 com useChat e 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: limites, storages e runtimes
Vercel maxDuration (abr/2026):
Plano | Default | Máx. com Fluid | Máx. sem Fluid |
|---|---|---|---|
Hobby | 300s | 300s | 60s |
Pro | 300s | 800s | 300s |
Enterprise | 300s | 900s | 900s |
Fluid Compute (enabled by default desde abr/2025) permite concorrência na mesma instância, active CPU pricing, e streams continuam após 300s se o primeiro byte sair em ~25s.
Storage crítico em serverless: LibSQLStore com file:./mastra.db NÃO funciona em FS efêmero (Vercel/Lambda). Use:
// 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: sempre export const runtime = 'nodejs' em rotas que importam Mastra. Edge runtime falha por dependências 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 função Vercel (sem Next na 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 com Zod
3.1 Zod como contrato quádruplo
Um schema Zod cumpre quatro papéis simultâneos:
Papel | Mecanismo | Momento |
|---|---|---|
Contrato estático |
| compile-time |
Validação runtime |
| pós-LLM |
Especificação para o LLM | JSON Schema (via | pré-request |
Documentação semântica |
| pré-request |
Regra crítica: .describe() impacta diretamente a qualidade da saída estruturada — é "prompt engineering via tipos". Sempre descreva campos ambíguos.
3.2 Padrões idiomáticos para LLMs
Use .nullable() em vez de .optional() — OpenAI strict mode e GPT-5 rejeitam optional() em 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 são o padrão para ações 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 em pipelines de IA
Aspecto | v3 | v4 | Impacto |
|---|---|---|---|
Parse strings/arrays | baseline | 14×/7× mais rápido (JIT) | Validação streaming quase grátis |
Compile TS | baseline | ~10× mais rápido | Monorepos com muitos schemas |
Bundle | baseline | 2,3× menor | Importante em edge |
| 1 arg | 2 args obrigatórios | Quebra migração |
| default ignorado se ausente | sempre retorna default | Cuidado em working memory |
Criação de schema | rápida | 17× mais lenta (JIT) | Não instancie em loops quentes |
| qualquer posição | deve ser último chain call | Não herda via |
Mastra ≥ beta.16 normaliza ambas via Standard Schema; Zod coexiste via zod/v3 e zod/v4.
3.4 Tools tipadas com 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() é discriminated union que inclui caminho de erro — narrowing via if ('error' in result && result.error). Evite o nome error como campo no outputSchema (colide com discriminator).
RuntimeContext tipado (⚠️ bug conhecido — .get() não infere; use 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 com Output.object(), já 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 com steps tipados
.then(step) só compila se step.inputSchema for compatível com o outputSchema do step anterior — o compiler segura a forma do 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();Validação de runtime context via requestContextSchema:
const workflow = createWorkflow({ id: 'tiered', inputSchema, outputSchema, requestContextSchema: z.object({ userTier: z.enum(['free','pro','enterprise']), locale: z.string() }),});3.7 Onde mora a type-safety
Camada | Ferramenta | Compile | Runtime | Enviado ao LLM |
|---|---|---|---|---|
Entrada HTTP/Form |
| ✅ | ✅ | — |
Tool input |
| ✅ | ✅ | ✅ |
Tool output |
| ✅ | ⚠️ informativo | ✅ |
Structured output |
| ✅ | ✅ | ✅ |
Workflow step |
| ✅ | ✅ | — |
Runtime context |
| ✅ no | ⚠️ opcional | ❌ |
Memory |
| ✅ | ✅ | ✅ |
4. Padrões de design para agentes e workflows
4.1 ReAct: implícito vs explícito
Abordagem | Quem decide | Implementação | Quando usar |
|---|---|---|---|
Implícito (agent loop) | LLM, via tool calling nativo |
| Tarefas abertas, N de passos desconhecido |
Explícito (workflow) | Código orchestrator |
| Auditabilidade, SLA, limites rígidos, HITL no loop |
Recomendação: comece sempre pelo ReAct implícito. Só migre para explícito quando precisar de trace granular, budget de passos, aprovação humana no meio ou A/B testing por tipo de ação.
// 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 reutilizável:
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, ex.: Claude Sonnet/GPT-5) que produz plano upfront, de Executor (LLMs baratos especializados em tool use). ~30% menos tokens que ReAct em tarefas multi-step complexas (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 padrões de orquestração
Padrão | API Mastra | Quando usar |
|---|---|---|
Pipeline |
| Passos fixos, dependência linear (ETL, content pipeline) |
Fan-out/Fan-in |
| N fixo de tasks independentes; output é objeto com chaves = step ids |
MapReduce |
| N dinâmico; processar lista |
Router/Branch |
| Roteamento por classificação; todos branches compartilham schemas |
Supervisor estático | Agent com sub-agents como tools | Coordenação determinística |
Supervisor dinâmico |
| LLM decide primitiva a chamar |
Evaluator-Optimizer |
| Refinamento iterativo convergente |
Human-in-the-loop |
| Aprovações, pagamento >$X, ações irreversíveis |
Handoff | workflow + agents com memory compartilhada | Especialista assume controle |
Council |
| Múltiplas opiniões para síntese |
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 Integração de ferramentas 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 em tools:
Estratégia | Quando | Exemplo |
|---|---|---|
Throw | Erro irrecuperável | Auth failure, timeout após retries |
Return estruturado | LLM deve reagir/re-tentar |
|
Retry interno | Falha transiente |
|
Circuit breaker | API instável |
|
Timeout | Prevenir agente preso |
|
// 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 Observabilidade
Logging estruturado (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 com OTel + múltiplos 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 | Forte em | Fraco em | Quando escolher |
|---|---|---|---|
Langfuse | LLM-native (prompts, cost, evals). Self-host. | Tracing infra genérico | Prompt engineering, custo por feature, evals |
Braintrust | Evals em produção, A/B side-by-side | Tracing menos rico | Teams com ênfase em regression testing |
LangSmith | LangChain integration, datasets | Vendor lock-in | Stack já LangChain/LangGraph |
SigNoz/Datadog (OTel) | APM full-stack | Não é LLM-first | APM unificado (não só IA) |
Mastra Studio + DuckDB | Built-in, zero setup, custo/latência | Local/single-node | Dev local, teams pequenos |
Evals / Scorers (Mastra 2026 — substitui evals legacy):
Scorers rodam assíncronos após resposta, com 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 com 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 arquitetural consolidado
TL;DR para arquitetos:
Comece simples. Single agent + tools. Workflow/multi-agent só quando passos são conhecidos ou contexto fica inviável.
Workflows para auditabilidade/SLO;
agent.network()para flexibilidade. Workflows = código dita fluxo. Networks = LLM dita fluxo.Zod everywhere. Todo tool
inputSchema/outputSchema, todo step, todo scorer. É sua única defesa contra hallucination em tool args.Persistência desde o dia 1. Postgres (prod) ou LibSQL (dev). Sem isso,
suspend/resumenão funciona e você perde traces em restart.serverExternalPackages: ['@mastra/*']+runtime = 'nodejs'. Não negociável em Next.js.Route Handlers para streaming; Server Actions para síncrono. Não tente fazer streaming via Server Action.
Vercel Fluid Compute + Postgres/Turso remoto para produção serverless. Jamais LibSQL local.
Observabilidade por ambiente via
configSelector: dev→Default, staging→10% Langfuse, prod→1% + Datadog.Scorers com sampling baixo em prod (5-20%), 100% em toxicity/safety.
MCP antes de reimplementar tools. GitHub, Slack, Notion, filesystem, Playwright já têm servers oficiais.
HITL via
suspend()sempre que custo/irreversibilidade > conveniência. Pagamento >$X, deletion, envio em massa.Prefira
agent.network()ou supervisor explícito sobreAgentNetworkclass (deprecated).Plan-and-Execute > ReAct para tarefas >5 passos. Planner potente + executors baratos economiza 20-30% de tokens.
Armadilhas críticas conhecidas
optional()quebra strict mode OpenAI/GPT-5 → use.nullable()com.describe()(mastra-ai/mastra#7234).Gemini 2.5 + tools + structured output → sempre
jsonPromptInjection: true.z.record()em Zod v4 precisa de 2 args obrigatórios.Campo chamado
errornooutputSchemaquebra narrowing dotool.execute().RuntimeContext.get()não infere — cast manual necessário..describe()/.meta()deve ser última chain call (não herda via.optional()/.extend()).tool()helper do AI SDK é obrigatório para inferência;createTooldo Mastra não sofre disso.generateObjectdeprecated → migre paragenerateText({ output: Output.object(...) }).Zod v4 cria schemas 17× mais lento (JIT) — nunca instancie em render/loop quente.
toDataStreamResponse()+output: zodSchemaconflita (mastra-ai/mastra#5544) — useexperimental_output.serverExternalPackagestem issue no build (vercel/next.js#74816) — tenha fallback Webpack.AgentNetworkclasse → deprecated; useagent.network().legacy_workflows→ substituído porcreateWorkflow/createStep.
Stack de referência para produção
┌──────────────────────────────────────────────────────────────┐│ 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) │└──────────────────────────────────────────────────────────────┘Conclusão
O ecossistema Mastra 1.x + Next.js 15 + AI SDK v5/v6 é hoje, em abril de 2026, a abordagem mais coesa e type-safe para construir agentes de IA em TypeScript — superando LangChain/LangGraph.js em ergonomia, DX e integração nativa com o runtime JavaScript. As três decisões arquiteturais que mais impactam escala e manutenibilidade são: (1) escolher entre Mastra embutido no Next.js (MVP, frontend único) vs. standalone (escala independente, múltiplos clientes), (2) migrar de LibSQL local para Postgres remoto no dia zero em serverless (sem isso suspend/resume e traces são ilusões), e (3) investir em Zod como contrato quádruplo (compile-time, runtime, prompt ao LLM, documentação semântica) desde a primeira tool.
A insight contra-intuitivo aqui é que o maior ganho de qualidade não vem do modelo mais potente, mas da granularidade dos schemas Zod: .describe() bem escritos em campos de outputSchema são prompt engineering disfarçado, e .nullable() em vez de .optional() elimina classes inteiras de falhas em strict mode da OpenAI. Combinado com workflows durables (suspend/resume/bail), agent.network() para roteamento dinâmico, MCP para interop sem reimplementação, e scorers contínuos com sampling estratificado, o stack entrega agentes auditáveis, resilientes e observáveis — requisitos não-negociáveis em produção.
O roadmap imediato do framework (pós-1.25) concentra-se em AI SDK v3 (ToolLoopAgent nativo), consolidação de MastraCompositeStore, expansão de providers no Model Router e amadurecimento de Agent Networks como substituto definitivo da classe deprecated. Para arquitetos decidindo hoje: adotar Mastra 1.x é seguro para produção, com a ressalva de monitorar deprecações semanais no changelog oficial (ritmo alto de evolução) e manter snippets canônicos referenciados contra node_modules/@mastra/*/dist/docs/ ou https://mastra.ai/llms.txt em vez de blog posts datados.


