Using Mastra with Agentuity — Agentuity Documentation

Using Mastra with Agentuity

Deploy Mastra agents on Agentuity with persistent state, observability, and the AI Gateway

Mastra gives you the building blocks for AI agents: tools, structured output, multi-agent workflows. But frameworks don't come with a place to run. Agentuity provides the deployment runtime, persistent state, and observability so you can focus on agent logic instead of infrastructure. Define your agents in Mastra, deploy them on Agentuity.

The Integration Pattern

Wrap a Mastra Agent in Agentuity's createAgent(). Mastra runs the LLM calls and tool orchestration. Agentuity provides schemas, thread-isolated state, logging via ctx.logger, and deployment.

Create the Mastra agent with your model and instructions:

import { Agent } from '@mastra/core/agent';
 
// Mastra handles the agent logic and LLM interaction
const chatAgent = new Agent({
  id: 'chat-agent',
  name: 'Chat Agent',
  instructions: 'You are a helpful assistant with memory.',
  model: 'openai/gpt-5.4',
});

Then wrap it with Agentuity's createAgent() for deployment, state management, and observability:

import { createAgent } from '@agentuity/runtime';
import { s } from '@agentuity/schema';
 
// Agentuity handles deployment, schemas, and state
export default createAgent('chat', {
  schema: {
    input: s.object({ message: s.string() }),
    output: s.object({ response: s.string() }),
  },
  handler: async (ctx, { message }) => {
    // Load history from thread state, isolated per conversation
    const history = (await ctx.thread.state.get<{ role: string; content: string }[]>('messages')) ?? [];
 
    const result = await chatAgent.generate([
      ...history,
      { role: 'user', content: message },
    ]);
 
    // Persist with a 20-message sliding window
    await ctx.thread.state.push('messages', { role: 'user', content: message }, 20); 
    await ctx.thread.state.push('messages', { role: 'assistant', content: result.text }, 20); 
 
    return { response: result.text };
  },
});

Mastra handles the LLM interaction and tool orchestration. Agentuity provides per-conversation state via ctx.thread.state (with a sliding window to cap history size), logs with searchable context, and deployment.

Tool Calling

Mastra's createTool() defines typed tools with Zod schemas. The agent calls them automatically based on the user's message. Agentuity wraps the agent for deployment and schema validation.

Define a tool with createTool() and attach it to a Mastra agent:

typescriptsrc/agent/weather/index.ts
import { createTool } from '@mastra/core/tools';
import { Agent } from '@mastra/core/agent';
import { z } from 'zod';
 
// Validate API responses before returning tool output
const GeoResponse = z.object({
  results: z.array(z.object({
    latitude: z.number(),
    longitude: z.number(),
  })).optional(),
});
 
const WeatherResponse = z.object({
  current: z.object({ temperature_2m: z.number() }),
});
 
// Tool input schema is shown to the LLM for parameter selection
const weatherTool = createTool({
  id: 'get-weather',
  description: 'Fetches current weather for a location',
  inputSchema: z.object({
    location: z.string().describe('City or location name'),
  }),
  execute: async ({ location }) => {
    const geo = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(location)}&count=1`);
    if (!geo.ok) {
      return `Unable to look up ${location}: ${geo.status}`;
    }
 
    const geoJson: unknown = await geo.json().catch(() => undefined);
    const geoData = GeoResponse.safeParse(geoJson);
    if (!geoData.success) {
      return `Unable to read location data for ${location}`;
    }
 
    const firstResult = geoData.data.results?.[0];
 
    if (!firstResult) {
      return `No weather data found for ${location}`;
    }
 
    const { latitude, longitude } = firstResult;
    const weather = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m`);
    if (!weather.ok) {
      return `Unable to fetch weather for ${location}: ${weather.status}`;
    }
 
    const weatherJson: unknown = await weather.json().catch(() => undefined);
    const data = WeatherResponse.safeParse(weatherJson);
    if (!data.success) {
      return `Unable to read weather data for ${location}`;
    }
 
    return `${location}: ${data.data.current.temperature_2m}°C`;
  },
});
 
const weatherAgent = new Agent({
  id: 'weather-agent',
  instructions: 'Use the get-weather tool when users ask about weather.',
  model: 'openai/gpt-5.4',
  tools: { weatherTool }, // Mastra handles the function calling loop
});

Then create the Agentuity handler that calls the agent and returns the result:

typescriptsrc/agent/weather/index.ts
import { createAgent } from '@agentuity/runtime';
import { s } from '@agentuity/schema';
 
export default createAgent('weather', {
  schema: {
    input: s.object({ message: s.string() }),
    output: s.object({ response: s.string() }),
  },
  handler: async (ctx, { message }) => {
    ctx.logger.info('Weather request', { message });
    const result = await weatherAgent.generate(message);
    return { response: result.text };
  },
});
  • Mastra tools use createTool() with Zod schemas for parameter validation
  • result.text contains the LLM's final response after all tool calls complete
  • result.usage contains token usage when the model provider returns it

Structured Output

When you need the LLM to return data in a specific shape, use Mastra's structuredOutput with a Zod schema. The LLM response is validated and parsed before your code sees it.

Define the Zod schema that describes the expected output shape:

typescriptsrc/agent/day-planner/index.ts
import { Agent } from '@mastra/core/agent';
import { z } from 'zod';
 
// Zod schema: validates the LLM's structured response
const DayPlanSchema = z.object({
  plan: z.array(z.object({
    name: z.string().describe('Time block name'),
    activities: z.array(z.object({
      name: z.string(),
      startTime: z.string().describe('HH:MM format'),
      endTime: z.string().describe('HH:MM format'),
      description: z.string(),
      priority: z.enum(['high', 'medium', 'low']),
    })),
  })),
  summary: z.string(),
});
 
const plannerAgent = new Agent({
  id: 'day-planner',
  instructions: 'Create structured daily plans from descriptions.',
  model: 'openai/gpt-5.4',
});

Then pass the schema to generate() via { structuredOutput: { schema } } and access the parsed result via result.object:

typescriptsrc/agent/day-planner/index.ts
import { createAgent } from '@agentuity/runtime';
import { s } from '@agentuity/schema';
 
export default createAgent('day-planner', {
  schema: {
    input: s.object({ prompt: s.string() }),
    output: s.object({ plan: s.unknown(), summary: s.string() }),
  },
  handler: async (ctx, { prompt }) => {
    // structuredOutput returns a typed, validated object
    const result = await plannerAgent.generate(prompt, {
      structuredOutput: { schema: DayPlanSchema }, 
    });
 
    // result.object is typed from DayPlanSchema
    const plan = result.object; 
    ctx.logger.info('Plan generated', {
      blocks: plan?.plan.length,
      activities: plan?.plan.reduce((sum, b) => sum + b.activities.length, 0),
    });
 
    return { plan: plan?.plan, summary: plan?.summary ?? '' };
  },
});

Access the parsed output via result.object instead of result.text. The two schema layers serve different purposes: Zod controls what the LLM returns, while @agentuity/schema validates the HTTP request and response payloads.

Full Examples

Each example is a complete project with agent code, a React frontend, and API routes:

ExamplePatternSource
Agent MemoryConversation history with sliding windowagent-memory
Using ToolsTool calling with real APIsusing-tools
Structured OutputType-safe LLM responses with Zodstructured-output
Agent ApprovalHuman-in-the-loop tool approvalagent-approval
Network AgentMulti-agent routing and delegationnetwork-agent
Network ApprovalApproval flows in multi-agent networksnetwork-approval

Next Steps