@interopio/ai-mastra-bridge
v1.1.0
Published
AG-UI protocol bridge for Mastra agent runtimes
Readme
io.Intelligence Mastra Bridge
@interopio/ai-mastra-bridge is an AG-UI protocol bridge for Mastra agent runtimes. It translates between the AG-UI streaming protocol and Mastra's agent API, exposing a set of Hono route handlers for streaming, thread management, and agent discovery.
Table of Contents
- Introduction
- Prerequisites
- Installation
- Quick Start
- Core Concepts
- API Reference
- Configuration
- TypeScript Types
- Architecture
- AG-UI Event Reference
- Advanced Topics
- Troubleshooting
Introduction
@interopio/ai-mastra-bridge bridges Mastra's proprietary streaming model and the open AG-UI protocol. It provides:
- AG-UI SSE Streaming: Translates Mastra's stream chunks into typed AG-UI Server-Sent Events with explicit lifecycle, text, and tool call phases.
- Thread CRUD: REST endpoints for creating, listing, updating, and deleting conversation threads backed by Mastra's memory system.
- Agent Discovery: Endpoints for listing and inspecting available Mastra agents.
- Frontend Tool Execution: Exposes frontend-declared tools to Mastra as proxy backend tools so the frontend can execute them while Mastra keeps control of memory and the tool loop.
- Server Tool Result Capture: Captures server-side tool execution results and emits them as AG-UI events even when Mastra omits the corresponding stream chunk.
- Client Tool Result Reporting: Accepts per-tool results on a dedicated endpoint so a single live Mastra run can resume naturally after each frontend tool finishes.
Why This Exists
Mastra's native streaming model uses opaque, inconsistently structured events that require workarounds to extract tool results, deduplicate events, and handle edge cases. This bridge replaces that with the AG-UI protocol — an open, typed, well-specified streaming standard with explicit tool lifecycle events and clean run boundaries.
Target Audience
This package is for developers building backends that:
- Use Mastra as the agent runtime.
- Need AG-UI protocol compliance for frontend streaming.
- Want a clean, versioned API contract between the frontend and backend.
- Require thread management independent of Mastra's built-in REST API.
Prerequisites
- Node.js 18+ — Required for
ReadableStream,TextEncoder, andcrypto.randomUUID(). reflect-metadata— Must be imported once at the entry point of your host application. This is a standard inversify requirement. The bridge manages its own DI container internally, butreflect-metadatamust be globally available.- At least one Mastra agent with memory configured — Thread operations (create, list, get, update, delete, and message retrieval) require a Mastra memory instance. The bridge resolves memory lazily from the first agent that has one configured.
Installation
npm install @interopio/ai-mastra-bridgePeer Dependencies
The following packages are peer dependencies and must be installed separately:
npm install @mastra/core hono| Package | Version |
|---------|---------|
| @mastra/core | ^1.0.4 |
| hono | ^4.0.0 |
Quick Start
import "reflect-metadata"; // Must be first
import { IoMastraBridgeFactory } from "@interopio/ai-mastra-bridge";
import { Mastra } from "@mastra/core/mastra";
import { myAgent } from "./agents/my-agent";
const bridge = IoMastraBridgeFactory({ prefix: "/io-bridge" });
export const mastra = new Mastra({
agents: { myAgent },
server: {
apiRoutes: [...bridge.createHonoRoutes()],
},
});This registers 10 routes under the configured prefix:
| Method | Path | Description |
|--------|------|-------------|
| POST | /io-bridge/run | AG-UI streaming endpoint |
| POST | /io-bridge/runs/:runId/tool-calls/:toolCallId/result | Submit one frontend tool result back to an active run |
| GET | /io-bridge/agents | List all agents |
| GET | /io-bridge/agents/:agentId | Get agent by ID |
| POST | /io-bridge/threads | Create a thread |
| GET | /io-bridge/threads | List threads |
| GET | /io-bridge/threads/:threadId | Get a thread |
| PATCH | /io-bridge/threads/:threadId | Update a thread |
| DELETE | /io-bridge/threads/:threadId | Delete a thread |
| GET | /io-bridge/threads/:threadId/messages | Get thread messages |
Core Concepts
AG-UI Protocol
AG-UI (Agent-User Interaction Protocol) is an open protocol for streaming agent responses to frontends. It defines typed events for run lifecycle, text streaming, tool calls, and step boundaries — all delivered over Server-Sent Events (SSE).
Key advantages over Mastra's native streaming:
- Explicit tool lifecycle:
TOOL_CALL_START→TOOL_CALL_ARGS→TOOL_CALL_END— no buried payloads. - Clean run boundaries:
RUN_STARTED→ events →RUN_FINISHED|RUN_ERROR. - Typed events: Every event has a defined schema.
- Debuggable: Standard SSE format, visible in browser DevTools.
Stream Translation
The bridge reads Mastra's agent.stream().fullStream (an async iterable of proprietary chunks) and emits AG-UI events via SSE. Translation is stateful per request — the translator tracks open text messages and tool calls to ensure all sequences are properly closed before any terminal event.
The following table shows how Mastra stream chunks map to AG-UI events:
| Mastra Chunk Type | AG-UI Event(s) Emitted |
|---|---|
| text-start | TEXT_MESSAGE_START |
| text-delta | TEXT_MESSAGE_CONTENT (auto-opens message if needed) |
| text-end | TEXT_MESSAGE_END |
| tool-call-input-streaming-start | TOOL_CALL_START |
| tool-call-delta | TOOL_CALL_ARGS |
| tool-call-input-streaming-end | TOOL_CALL_END |
| tool-call (atomic) | TOOL_CALL_START + TOOL_CALL_ARGS + TOOL_CALL_END |
| tool-result | TOOL_CALL_RESULT |
| step-start | STEP_STARTED |
| step-finish | STEP_FINISHED |
| finish | RUN_FINISHED |
| error | RUN_ERROR |
Deduplication note: Mastra sometimes replays atomic tool-call chunks after already emitting them via the streaming variant (tool-call-input-streaming-start / tool-call-delta / tool-call-input-streaming-end). The bridge tracks emitted tool call IDs and silently discards duplicate atomic replays.
Tool Execution Model
The bridge supports AG-UI's frontend tool execution model for tools the LLM delegates to the client:
- The LLM decides to call a frontend tool and emits
TOOL_CALL_START/TOOL_CALL_ARGS/TOOL_CALL_END. - The frontend executes the tool as soon as
TOOL_CALL_ENDarrives. - The frontend sends
POST /runs/:runId/tool-calls/:toolCallId/result. - The bridge resolves the pending proxy tool execution inside Mastra.
- Mastra resumes the same run, emits
TOOL_CALL_RESULT, and continues reasoning with memory intact.
Frontend tools are not registered with Mastra as clientTools. The bridge creates temporary proxy backend tools through toolsets so Mastra remains inside its supported execution and persistence flow.
For server-side tools (tools registered directly with the Mastra agent), the bridge intercepts execution results and emits them as TOOL_CALL_RESULT events. See Server Tool Result Capture.
Thread Management
Threads are persistent conversation sessions backed by Mastra's memory system. The bridge exposes a full CRUD surface that proxies to Mastra internally, so frontends never need to interact with Mastra's own REST API.
Memory and Thread Persistence
The bridge resolves the Mastra memory instance lazily: on the first call that requires memory, it iterates over all registered agents and uses the memory from the first agent that has one configured. The resolved instance is cached for subsequent calls.
If no agent has memory configured:
- Thread CRUD operations throw an error with
"No agent with memory configured found". - Streaming without a
threadIdworks normally (stateless mode). - Streaming with a
threadIdbut without memory logs a warning and skips persistence.
API Reference
IoMastraBridge Factory Function
IoMastraBridgeFactory(config?)
Creates a bridge instance. The factory validates the provided configuration and returns an IoMastraBridge.API object.
Parameters:
| Name | Type | Required | Description |
|------|------|----------|-------------|
| config | IoMastraBridge.BridgeConfig | No | Bridge configuration. Defaults to {} (prefix "/io-bridge"). |
Returns: IoMastraBridge.API
Throws: ValidationError if config fails schema validation (for example, if prefix does not start with "/").
Example:
import { IoMastraBridgeFactory } from "@interopio/ai-mastra-bridge";
// Default prefix: "/io-bridge"
const bridge = IoMastraBridgeFactory();
// Custom prefix
const bridge = IoMastraBridgeFactory({ prefix: "/api/v1/agent" });IoMastraBridge.API
bridge.createHonoRoutes()
Returns an array of Hono route definitions compatible with Mastra's apiRoutes configuration.
Returns: IoMastraBridge.HonoRoute[]
Each element in the returned array contains:
| Property | Type | Description |
|----------|------|-------------|
| path | string | Full route path including the configured prefix. |
| method | "GET" \| "POST" \| "PUT" \| "DELETE" \| "PATCH" \| "ALL" | HTTP method. |
| handler | (c: Context) => Response \| Promise<Response> | Hono request handler. |
Example:
const bridge = IoMastraBridgeFactory({ prefix: "/io-bridge" });
export const mastra = new Mastra({
agents: { myAgent },
server: {
apiRoutes: [...bridge.createHonoRoutes()],
},
});Route Endpoints
POST /run
Starts a streaming agent run. Accepts an AG-UI RunAgentInput request body and returns an SSE stream of AG-UI events.
Request Body: RunAgentInput (AG-UI standard type)
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| threadId | string | Yes | Thread identifier for the conversation. |
| runId | string | Yes | Unique identifier for this run. |
| messages | Message[] | Yes | Full conversation history in AG-UI message format. |
| tools | Tool[] | No | Frontend tool declarations. Registered as client tools so the LLM can call them. |
| context | Context[] | No | AG-UI context items (passed through as-is). |
| forwardedProps | object | No | Application-specific properties passed to the backend. |
forwardedProps fields:
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| agentId | string | No | "io-agent" | ID or registration key of the Mastra agent to use. |
| resourceId | string | No | "default" | User or resource identifier for memory association. |
| memoryOptions | object | No | — | Mastra memory options passed directly to agent.stream(). |
| structuredOutput | object \| string | No | — | JSON Schema (object or JSON string) that constrains the LLM response format. See Structured Output. |
Response: text/event-stream — SSE stream of AG-UI events.
Example request:
const response = await fetch("/io-bridge/run", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
threadId: "thread-123",
runId: crypto.randomUUID(),
messages: [
{ id: "msg-1", role: "user", content: "What is the weather in London?" },
],
tools: [
{
name: "get_weather",
description: "Fetches current weather for a city.",
parameters: {
type: "object",
properties: {
city: { type: "string" },
},
required: ["city"],
},
},
],
context: [],
forwardedProps: {
agentId: "my-agent",
resourceId: "user-42",
},
}),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
process.stdout.write(decoder.decode(value));
}GET /agents
Lists all registered Mastra agents.
Response: 200 OK — AgentInfo[]
[
{ "id": "my-agent", "name": "My Agent" },
{ "id": "another-agent", "name": "Another Agent" }
]GET /agents/:agentId
Gets a single agent by ID.
Path Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| agentId | string | Agent ID or registration key. |
Response: 200 OK — AgentInfo
Errors:
| Status | Code | Condition |
|--------|------|-----------|
| 400 | INVALID_INPUT | agentId path parameter is empty. |
| 404 | AGENT_NOT_FOUND | No agent with the given ID exists. |
POST /threads
Creates a new conversation thread.
Request Body:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| resourceId | string | Yes | User or resource identifier to associate with the thread. |
| title | string | No | Human-readable thread title. |
| metadata | Record<string, unknown> | No | Arbitrary metadata to store with the thread. |
Response: 201 Created — ThreadInfo
Errors:
| Status | Code | Condition |
|--------|------|-----------|
| 400 | INVALID_INPUT | Request body fails validation (e.g., missing resourceId). |
Example:
const response = await fetch("/io-bridge/threads", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
resourceId: "user-42",
title: "My Conversation",
metadata: { source: "web-app" },
}),
});
const thread = await response.json();
// { id: "...", resourceId: "user-42", title: "My Conversation", createdAt: "...", updatedAt: "..." }GET /threads
Lists threads for a resource.
Query Parameters:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| resourceId | string | Yes | Filter threads by resource/user ID. |
| agentId | string | No | Accepted but currently has no effect on the result set. |
Response: 200 OK — ThreadInfo[]
Errors:
| Status | Code | Condition |
|--------|------|-----------|
| 400 | INVALID_INPUT | resourceId query parameter is missing or empty. |
GET /threads/:threadId
Gets a thread by ID.
Path Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| threadId | string | Thread ID. |
Response: 200 OK — ThreadInfo
Errors:
| Status | Code | Condition |
|--------|------|-----------|
| 400 | INVALID_INPUT | threadId path parameter is empty. |
| 404 | THREAD_NOT_FOUND | No thread with the given ID exists. |
PATCH /threads/:threadId
Updates a thread's title or metadata. Metadata updates are merged with the existing metadata (not replaced).
Path Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| threadId | string | Thread ID. |
Request Body:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| title | string | No | New thread title. |
| metadata | Record<string, unknown> | No | Metadata fields to merge into the existing metadata. |
Response: 200 OK — ThreadInfo
Errors:
| Status | Code | Condition |
|--------|------|-----------|
| 400 | INVALID_INPUT | Path parameter or body fails validation. |
| 404 | THREAD_NOT_FOUND | No thread with the given ID exists. |
DELETE /threads/:threadId
Deletes a thread.
Path Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| threadId | string | Thread ID. |
Response: 204 No Content
Errors:
| Status | Code | Condition |
|--------|------|-----------|
| 400 | INVALID_INPUT | threadId path parameter is empty. |
| 404 | THREAD_NOT_FOUND | No thread with the given ID exists. |
GET /threads/:threadId/messages
Retrieves persisted messages for a thread. Returns 404 if the thread does not exist.
Path Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| threadId | string | Thread ID. |
Query Parameters:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| limit | number | No | Maximum number of messages to return. Must be a positive integer. |
| offset | number | No | Page number offset (zero-based) for pagination. Must be a non-negative integer. |
Response: 200 OK — ThreadMessage[]
Errors:
| Status | Code | Condition |
|--------|------|-----------|
| 400 | INVALID_INPUT | Path parameter or query parameter fails validation. |
| 404 | THREAD_NOT_FOUND | No thread with the given ID exists. |
Configuration
BridgeConfig
interface BridgeConfig {
/**
* Route path prefix for all registered routes.
* Must start with "/". Defaults to "/io-bridge".
*/
prefix?: string;
}The prefix is prepended to every route path. For example, with prefix: "/api/v1", the streaming endpoint becomes POST /api/v1/run.
Examples:
// Default — all routes under /io-bridge/
IoMastraBridgeFactory()
// All routes under /api/v1/
IoMastraBridgeFactory({ prefix: "/api/v1" })
// All routes under /agent/bridge/
IoMastraBridgeFactory({ prefix: "/agent/bridge" })TypeScript Types
All public types are exported from the IoMastraBridge namespace. Import them as follows:
import type { IoMastraBridge } from "@interopio/ai-mastra-bridge";BridgeConfig
Configuration accepted by IoMastraBridgeFactory.
interface IoMastraBridge.BridgeConfig {
prefix?: string; // Defaults to "/io-bridge". Must start with "/".
}HonoRoute
A single route definition returned by createHonoRoutes().
interface IoMastraBridge.HonoRoute {
path: string;
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "ALL";
handler: (c: Context) => Response | Promise<Response>;
}API
The object returned by IoMastraBridgeFactory.
interface IoMastraBridge.API {
createHonoRoutes(): IoMastraBridge.HonoRoute[];
}AgentInfo
Represents a registered Mastra agent.
interface IoMastraBridge.AgentInfo {
id: string;
name: string;
model?: {
provider: string;
name: string;
};
}Properties:
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| id | string | Yes | Agent ID as registered with Mastra. |
| name | string | Yes | Human-readable agent name. |
| model | object | No | Model information if available from the agent. |
| model.provider | string | Yes (if model present) | LLM provider name (e.g., "openai"). |
| model.name | string | Yes (if model present) | Model name (e.g., "gpt-4o"). |
ToolDeclaration
A frontend tool declaration sent in the POST /run request body.
interface IoMastraBridge.ToolDeclaration {
name: string;
description: string;
parameters?: unknown; // JSON Schema object
}Tools declared here are proxied by the bridge into temporary Mastra backend tools for the current run. The frontend still performs the real execution, but Mastra sees a normal backend tool call and keeps ownership of memory and the tool loop.
ThreadInfo
Represents a conversation thread.
interface IoMastraBridge.ThreadInfo {
id: string;
resourceId: string;
title?: string;
createdAt: string; // ISO 8601
updatedAt: string; // ISO 8601
metadata?: Record<string, unknown> | null;
}ThreadMessage
A single message in a thread's history.
interface IoMastraBridge.ThreadMessage {
id: string;
role: string; // "user" | "assistant" | "system" | "tool"
content: unknown; // Format depends on role and Mastra version
createdAt: string; // ISO 8601
threadId: string;
}StreamAgentParams
Parameters used internally when calling agent.stream(). Exposed as a type for reference.
interface IoMastraBridge.StreamAgentParams {
messages: unknown[];
runId: string;
threadId?: string;
resourceId?: string;
memoryOptions?: unknown;
abortSignal?: AbortSignal;
tools?: IoMastraBridge.ToolDeclaration[];
structuredOutput?: unknown;
}AgentStreamResult
Return type of the internal stream call. Exposed as a type for reference.
interface IoMastraBridge.AgentStreamResult {
fullStream: AsyncIterable<unknown>;
onSuccess?: () => Promise<void>;
onError?: (error: unknown) => Promise<void>;
onAbort?: (reason: unknown) => Promise<void>;
}CreateThreadParams
Parameters for thread creation (maps to the POST /threads body).
interface IoMastraBridge.CreateThreadParams {
resourceId: string;
title?: string;
metadata?: Record<string, unknown>;
}ListThreadsParams
Parameters for listing threads (maps to GET /threads query parameters).
interface IoMastraBridge.ListThreadsParams {
resourceId: string;
agentId?: string;
}UpdateThreadParams
Parameters for updating a thread (maps to the PATCH /threads/:threadId body).
interface IoMastraBridge.UpdateThreadParams {
title?: string;
metadata?: Record<string, unknown>;
}GetMessagesParams
Parameters for retrieving thread messages (maps to GET /threads/:threadId/messages query parameters).
interface IoMastraBridge.GetMessagesParams {
limit?: number;
offset?: number;
}IoMastraBridgeFactoryFunction
The type of the exported IoMastraBridgeFactory function.
type IoMastraBridgeFactoryFunction =
(config?: IoMastraBridge.BridgeConfig) => IoMastraBridge.API;Architecture
Module Structure
libs/ai-mastra-bridge/
├── src/
│ ├── index.ts # Package entry: IoMastraBridgeFactory + type re-exports
│ ├── factory.ts # Factory function implementation
│ ├── common/
│ │ └── constants.ts # DI token identifiers
│ ├── main/
│ │ ├── container.ts # Inversify DI container setup
│ │ └── controller.ts # BridgeController — route creation and request handling
│ ├── services/
│ │ ├── agent-provider.ts # Agent listing, resolution, and streaming
│ │ ├── thread-storage.ts # Thread and message CRUD via Mastra memory
│ │ ├── message-converter.ts # AG-UI <-> Mastra message format conversion
│ │ ├── stream-translator.ts # Mastra chunks -> AG-UI SSE events
│ │ ├── continuation-persistence.ts # Post-stream message saving for continuation runs
│ │ └── server-tool-result-capture.ts # Intercepts server tool results for SSE emission
│ └── shared/
│ ├── schemas.ts # Zod schemas for all request/response boundaries
│ ├── validation.ts # Generic Zod validation helper
│ ├── errors.ts # NotFoundError, ValidationError
│ ├── route-definitions.ts # Static route metadata (path, method, name)
│ ├── private-interfaces.ts # Internal interfaces and Mastra duck types
│ └── private-types.ts # Internal Mastra memory parameter types
├── ai-mastra-bridge.d.ts # Public type declarations (IoMastraBridge namespace)
├── package.json
├── tsconfig.json
└── rollup.config.jsDependency Injection
All internal services are managed by inversify. The container is private and never exposed to consumers.
Container
├── BridgeController (singleton) — Route creation and request dispatch
├── AgentProvider (singleton) — Agent resolution and stream initiation
├── ThreadStorage (singleton) — Thread and message CRUD
├── MessageConverter (singleton) — AG-UI <-> Mastra message conversion
├── StreamTranslator (singleton) — Mastra stream -> AG-UI SSE translation
├── ServerToolResultCaptureService (singleton) — Server tool execution interception
└── ProxyToolRegistryService (singleton) — In-memory run/tool-call wait/resolve registryStream Translation Pipeline
POST /run request
│
▼
BridgeController.handleRun()
│ Validates RunAgentInput (Zod)
│ Converts AG-UI messages -> Mastra format
▼
AgentProvider.streamAgent()
│ Resolves agent by ID
│ Wraps server tools for result capture
│ Builds proxy frontend tools through Mastra toolsets
│ Calls agent.stream() with memory when thread-backed
│ Returns translator lifecycle hooks for proxy-tool cleanup
▼
AsyncIterable<MastraChunk>
│
▼
StreamTranslator.translate()
│ Creates fresh TranslationState per call
│ Emits RUN_STARTED
│ Maps each Mastra chunk to AG-UI event(s)
│ Closes open sequences before terminal events
│ Emits RUN_FINISHED or RUN_ERROR
│ Fires onSuccess/onError/onAbort after the terminal AG-UI event
▼
ReadableStream<Uint8Array> (AG-UI SSE format)
│
▼
HTTP Response (text/event-stream)Server Tool Result Capture
Mastra does not consistently emit tool-result stream chunks for server-side tools in all scenarios (notably in mixed-turn agentic loops). The ServerToolResultCaptureService addresses this by monkey-patching the execute function of each server tool before the stream starts.
When a patched tool executes, its result (or error) is stored keyed by toolCallId. After the stream finishes, the translator checks whether each server-owned tool call received a raw tool-result chunk. For any that did not, it retrieves the captured result and emits a synthetic TOOL_CALL_RESULT AG-UI event.
Tools are wrapped once per agent (idempotent — already-wrapped tools are skipped).
Proxy Tool Registry
Frontend-declared tools are converted into temporary Mastra backend tools for each run. When Mastra executes one of those proxy tools, the bridge registers a pending promise keyed by runId + toolCallId and waits for the frontend to submit the result.
The ProxyToolRegistryService is responsible for:
- creating pending entries for active proxy tool executions
- resolving results received from
POST /runs/:runId/tool-calls/:toolCallId/result - rejecting unresolved entries if the run aborts or the stream closes unexpectedly
- enforcing a hard-coded five minute timeout per pending frontend tool
AG-UI Event Reference
Lifecycle Events
Every stream begins with RUN_STARTED and ends with exactly one of RUN_FINISHED or RUN_ERROR. No events are emitted after the terminal event.
RUN_STARTED { type, threadId, runId, timestamp }
... stream events ...
RUN_FINISHED { type, threadId, runId, timestamp }Text Streaming Events
Text content is streamed as a three-event sequence:
TEXT_MESSAGE_START { type, messageId, role: "assistant", timestamp }
TEXT_MESSAGE_CONTENT { type, messageId, delta: "Hello", timestamp }
TEXT_MESSAGE_CONTENT { type, messageId, delta: " world", timestamp }
TEXT_MESSAGE_END { type, messageId, timestamp }If Mastra emits a text-delta chunk before a text-start chunk, the bridge auto-opens a text message to preserve AG-UI lifecycle invariants.
Tool Call Events
Tool calls follow a four-event lifecycle:
TOOL_CALL_START { type, toolCallId, toolCallName, parentMessageId, timestamp }
TOOL_CALL_ARGS { type, toolCallId, delta: '{"query":', timestamp }
TOOL_CALL_ARGS { type, toolCallId, delta: '"search term"}', timestamp }
TOOL_CALL_END { type, toolCallId, timestamp }Server-side tool results are emitted as:
TOOL_CALL_RESULT { type, messageId, toolCallId, content: "...", role: "tool", timestamp }messageId for tool results follows the format "tool-result-{toolCallId}".
Step Events
Mastra step boundaries are forwarded as:
STEP_STARTED { type, stepName: "step", timestamp }
STEP_FINISHED { type, stepName: "step", timestamp }Advanced Topics
Multi-Turn Tool Loops
When the LLM calls a frontend tool, the bridge keeps Mastra in charge of the tool loop by exposing that tool as a temporary backend proxy:
Single run: POST /run
→ RUN_STARTED
→ TEXT_MESSAGE_* (optional reasoning)
→ TOOL_CALL_START / TOOL_CALL_ARGS / TOOL_CALL_END
(frontend executes the tool after TOOL_CALL_END)
POST /runs/:runId/tool-calls/:toolCallId/result
→ bridge resolves proxy tool promise
→ Mastra emits TOOL_CALL_RESULT
→ TEXT_MESSAGE_* (follow-up or final answer)
→ RUN_FINISHEDThis design removes the old continuation rerun workaround. Mastra now persists tool calls, tool results, and follow-up assistant output through its own normal memory flow.
Structured Output
Pass a JSON Schema to forwardedProps.structuredOutput to constrain the LLM to respond with valid JSON:
forwardedProps: {
structuredOutput: {
type: "object",
properties: {
answer: { type: "string" },
confidence: { type: "number", minimum: 0, maximum: 1 },
},
required: ["answer", "confidence"],
},
}The bridge injects a system instruction before the user messages, instructing the LLM to return only raw JSON matching the schema (no markdown fences, no commentary). This approach preserves streaming text output — the structured JSON arrives as TEXT_MESSAGE_CONTENT events rather than tool call events.
structuredOutput can be provided as either a JSON Schema object or a pre-serialized JSON string.
Agent Resolution
The bridge resolves an agent from a given agentId using a two-step fallback:
- Calls
mastra.getAgentById(agentId)— matches theagent.idproperty. - If that throws, calls
mastra.getAgent(agentId)— matches the agent's registration key in theMastraconstructor. - If both throw, returns a
404response with codeAGENT_NOT_FOUND.
The default agentId when none is specified in forwardedProps is "io-agent".
Error Handling
REST endpoint errors
| Error Class | HTTP Status | Response Body |
|---|---|---|
| ValidationError | 400 | { error: string, code: "INVALID_INPUT", details: Array<{ path: string, message: string }> } |
| NotFoundError | 404 | { error: string, code: "AGENT_NOT_FOUND" \| "THREAD_NOT_FOUND" } |
| Unhandled exceptions | 500 | { error: "Internal server error: ...", code: "INTERNAL_ERROR" } |
Stream errors
Errors during stream translation emit a RUN_ERROR event before closing:
RUN_ERROR { type, message: "...", code: "STREAM_ERROR", timestamp }Open text messages and tool calls are always closed before any terminal event (RUN_FINISHED or RUN_ERROR) to maintain AG-UI lifecycle invariants. Optional onSuccess, onError, and onAbort hooks run only after the terminal AG-UI event has already been emitted.
Troubleshooting
Streams end immediately with RUN_ERROR
Check that reflect-metadata is imported before any inversify-decorated class is loaded. Missing reflect-metadata causes the DI container to fail during the first request.
Thread operations return 500 with "No agent with memory configured found"
At least one of your Mastra agents must have a memory property configured. The bridge uses the first agent's memory for all thread operations. Verify your agent definition includes a valid Mastra memory adapter.
Tool results are missing from the SSE stream for server-side tools
The bridge attempts to capture server tool results via execution interception. This requires the agent to expose listTools() and for each tool to have an execute function. If either condition is not met, a warning is logged. Inspect your Mastra agent and tool configuration.
Frontend tool runs hang and then fail
The bridge waits up to five minutes for each frontend tool result. Check that the frontend is posting to POST /runs/:runId/tool-calls/:toolCallId/result after TOOL_CALL_END, and verify that the runId and toolCallId match the active stream.
Agent not found despite correct ID
The bridge tries both getAgentById (matches agent.id) and getAgent (matches the registration key). If both fail, a 404 is returned. Verify that the agentId passed in forwardedProps matches either the id property of the agent class or its key in the agents map passed to the Mastra constructor.
License
MIT — see LICENSE for details.
Author: Interop.IO
