@ratel-ai/mastra-adapter
The Mastra (@mastra/core) adapter for Ratel. ratel(config).adaptTo(mastra()) layers a framework-shaped view over the framework-neutral core (ADR-0013), so a Mastra agent registers its own createTool()s, hands the model Ratel's capability funnel, and gets per-turn recall — all in Mastra's native Tool and MastraDBMessage shapes, with no glue in app code.
Ratel keeps the model's tool list small and stable: instead of advertising every tool, it exposes three capability tools (search_capabilities / invoke_tool / get_skill_content) and injects a ranked, per-turn search_capabilities result for the current user message. The core owns all state and every guard (reserved ids, top-K clamp, first-registration-wins, recall-id counter); the adapter is just three codecs plus one recall idiom.
Usage
import { Agent } from "@mastra/core/agent";
import { createTool } from "@mastra/core/tools";
import { mastra } from "@ratel-ai/mastra-adapter";
import { ratel } from "@ratel-ai/sdk";
import { z } from "zod";
const r = ratel({ method: "hybrid", recallTopK: 5 }).adaptTo(mastra());
// Register the app's Mastra tools into the shared catalog (any time, also after
// expose()). Tools without an `execute` (client/provider-executed) pass through eagerly.
r.tools.register({
weather: createTool({
id: "weather",
description: "Get the weather in a location",
inputSchema: z.object({ location: z.string() }),
execute: async ({ location }) => ({ location, tempF: 72 }),
}),
});
const agent = new Agent({
name: "assistant",
instructions: "Help the user with their tasks.",
model: "openai/gpt-4o-mini",
// The three capability tools in Mastra shape. Take the set ONCE per agent and
// reuse it: it never changes across turns, so the prompt cache survives.
tools: r.expose(),
// Rank the catalog for each user turn and inject the synthetic search_capabilities
// call+result before the model runs (recall mode).
inputProcessors: [r.recallProcessor()],
});
const result = await agent.generate("what's the weather in Paris?");
console.log(result.text);
Standalone (framework-free) use of the same core is also fine — r is ratel(config) before .adaptTo, exposing native ExecutableTools. See @ratel-ai/sdk.
The recallProcessor() idiom
r.recallProcessor() returns a fresh Mastra Processor each call (so several agents each get their own). It implements processInput, which Mastra runs once at the start of every generation — i.e. once per user turn. On each turn it:
- reads the last message's text iff that message is the user's turn (multi-part text joins with newlines);
- ranks the catalog with the core's
recall(query); - if there are hits, appends the synthetic
search_capabilitiescall+result to the messages the model sees.
It is a no-op — spending no recall-id — when the last message is not a user turn, the user text is empty, or nothing matched. Because processInput runs once per generation (not per step), the pair is injected once and is not re-injected during the agent's tool-call loop.
Limitations
- Single-message recall encoding. A
MastraDBMessagehas notoolrole: a completed call+result is one assistant message withcontent.format: 2and a single resolvedtool-invocationpart. The recall pair is therefore encoded as one assistant message (Mastra renders it to the model as an assistant tool-call followed by a tool result). - Fabricated execution context for catalog-invoked tools. When the model runs one of your tools through
invoke_tool, the catalog calls itsexecute(args, context)with a minimal fabricated context ({ observe }no-op;mastra/agent/workflow/abortSignalabsent,requestContexta fresh empty one). A tool that reads those sees the fakes; tools that read only their input args are unaffected. Mastra re-validates the args against the tool's schema on this call; invalid args (or a requiredrequestContextSchemathe fabricated context can't satisfy) surface as a thrown error, which the capability funnel reports as a failed call. - Any Mastra tool schema works.
ingestreads Mastra's already-normalized input schema, so tools built with zod 3, zod 4, or a raw JSON Schema all catalog correctly — the adapter never re-converts schemas itself. (zodis a peer only because the exposed capability tools carry hand-written zod schemas.) - Persist the conversation across turns. Recall fires only when the last message is the user's turn. Standard Mastra memory hygiene applies; if you rebuild the message history per call, keep the user turn last so recall can find it.
Package shape
- Package name:
@ratel-ai/mastra-adapter - Pure TypeScript, zero runtime dependencies — the adapter is glue.
@mastra/core@^1.51.0,zod@^3.25.0 || ^4.0.0(matching Mastra's own zod peer), and@ratel-ai/sdkare peers the host already installs. - MIT (ADR-0009); member of the pnpm workspace;
publishConfigprovenance on.
Build & test
From the repo root (the SDK is built first by pnpm -r build, which the tests import):
pnpm --filter @ratel-ai/mastra-adapter build
pnpm --filter @ratel-ai/mastra-adapter typecheck
pnpm --filter @ratel-ai/mastra-adapter lint
pnpm --filter @ratel-ai/mastra-adapter test
The suite covers the three codecs, the recall processor (including id economy on the no-op paths), a mock-model integration test that drives the real Mastra Agent loop, a compile-only type-test locking the @mastra/core surface, and the @ratel-ai/sdk/testkit conformance battery (21 cases, 0 skipped).