Getting started
mastra-effect wraps Mastra with Effect 4. Use it when you want typed tool handlers, Layer-based dependencies, and Assistant UI integration for human-in-the-loop flows — without replacing Mastra's agent loop.
Early / experimental. API may change and Mastra surface coverage is incomplete.
Install
pnpm add mastra-effect effect@rc @mastra/core
# optional — only when using MastraMemory
pnpm add @mastra/memoryFor client HITL rendering:
pnpm add mastra-effect @assistant-ui/react reactPeer dependencies
| Package | Role |
|---|---|
effect@^4.0.0-rc.113 | Effect 4 runtime |
@mastra/core@^1.66.0 | Agent, tools, stream types |
@mastra/memory@^1.29.0 | Memory implementation (optional peer) |
@assistant-ui/react | Assistant UI runtime (mastra-effect/assistant-ui) |
react | UI runtime (mastra-effect/assistant-ui) |
Server tools
Define a tool, build a toolkit, and wire an agent with Context.Service + Agent.of:
import { Agent, MastraTool, Toolkit } from "mastra-effect"
import { Context, Effect, Layer, Schema } from "effect"
class ApiKey extends Context.Service<ApiKey, { readonly token: string }>()("app/ApiKey") {}
const SearchDocs = MastraTool.make("search_docs", {
description: "Search project documentation",
inputSchema: Schema.Struct({ query: Schema.String }),
outputSchema: Schema.String,
})
const toolkit = Toolkit.make(SearchDocs)
class DocsAgent extends Context.Service<DocsAgent>()("app/DocsAgent", {
make: Effect.gen(function* () {
return yield* Agent.of("docs-agent", {
name: "Docs Agent",
instructions: "Be helpful.",
model: "openai/gpt-4o",
toolkit,
handlers: {
search_docs: Effect.fn(function* ({ query }) {
const api = yield* ApiKey
return `${api.token}:${query}`
}),
},
})
}),
}) {
static readonly layer = Layer.effect(DocsAgent, DocsAgent.make)
}
Effect.gen(function* () {
const agent = yield* DocsAgent
return yield* agent.stream("Find docs").pipe(
Effect.provide(Layer.succeed(ApiKey, { token: process.env.API_KEY! })),
)
}).pipe(Effect.provide(DocsAgent.layer), Effect.runPromise)DocsAgent.layer carries the agent service only. Handler dependencies like ApiKey are required on the stream/generate Effect — TypeScript errors if you omit Effect.provide(Layer.succeed(ApiKey, …)).
For Mastra ↔ Effect span stitching at the tool FFI, provide MastraObservability.layer() in your app layer — see Observability.
See Agents for service patterns.
Client HITL
Two patterns — see Client tools for the full model.
Assistant UI human tool (annotate with a Tool UI schema):
import { Schema } from "effect"
import { MastraTool, ToolUI } from "mastra-effect"
const PickAssetSchema = ToolUI.make("pick_asset", {
stateSchema: Schema.Struct({ prompt: Schema.String }),
answerSchema: Schema.String,
})
const PickAsset = MastraTool.make("pick_asset", {
description: "Pick an asset",
inputSchema: Schema.Struct({ prompt: Schema.String }),
outputSchema: Schema.String,
}).annotate(ToolUI.Schema, PickAssetSchema)Stream-only status card (extra handler key):
import { ClientToolkit } from "mastra-effect/assistant-ui"
const clientToolkit = ClientToolkit.of(toolkit, {
pick_asset: ({ toolUI, resolve }) => <AssetPicker state={toolUI.state} onConfirm={resolve} />,
status: ({ toolUI }) => <StatusCard text={toolUI.state.text} />,
})See HITL suspend for server-tool suspend.
Package layout
packages/mastra-effect — mastra-effect (subpath exports)
docs/ — documentation siteDevelopment
vp install
vp check
vp run -r test
vp run docs:dev