Mastra
Give a Mastra agent a memory of each of your users. What is known goes into the agent's instructions through its runtime context, or the agent looks it up with a tool that reads the user from that context; either way, each exchange is saved after the agent answers.
Install
npm install @mastra/core geniffy zodpnpm add @mastra/core geniffy zodbun add @mastra/core geniffy zodSet ANTHROPIC_API_KEY, and GENIFFY_API_KEY from API keys in the Geniffy app. Any model Mastra
supports works the same way.
Remember each user
import { Agent } from "@mastra/core/agent";
import { RuntimeContext } from "@mastra/core/runtime-context";
import { Geniffy } from "geniffy";
const geniffy = new Geniffy(); // reads GENIFFY_API_KEY
export const assistant = new Agent({
name: "Assistant",
instructions: ({ runtimeContext }) => {
const memory = runtimeContext.get("memory");
return `You are a helpful assistant.\n\n<memory>\n${memory}\n</memory>`;
},
model: "anthropic/claude-opus-5-5",
});
export async function chat(userId: string, message: string) {
const mem = geniffy.space(`user_${userId}`);
const runtimeContext = new RuntimeContext();
runtimeContext.set("memory", await mem.context(message)); // what bears on the message
const result = await assistant.generate(message, { runtimeContext });
await mem.memories.add({
messages: [
{ role: "user", content: message },
{ role: "assistant", content: result.text },
],
});
return result.text;
}The agent sees what is known about this user, each line with where it came from. When nothing is known, the block says so in one sentence, so the agent says it doesn't know instead of guessing.
Let the agent look things up
To let the agent decide when to look something up, give it a recall tool. The tool reads the user from the
runtime context, so the model never sees or chooses whose memory it reads.
import { Agent } from "@mastra/core/agent";
import { RuntimeContext } from "@mastra/core/runtime-context";
import { createTool } from "@mastra/core/tools";
import { Geniffy } from "geniffy";
import { z } from "zod";
const geniffy = new Geniffy();
const recall = createTool({
id: "recall",
description: "Look up what is known about the user, with where it came from.",
inputSchema: z.object({ query: z.string().describe("What to look up") }),
execute: async ({ context, runtimeContext }) =>
geniffy.space(`user_${runtimeContext.get("userId")}`).context(context.query),
});
export const assistant = new Agent({
name: "Assistant",
instructions:
"You are a helpful assistant. Use recall before answering anything " +
"that depends on what the user said before.",
model: "anthropic/claude-opus-5-5",
tools: { recall },
});
export async function chat(userId: string, message: string) {
const runtimeContext = new RuntimeContext();
runtimeContext.set("userId", userId);
const result = await assistant.generate(message, { runtimeContext });
await geniffy.space(`user_${userId}`).memories.add({
messages: [
{ role: "user", content: message },
{ role: "assistant", content: result.text },
],
});
return result.text;
}Mastra's own memory keeps a thread going; Geniffy is what the agent knows about the user across every thread, with where each fact came from.