Skip to content

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 ​

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

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

Compared to bare Mastra createTool:

Mastra createToolmastra-effect
Handler typeasync / PromiseEffect.fn with inferred R
RegistrationManualAgent.of or toolkit.toMastraTools()
Per-request servicesManual wiringEffect.provide at execute/stream
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 honors it in both execute paths:

  1. toolkit.toMastraTools() — passes context.abortSignal to Effect.runPromise(..., { signal }).
  2. tool.execute / executeEffectTool — run as Effect programs; interrupt the fiber or pass { signal } to Effect.runPromise at 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.stream from Agent.of) 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.