CookbookMastra agent
Mastra agent
Tool-using agent with memory, evals and CleverRouter as the LLM backend.
A production-grade agent in Mastra, backed by an EU-hosted chat model and a Postgres memory store. Tools call into your own domain — CleverRouter only sees the inference payload.
At a glance
| Stack piece | Choice |
|---|---|
| Agent framework | @mastra/core |
| Memory store | @mastra/store-pg against your Postgres |
| LLM | mistral/mistral-large-2 via CleverRouter |
| Tools | Zod-typed createTool({ ... }) |
Setup
bun add @cleverrouter/sdk@alpha @mastra/core @mastra/memory @mastra/store-pg zodimport { createCleverRouterMastra } from '@cleverrouter/sdk/mastra';
export const cr = createCleverRouterMastra({
apiKey: process.env.CLEVERROUTER_API_KEY!,
});The agent
import { Agent } from '@mastra/core/agent';
import { createTool } from '@mastra/core/tools';
import { Memory } from '@mastra/memory';
import { PostgresStore } from '@mastra/store-pg';
import { z } from 'zod';
import { cr } from './cr';
// A domain tool — your own code, your own data
const getKeyUsage = createTool({
id: 'get-key-usage',
description: 'Returns the last 7 days of token usage for a CleverRouter key.',
inputSchema: z.object({ keyId: z.string() }),
execute: async ({ context }) => {
// your DB call
const rows = await db.usageDaily.findMany({
where: { apiKeyId: context.keyId },
orderBy: { day: 'desc' },
take: 7,
});
return { days: rows };
},
});
const memory = new Memory({
storage: new PostgresStore({
connectionString: process.env.DATABASE_URL!,
}),
});
export const supportAgent = new Agent({
name: 'support',
model: cr('mistral/mistral-large-2'),
instructions: `You are CleverRouter customer support.
Always answer in German.
If a question touches usage or billing, call get-key-usage first.
Be concise. Cite the key id in your reply when relevant.`,
tools: { getKeyUsage },
memory,
});Calling the agent
import { supportAgent } from './agent';
const reply = await supportAgent.generate(
'Wie viele Tokens habe ich diese Woche auf cr_live_abc verbraucht?',
{ resourceId: 'user-42', threadId: 'thread-xyz' },
);
console.log(reply.text);resourceId + threadId scope the Mastra memory to a specific
user and conversation thread. The agent reads relevant past turns
automatically before sending the new prompt.
Workflow with multiple steps
import { Workflow, Step } from '@mastra/core/workflows';
import { z } from 'zod';
import { supportAgent } from './agent';
export const triageWorkflow = new Workflow({
name: 'triage-ticket',
triggerSchema: z.object({ message: z.string(), userId: z.string() }),
})
.step(
new Step({
id: 'agent-answer',
execute: async ({ context }) => {
const out = await supportAgent.generate(context.triggerData.message, {
resourceId: context.triggerData.userId,
});
return { reply: out.text };
},
}),
)
.step(
new Step({
id: 'log-to-slack',
execute: async ({ context }) => {
await postToSlack(context.steps.agentAnswer.output.reply);
return { ok: true };
},
}),
)
.commit();Per-call routing
Pin the model run to a specific EU provider for the lifetime of one call:
await supportAgent.generate('Hi.', {
model: cr('mistral/mistral-large-2', {
provider: 'tensorix',
strategy: 'fastest',
}),
});Evaluation
Mastra evals work the same way they would against OpenAI:
import { evaluate } from '@mastra/evals';
import { supportAgent } from './agent';
const result = await evaluate({
agent: supportAgent,
cases: [
{ input: 'How do I rotate a key?', expected: /Settings.*Keys|create.*revoke/i },
{ input: 'Wie lösche ich meinen Account?', expected: /privacy@/i },
],
});
console.log(result.passRate);Where the data sits
Memory is yours, inference is ours
Mastra memory writes to your Postgres. CleverRouter's ZDR applies to inference only — nothing the agent stores in your memory store lives in our systems.
Common gotchas
- Bad tool descriptions = bad tool use. Mastra forwards your
descriptionto the model verbatim. Be specific. - Memory size. Mastra's default memory window is small; bump it for long conversations or use semantic recall.
- Don't put PII in the agent name or system prompt. Both end up in your own logs; treat them like any other prompt.