Mastra adapter

Connect a Mastra Agent to the Astro AI runtime
View as Markdown

@astropods/adapter-mastra wraps a Mastra Agent and connects it to the Astro AI messaging sidecar. It translates Mastra’s fullStream chunks into the shared StreamHooks lifecycle, wires STT/TTS through Mastra’s voice provider, and auto-configures OTEL tracing when an exporter endpoint is set.

Install

bun add @astropods/adapter-mastra
# or: npm install @astropods/adapter-mastra

Requires @mastra/core >= 1.14.0 as a peer dependency.

Quick start

import { Agent } from '@mastra/core/agent';
import { serve } from '@astropods/adapter-mastra';
const agent = new Agent({
name: 'My Agent',
instructions: 'You are a helpful assistant.',
model: 'openai/gpt-4o',
});
serve(agent);

serve(agent) connects to the messaging sidecar at localhost:9090 (or GRPC_SERVER_ADDR) and runs until the process exits. Under ast project and in production the address is injected for you.

API

serve(agent, options?)

Connects a Mastra Agent to the messaging service and starts listening.

ParameterTypeRequiredNotes
agentAgent (from @mastra/core)yesA Mastra Agent. Must have at least name, model, and instructions.
optionsServeOptions & MastraAdapterOptionsnoBridge overrides and agent capabilities.

ServeOptions

FieldTypeDefaultNotes
serverAddressstringprocess.env.GRPC_SERVER_ADDR || 'localhost:9090'Override the messaging gRPC address.

MastraAdapterOptions

FieldTypeDefaultNotes
supportsFilesbooleanfalseShow the chat’s upload controls. The chat hides them until an agent sets this. See Files in chat.
serve(agent, { supportsFiles: true });

This adapter passes attached images to the model as visual content. For any other file type it appends a line to the turn’s text naming each file and its path on the agent’s disk, so give the agent a tool that reads a path and the model can act on documents. The adapter does not read file bytes itself.

Give an agent that accepts documents a file-reading tool. Without one the model sees a path it cannot open, and it tends to describe a file it never read. For full control over how attachments are handled, author a custom adapter and read attachments in your own stream().

MastraAdapter

The class behind serve(). Use it directly if you need custom lifecycle control:

import { MastraAdapter } from '@astropods/adapter-mastra';
import { serve } from '@astropods/adapter-core';
serve(new MastraAdapter(agent));

How Mastra chunks map to hooks

Mastra fullStream chunkHook call
text-deltaonChunk(payload.text)
reasoning-startonStatusUpdate({ status: 'THINKING' })
reasoning-endonStatusUpdate({ status: 'GENERATING' })
tool-call-input-streaming-startonStatusUpdate({ status: 'PROCESSING', customMessage: 'Running <tool>' })
tool-call-input-streaming-endonStatusUpdate({ status: 'ANALYZING', customMessage: 'Finished <tool>' })
finishonFinish()
erroronError(err)

Other chunk types are ignored. Add new mappings by subclassing MastraAdapter and overriding stream() if you need custom behavior.

Memory

The adapter passes per-request context into Mastra’s memory and tracing, so conversation memory and per-user traces work out of the box. The Mastra memory thread is set to the conversation ID and resource is set to the user ID — no extra wiring needed.

Voice (STT + TTS)

If the wrapped Agent has a voice provider configured, the adapter handles audio messages automatically:

StepAction
1Receive audio_config + audio_chunks from the messaging sidecar.
2Call voice.listen(audioStream, { filetype }) for STT.
3Send the transcript back so the platform updates the placeholder.
4Run agent.stream(transcript, ...) to generate the reply.
5If voice.speak exists, synthesize TTS and stream audio chunks back.

filetype is derived from the incoming AudioStreamConfig.encoding (see Audio).

To enable voice, configure a Mastra voice provider when constructing the agent (see Mastra’s voice docs). If voice is absent, audio messages are rejected with a friendly error.

Tracing

When OTEL_EXPORTER_OTLP_ENDPOINT is set, serve() automatically wires Mastra observability so every LLM call and tool invocation produces a trace span. No code changes needed — Astro AI sets the env var on deployed agents.

The adapter also derives the turn’s W3C trace context from Mastra’s stream (traceId / spanId) and attaches it to the responses it emits, so any response — and the feedback that references it — can be correlated back to its trace. This is automatic; see Trace context for the wire shape.

Example: an agent with tools

import { Agent } from '@mastra/core/agent';
import { createTool } from '@mastra/core/tools';
import { serve } from '@astropods/adapter-mastra';
import { z } from 'zod';
const lookup = createTool({
id: 'customer_lookup',
description: 'Look up a customer by ID',
inputSchema: z.object({ id: z.string() }),
execute: async ({ context }) => {
return await fetch(`https://api.example.com/customers/${context.id}`).then(r => r.json());
},
});
const agent = new Agent({
name: 'Support Agent',
instructions: 'Help the user troubleshoot. Use customer_lookup when you need account details.',
model: 'anthropic/claude-sonnet-4-6',
tools: { lookup },
});
serve(agent);

Tool names and descriptions show up in the playground via getConfig(). When a tool runs, the user sees Running customer_lookupFinished customer_lookup as a status indicator.

Local development

ast project

ast project runs the messaging sidecar on localhost:9090, sets GRPC_SERVER_ADDR, and serves the chat interface at http://localhost:3100. The same serve(agent) code works locally and in production with no changes.

Troubleshooting

SymptomLikely cause
Waiting for messaging service (attempt N/10, ...)Sidecar isn’t up yet. Connection retries with exponential backoff.
Agent has no voice provider configuredAn audio message arrived but agent.voice is not configured.
Status indicator shows Running undefinedA tool was defined without an id. Set id on every createTool call.
Traces missingCheck OTEL_EXPORTER_OTLP_ENDPOINT is set.