Mastra adapter

Connect a Mastra Agent to the Astro runtime with one line
View as Markdown

@astropods/adapter-mastra wraps a Mastra Agent and connects it to the Astro 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

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

serve(agent) connects to the messaging sidecar at localhost:9090 (or GRPC_SERVER_ADDR) and runs until the process exits. Under ast dev 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.
optionsServeOptionsnoOverrides for the underlying MessagingBridge.

ServeOptions

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

MastraAdapter

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

1import { MastraAdapter } from '@astropods/adapter-mastra';
2import { serve } from '@astropods/adapter-core';
3
4serve(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 Messaging SDK — 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 sets the env var on deployed agents.

Example: an agent with tools

1import { Agent } from '@mastra/core/agent';
2import { createTool } from '@mastra/core/tools';
3import { serve } from '@astropods/adapter-mastra';
4import { z } from 'zod';
5
6const lookup = createTool({
7 id: 'customer_lookup',
8 description: 'Look up a customer by ID',
9 inputSchema: z.object({ id: z.string() }),
10 execute: async ({ context }) => {
11 return await fetch(`https://api.example.com/customers/${context.id}`).then(r => r.json());
12 },
13});
14
15const agent = new Agent({
16 name: 'Support Agent',
17 instructions: 'Help the user troubleshoot. Use customer_lookup when you need account details.',
18 model: 'anthropic/claude-sonnet-4-6',
19 tools: { lookup },
20});
21
22serve(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 dev project start

ast dev runs the messaging sidecar on localhost:9090, sets GRPC_SERVER_ADDR, and opens the bundled playground at http://localhost:8080. 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.