@astropods/adapter-mastra
The sender's name
Each turn's request context carries userName, the sender's name on the source platform (their full name in Slack, or their Astro name in web chat). It is absent when the platform sent none, so check for it. Tell the model how to use the name, or it may repeat it in every reply:
const agent = new Agent({
id: "greeter",
name: "Greeter",
model,
instructions: ({ requestContext }) => {
const name = requestContext.get("userName");
return name
? `The user's name is ${name}. Greet them by name in your first reply only; don't repeat it after that.`
: "You are a helpful assistant.";
},
});
The request context also carries threadId and resourceId. resourceId is the user who sent the turn.
Memory
The memory thread is the conversation. The memory resource depends on who shares it:
- In a 1:1 chat, such as a DM or the web chat, the resource is the user, so resource-scoped memory follows them between chats.
- In a channel conversation, such as a Slack thread, the resource is the conversation. Anyone in the thread can talk to the agent, and nobody's own memory mixes into it.
- A thread already in memory keeps the resource it was created with, because Mastra rejects a thread used with a different resource.
Working memory and semantic recall default to scope: "resource", which carries a person's memory across their threads. To keep each thread's memory to that thread, set both to scope: "thread":
new Memory({
storage,
options: {
workingMemory: { enabled: true, scope: "thread" },
semanticRecall: { topK: 5, messageRange: 2, scope: "thread" },
},
});
The chosen model
Pass the models a chat user can pick from to serve(); the first is the default. With two or more, the web chat shows a model picker, and each turn's request context carries the pick as model. It is absent when no model was picked, as on a Slack turn, so fall back to your default:
const MODELS = [process.env.MODEL_DEFAULT ?? "claude-sonnet-4-6", "claude-opus-4-7"];
const agent = new Agent({
id: "assistant",
name: "Assistant",
instructions: "You are a helpful assistant.",
model: ({ requestContext }) =>
gateway.chat(requestContext.get<"model", string | undefined>("model") ?? MODELS[0]),
});
serve(agent, { models: MODELS });
A resumed run, after a tool approval or a form, gets the same request context.
Files
The adapter appends each attached file's name and path to the turn's text. fileTools() gives the model tools that open those paths. They work on files of any size without loading a file into the prompt:
| Tool | What it does |
|---|---|
inspect_file | Reports a file's kind (PDF, DOCX, XLSX, text), its size, its text length, and for a table each sheet's columns and row count. |
read_file | Returns one 20,000-character page of a PDF, DOCX, or text file, with nextOffset for the next page. |
read_table | Returns one page of rows from a CSV or XLSX file, 100 by default, with nextOffset for the next page. |
create_file | Writes a text file to the files dir, attaches it to the reply with attachFile, and returns its name and the path to read it back. |
import { fileTools, serve } from "@astropods/adapter-mastra";
const agent = new Agent({
id: "assistant",
name: "Assistant",
model,
instructions: "Answer questions about the files the user attaches.",
tools: { ...fileTools() },
});
serve(agent, { supportsFiles: true });
supportsFiles turns on web chat uploads. Slack sends files either way. The tools open only the files attached to or created in the current conversation, so a follow-up question can go back to an earlier file. The list is saved with the files, so it lasts through a restart. The tools return a file's contents and do no analysis. For counts or totals over a large table, give the agent a query engine such as DuckDB on the attachment's path, as the adapter-core README describes. A scanned PDF has no text layer, so reading one takes your own OCR tool.
Sandboxes
AstroSandbox is a Mastra sandbox provider, so Mastra's own workspace drives
it: the filesystem, the mounts and the built-in workspace tools all run
against the MicroVM. There is no parallel toolset to learn.
import { AstroSandbox } from "@astropods/adapter-mastra";
import { Workspace } from "@mastra/core/workspace";
const workspace = new Workspace({
sandbox: new AstroSandbox({ name: threadId }),
});
The name is the sandbox. Use the thread id: one thread is one sandbox, so a conversation that resumes reattaches to its own files and two threads never share a filesystem.
An edited message does not reset the sandbox. When a user edits a message in
the web chat, the adapter rebuilds the thread's memory, but a sandbox keyed on
the thread keeps the files the replaced turns wrote. The agent can then see
work from a version of the conversation the user abandoned. If that matters,
pass supportsHistory: false to serve(), which turns off editing for the
agent.
What it maps onto
| Mastra | Astro |
|---|---|
start() | One attach. Reports created on the first call, connected after |
executeCommand() | A short command, or a background process when you pass onStdout/onStderr, because a plain exec cannot stream |
processes | spawn, list, get, kill over real background processes |
handle.wait() | Polls to exit, handing each chunk to your callbacks as it arrives |
stop() / destroy() | Suspend, and delete |
getInfo() | Includes timeoutAt, the sandbox's 8 hour ceiling |
Mastra starts the sandbox on demand: its process manager calls
ensureRunning() before a spawn, so nothing has to start it by hand.
What it does not do
supportsCheckpoints is false. snapshot() suspends, which does snapshot
the MicroVM's memory so the next attach resumes from it, but there is only
ever the latest state to return to. Persistence past the ceiling is not
built yet.
A process has no stdin. sendStdin() throws rather than accepting the write
and dropping it; pass input through the command or a file.