Inteligência Artificial

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.

Guia técnico: arquitetando IA com Mastra, Next.js e TypeScript

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

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...});

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

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

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

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 no retorno de run.start() ('success' | 'failed' | 'suspended' | 'tripwire') garante narrowing tipado.

1.5 Orquestração multi-agente

Três abordagens disponíveis:

  1. Agent-as-tool (supervisor estático): sub-agente envolvido em createTool(). Coordenação determinística, fluxo previsível.

  2. agent.network() (roteamento dinâmico): um Agent com agents, workflows e tools registrados; o LLM decide qual primitiva chamar. Requer memory (persiste task history e detecta conclusão). Suporta suspensão com agent-execution-approval / tool-execution-approval.

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

Deprecação importante (2026): a classe AgentNetwork foi deprecada. Use agent.network() ou 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 delegou roteamento ao Vercel AI SDK (v1/v2/v3 compatíveis). Duas 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 e servidor. Transports stdio, SSE e 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. Integração com Next.js App Router

2.1 Monorepo vs serviço separado

Critério

Monorepo (Mastra embutido)

Serviço separado (mastra dev + @mastra/client-js)

Deploy

Único (vercel deploy)

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

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)

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

Configuração obrigatória:

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 conhecido (vercel/next.js#74816): em algumas versões serverExternalPackages funciona em dev mas falha no build. 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.

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 no client com 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>  );}

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

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 ?? []));}

Low-level alternativo (controle 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()}

Serviço separado (Next.js proxy para Mastra standalone em :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 com useChat e 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: 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:

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

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 função Vercel (sem Next na 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 com Zod

3.1 Zod como contrato quádruplo

Um schema Zod cumpre quatro papéis simultâneos:

Papel

Mecanismo

Momento

Contrato estático

z.infer<typeof schema>

compile-time

Validação runtime

.parse() / .safeParse()

pós-LLM

Especificação para o LLM

JSON Schema (via zodSchema() do AI SDK)

pré-request

Documentação semântica

.describe() lido pelo modelo

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

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 são o padrão para ações 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 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 core

baseline

2,3× menor

Importante em edge

z.record()

1 arg

2 args obrigatórios

Quebra migração

.optional().default()

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

.describe()/.meta()

qualquer posição

deve ser último chain call

Não herda via .optional()/.extend()

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

3.4 Tools tipadas com 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() é 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):

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 com Output.object(), já 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 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.

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

Validação de runtime context via requestContextSchema:

TS
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

safeParse em Server Action

Tool input

createTool({ inputSchema })

Tool output

createTool({ outputSchema })

⚠️ informativo

Structured output

generate({ structuredOutput })

Workflow step

createStep({ inputSchema, outputSchema })

Runtime context

RuntimeContext<T>

✅ no set; ⚠️ no get

⚠️ opcional

Memory

workingMemory: { schema }


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

Agent com tools; Mastra roda loop automaticamente

Tarefas abertas, N de passos desconhecido

Explícito (workflow)

Código orchestrator

createWorkflow + .dountil() chamando step com agente

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.

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 reutilizável:

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

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 padrões de orquestração

Padrão

API Mastra

Quando usar

Pipeline

.then()

Passos fixos, dependência linear (ETL, content pipeline)

Fan-out/Fan-in

.parallel([])

N fixo de tasks independentes; output é objeto com chaves = step ids

MapReduce

.foreach(step, {concurrency})

N dinâmico; processar lista

Router/Branch

.branch([[cond, step]])

Roteamento por classificação; todos branches compartilham schemas

Supervisor estático

Agent com sub-agents como tools

Coordenação determinística

Supervisor dinâmico

agent.network()

LLM decide primitiva a chamar

Evaluator-Optimizer

.dowhile() / .dountil() + scorer

Refinamento iterativo convergente

Human-in-the-loop

suspend() / resume() / bail()

Aprovações, pagamento >$X, ações irreversíveis

Handoff

workflow + agents com memory compartilhada

Especialista assume controle

Council

.parallel() + synthesis step

Múltiplas opiniões para síntese

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 Integração de ferramentas 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 em tools:

Estratégia

Quando

Exemplo

Throw

Erro irrecuperável

Auth failure, timeout após retries

Return estruturado

LLM deve reagir/re-tentar

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

Retry interno

Falha transiente

p-retry dentro do execute

Circuit breaker

API instável

opossum, abre após N falhas

Timeout

Prevenir agente preso

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 Observabilidade

Logging estruturado (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 com OTel + múltiplos 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

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.

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 com 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 arquitetural consolidado

TL;DR para arquitetos:

  1. Comece simples. Single agent + tools. Workflow/multi-agent só quando passos são conhecidos ou contexto fica inviável.

  2. Workflows para auditabilidade/SLO; agent.network() para flexibilidade. Workflows = código dita fluxo. Networks = LLM dita fluxo.

  3. Zod everywhere. Todo tool inputSchema/outputSchema, todo step, todo scorer. É sua única defesa contra hallucination em tool args.

  4. Persistência desde o dia 1. Postgres (prod) ou LibSQL (dev). Sem isso, suspend/resume não funciona e você perde traces em restart.

  5. serverExternalPackages: ['@mastra/*'] + runtime = 'nodejs'. Não negociável em Next.js.

  6. Route Handlers para streaming; Server Actions para síncrono. Não tente fazer streaming via Server Action.

  7. Vercel Fluid Compute + Postgres/Turso remoto para produção serverless. Jamais LibSQL local.

  8. Observabilidade por ambiente via configSelector: dev→Default, staging→10% Langfuse, prod→1% + Datadog.

  9. Scorers com sampling baixo em prod (5-20%), 100% em toxicity/safety.

  10. MCP antes de reimplementar tools. GitHub, Slack, Notion, filesystem, Playwright já têm servers oficiais.

  11. HITL via suspend() sempre que custo/irreversibilidade > conveniência. Pagamento >$X, deletion, envio em massa.

  12. Prefira agent.network() ou supervisor explícito sobre AgentNetwork class (deprecated).

  13. 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 error no outputSchema quebra narrowing do tool.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; createTool do Mastra não sofre disso.

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

  • Zod v4 cria schemas 17× mais lento (JIT) — nunca instancie em render/loop quente.

  • toDataStreamResponse() + output: zodSchema conflita (mastra-ai/mastra#5544) — use experimental_output.

  • serverExternalPackages tem issue no build (vercel/next.js#74816) — tenha fallback Webpack.

  • AgentNetwork classe → deprecated; use agent.network().

  • legacy_workflows → substituído por createWorkflow/createStep.

Stack de referência para produção

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)                │└──────────────────────────────────────────────────────────────┘

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.