Mastra gives you a typed Agent class with .generate(), .stream(), and tool composition. Drop a Mastra agent into a Hono route, store conversation history in key-value storage, and deploy with the rest of your app.
npm install hono @mastra/core @agentuity/keyvalue valibotDefine the Agent
Agent accepts a model spec, instructions, and an optional tool map. Mastra resolves provider strings (openai/..., anthropic/...) through its built-in registry. Run locally with agentuity dev when you want those provider calls to use AI Gateway env wiring.
import { Agent } from '@mastra/core/agent';
const model = process.env.MASTRA_MODEL;
if (!model) {
throw new Error('Set MASTRA_MODEL to the provider model this agent should use.');
}
export const chatAgent = new Agent({
id: 'chat',
name: 'Chat Agent',
instructions: 'You are a concise product support assistant.',
model,
});Wire the Route
Validate the request, load the conversation, call agent.generate(), store the new turn. The route is thin; the Mastra agent owns the model call.
import { Hono } from 'hono';
import { KeyValueClient } from '@agentuity/keyvalue';
import * as v from 'valibot';
import { chatAgent } from './lib/chat-agent';
const HISTORY_NAMESPACE = 'mastra-chat';
const HISTORY_LIMIT = 20;
const messageSchema = v.object({
role: v.picklist(['user', 'assistant']),
content: v.string(),
});
const requestSchema = v.object({
conversationId: v.string(),
message: v.string(),
});
const historySchema = v.array(messageSchema);
type ChatMessage = v.InferOutput<typeof messageSchema>;
const kv = new KeyValueClient();
const app = new Hono();
app.post('/api/chat', async (c) => {
const body: unknown = await c.req.json();
const input = v.parse(requestSchema, body);
const stored = await kv.get<unknown>(HISTORY_NAMESPACE, input.conversationId);
const history: readonly ChatMessage[] = stored.exists
? v.parse(historySchema, stored.data)
: [];
const userMessage: ChatMessage = { role: 'user', content: input.message };
const result = await chatAgent.generate([...history, userMessage]);
const assistantMessage: ChatMessage = {
role: 'assistant',
content: result.text,
};
const next = [...history, userMessage, assistantMessage].slice(-HISTORY_LIMIT);
await kv.set(HISTORY_NAMESPACE, input.conversationId, next, {
ttl: 60 * 60 * 24 * 30,
});
return c.json({
conversationId: input.conversationId,
message: assistantMessage,
messageCount: next.length,
});
});
export default app;Tools
Mastra tools are typed with the same createTool() pattern Mastra users already know. Pass them through the tools field on new Agent({ ... }).
import { createTool } from '@mastra/core/tools';
import { z } from 'zod';
const model = process.env.MASTRA_MODEL;
if (!model) {
throw new Error('Set MASTRA_MODEL to the provider model this agent should use.');
}
const lookupOrder = createTool({
id: 'lookup-order',
description: 'Look up an order by ID',
inputSchema: z.object({ orderId: z.string() }),
execute: async (input) => {
// call your database, API, etc.
return { status: 'shipped', orderId: input.orderId };
},
});
export const supportAgent = new Agent({
id: 'support',
name: 'Order Support',
instructions: 'Help customers with order questions. Use lookup-order when asked about a specific order.',
model,
tools: { lookupOrder },
});The Mastra tool loop runs entirely inside agent.generate(). The route still owns HTTP and storage.
When to reach for Mastra
Pick Mastra when you want its agent abstractions (workflows, multi-agent handoffs, structured output via Mastra primitives) and you do not want to build them on AI SDK directly. Pick raw AI SDK when you want fewer layers.
Next Steps
- Agents: the plain-function pattern this page wraps
- Chat with History: the same KV layout, without Mastra
- AI Gateway: local dev routing and deployed provider env choices