Skip to content

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.

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.

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)
)
}
  1. The model picks a tool and produces a call with decoded, schema-validated parameters.

  2. The framework runs your handler for that tool and captures its result (or failure, per failureMode).

  3. The result is appended to the conversation and sent back to the model.

  4. The loop repeats until the model stops requesting tools and returns text.

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.

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 }
}
)
})
}))
  • Language Model — the generateText API tools plug into.
  • Chat — drive an agentic tool-calling loop over a stateful session.
  • Schema — define tool parameters and results.