Tool execution & cancellation
mastra-effect keeps tool handlers in Effect end to end. Mastra's agent loop still calls tools as async functions; the bridge converts at that boundary only.
Effect-native execute
import { Schema } from "effect"
import { Context, Effect, Layer } from "effect"
import { MastraTool, Toolkit, ToolHandlers } from "mastra-effect"
class ApiKey extends Context.Service<ApiKey, { readonly token: string }>()("app/ApiKey") {}
const SearchDocs = MastraTool.make("search_docs", {
description: "Search docs",
inputSchema: Schema.Struct({ query: Schema.String }),
outputSchema: Schema.String,
})
const toolkit = Toolkit.make(SearchDocs)
const handlers = Layer.succeed(ToolHandlers, {
search_docs: Effect.fn(function* ({ query }) {
const api = yield* ApiKey
return `${api.token}:${query}`
}),
})
const result = await Effect.runPromise(
SearchDocs.execute({ query: "layers" }, ctx).pipe(
Effect.provide(handlers),
Effect.provide(Layer.succeed(ApiKey, { token: "secret" })),
),
)Mastra agent loop bridge
Register tools on the agent with Agent.of (preferred) or toolkit.toMastraTools() for loose composition:
const mastraTools = yield* toolkit.toMastraTools()
// mastraTools.search_docs.execute(input, context) → Promise, backed by Effect.runPromiseCompared to bare Mastra createTool:
Mastra createTool | mastra-effect | |
|---|---|---|
| Handler type | async / Promise | Effect.fn with inferred R |
| Registration | Manual | Agent.of or toolkit.toMastraTools() |
| Per-request services | Manual wiring | Effect.provide at execute/stream |
| Cancellation | Mastra abortSignal on context | Propagated into Effect (see below) |
Abort and stream cancellation
Mastra sets abortSignal on ToolExecutionContext when the agent run or stream is cancelled. mastra-effect honors it in both execute paths:
toolkit.toMastraTools()— passescontext.abortSignaltoEffect.runPromise(..., { signal }).tool.execute/executeEffectTool— run as Effect programs; interrupt the fiber or pass{ signal }toEffect.runPromiseat the boundary.
When you consume agent.stream (from Agent.of) as an Effect Stream (fromMastraOutput), interrupting that stream cancels the underlying Mastra ReadableStream. Agent.of forwards Effect's tryPromise abort signal into Mastra abortSignal, so in-flight tool calls receive the same cancellation on their execute context.
Write interruptible handlers — long polls, sleeps, and I/O should use Effect APIs that respect fiber interruption or the Effect-provided signal inside Effect.tryPromise.
What stays Promise-bound
- Mastra's agent loop (
agent.generate,agent.streamfromAgent.of) dispatches tool calls as async functions. toolkit.toMastraTools()usesEffect.runPromiseonly at that boundary.
Everything else — handler registration, direct tool.execute, stream adapters — stays in Effect.