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 pieceChoice
Agent framework@mastra/core
Memory store@mastra/store-pg against your Postgres
LLMmistral/mistral-large-2 via CleverRouter
ToolsZod-typed createTool({ ... })

Setup

bun add @cleverrouter/sdk@alpha @mastra/core @mastra/memory @mastra/store-pg zod
cr.ts
import { createCleverRouterMastra } from '@cleverrouter/sdk/mastra';

export const cr = createCleverRouterMastra({
  apiKey: process.env.CLEVERROUTER_API_KEY!,
});

The agent

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

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

workflow.ts
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:

eval.ts
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 description to 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.