> ## Documentation Index
> Fetch the complete documentation index at: https://docs.superserve.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Mastra

> Use Superserve sandboxes as the execution runtime for agents built with Mastra.

[Mastra](https://mastra.ai) is a TypeScript framework for building agents with tools, workflows, and memory. Back an agent's tools with `sandbox.commands.run` and `sandbox.files` so every command the model runs, and every file it writes, stays in an isolated VM instead of on your server. The model API key stays in your app; the sandbox never sees it.

## Setup

```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
npm install @superserve/sdk @mastra/core zod
export SUPERSERVE_API_KEY=ss_live_...
export ANTHROPIC_API_KEY=sk-ant-...
```

The examples use Anthropic through Mastra's model router, which reads the provider key from the environment. Any supported provider works: set `OPENAI_API_KEY` and pass `"openai/gpt-5.5"` as the `model`. See [Models](https://mastra.ai/models) in the Mastra docs for the full list, and [API keys](https://docs.superserve.ai/api-key.md) to create a Superserve API key.

## Wire the sandbox into the agent

The highlighted block defines a `bash` tool backed by the sandbox. Everything else is standard Mastra usage. The agent loops for up to 5 steps by default, so the model can run commands, read the results, and answer.

```typescript {8-18} theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { Agent } from "@mastra/core/agent"
import { createTool } from "@mastra/core/tools"
import { Sandbox } from "@superserve/sdk"
import { z } from "zod"

const sandbox = await Sandbox.create({ name: "agent-runtime" })

const bash = createTool({
  id: "bash",
  description: "Run a shell command in the sandbox.",
  inputSchema: z.object({ command: z.string() }),
  execute: async ({ command }) => {
    const { stdout, stderr, exitCode } = await sandbox.commands.run(command, {
      timeoutMs: 60_000,
    })
    return { stdout, stderr, exitCode }
  },
})

const agent = new Agent({
  id: "workspace-assistant",
  name: "Workspace Assistant",
  instructions: "Use the bash tool to inspect the sandbox before answering.",
  model: "anthropic/claude-sonnet-5-5",
  tools: { bash },
})

try {
  const result = await agent.generate(
    "List the files in /tmp and tell me how many there are.",
  )
  console.log(result.text)
} finally {
  await sandbox.kill()
}
```

The tool returns `stdout`, `stderr`, and `exitCode` as an object, so the model sees failed commands as well as their output.

## Run code with file tools

Add file tools next to `bash` to let the model write a script, run it, and read the result. Pass `maxSteps` to `generate` to allow longer loops.

```typescript {20-35} theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { Agent } from "@mastra/core/agent"
import { createTool } from "@mastra/core/tools"
import { Sandbox } from "@superserve/sdk"
import { z } from "zod"

const sandbox = await Sandbox.create({ name: "code-runner" })

const bash = createTool({
  id: "bash",
  description: "Run a shell command in the sandbox.",
  inputSchema: z.object({ command: z.string() }),
  execute: async ({ command }) => {
    const { stdout, stderr, exitCode } = await sandbox.commands.run(command, {
      timeoutMs: 60_000,
    })
    return { stdout, stderr, exitCode }
  },
})

const writeFile = createTool({
  id: "write-file",
  description: "Write a text file in the sandbox. Use an absolute path.",
  inputSchema: z.object({ path: z.string(), content: z.string() }),
  execute: async ({ path, content }) => {
    await sandbox.files.write(path, content)
    return `Wrote ${content.length} characters to ${path}`
  },
})

const readFile = createTool({
  id: "read-file",
  description: "Read a text file from the sandbox. Use an absolute path.",
  inputSchema: z.object({ path: z.string() }),
  execute: async ({ path }) => ({ content: await sandbox.files.readText(path) }),
})

const agent = new Agent({
  id: "code-runner",
  name: "Code Runner",
  instructions:
    "You are a coding assistant. Save code under /home/user, run it with " +
    "the bash tool, and report the real output. Use python3, not python.",
  model: "anthropic/claude-sonnet-5-5",
  tools: { bash, writeFile, readFile },
})

try {
  const result = await agent.generate(
    "Write a Python script that prints the first 10 prime numbers, " +
      "run it, and show me the output.",
    { maxSteps: 10 },
  )
  for (const call of result.steps.flatMap((step) => step.toolCalls)) {
    console.log(`> ${call.payload.toolName}`, JSON.stringify(call.payload.args))
  }
  console.log(result.text)
} finally {
  await sandbox.kill()
}
```

Output from a run with `claude-haiku-4-5` (trimmed; the model's wording varies):

```text theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
> writeFile {"path":"/home/user/primes.py","content":"def is_prime(n):\n ..."}
> bash {"command":"python3 /home/user/primes.py"}
The first 10 prime numbers are:
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
```

The tool name in `payload.toolName` is the key in the agent's `tools` object, not the tool `id`. `sandbox.files` paths must be absolute. See [Read & write files](https://docs.superserve.ai/filesystem/read-write.md) for binary content and directory downloads.

## Stream tool calls

`agent.stream()` takes the same arguments as `generate()`. Read `fullStream` to show each tool call and its result while the agent works.

```typescript {32-40} theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { Agent } from "@mastra/core/agent"
import { createTool } from "@mastra/core/tools"
import { Sandbox } from "@superserve/sdk"
import { z } from "zod"

const sandbox = await Sandbox.create({ name: "agent-runtime" })

const bash = createTool({
  id: "bash",
  description: "Run a shell command in the sandbox.",
  inputSchema: z.object({ command: z.string() }),
  execute: async ({ command }) => {
    const { stdout, stderr, exitCode } = await sandbox.commands.run(command, {
      timeoutMs: 60_000,
    })
    return { stdout, stderr, exitCode }
  },
})

const agent = new Agent({
  id: "workspace-assistant",
  name: "Workspace Assistant",
  instructions: "Use the bash tool to inspect the sandbox before answering.",
  model: "anthropic/claude-sonnet-5-5",
  tools: { bash },
})

try {
  const stream = await agent.stream(
    "Which Linux distribution and Python version does this machine run?",
  )
  for await (const chunk of stream.fullStream) {
    if (chunk.type === "text-delta") process.stdout.write(chunk.payload.text)
    if (chunk.type === "tool-call") {
      console.log(`\n> ${chunk.payload.toolName}`, JSON.stringify(chunk.payload.args))
    }
    if (chunk.type === "tool-result") {
      console.log(JSON.stringify(chunk.payload.result))
    }
  }
} finally {
  await sandbox.kill()
}
```

See [Streaming](https://docs.superserve.ai/commands/streaming.md) to stream a single command's output.

## One sandbox per conversation

The examples above close over one sandbox. To serve many conversations with one agent, pass the sandbox ID to the tools through [request context](https://mastra.ai/docs/server/request-context). Store `sandbox.id` with the conversation, reconnect on each turn, and pause between turns so files and installed packages stay available without paying for an idle VM.

```typescript {7-8,14-17,34-38,51} theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { Agent } from "@mastra/core/agent"
import { RequestContext } from "@mastra/core/request-context"
import { createTool } from "@mastra/core/tools"
import { Sandbox } from "@superserve/sdk"
import { z } from "zod"

// Sandboxes with a turn in progress, keyed by ID. They stay in your process.
const sandboxes = new Map<string, Promise<Sandbox>>()

const bash = createTool({
  id: "bash",
  description: "Run a shell command in the sandbox.",
  inputSchema: z.object({ command: z.string() }),
  requestContextSchema: z.object({ sandboxId: z.string() }),
  execute: async ({ command }, { requestContext }) => {
    const sandbox = await sandboxes.get(requestContext.get("sandboxId"))
    if (!sandbox) throw new Error("No sandbox is connected for this turn.")
    const { stdout, stderr, exitCode } = await sandbox.commands.run(command, {
      timeoutMs: 60_000,
    })
    return { stdout, stderr, exitCode }
  },
})

const agent = new Agent({
  id: "workspace-assistant",
  name: "Workspace Assistant",
  instructions: "Use the bash tool to work in the sandbox.",
  model: "anthropic/claude-sonnet-5-5",
  tools: { bash },
})

async function runTurn(sandboxId: string, prompt: string) {
  // The turn pauses the sandbox when it ends, so allow one turn at a time.
  if (sandboxes.has(sandboxId)) throw new Error("A turn is already running.")
  // Resumes the sandbox if it is paused.
  const connecting = Sandbox.connect(sandboxId)
  sandboxes.set(sandboxId, connecting)
  try {
    const sandbox = await connecting
    const requestContext = new RequestContext<{ sandboxId: string }>()
    requestContext.set("sandboxId", sandboxId)
    try {
      const result = await agent.generate(prompt, { requestContext, maxSteps: 10 })
      return result.text
    } finally {
      // Pause between turns. Files and memory are kept until the next connect.
      await sandbox.pause()
    }
  } finally {
    sandboxes.delete(sandboxId)
  }
}

const { id } = await Sandbox.create({ name: "chat-session" })

try {
  await runTurn(id, "Create /home/user/todo.txt with three short items.")
  console.log(
    await runTurn(id, "Add a fourth item to /home/user/todo.txt, then print the file."),
  )
} finally {
  await Sandbox.killById(id)
}
```

Put only the ID in request context, not the `Sandbox` object. Mastra serializes request context, for example into traces and stored workflow snapshots, and a `Sandbox` object holds your API key and a sandbox access token. The connected `Sandbox` objects stay in a map in your process.

Each turn pauses the sandbox when it ends, so `runTurn` rejects a second turn for a conversation while one is running. The map works in one process. If several servers handle the same conversation, use a shared lock or a queue per conversation.

`requestContextSchema` types `requestContext.get("sandboxId")` and checks the value before the tool runs. If a call has no sandbox ID in its context, the tool returns a validation error to the model instead of running. `Sandbox.connect()` resumes a paused sandbox, or one that is still pausing, before it returns. See [Lifecycle](https://docs.superserve.ai/sandbox/lifecycle.md) and [Connect](https://docs.superserve.ai/sandbox/connect.md).

The second turn finds the file because it lives in the sandbox, not because the agent remembers the first turn. To keep the chat history too, add [Mastra memory](https://mastra.ai/docs/memory/overview) to the agent.

## Good to know

* The model can call only the tools you define. Keep `kill()`, `pause()`, and `update()` in your application code, not in a tool.
* To limit what model-written code can reach, create the sandbox with [network rules](https://docs.superserve.ai/sandbox/networking.md).
* Pass `timeoutMs` to `commands.run`. Without it, the SDK request times out after 30 seconds, so the tool call fails on longer commands such as package installs.
* The default sandbox has `python3` but no `python`. Tell the model in `instructions`, or use a [template](https://docs.superserve.ai/templates/overview.md) with the tools you need.
* As a backstop for crashes, set `timeoutSeconds` and `autoDeleteSeconds` when you create the sandbox. See [Auto-delete](https://docs.superserve.ai/sandbox/lifecycle.md#auto-delete).

## Resources

> [card] [Mastra docs](https://mastra.ai/docs)
>
> Agents, tools, memory, and workflows in Mastra.

> [card] [Run commands](https://docs.superserve.ai/commands/overview.md)
>
> Options for `commands.run`, including environment variables and working directory.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.