Tool execution & cancellation
@mastra-effect/core 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 } from "@mastra-effect/core"
class ApiKey extends Context.Service<ApiKey, { readonly token: string }>()("app/ApiKey") {}
const SearchDocs = MastraTool.make({
id: "search_docs",
description: "Search docs",
inputSchema: Schema.Struct({ query: Schema.String }),
outputSchema: Schema.String,
})
const toolkit = Toolkit.make(SearchDocs)
const handlers = toolkit.toLayer(
Effect.fn(function* () {
return {
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 toolkit.toMastraTools():
const mastraTools = yield * toolkit.toMastraTools()
// mastraTools.search_docs.execute(input, context) → Promise, backed by Effect.runPromiseCompared to bare Mastra createTool:
Mastra createTool | @mastra-effect/core | |
|---|---|---|
| Handler type | async / Promise | Effect.fn with inferred R |
| Registration | Manual | toolkit.toMastraTools() |
| Per-request services | Manual wiring | toolkit.toLayer(Effect.fn(...)) |
| 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/core honors it in both execute paths:
toolkit.toMastraTools()— passescontext.abortSignaltoEffect.runPromise(..., { signal }).tool.execute/executeEffectTool— races the handler against the signal and interrupts the fiber when it fires.
When you consume Agent.stream as an Effect Stream (fromMastraOutput), interrupting that stream cancels the underlying Mastra ReadableStream. Mastra aborts the run in parallel; any in-flight tool call receives the same abortSignal on its execute context.
Write interruptible handlers — long polls, sleeps, and I/O should use Effect APIs that respect fiber interruption or the Effect-provided abort signal inside Effect.tryPromise.
What stays Promise-bound
- Mastra's agent loop (
Agent.generate,Agent.stream) 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.