Skip to content

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

ts
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():

ts
const mastraTools = yield * toolkit.toMastraTools()
// mastraTools.search_docs.execute(input, context) → Promise, backed by Effect.runPromise

Compared to bare Mastra createTool:

Mastra createTool@mastra-effect/core
Handler typeasync / PromiseEffect.fn with inferred R
RegistrationManualtoolkit.toMastraTools()
Per-request servicesManual wiringtoolkit.toLayer(Effect.fn(...))
CancellationMastra abortSignal on contextPropagated 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:

  1. toolkit.toMastraTools() — passes context.abortSignal to Effect.runPromise(..., { signal }).
  2. 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() uses Effect.runPromise only at that boundary.

Everything else — handler registration, direct tool.execute, stream adapters — stays in Effect.