Tools
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 })})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.
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:
"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: [...] }— restrict to a subset of tools.
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. Provider packages ship pre-built definitions you can drop into any toolkit.
import { OpenAiTool } from "@effect/ai-openai"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 } } ) })}))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.