effect-ai-tool
/effect-ai-tool
Definiuje narzędzia AI w Effect przez API Tool i Toolkit, z walidacją parametrów i obsługą wywołań modelu.
- Autor
- mpsuesser ↗
- Repozytorium
- opencode-effect-enforcer ↗
- Licencja
- MIT
- Ostatnia zmiana repozytorium
- 26.10.2026
Ocena i bezpieczeństwo
- Pliki w paczce
- 1
- Skrypty
- 0
- Komentarze HTML
- 3
- Adresy URL w SKILL.md
- 1
Brak sygnałów. Repozytorium zmienione 26.10.2026, bez odwołań do wycofanych modeli i API.
MIT, otwarta: można używać i zmieniać, z zachowaniem informacji o autorze.
- SKILL.md zawiera komentarze HTML (3). Nie widać ich w podglądzie Markdown, a model je czyta.
Co robi i kiedy
Definiuje narzędzia AI w Effect przez API Tool i Toolkit, z walidacją parametrów i obsługą wywołań modelu.
Oryginalny opis (EN)
Define and implement AI tools using Effect AI's Tool and Toolkit APIs. Use when building LLM integrations with type-safe tool definitions, parameter validation, and handler implementations. Covers user-defined tools, provider-defined tools, and toolkit composition.
Jak działa
- Effect AI Documentation Access
- Core Concepts
- Production Harness Pattern: Effectful Local Tool Wrappers
- Creating User-Defined Tools
- Creating Toolkits
- Dynamic Tools
- Registry-Owned Tool Resolution
- Provider-Defined Tools
- Tool Result Flow
- Advanced Patterns
Podgląd SKILL.md
---
name: effect-ai-tool
description: Define and implement AI tools using Effect AI's Tool and Toolkit APIs. Use when building LLM integrations with type-safe tool definitions, parameter validation, and handler implementations. Covers user-defined tools, provider-defined tools, and toolkit composition.
---
# Effect AI Tool Skill
Use this skill when implementing tools for AI language models using the Effect AI library. This covers tool definition, parameter schemas, success/failure handling, and toolkit composition.
## Effect AI Documentation Access
For core AI APIs inspect `packages/effect/src/ai/` at `effect@4.0.6` in the
Effect source reference; provider implementations live under `packages/ai/`.
Keep all Effect-family packages on the same version. These APIs are tagged
`@stability unstable`, so minor releases may break them despite stable-looking
import paths.
Reference this for:
- Tool.make, Tool.dynamic, and Tool.providerDefined APIs
- Toolkit.make for composing multiple tools
- Schema.Struct(...) for Tool.make parameters
- Handler implementation patterns
- OpenAI provider-defined tools via `@effect/ai-openai/OpenAiTool`
## Core Concepts
### Tool Anatomy
```typescript
Effect<A, E, R>
↓
Tool<Name, Config, Requirements>
Config := {
parameters: Schema.Struct<Fields>
success: Schema<A>
failure: Schema<E>
failureMode: "error" | "return"
}
```
### Toolkit Flow
```typescript
Tool₁, Tool₂, Tool₃
↓ Toolkit.make
Toolkit<{tool1: Tool₁, tool2: Tool₂, tool3: Tool₃}>
↓ .toLayer(handlers)
Layer<Handlers>
↓ Effect.provide
Effect with tool execution capability
```
## Production Harness Pattern: Effectful Local Tool Wrappers
`Tool.make` is the core user-defined Effect AI tool API. Effect AI also has `Tool.dynamic` for runtime-discovered tools and `Tool.providerDefined` for native provider capabilities. In larger coding-agent harnesses, it is common to wrap local tool definitions in an effectful layer so it can capture services once and keep a single `Effect.runPromise` bridge at the outer async framework boundary.
This wrapper is local to your harness, not part of Effect AI itself.
```typescript
import { Effect } from 'effect';
export const ReadTool = defineEffect(
'read',
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service;
const run = Effect.fn('ReadTool.execute')(function* (params: Params) {
return yield* fs.readFileString(params.path);
});
return {
description: 'Read a file',
async execute(params: Params) {
return Effect.runPromise(run(params));
}
};
})
);
```
Use this pattern when the surrounding framework wants an async callback surface but your real implementation should stay in Effect.
## Creating User-Defined Tools
### Basic Tool Definition
```typescript
import * as Tool from 'effect/ai/Tool';
import * as Schema from 'effect/Schema';
const GetCurrentTime = Tool.make('GetCurrentTime', {
description: 'Returns the current timestamp in milliseconds',
success: Schema.Number
});
type Result = Tool.Success<typeof GetCurrentTime>;
```
**Key Pattern: Tool.make**
- First parameter: tool name (string literal)
- Second parameter: configuration object
- No parameters field = empty parameters `{}`
- Default success: `Schema.Void`
- Default failure: `Schema.Never`
### Tool from Domain Schema
If you have an existing domain schema you want to use as a tool, create the tool with `Tool.make` and reference the schema directly:
```typescript
import * as Tool from 'effect/ai/Tool';
import * as Schema from 'effect/Schema';
const UserResult = Schema.Struct({
id: Schema.String,
name: Schema.String
});
const GetUserTool = Tool.make('GetUser', {
description: 'Retrieve user information by ID',
parameters: Schema.Struct({
userId: Schema.String
}),
success: UserResult
});
type Params = Tool.Parameters<typeof GetUserTool>;
type ParamsEncoded = Tool.ParametersEncoded<typeof GetUserTool>;
type Success = Tool.Success<typeof GetUserTool>;
```
**Key Pattern: Tool.make with domain schemas**
- Use `Tool.make` for user-defined tools
- Reference existing domain schemas in `parameters`, `success`, and `failure` fields
- Tool name is the first argument (string literal)
- Use `Tool.dynamic` for runtime-discovered tools and `Tool.providerDefined` / `OpenAiTool` for provider-native tools
- There is no `Tool.fromTaggedRequest` in v4
### Tool with Parameters
```typescript
import * as Tool from 'effect/ai/Tool';
import { Schema } from 'effect';
const GetWeather = Tool.make('GetWeather', {
description: 'Get weather information for a location',
parameters: Schema.Struct({
location: Schema.String,
units: Schema.optional(Schema.Literals(['celsius', 'fahrenheit']))
}),
success: Schema.Struct({
temperature: Schema.Number,
condition: Schema.String,
humidity: Schema.Number
})
});
type Params = Tool.Parameters<typeof GetWeather>;
type Success = Tool.Success<typeof GetWeather>;
```
**Key Pattern: parameters**
- `Tool.make` parameters must be an Effect `Schema.Top`
- Wrap field records with `Schema.Struct({...})`; `Tool.make` does not wrap raw field objects automatically
- `Tool.dynamic` can use either typed Effect schemas or raw JSON Schema discovered at runtime
- Use `Schema.optional()` for optional parameters
### Tool with Failure Handling
```typescript
import * as Tool from 'effect/ai/Tool';
import { Schema } from 'effect';
class UserNotFound extends Schema.TaggedError<UserNotFound>()(
'UserNotFound',
{
userId: Schema.String
}
) {}
class DatabaseError extends Schema.TaggedError<DatabaseError>()(
'DatabaseError',
{
message: Schema.String
}
) {}
const FindUser = Tool.make('FindUser', {
description: 'Find user by ID',
parameters: Schema.Struct({
userId: Schema.String
}),
success: Schema.Struct({
id: Schema.String,
name: Schema.String,
email: Schema.String
}),
failure: Schema.Union([UserNotFound, DatabaseError]),
failureMode: 'error'
});
type Error = Tool.Failure<typeof FindUser>;
```
**Key Pattern: failureMode**
Parameter validation follows the tool's `failureMode`. Failure results include
`Tool.ExecutionFailure` for framework failures (AI errors, denial, interruption)
as well as declared user errors. Select result codecs using `isFailure` and
`Tool.failureResultSchema(tool)`; preserve `encodedResult` when storing or
replaying response parts. A failure with `Schema.NumberFromString` is stored as
a string, even when the success schema stores a number. Validation errors do not
expose a `toolParams` field.
`Toolkit.handle` accepts parse options for parameter decoding. Returned failures
carry `failureOrigin`, also available through the `Toolkit.FailureOrigin` cause
annotation and `Tool.FailureOrigin` type. Keep validation, declared handler, and
internal failures distinct when presenting or reporting them.
- `"error"` (default): Failures go to Effect error channel
- `"return"`: Parameter and handler failures become failed tool results. Result
encoding can still fail the returned Stream with `AiError`; return mode does
not make stream consumption infallible.
In error mode, the full failure cause is preserved, including multiple typed
failures and existing annotations; `FailureOrigin` is added per failure rather
than collapsing the cause to its first error. Return mode captures the first
typed failure as a result, but defects in mixed causes still escape as defects.
Do not use return mode as a way to swallow a failing finalizer or concurrent
defect. Result-encoding failures retain origin `result`.
### Tool with Service Dependencies
```typescript
import * as Tool from 'effect/ai/Tool';
import * as Context from 'effect/Context';
import { Schema } from 'effect';
class Database extends Context.Service<
Database,
{
readonly query: (sql: string) => Effect.Effect<unknown>;
}
>()('Database') {}
const QueryDatabase = Tool.make('QueryDatabase', {
description: 'Execute a database query',
parameters: Schema.Struct({
sql: Schema.String
}),
success: Schema.Unknown,
dependencies: [Database]
});
type Requirements = Tool.HandlerServices<typeof QueryDatabase>;
```
**Key Pattern: dependencies**
- Array of service tags
- `Tool.HandlerServices<T>` includes declared dependencies, parameter-decoding services, and result-encoding services
- Must be provided when creating handlers
## Creating Toolkits
### Basic Toolkit
```typescript
import * as Toolkit from 'effect/ai/Toolkit';
import * as Tool from 'effect/ai/Tool';
import { Effect, Schema } from 'effect';
const GetCurrentTime = Tool.make('GetCurrentTime', {
description: 'Get the current timestamp',
success: Schema.Number
});
const GetWeather = Tool.make('GetWeather', {
description: 'Get weather for a location',
parameters: Schema.Struct({
location: Schema.String
}),
success: Schema.Struct({
temperature: Schema.Number,
condition: Schema.String
})
});
const MyToolkit = Toolkit.make(GetCurrentTime, GetWeather);
type Tools = Toolkit.Tools<typeof MyToolkit>;
```
**Key Pattern: Toolkit.make**
- Accepts variadic tool arguments
- Returns `Toolkit<Record<Name, Tool>>`
- Tools indexed by their name property
### Implementing Tool Handlers
```typescript
import { Effect } from 'effect';
const MyToolkitLayer = MyToolkit.toLayer({
GetCurrentTime: () => Effect.succeed(Date.now()),
GetWeather: ({ location }) =>
Effect.gen(function* () {
const data = yield* fetchWeatherData(location);
return {
temperature: data.temp,
condition: data.conditions
};
})
});
declare const fetchWeatherData: (location: string) => Effect.Effect<{
readonly temp: number;
readonly conditions: string;
}>;
```
**Key Pattern: toLayer**
- Object mapping tool names to handler functions
- Handler signature: `(params, context) => Effect<Success, Failure, Requirements>`; `context.toolCallId` exposes the provider call ID when available, and `context.preliminary(...)` emits progress updates
- Handler parameters are decoded `Tool.Parameters<T>` values
- Returns `Layer<Handlers>`
### Alternative: Handlers as Context
```typescript
import { Effect } from 'effect';
const program = Effect.gen(function* () {
const handlers = yield* MyToolkit.toHandlers({
GetCurrentTime: () => Effect.succeed(Date.now()),
GetWeather: ({ location }) =>
Effect.gen(function* () {
const data = yield* fetchWeatherData(location);
return {
temperature: data.temp,
condition: data.conditions
};
})
});
const result = yield* Effect.provide(myEffect, handlers);
return result;
});
declare const fetchWeatherData: (location: string) => Effect.Effect<{
readonly temp: number;
readonly conditions: string;
}>;
declare const myEffect: Effect.Effect<unknown, never, Handlers>;
```
**Key Pattern: toHandlers**
- Similar to toLayer but returns a `Context` instead of Layer
- Use when you need direct handler context (not Layer composition)
- Returns `Effect<Context<Handlers>>`
- Provide directly to effects that require handlers
### Providing Dependencies to Handlers
```typescript
import * as Context from 'effect/Context';
import { Effect } from 'effect';
interface WeatherData {
readonly temperature: number;
readonly condition: string;
}
class WeatherService extends Context.Service<
WeatherService,
{
readonly fetch: (location: string) => Effect.Effect<WeatherData>;
}
>()('WeatherService') {}
const GetWeatherWithDeps = Tool.make('GetWeather', {
parameters: Schema.Struct({
location: Schema.String
}),
success: Schema.Struct({
temperature: Schema.Number,
condition: Schema.String
}),
dependencies: [WeatherService]
});
const toolkit = Toolkit.make(GetWeatherWithDeps);
const toolkitLayer = toolkit.toLayer({
GetWeather: ({ location }) =>
Effect.gen(function* () {
const service = yield* WeatherService;
const data = yield* service.fetch(location);
return {
temperature: data.temperature,
condition: data.condition
};
})
});
const program = Effect.gen(function* () {
const handlers = yield* toolkit;
const resultStream = yield* handlers.handle('GetWeather', { location: 'NYC' });
return yield* Stream.runCollect(resultStream);
}).pipe(Effect.provide(toolkitLayer), Effect.provide(WeatherServiceLive));
declare const WeatherServiceLive: Layer.Layer<WeatherService>;
```
**Key Pattern: Handler Context**
- Handlers run with injected dependencies
- Access via `yield* Tag` in Effect.gen
- Dependencies must be provided to final effect
## Dynamic Tools
Use `Tool.dynamic` when tool schemas are discovered at runtime, such as from MCP or external configuration. If `parameters` is an Effect `Schema`, handlers receive typed decoded params; if `parameters` is raw JSON Schema, handler params are `unknown`.
```typescript
const TypedDynamic = Tool.dynamic('runtimeSearch', {
description: 'Search with a runtime-provided typed schema',
parameters: Schema.Struct({ query: Schema.String }),
success: Schema.Array(Schema.String)
});
type TypedParams = Tool.Parameters<typeof TypedDynamic>; // { query: string }
const UntypedDynamic = Tool.dynamic('mcpTool', {
description: 'Runtime MCP tool with raw JSON Schema',
parameters: {
type: 'object',
properties: { query: { type: 'string' } },
required: ['query']
}
});
type UntypedParams = Tool.Parameters<typeof UntypedDynamic>; // unknown
```
## Registry-Owned Tool Resolution
In production agent harnesses, resolve tools through a registry service instead of scattering direct imports across prompt/session code.
The registry is the right place to:
- initialize effect-built tools once
- compute dynamic descriptions from permissions, config, or available subagents
- expose stable named handles for orchestration and tests
```typescript
export namespace ToolRegistry {
export interface Interface {
readonly named: {
task: LocalToolInfo;
read: LocalToolInfo;
};
}
}
const registry = yield* ToolRegistry.Service;
const task = yield* Effect.promise(() => registry.named.task.init());
```
Keep static descriptions on the tool for invariant behavior. Put runtime-specific descriptions and policy shaping in the registry layer.
### Merging Toolkits
```typescript
import * as Toolkit from 'effect/ai/Toolkit';
const mathToolkit = Toolkit.make(
Tool.make('add', {
parameters: Schema.Struct({ a: Schema.Number, b: Schema.Number }),
success: Schema.Number
}),
Tool.make('subtract', {
parameters: Schema.Struct({ a: Schema.Number, b: Schema.Number }),
success: Schema.Number
})
);
const utilityToolkit = Toolkit.make(
Tool.make('getCurrentTime', { success: Schema.Number }),
Tool.make('generateUUID', { success: Schema.String })
);
const combined = Toolkit.merge(mathToolkit, utilityToolkit);
type AllTools = Toolkit.Tools<typeof combined>;
```
**Key Pattern: Toolkit.merge**
- Combines multiple toolkits into one
- Later toolkits override earlier ones on name collision
- Type-safe union of all tools
- **Note**: In v4, the preferred pattern is `Toolkit.make(tool1, tool2, ...)` — passing all tools to the constructor rather than merging separate toolkits. Use `Toolkit.make` with all tools when possible.
## Provider-Defined Tools
`Tool.providerDefined` models provider-native capabilities. For OpenAI, prefer the curated `OpenAiTool` constructors from `@effect/ai-openai` (`WebSearch`, `CodeInterpreter`, `FileSearch`, `ImageGeneration`, `Mcp`, plus handler-required local tools such as `Shell`, `LocalShell`, and `ApplyPatch`).
### Basic Provider Tool
```typescript
import * as Tool from 'effect/ai/Tool';
import { Schema } from 'effect';
const AnthropicBash = Tool.providerDefined({
id: 'anthropic.bash',
customName: 'Bash',
providerName: 'bash_20241022',
args: Schema.Struct({
command: Schema.String
})
});
const bashTool = AnthropicBash({ command: 'ls -la' });
type ToolType = typeof bashTool;
```
**Key Pattern: Tool.providerDefined**
- Returns a function that accepts args
- `id`: Unique identifier `<provider>.<tool-name>`
- `customName`: Name used by Effect AI / Toolkit to identify this tool
- `providerName`: Name recognized by the AI provider
- `args`: Schema for provider-specific configuration arguments
- `requiresHandler`: Set to `true` only when your application must execute/process provider tool calls locally
### OpenAI Provider Tools
```typescript
import { OpenAiTool } from '@effect/ai-openai';
const nativeTools = Toolkit.make(
OpenAiTool.WebSearch({}),
OpenAiTool.FileSearch({ vector_store_ids: ['vs_123'] }),
OpenAiTool.ImageGeneration({}),
OpenAiTool.Mcp({
server_label: 'docs',
server_url: 'https://mcp.example.com/mcp'
})
);
```
`OpenAiTool.Mcp` uses the canonical custom name `OpenAiMcp`. Treat handler-required local OpenAI tools (`Shell`, `LocalShell`, `ApplyPatch`) as privileged operations: provide handlers only behind sandboxing, authorization, and audit policy.
### Provider Tool with Handler
```typescript
import * as Tool from 'effect/ai/Tool';
import { Schema } from 'effect';
const WebSearch = Tool.providerDefined({
id: 'custom.web_search',
customName: 'WebSearch',
providerName: 'web_search',
args: Schema.Struct({
maxResults: Schema.Number
}),
requiresHandler: true,
parameters: Schema.Struct({
query: Schema.String
}),
success: Schema.Struct({
results: Schema.Array(
Schema.Struct({
title: Schema.String,
url: Schema.String,
snippet: Schema.String
})
)
})
});
const searchTool = WebSearch({ maxResults: 10, failureMode: 'return' });
const toolkit = Toolkit.make(searchTool);
const toolkitLayer = toolkit.toLayer({
WebSearch: ({ query }) =>
Effect.gen(function* () {
const results = yield* performSearch(query);
return { results };
})
});
declare const performSearch: (query: string) => Effect.Effect<
Array<{
readonly title: string;
readonly url: string;
readonly snippet: string;
}>
>;
```
**Key Pattern: requiresHandler**
- `false` (default): Provider executes tool completely
- `true`: Your handler processes provider results
- Handler receives `parameters` from provider
## Tool Result Flow
### Understanding ToolCallPart and ToolResultPart
```typescript
import * as Prompt from 'effect/ai/Prompt';
const toolCallPart = Prompt.makePart('tool-call', {
id: 'call_123',
name: 'GetWeather',
params: { location: 'NYC' },
providerExecuted: false
});
const toolResultPart = Prompt.makePart('tool-result', {
id: 'call_123',
name: 'GetWeather',
result: {
temperature: 72,
condition: 'sunny'
},
isFailure: false
});
```
**Key Pattern: Tool Call Flow**
1. LLM generates `ToolCallPart` in response
2. Your code runs `yield* toolkit.handle(...)` to get a `Stream` of handler results
3. Preliminary results can be emitted for progress; the final result is authoritative
4. Create a `ToolResultPart` from the final handler result for manual loops (LanguageModel does this automatically when tool resolution is enabled)
5. Send the tool result back to the LLM
### Approval Flow
`Tool.make` and `Tool.dynamic` support `needsApproval`. When approval is needed, automatic tool resolution emits a `tool-approval-request` instead of executing the handler. The caller appends a `Prompt.toolApprovalResponsePart` in a tool message and calls the model again.
```typescript
const DeleteFile = Tool.make('DeleteFile', {
parameters: Schema.Struct({ path: Schema.String }),
success: Schema.Void,
needsApproval: true
});
// Replace an existing tool's static or dynamic approval policy.
const DeleteFileWithoutApproval = DeleteFile.setNeedsApproval(false);
const DeleteOutsideWorkspace = DeleteFile.setNeedsApproval(
({ path }, { toolCallId, messages }) =>
path.startsWith('/tmp/') ||
(toolCallId.length > 0 && messages.length === 0)
);
const approvalResponse = Prompt.toolApprovalResponsePart({
approvalId: 'approval_123',
approved: false,
reason: 'User denied deletion'
});
```
Approved calls execute on the next model call; denied calls are converted to failed tool results with `{ type: 'execution-denied', reason }`.
`setNeedsApproval` returns a cloned tool with the replacement policy. A dynamic policy receives decoded parameters plus `{ toolCallId, messages }` and may return either `boolean` or `Effect<boolean>`.
### Executing Tool Handlers
```typescript
import { Effect, Stream } from 'effect';
const program = Effect.gen(function* () {
const toolkit = yield* MyToolkit;
const resultStream = yield* toolkit.handle('GetWeather', {
location: 'San Francisco'
});
yield* resultStream.pipe(
Stream.runForEach((result) =>
Effect.gen(function* () {
yield* Effect.log(result.preliminary ? 'progress' : 'final');
yield* Effect.log(result.isFailure);
yield* Effect.log(result.result);
yield* Effect.log(result.encodedResult);
})
)
);
});
interface HandlerResult<T> {
readonly isFailure: boolean;
readonly result: Result<T>;
readonly encodedResult: unknown;
readonly preliminary: boolean;
}
type Result<T> = Tool.Success<T> | Tool.Failure<T>;
```
Handlers can emit progress before the final result with `context.preliminary(...)`:
```typescript
const toolkitLayer = LongRunningToolkit.toLayer({
LongTask: (params, context) =>
Effect.gen(function* () {
yield* Effect.logDebug('handling tool call').pipe(
Effect.annotateLogs({ toolCallId: context.toolCallId })
);
yield* context.preliminary({ status: 'started' });
const result = yield* runLongTask(params);
return { status: 'done', result };
})
});
```
**Key Pattern: toolkit.handle**
- `toolkit.handle(name, params, toolCallId?, options?)` accepts
`Tool.ParametersEncoded<T>` and optional schema parse options. Its full shape is
`Effect<Stream<HandlerResult<T>, Tool.HandlerError<T> | AiError, Tool.HandlerServices<T>>, AiError, Tool.HandlerServices<T>>`.
- `handle` decodes the encoded input with the tool's parameter schema before invoking the handler; the handler still receives `Tool.Parameters<Tool>`
- Provide handler services to the outer Effect as well as the Stream. A
service-dependent parameter decoder runs before the Stream exists; providing
services only around `Stream.runCollect` is too late. Consuming the Stream
inside the same provided `Effect.gen` keeps both stages wired.
- The optional call ID is forwarded to the handler as `context.toolCallId`
- `preliminary: true`: progress update; do not persist as final history
- `preliminary: false`: final result to send/persist
- `isFailure`: Whether handler failed
- `result`: Typed success or failure value
- `encodedResult`: JSON-serializable for LLM
The encoded/decoded distinction matters for transforming schemas:
```typescript
const Repeat = Tool.make('Repeat', {
parameters: Schema.Struct({ times: Schema.NumberFromString }),
success: Schema.Number
});
const RepeatToolkit = Toolkit.make(Repeat);
const RepeatLive = RepeatToolkit.toLayer({
Repeat: ({ times }) => Effect.succeed(times + 1) // times is number
});
const program = Effect.gen(function* () {
const handlers = yield* RepeatToolkit;
const stream = yield* handlers.handle('Repeat', { times: '3' }); // encoded input is string
return yield* Stream.runCollect(stream);
}).pipe(Effect.provide(RepeatLive));
```
Passing the decoded `{ times: 3 }` directly to `handle` is now a type error. Use the encoded boundary type and let `handle` perform the schema decode.
### Bounded preliminary results and cleanup
The result stream owns the handler lifetime: closing it interrupts the handler,
even when a consumer stops early (for example `Stream.take`). Consume or close
every stream rather than treating `handle` as detached background work.
`Toolkit.PreliminaryBuffer` defaults to `'unbounded'`. Provide a positive integer
around `handle` or its calling program to bound that call's preliminary queue.
`context.preliminary` suspends when it fills; invalid values defect. Consume the
stream while the producer runs, not after waiting for producer completion, or a
full queue can deadlock. `LanguageModel` has additional buffers, so this is not
an end-to-end memory bound. The export exists at exact 4.0.6 despite an upstream
`@since 4.1.0` annotation.
<!-- typecheck -->
```ts
import * as Tool from 'effect/ai/Tool';
import * as Toolkit from 'effect/ai/Toolkit';
import * as Arr from 'effect/Array';
import * as Effect from 'effect/Effect';
import * as Schema from 'effect/Schema';
import * as Stream from 'effect/Stream';
const Progress = Toolkit.make(Tool.make('Progress', { success: Schema.Number }));
const ProgressLive = Progress.toLayer({
Progress: Effect.fnUntraced(function* (_, context) {
yield* Effect.forEach(Arr.range(0, 63), context.preliminary, {
concurrency: 1,
discard: true
});
return 64;
})
});
const consume = Effect.gen(function* () {
const handlers = yield* Progress;
const stream = yield* handlers.handle('Progress', {}).pipe(
Effect.provideService(Toolkit.PreliminaryBuffer, 16)
);
// Pull while the handler runs; do not join its producer first.
return yield* Stream.runFold(stream, () => 0, (count) => count + 1);
}).pipe(Effect.provide(ProgressLive));
// 64 preliminary updates plus one final result.
```
## Advanced Patterns
### Tool Annotations
```typescript
import * as Tool from 'effect/ai/Tool';
import { Schema } from 'effect';
const ReadOnlyQuery = Tool.make('query', {
parameters: Schema.Struct({ sql: Schema.String }),
success: Schema.Unknown
})
.annotate(Tool.Readonly, true)
.annotate(Tool.Destructive, false)
.annotate(Tool.Idempotent, true);
```
**Available Annotations and Defaults:**
- `Tool.Readonly`: Tool only reads data; default `false`
- `Tool.Destructive`: Tool performs destructive operations; default `true`
- `Tool.Idempotent`: Safe to call multiple times; default `false`
- `Tool.OpenWorld`: Can handle arbitrary external data; default `true`
- `Tool.Strict`: OpenAI strict JSON Schema override; default `undefined` so provider/config decides
- `Tool.Title`: Human-readable title
MCP emits the first four annotations as tool hints. They are hints, not authorization decisions. OpenAI function tools use `Tool.Strict` or `OpenAiLanguageModel.Config.strictJsonSchema` for strict mode.
### JSON Schema Generation
```typescript
import * as Tool from 'effect/ai/Tool';
const tool = Tool.make('example', {
parameters: Schema.Struct({
name: Schema.String,
age: Schema.optional(Schema.Number)
})
});
const jsonSchema = Tool.getJsonSchema(tool);
```
**Output:**
```json
{
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "number" }
},
"required": ["name"],
"additionalProperties": false
}
```
### Tool Guards
```typescript
import * as Tool from 'effect/ai/Tool';
const userTool = Tool.make('example');
const providerTool = Tool.providerDefined({
id: 'provider.tool',
customName: 'Example',
providerName: 'example',
args: Schema.Struct({})
})({});
Tool.isUserDefined(userTool);
Tool.isProviderDefined(providerTool);
```
### Dynamic Tool Selection
Manual model calls default to strict encoded params. With disabled resolution
and `toolCallValidation: 'deferred'`, params are unknown and names may be
undeclared, even without a toolkit. Validate names, encoded parameters, and
authorization before dispatch; provider-executed calls remain validated.
`Toolkit.handle` is not a permission/approval gate by itself.
<!-- typecheck -->
```ts
import * as Tool from 'effect/ai/Tool';
import * as Toolkit from 'effect/ai/Toolkit';
import * as Effect from 'effect/Effect';
import * as Schema from 'effect/Schema';
import * as Stream from 'effect/Stream';
class UnknownTool extends Schema.TaggedError<UnknownTool>()('UnknownTool', {
name: Schema.String
}) {}
const Repeat = Tool.make('Repeat', {
parameters: Schema.Struct({ times: Schema.NumberFromString }),
success: Schema.Number
});
const Tools = Toolkit.make(Repeat);
const ToolsLive = Tools.toLayer({ Repeat: ({ times }) => Effect.succeed(times + 1) });
// This demo tool is pure; mutating tools need an authorization/approval boundary too.
const executeTool = Effect.fn('Tools.execute')(function* (name: string, raw: unknown) {
if (name !== 'Repeat') return yield* Effect.fail(new UnknownTool({ name }));
const params = yield* Schema.decodeUnknownEffect(
Schema.toEncoded(Repeat.parametersSchema)
)(raw);
const handlers = yield* Tools;
const stream = yield* handlers.handle('Repeat', params);
return yield* Stream.runCollect(stream);
}, Effect.provide(ToolsLive));
```
## Complete Example
<!-- typecheck -->
```typescript
import * as Tool from 'effect/ai/Tool';
import * as Toolkit from 'effect/ai/Toolkit';
import * as Schema from 'effect/Schema';
import { Clock, Context, Effect, Layer, Stream } from 'effect';
class UserNotFound extends Schema.TaggedError<UserNotFound>()(
'UserNotFound',
{
userId: Schema.String
}
) {}
class User extends Schema.Class<User>('User')({
id: Schema.String,
name: Schema.String,
email: Schema.String
}) {}
class UserQuery extends Schema.Class<UserQuery>('UserQuery')({
userId: Schema.String
}) {}
class Users extends Context.Service<Users, {
readonly get: (id: string) => Effect.Effect<User, UserNotFound>;
}>()('app/Users') {}
const GetUser = Tool.make('GetUser', {
description: 'Retrieve user information by ID',
parameters: UserQuery,
success: User,
failure: UserNotFound,
failureMode: 'error',
dependencies: [Users]
});
const GetCurrentTime = Tool.make('GetCurrentTime', {
description: 'Get the current Unix timestamp',
success: Schema.Number
});
const UserToolkit = Toolkit.make(GetUser, GetCurrentTime);
const UserToolkitLive = UserToolkit.toLayer({
GetUser: Effect.fn('Tools.GetUser')(function* ({ userId }) {
const users = yield* Users;
return yield* users.get(userId);
}),
GetCurrentTime: Effect.fn('Tools.GetCurrentTime')(() => Clock.currentTimeMillis)
});
// A complete, typed demo implementation; production supplies a repository adapter.
const exampleUser = new User({ id: 'user-123', name: 'Alice', email: 'alice@example.com' });
const UsersTest = Layer.succeed(Users, Users.of({
get: Effect.fn('Users.get')((userId: string): Effect.Effect<User, UserNotFound> => userId === exampleUser.id
? Effect.succeed(exampleUser)
: Effect.fail(new UserNotFound({ userId })))
}));
const program = Effect.gen(function* () {
const toolkit = yield* UserToolkit;
const getStream = yield* toolkit.handle('GetUser', {
userId: 'user-123'
});
yield* getStream.pipe(
Stream.runForEach((result) => Effect.log('Retrieved user:', result.result))
);
const timeStream = yield* toolkit.handle('GetCurrentTime', {});
yield* timeStream.pipe(
Stream.runForEach((result) => Effect.log('Current time:', result.result))
);
}).pipe(Effect.provide(UserToolkitLive), Effect.provide(UsersTest));
```
Yield the **Toolkit**, then provide its handler **Layer**. A Layer is not a
yieldable toolkit instance. Encode tagged failures with the error schema itself
rather than `Schema.instanceOf(ErrorClass)`, which only checks an existing
instance and cannot reconstruct it from JSON. Handlers get decoded class values;
manual `handle` calls accept their encoded representation.
## Import Patterns
**CRITICAL**: Always use namespace imports:
```typescript
import * as Tool from 'effect/ai/Tool';
import * as Toolkit from 'effect/ai/Toolkit';
import * as Prompt from 'effect/ai/Prompt';
import { Schema, Effect, Data, Context, Layer } from 'effect';
const myTool = Tool.make('example');
const myToolkit = Toolkit.make(myTool);
```
**NEVER** do this:
```typescript
import { make } from 'effect/ai/Tool';
import { make as makeToolkit } from 'effect/ai/Toolkit';
```
## Quality Checklist
### Mandatory - Every Tool
- [ ] Tool name is descriptive and unique
- [ ] Description explains what the tool does
- [ ] Parameters use `Schema.Struct(...)` or another `Schema.Top` schema (not raw field objects)
- [ ] Success schema matches handler return type
- [ ] Failure schema includes all tagged errors
- [ ] failureMode matches recovery strategy
- [ ] Dependencies declared if accessing services
- [ ] Handler implements correct signature
- [ ] Type signatures use Tool.Parameters, Tool.Success, Tool.Failure
### Conditional - Include When Appropriate
- [ ] Tool.Readonly annotation for read-only tools
- [ ] Tool.Destructive annotation for mutating operations
- [ ] Tool.Idempotent annotation for safe retries
- [ ] Custom annotations via the tool's `.annotate` method
- [ ] Provider-defined tools for native provider features
- [ ] Toolkit.make with all tools (preferred in v4) or Toolkit.merge for combining tool collections
- [ ] Error handling with catchTag in handlers
## Common Patterns
### Validation in Handlers
```typescript
const ValidatedTool = Tool.make('validate', {
parameters: Schema.Struct({
input: Schema.String
}),
success: Schema.Struct({
valid: Schema.Boolean,
errors: Schema.Array(Schema.String)
})
});
const toolkit = Toolkit.make(ValidatedTool);
const toolkitLayer = toolkit.toLayer({
validate: ({ input }) =>
Effect.gen(function* () {
const errors: Array<string> = [];
if (input.length < 3) {
errors.push('Input too short');
}
if (!/^[a-z]+$/.test(input)) {
errors.push('Input must be lowercase letters');
}
return {
valid: errors.length === 0,
errors
};
})
});
```
### Async Operations in Handlers
```typescript
const FetchTool = Tool.make('fetch', {
parameters: Schema.Struct({
url: Schema.String
}),
success: Schema.String
});
const toolkit = Toolkit.make(FetchTool);
const toolkitLayer = toolkit.toLayer({
fetch: ({ url }) =>
Effect.tryPromise({
try: () => fetch(url).then((r) => r.text()),
catch: (error) => new Error(`Fetch failed: ${error}`)
})
});
```
### Conditional Tool Execution
```typescript
const ConditionalTool = Tool.make('process', {
parameters: Schema.Struct({
mode: Schema.Literals(['fast', 'thorough'])
}),
success: Schema.String
});
const toolkit = Toolkit.make(ConditionalTool);
const toolkitLayer = toolkit.toLayer({
process: ({ mode }) =>
mode === 'fast'
? Effect.succeed('Fast result')
: Effect.gen(function* () {
yield* Effect.sleep('1 second');
return 'Thorough result';
})
});
```
## When to Use This Skill
- Building LLM integrations with tool calling
- Creating type-safe AI agent capabilities
- Implementing function calling for Claude/OpenAI
- Defining validated tool parameters and results
- Composing multiple tools into toolkits
- Managing tool handler dependencies
- Integrating provider-native tools (bash, web search)
## Key Principles Summary
1. **Tool.make** - Define user tools with parameters, success, and failure schemas
2. **Tool.dynamic** - Define runtime-discovered tools; raw JSON Schema params are `unknown`
3. **Tool.providerDefined / OpenAiTool** - Use provider-native tools with `customName`
4. **Parameters** - `Tool.make` takes `Schema.Top` schemas such as `Schema.Struct(...)`; raw JSON Schema belongs with `Tool.dynamic`
5. **Toolkit.make** - Compose multiple tools together
6. **toLayer** - Implement handlers returning Layer
7. **toHandlers** - Implement handlers returning Context
8. **toolkit.handle** - Execute tools and consume a Stream of preliminary/final results
9. **ParametersEncoded** - Pass encoded schema input to `handle`; handlers receive decoded `Parameters`
10. **HandlerResult** - Access typed result, encoded JSON, failure flag, and preliminary flag
11. **needsApproval** - Produces approval requests until caller supplies approval responses
12. **setNeedsApproval** - Clone a tool with a replacement static or effectful approval policy
13. **toolCallId** - Pass the call ID through `toolkit.handle` and read it from handler context
14. **failureMode** - Control error vs return failure strategy
15. **dependencies** - Declare service requirements
16. **Namespace imports** - Always `import * as Tool`
17. **Prompt.makePart** - Create tool-call, tool-result, and approval parts with params
Your tool implementations should be type-safe, validated, and provide excellent developer experience with full schema support.
## Related Skills
- effect-ai-language-model - Using tools with generateText/streamText
- effect-ai-prompt - Tool call/result message integration
- effect-ai-streaming - Processing tool call streams
- effect-ai-provider - Provider-defined tools
Wersje
W bazie nie ma innych skilli o nazwie effect-ai-tool.
Migawka bazy z 28.10.2026 · wersja paczki ef79e8cb2a30 (skrót SHA-256 całej paczki)
Zgłoś problem z tym skillem: szkodliwa treść, błędny opis albo prośba autora o usunięcie.
Podobne skille
- 100effect-v3-to-v4
Prowadzi migrację kodu z Effect v3 do Effect v4, w tym aktualizację pakietów effect i @effect/* w obrębie tej granicy wersji. - 95effect-schema
Opisuje wzorce @effect/schema: definiowanie schematów, walidację, parsowanie, kodowanie i transformacje dla typobezpiecznej walidacji danych w Effect. - 95effect-mcp-server
Buduje serwery MCP w Effect, wystawiające narzędzia, zasoby i prompty dla klientów LLM przez stdio lub HTTP. - 95effect-rpc-api
Definiuje kontrakty RPC w Effect z typami, wspólne dla klienta i serwera, z obsługą strumieni i middleware. - 90effect-v4-mcp
Pomaga budować, podłączać i testować serwer MCP na Effect v4, tak by stdout pozostał kanałem JSON-RPC, a błędy narzędzi były czytelne. - 100effect-ts
Prowadzi przez Effect-TS w TypeScripcie (domyślnie v4, z obsługą v3): błędy, współbieżność, serwisy, strumienie i schema, także w kodzie importującym z „effect”.