# 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

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.

```ts
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`](#toolemptyparams) (an empty object). Likewise `success`
defaults to `Schema.Void` and `failure` to `Schema.Never`.

```ts
const GetCurrentTime = Tool.make("GetCurrentTime", {
  description: "Returns the current timestamp",
  success: Schema.Number
})
```

## 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.

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

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.

```ts
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.

## 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

`failureMode` decides what happens when a handler fails:

```ts
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"
})
```
**What ends up in each channel:** With `failureMode: "error"` the handler's typed `failure` (and any `AiError`)
appears in the error channel of `generateText`. With `failureMode: "return"`, the
tool result is the union of `success`, `failure`, **and** `AiError` — the model
receives it and the run continues.

## 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>`.

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

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.

```ts
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`](#toolproviderdefined).
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

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

### `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"`.

```ts
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.

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

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.

```ts
import { Schema } from "effect"
import { Tool } from "effect/unstable/ai"

// Typed via Effect Schema
const 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 Schema
const 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`

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.

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

### `Tool.isUserDefined`

Returns `true` for tools created with `Tool.make` (not provider-defined, not
dynamic).

```ts
import { Tool } from "effect/unstable/ai"

console.log(Tool.isUserDefined(Tool.make("A"))) // => true
```

### `Tool.isProviderDefined`

Returns `true` for tools created with `Tool.providerDefined`.

```ts
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)) // => true
console.log(Tool.isProviderDefined(Tool.make("A"))) // => false
```

### `Tool.isDynamic`

Returns `true` for tools created with `Tool.dynamic`.

```ts
import { Tool } from "effect/unstable/ai"

const D = Tool.dynamic("D", { parameters: { type: "object", properties: {} } })
console.log(Tool.isDynamic(D)) // => true
console.log(Tool.isDynamic(Tool.make("A"))) // => false
```

## Approval

### `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`

The type of the `needsApproval` option: either a static `boolean` or a
`NeedsApprovalFunction`. `true` always requires approval; `false`/`undefined`
executes immediately.

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

A function `(params, context) => boolean | Effect<boolean>` that decides approval
per call from the decoded parameters and a `NeedsApprovalContext`.

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

The second argument to a `NeedsApprovalFunction`: `{ toolCallId: string, messages:
ReadonlyArray<Prompt.Message> }`. Inspect the conversation when deciding whether to
require approval.

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

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`

A human-readable title for the tool (MCP `title`).

```ts
import { Tool } from "effect/unstable/ai"

const t = Tool.make("calculate_tip").annotate(Tool.Title, "Tip Calculator")
```

### `Tool.Meta`

Arbitrary metadata record exposed to MCP clients (e.g. UI hints).

```ts
import { Tool } from "effect/unstable/ai"

const t = Tool.make("calculator_ui")
  .annotate(Tool.Meta, { ui: { resourceUri: "ui://example/calculator-ui" } })
```

### `Tool.Readonly`

Whether the tool only reads data (MCP `readOnlyHint`). Default `false`.

```ts
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)) // => true
```

### `Tool.Destructive`

Whether the tool may perform destructive operations (MCP `destructiveHint`).
Default `true`, so annotate safe tools with `false`.

```ts
import { Tool } from "effect/unstable/ai"

const t = Tool.make("search_database").annotate(Tool.Destructive, false)
```

### `Tool.Idempotent`

Whether repeated calls with the same parameters have no further effect (MCP
`idempotentHint`). Default `false`.

```ts
import { Tool } from "effect/unstable/ai"

const t = Tool.make("get_current_time").annotate(Tool.Idempotent, true)
```

### `Tool.OpenWorld`

Whether the tool interacts with arbitrary external systems (MCP `openWorldHint`).
Default `true`.

```ts
import { Tool } from "effect/unstable/ai"

const t = Tool.make("internal_operation").annotate(Tool.OpenWorld, false)
```

### `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.

```ts
import { Tool } from "effect/unstable/ai"

const t = Tool.make("search").annotate(Tool.Strict, false)
```

### `Tool.getStrictMode`

Reads the `Strict` annotation; returns `undefined` when no per-tool override is
set (distinct from `false`).

```ts
import { Tool } from "effect/unstable/ai"

const t = Tool.make("search").annotate(Tool.Strict, false)
console.log(Tool.getStrictMode(t)) // => false
console.log(Tool.getStrictMode(Tool.make("other"))) // => undefined
```

### `Tool.getDescription`

Returns the tool's `description`, falling back to the parameter schema's
description annotation if none was set explicitly.

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

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.

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

Generates JSON Schema directly from any Effect `Schema` (the lower-level building
block behind `getJsonSchema`).

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

### `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.

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

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.

```ts
import { Tool } from "effect/unstable/ai"

const t = Tool.make("ping", { parameters: Tool.EmptyParams })
console.log(Tool.getJsonSchema(t).type) // => "object"
```

### `Tool.unsafeSecureJsonParse`

Parses JSON text while rejecting prototype-pollution keys (`__proto__`, dangerous
`constructor.prototype`). Throws `SyntaxError` on invalid JSON or unsafe shapes.

```ts
import { Tool } from "effect/unstable/ai"

console.log(Tool.unsafeSecureJsonParse(`{"a":1}`)) // => { a: 1 }
// Tool.unsafeSecureJsonParse(`{"__proto__":{}}`) throws SyntaxError
```

### 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 (`Encoded` variant:
  `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` (plus `AiError` when
  `failureMode: "return"`); encoded variant `ResultEncoded<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>` — `true` unless the tool is provider-defined with
  `requiresHandler: false`.

---

# Toolkit reference

The `Toolkit` module groups tools into a typed service that executes calls by name.

## `Toolkit.make`

Builds a toolkit from any number of tools. Tool names become the lookup keys.

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

An empty toolkit — a useful starting point or default that you extend with
`merge`.

```ts
import { Toolkit } from "effect/unstable/ai"

console.log(Object.keys(Toolkit.empty.tools)) // => []
```

## `Toolkit.merge`

Combines multiple toolkits into one. On name conflicts, tools from later toolkits
win.

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

A toolkit is itself an `Effect`: yielding it produces a [`WithHandler`](#withhandler)
value once handlers have been provided via `toLayer` / `toHandlers`. The handler
record must match [`HandlersFrom`](#type-helpers) — one function per tool that
requires a handler.

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

`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`

A type-only identity helper for declaring handler records with full inference of
parameter and result types.

```ts
import { Effect } from "effect"
// Using WeatherToolkit from above:
const handlers = WeatherToolkit.of({
  GetWeather: ({ location }) =>
    Effect.succeed({ temperature: 70, condition: `clear in ${location}` })
})
```

## `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`

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.

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

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 (what `make` uses internally).
- `Toolkit.HandlersFrom<Tools>` — the required handler record shape; handlers may
  fail with the tool's `failure`, an `AiError`, or an `AiErrorReason`.
- `Toolkit.MergedTools<Toolkits>` — the merged tool record of several toolkits
  (what `merge` produces).
- `Toolkit.WithHandlerTools<T>` — extract the tool record from a `WithHandler`.

## Related

- [Language Model](https://effect.plants.sh/ai/language-model/) — the `generateText` API tools plug into.
- [Chat](https://effect.plants.sh/ai/chat/) — drive an agentic tool-calling loop over a stateful session.
- [Schema](https://effect.plants.sh/schema/) — define tool parameters and results.