Tools and Toolkits
Tools let a language model call your code. You describe each tool with a name, a
description, and a Schema for its parameters; the model decides when to call it
and fills in the parameters; the framework decodes them, runs your handler, and
feeds the result back to the model. Group related tools into a toolkit and
hand it to LanguageModel.generateText — resolution happens automatically.
Defining a tool
Section titled “Defining a tool”A Tool pairs a parameter schema (what the model fills in) with a success schema
(what your handler returns). Descriptions on the tool and on individual parameters
are sent to the model to help it decide when and how to call.
import { Effect, Schema } from "effect"import { Tool } from "effect/unstable/ai"
const ProductId = Schema.String.pipe(Schema.brand("ProductId")).annotate({ description: "A unique identifier for a product, e.g. 'p-123'"})
class Product extends Schema.Class<Product>("acme/domain/Product")({ id: ProductId, name: Schema.String, price: Schema.Number}) {}
const SearchProducts = Tool.make("SearchProducts", { description: "Search the product catalog by keyword", parameters: Schema.Struct({ query: Schema.String.annotate({ // Per-parameter descriptions give the model even better guidance. description: "The search query, e.g. 'wireless headphones'" }), // Schema defaults mean the model can omit this and still get a valid value. maxResults: Schema.Number.pipe( Schema.withDecodingDefault(Effect.succeed(10)) ).annotate({ description: "Maximum number of results to return" }) }), // The handler must return a value matching this schema. success: Schema.Array(Product), // How handler errors are surfaced: // - "error" (default): handler failures go to the calling effect's error channel. // - "return": handler failures are captured and returned as the tool result. failureMode: "error"})
const GetInventory = Tool.make("GetInventory", { description: "Check current stock level for a product", parameters: Schema.Struct({ productId: ProductId }), success: Schema.Struct({ productId: ProductId, available: Schema.Number })})A tool with no parameters can omit parameters entirely; Tool.make defaults it
to Tool.EmptyParams (an empty object). Likewise success
defaults to Schema.Void and failure to Schema.Never.
const GetCurrentTime = Tool.make("GetCurrentTime", { description: "Returns the current timestamp", success: Schema.Number})Grouping tools into a toolkit and implementing handlers
Section titled “Grouping tools into a toolkit and implementing handlers”Toolkit.make collects any number of tools into a single, typed toolkit. Its
toLayer method takes an effect producing the handlers — one per tool — and
returns a Layer that satisfies the toolkit’s handler requirements. Each handler
receives decoded parameters and returns an Effect of the success type.
import { Effect } from "effect"import { Toolkit } from "effect/unstable/ai"
const ProductToolkit = Toolkit.make(SearchProducts, GetInventory)
const ProductToolkitLayer = ProductToolkit.toLayer(Effect.gen(function*() { // The handler-building effect can acquire other services first — // e.g. a database client used inside the handlers below. // const db = yield* Database return ProductToolkit.of({ SearchProducts: Effect.fn("ProductToolkit.SearchProducts")( function*({ query, maxResults }) { return [ new Product({ id: ProductId.make("p-1"), name: `${query} widget`, price: 19.99 }), new Product({ id: ProductId.make("p-2"), name: `${query} gadget`, price: 29.99 }) ].slice(0, maxResults) } ), GetInventory: Effect.fn("ProductToolkit.GetInventory")( function*({ productId }) { return { productId, available: 42 } } ) })}))ProductToolkit.of({ ... }) gives you type-checked handlers: the keys must match
the tool names, each handler’s argument is the decoded parameter type, and its
result must match the tool’s success schema. If you don’t need to acquire
services first, you can pass the handler record directly to toLayer(...) without
wrapping it in Effect.gen.
Using a toolkit with the model
Section titled “Using a toolkit with the model”Pass the toolkit to generateText. The model may call any tool in it; the
framework resolves parameters, invokes your handlers, feeds results back, and loops
until the model produces a final answer. The resulting response exposes
toolCalls and toolResults so you can inspect what happened.
import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai"import { Config, Context, Effect, Layer, Schema } from "effect"import { AiError, LanguageModel } from "effect/unstable/ai"import { FetchHttpClient } from "effect/unstable/http"
const OpenAiClientLayer = OpenAiClient.layerConfig({ apiKey: Config.redacted("OPENAI_API_KEY")}).pipe(Layer.provide(FetchHttpClient.layer))
class ProductAssistantError extends Schema.TaggedErrorClass<ProductAssistantError>()( "ProductAssistantError", { reason: AiError.AiErrorReason }) {}
class ProductAssistant extends Context.Service<ProductAssistant, { answer(question: string): Effect.Effect<string, ProductAssistantError>}>()("docs/ProductAssistant") { static readonly layer = Layer.effect( ProductAssistant, Effect.gen(function*() { // Yield the toolkit definition to get the live handlers. const toolkit = yield* ProductToolkit const model = yield* OpenAiLanguageModel.model("gpt-5.2").captureRequirements
const answer = Effect.fn("ProductAssistant.answer")( function*(question: string) { const response = yield* LanguageModel.generateText({ prompt: question, toolkit, // "required" forces a tool call before any text. Default is "auto". toolChoice: "required" })
// Inspect what the model did this turn. for (const call of response.toolCalls) { yield* Effect.log(`Tool call: ${call.name} id=${call.id}`) } for (const result of response.toolResults) { yield* Effect.log(`Tool result: ${result.name} isFailure=${result.isFailure}`) }
return response.text }, Effect.provide(model), // Map the AI error (the only thing in the error channel) to our domain error. Effect.catchTag( "AiError", (error) => Effect.fail(new ProductAssistantError({ reason: error.reason })) ) )
return ProductAssistant.of({ answer }) }) ).pipe( // The handler Layer must be provided so the framework can run tool calls. Layer.provide(ProductToolkitLayer), Layer.provide(OpenAiClientLayer) )}-
The model picks a tool and produces a call with decoded, schema-validated parameters.
-
The framework runs your handler for that tool and captures its result (or failure, per
failureMode). -
The result is appended to the conversation and sent back to the model.
-
The loop repeats until the model stops requesting tools and returns text.
Controlling tool choice
Section titled “Controlling tool choice”toolChoice on generateText steers the model (ToolChoice in
LanguageModel):
"auto"(default) — the model decides whether and which tool to call."required"— the model must call some tool before answering."none"— the model must not call a tool.{ tool: "SearchProducts" }— the model must call that specific tool.{ mode?: "auto" | "required", oneOf: ["SearchProducts"] }— restrict to a subset of tools, optionally forcing a call from that subset.
Failure modes
Section titled “Failure modes”failureMode decides what happens when a handler fails:
import { Schema } from "effect"import { Tool } from "effect/unstable/ai"
// "error" (default): a handler failure short-circuits into the calling effect's// error channel. The model never sees the failure.const ChargeCard = Tool.make("ChargeCard", { parameters: Schema.Struct({ amount: Schema.Number }), success: Schema.String, failure: Schema.Struct({ code: Schema.String }), failureMode: "error"})
// "return": a handler failure is captured and returned to the model as the tool// result, so the model can read it and try to recover (e.g. retry differently).const LookupOrder = Tool.make("LookupOrder", { parameters: Schema.Struct({ orderId: Schema.String }), success: Schema.Struct({ status: Schema.String }), failure: Schema.Struct({ message: Schema.String }), failureMode: "return"})Requiring approval before execution
Section titled “Requiring approval before execution”Mark a tool with needsApproval to gate execution. It can be a static boolean
or a function that inspects the decoded parameters and a NeedsApprovalContext
(the tool-call id and the messages leading up to the call). The function may
return a boolean or an Effect<boolean>.
import { Effect, Schema } from "effect"import { Tool } from "effect/unstable/ai"
const DeleteFile = Tool.make("DeleteFile", { description: "Delete a file from the workspace", parameters: Schema.Struct({ path: Schema.String }), success: Schema.Void, // Always require approval for this destructive tool. needsApproval: true})
const Refund = Tool.make("Refund", { parameters: Schema.Struct({ amount: Schema.Number }), success: Schema.String, // Dynamically require approval only for large refunds. needsApproval: ({ amount }, ctx) => Effect.sync(() => amount > 100 || ctx.messages.length > 20)})Provider-defined tools
Section titled “Provider-defined tools”Some providers offer built-in, server-side tools (web search, code interpreter, …). These run on the provider and need no handler from you (unless the provider returns data you want to post-process). Provider packages ship pre-built definitions you can drop into any toolkit.
import { OpenAiTool } from "@effect/ai-openai"import { Effect } from "effect"import { Toolkit } from "effect/unstable/ai"
// A provider-defined tool, executed server-side by OpenAI.const webSearch = OpenAiTool.WebSearch({ search_context_size: "medium" })
// Mix user-defined and provider-defined tools freely.const AssistantToolkit = Toolkit.make(SearchProducts, GetInventory, webSearch)
// Only the user-defined tools need handlers in `toLayer`; the provider-defined// `WebSearch` is resolved by the provider and is absent here.const AssistantToolkitLayer = AssistantToolkit.toLayer(Effect.gen(function*() { return AssistantToolkit.of({ SearchProducts: Effect.fn("AssistantToolkit.SearchProducts")( function*({ query, maxResults }) { return [ new Product({ id: ProductId.make("p-1"), name: `${query} widget`, price: 19.99 }) ].slice(0, maxResults) } ), GetInventory: Effect.fn("AssistantToolkit.GetInventory")( function*({ productId }) { return { productId, available: 42 } } ) })}))To define your own provider-defined tool, use Tool.providerDefined.
Set requiresHandler: true if your application must process the provider’s
returned data — only then does the tool appear in the toolkit’s handler record.
Tool reference
Section titled “Tool reference”Everything below comes from the Tool module (effect/unstable/ai). Constructors
build tool definitions; tools do nothing on their own until placed in a toolkit
with handlers.
Constructors
Section titled “Constructors”Tool.make
Section titled “Tool.make”The primary constructor for an application-owned tool. Options: description,
parameters, success, failure, failureMode ("error" | "return"),
dependencies (request-level service requirements), and needsApproval. All are
optional — omitted parameters defaults to EmptyParams, success to
Schema.Void, failure to Schema.Never, failureMode to "error".
import { Schema } from "effect"import { Tool } from "effect/unstable/ai"
const GetWeather = Tool.make("GetWeather", { description: "Get current weather for a location", parameters: Schema.Struct({ location: Schema.String }), success: Schema.Struct({ temperature: Schema.Number })})
console.log(GetWeather.name) // => "GetWeather"console.log(GetWeather.id) // => "effect/ai/Tool/GetWeather"console.log(GetWeather.failureMode) // => "error"Tool values also expose builder methods: addDependency(tag),
setParameters(schema), setSuccess(schema), setFailure(schema),
annotate(tag, value), and annotateMerge(context) — each returns a new tool.
import { Context, Schema } from "effect"import { Tool } from "effect/unstable/ai"
class Db extends Context.Service<Db, { query: () => string }>()("Db") {}
const Search = Tool.make("Search") .setParameters(Schema.Struct({ q: Schema.String })) .setSuccess(Schema.Array(Schema.String)) .addDependency(Db) // handler can now use the Db service .annotate(Tool.Readonly, true)Tool.dynamic
Section titled “Tool.dynamic”Creates a tool whose schema may not be known at compile time. Pass an Effect
Schema for full type safety, or a raw JSON Schema (handler then receives
unknown). Useful for MCP tools discovered at runtime or plugin systems.
import { Schema } from "effect"import { Tool } from "effect/unstable/ai"
// Typed via Effect Schemaconst Calculator = Tool.dynamic("Calculator", { parameters: Schema.Struct({ a: Schema.Number, b: Schema.Number }), success: Schema.Number})console.log(Calculator.jsonSchema) // => undefined (an Effect Schema was used)
// Untyped via raw JSON Schemaconst McpTool = Tool.dynamic("McpTool", { description: "Tool from MCP server", parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] }})console.log(McpTool.jsonSchema?.type) // => "object"Tool.providerDefined
Section titled “Tool.providerDefined”Models a tool built into a provider (web search, code execution). Takes an id
("provider.name"), a customName used by the toolkit, the provider’s
providerName, optional args/parameters/success/failure schemas, and
requiresHandler. It returns a function: call it with the configuration args to
get the tool instance.
import { Schema } from "effect"import { Tool } from "effect/unstable/ai"
const WebSearch = Tool.providerDefined({ id: "openai.web_search", customName: "OpenAiWebSearch", providerName: "web_search", args: Schema.Struct({ query: Schema.String }), success: Schema.Struct({ title: Schema.String, url: Schema.String })})({ query: "effect-ts" }) // apply the args to build the instance
console.log(WebSearch.providerName) // => "web_search"console.log(WebSearch.name) // => "OpenAiWebSearch"Guards
Section titled “Guards”Tool.isUserDefined
Section titled “Tool.isUserDefined”Returns true for tools created with Tool.make (not provider-defined, not
dynamic).
import { Tool } from "effect/unstable/ai"
console.log(Tool.isUserDefined(Tool.make("A"))) // => trueTool.isProviderDefined
Section titled “Tool.isProviderDefined”Returns true for tools created with Tool.providerDefined.
import { Schema } from "effect"import { Tool } from "effect/unstable/ai"
const Web = Tool.providerDefined({ id: "openai.web_search", customName: "Web", providerName: "web_search", args: Schema.Struct({ query: Schema.String })})({ query: "x" })
console.log(Tool.isProviderDefined(Web)) // => trueconsole.log(Tool.isProviderDefined(Tool.make("A"))) // => falseTool.isDynamic
Section titled “Tool.isDynamic”Returns true for tools created with Tool.dynamic.
import { Tool } from "effect/unstable/ai"
const D = Tool.dynamic("D", { parameters: { type: "object", properties: {} } })console.log(Tool.isDynamic(D)) // => trueconsole.log(Tool.isDynamic(Tool.make("A"))) // => falseApproval
Section titled “Approval”Tool.FailureMode
Section titled “Tool.FailureMode”The string union "error" | "return" controlling whether handler failures go to
the effect’s error channel ("error", default) or are returned as the tool result
("return"). Set via the failureMode option on make / dynamic /
providerDefined.
Tool.NeedsApproval
Section titled “Tool.NeedsApproval”The type of the needsApproval option: either a static boolean or a
NeedsApprovalFunction. true always requires approval; false/undefined
executes immediately.
import { Schema } from "effect"import { Tool } from "effect/unstable/ai"
const Wipe = Tool.make("Wipe", { parameters: Schema.Struct({ confirm: Schema.Boolean }), needsApproval: true // boolean form of NeedsApproval})Tool.NeedsApprovalFunction
Section titled “Tool.NeedsApprovalFunction”A function (params, context) => boolean | Effect<boolean> that decides approval
per call from the decoded parameters and a NeedsApprovalContext.
import { Schema } from "effect"import { Tool } from "effect/unstable/ai"
const SendEmail = Tool.make("SendEmail", { parameters: Schema.Struct({ to: Schema.String, body: Schema.String }), needsApproval: (params, _context) => params.to.endsWith("@external.com")})Tool.NeedsApprovalContext
Section titled “Tool.NeedsApprovalContext”The second argument to a NeedsApprovalFunction: { toolCallId: string, messages: ReadonlyArray<Prompt.Message> }. Inspect the conversation when deciding whether to
require approval.
import { Schema } from "effect"import { Tool } from "effect/unstable/ai"
const RunQuery = Tool.make("RunQuery", { parameters: Schema.Struct({ sql: Schema.String }), // Require approval once the conversation grows long. needsApproval: (_params, ctx) => ctx.messages.length > 10})Annotations
Section titled “Annotations”Annotations attach metadata via Context references on tool.annotations. Set
them with .annotate(tag, value). Most map to MCP hints; note the defaults below.
Tool.Title
Section titled “Tool.Title”A human-readable title for the tool (MCP title).
import { Tool } from "effect/unstable/ai"
const t = Tool.make("calculate_tip").annotate(Tool.Title, "Tip Calculator")Tool.Meta
Section titled “Tool.Meta”Arbitrary metadata record exposed to MCP clients (e.g. UI hints).
import { Tool } from "effect/unstable/ai"
const t = Tool.make("calculator_ui") .annotate(Tool.Meta, { ui: { resourceUri: "ui://example/calculator-ui" } })Tool.Readonly
Section titled “Tool.Readonly”Whether the tool only reads data (MCP readOnlyHint). Default false.
import { Context } from "effect"import { Tool } from "effect/unstable/ai"
const t = Tool.make("get_user_info").annotate(Tool.Readonly, true)console.log(Context.get(t.annotations, Tool.Readonly)) // => trueTool.Destructive
Section titled “Tool.Destructive”Whether the tool may perform destructive operations (MCP destructiveHint).
Default true, so annotate safe tools with false.
import { Tool } from "effect/unstable/ai"
const t = Tool.make("search_database").annotate(Tool.Destructive, false)Tool.Idempotent
Section titled “Tool.Idempotent”Whether repeated calls with the same parameters have no further effect (MCP
idempotentHint). Default false.
import { Tool } from "effect/unstable/ai"
const t = Tool.make("get_current_time").annotate(Tool.Idempotent, true)Tool.OpenWorld
Section titled “Tool.OpenWorld”Whether the tool interacts with arbitrary external systems (MCP openWorldHint).
Default true.
import { Tool } from "effect/unstable/ai"
const t = Tool.make("internal_operation").annotate(Tool.OpenWorld, false)Tool.Strict
Section titled “Tool.Strict”Per-tool override for strict JSON Schema mode. true/false force the provider’s
strict flag; undefined (default) defers to provider/global config.
import { Tool } from "effect/unstable/ai"
const t = Tool.make("search").annotate(Tool.Strict, false)Tool.getStrictMode
Section titled “Tool.getStrictMode”Reads the Strict annotation; returns undefined when no per-tool override is
set (distinct from false).
import { Tool } from "effect/unstable/ai"
const t = Tool.make("search").annotate(Tool.Strict, false)console.log(Tool.getStrictMode(t)) // => falseconsole.log(Tool.getStrictMode(Tool.make("other"))) // => undefinedTool.getDescription
Section titled “Tool.getDescription”Returns the tool’s description, falling back to the parameter schema’s
description annotation if none was set explicitly.
import { Tool } from "effect/unstable/ai"
const t = Tool.make("example", { description: "This is an example tool" })console.log(Tool.getDescription(t)) // => "This is an example tool"Tool.getJsonSchema
Section titled “Tool.getJsonSchema”Generates the provider-facing JSON Schema for a tool’s parameters. For dynamic
tools built from raw JSON Schema it returns that schema directly. Accepts an
optional { transformer } for provider-specific shaping.
import { Schema } from "effect"import { Tool } from "effect/unstable/ai"
const weather = Tool.make("get_weather", { parameters: Schema.Struct({ location: Schema.String })})console.log(Tool.getJsonSchema(weather))// => { type: "object", properties: { location: { type: "string" } }, required: ["location"], ... }Tool.getJsonSchemaFromSchema
Section titled “Tool.getJsonSchemaFromSchema”Generates JSON Schema directly from any Effect Schema (the lower-level building
block behind getJsonSchema).
import { Schema } from "effect"import { Tool } from "effect/unstable/ai"
console.log(Tool.getJsonSchemaFromSchema(Schema.Struct({ n: Schema.Number })))// => { type: "object", properties: { n: { type: "number" } }, required: ["n"], ... }Helpers and schemas
Section titled “Helpers and schemas”Tool.NameMapper
Section titled “Tool.NameMapper”Maps between a tool’s custom (Effect) name and its provider name, so toolkits can hold similarly named native tools from different providers without conflict.
import { Schema } from "effect"import { Tool } from "effect/unstable/ai"
const Web = Tool.providerDefined({ id: "openai.web_search", customName: "OpenAiWebSearch", providerName: "web_search", args: Schema.Struct({ query: Schema.String })})({ query: "x" })
const mapper = new Tool.NameMapper([Web])console.log(mapper.getProviderName("OpenAiWebSearch")) // => "web_search"console.log(mapper.getCustomName("web_search")) // => "OpenAiWebSearch"Tool.EmptyParams
Section titled “Tool.EmptyParams”The schema used for tools that take no parameters —
Schema.Record(Schema.String, Schema.Never), i.e. an empty object. Tool.make
uses it automatically when parameters is omitted.
import { Tool } from "effect/unstable/ai"
const t = Tool.make("ping", { parameters: Tool.EmptyParams })console.log(Tool.getJsonSchema(t).type) // => "object"Tool.unsafeSecureJsonParse
Section titled “Tool.unsafeSecureJsonParse”Parses JSON text while rejecting prototype-pollution keys (__proto__, dangerous
constructor.prototype). Throws SyntaxError on invalid JSON or unsafe shapes.
import { Tool } from "effect/unstable/ai"
console.log(Tool.unsafeSecureJsonParse(`{"a":1}`)) // => { a: 1 }// Tool.unsafeSecureJsonParse(`{"__proto__":{}}`) throws SyntaxErrorType accessors
Section titled “Type accessors”The module also exports type-level helpers for extracting parts of a tool’s definition. These exist only at the type level (no runtime value):
Tool.Name<T>— the tool’s name literal.Tool.Parameters<T>— the decoded parameter type (Encodedvariant:ParametersEncoded<T>; schema:ParametersSchema<T>).Tool.Success<T>— the success type (SuccessEncoded,SuccessSchema).Tool.Failure<T>— the failure type (FailureEncoded,FailureResult,FailureResultEncoded).Tool.Result<T>—Success | Failure(plusAiErrorwhenfailureMode: "return"); encoded variantResultEncoded<T>.Tool.Handler<Name>/Tool.HandlerResult<T>/Tool.HandlerError<T>/Tool.HandlerServices<T>— handler-related shapes.Tool.HandlersFor<Tools>— the union of handler requirements for a tool record.Tool.RequiresHandler<T>—trueunless the tool is provider-defined withrequiresHandler: false.
Toolkit reference
Section titled “Toolkit reference”The Toolkit module groups tools into a typed service that executes calls by name.
Toolkit.make
Section titled “Toolkit.make”Builds a toolkit from any number of tools. Tool names become the lookup keys.
import { Schema } from "effect"import { Tool, Toolkit } from "effect/unstable/ai"
const SearchDocs = Tool.make("SearchDocs", { parameters: Schema.Struct({ query: Schema.String }), success: Schema.Array(Schema.String)})const kit = Toolkit.make(SearchDocs)console.log(Object.keys(kit.tools)) // => ["SearchDocs"]Toolkit.empty
Section titled “Toolkit.empty”An empty toolkit — a useful starting point or default that you extend with
merge.
import { Toolkit } from "effect/unstable/ai"
console.log(Object.keys(Toolkit.empty.tools)) // => []Toolkit.merge
Section titled “Toolkit.merge”Combines multiple toolkits into one. On name conflicts, tools from later toolkits win.
import { Schema } from "effect"import { Tool, Toolkit } from "effect/unstable/ai"
const math = Toolkit.make( Tool.make("add", { success: Schema.Number }), Tool.make("subtract", { success: Schema.Number }))const util = Toolkit.make(Tool.make("get_time", { success: Schema.Number }))
const combined = Toolkit.merge(math, util)console.log(Object.keys(combined.tools)) // => ["add", "subtract", "get_time"]Implementing and running handlers
Section titled “Implementing and running handlers”A toolkit is itself an Effect: yielding it produces a WithHandler
value once handlers have been provided via toLayer / toHandlers. The handler
record must match HandlersFrom — one function per tool that
requires a handler.
import { Effect, Schema, Stream } from "effect"import { Tool, Toolkit } from "effect/unstable/ai"
const GetWeather = Tool.make("GetWeather", { parameters: Schema.Struct({ location: Schema.String }), success: Schema.Struct({ temperature: Schema.Number, condition: Schema.String })})
const WeatherToolkit = Toolkit.make(GetWeather)
const WeatherLayer = WeatherToolkit.toLayer({ GetWeather: ({ location }) => Effect.succeed({ temperature: 72, condition: `sunny in ${location}` })})
const program = Effect.gen(function*() { const toolkit = yield* WeatherToolkit // `handle` validates params, runs the matching handler, and returns a Stream // of preliminary + final results. const stream = yield* toolkit.handle("GetWeather", { location: "SF" }) const results = yield* Stream.runCollect(stream) return Array.from(results, ({ result }) => result)}).pipe(Effect.provide(WeatherLayer))
console.log(Effect.runSync(program))// => [{ temperature: 72, condition: "sunny in SF" }]toLayer vs toHandlers
Section titled “toLayer vs toHandlers”toLayer(build) returns a Layer providing the handlers. toHandlers(build)
returns an Effect producing the raw handler Context instead — use it when you
need the context value directly. Both accept either a handler record or an
Effect that produces one.
Toolkit.of
Section titled “Toolkit.of”A type-only identity helper for declaring handler records with full inference of parameter and result types.
import { Effect } from "effect"// Using WeatherToolkit from above:const handlers = WeatherToolkit.of({ GetWeather: ({ location }) => Effect.succeed({ temperature: 70, condition: `clear in ${location}` })})WithHandler
Section titled “WithHandler”The shape produced by yielding a live toolkit: { tools, handle }. handle(name, params) validates the parameters, runs the matching handler, and returns an
Effect of a Stream of HandlerResult values (so handlers can emit progress
before the final result). A missing tool fails with an AiError.
HandlerContext
Section titled “HandlerContext”The second argument passed to each handler: { preliminary }. Call
context.preliminary(value) to stream an intermediate result before the handler
returns its authoritative final value.
import { Effect, Schema } from "effect"import { Tool, Toolkit } from "effect/unstable/ai"
const LongJob = Tool.make("LongJob", { success: Schema.String })
const layer = Toolkit.make(LongJob).toLayer({ LongJob: (_params, context) => Effect.gen(function*() { yield* context.preliminary("working...") // intermediate progress return "done" // final, authoritative result })})Type helpers
Section titled “Type helpers”Toolkit-level type utilities (type level only):
Toolkit.Tools<T>— extract the tool record from a toolkit.Toolkit.ToolsByName<Tools>— normalize a tool array/record into a name-keyed record (whatmakeuses internally).Toolkit.HandlersFrom<Tools>— the required handler record shape; handlers may fail with the tool’sfailure, anAiError, or anAiErrorReason.Toolkit.MergedTools<Toolkits>— the merged tool record of several toolkits (whatmergeproduces).Toolkit.WithHandlerTools<T>— extract the tool record from aWithHandler.
Related
Section titled “Related”- Language Model — the
generateTextAPI tools plug into. - Chat — drive an agentic tool-calling loop over a stateful session.
- Schema — define tool parameters and results.