opencode-effect-enforcer 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +278 -0
- package/guidance/effect-first-development.md +1247 -0
- package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
- package/guidance/post__parse-dont-validate.md +109 -0
- package/guidance/progressive-disclosure-guidance.md +38 -0
- package/package.json +63 -0
- package/patterns/avoid-any.md +37 -0
- package/patterns/avoid-data-tagged-error.md +34 -0
- package/patterns/avoid-direct-json.md +51 -0
- package/patterns/avoid-direct-tag-checks.md +54 -0
- package/patterns/avoid-expect-in-if.md +52 -0
- package/patterns/avoid-mutable-state.md +70 -0
- package/patterns/avoid-native-fetch.md +61 -0
- package/patterns/avoid-node-imports.md +86 -0
- package/patterns/avoid-non-null-assertion.md +44 -0
- package/patterns/avoid-object-type.md +46 -0
- package/patterns/avoid-option-getorthrow.md +39 -0
- package/patterns/avoid-platform-coupling.md +43 -0
- package/patterns/avoid-process-env.md +43 -0
- package/patterns/avoid-react-hooks.md +73 -0
- package/patterns/avoid-schema-suffix.md +45 -0
- package/patterns/avoid-sync-fs.md +68 -0
- package/patterns/avoid-try-catch.md +47 -0
- package/patterns/avoid-ts-ignore.md +38 -0
- package/patterns/avoid-untagged-errors.md +67 -0
- package/patterns/avoid-yield-ref.md +46 -0
- package/patterns/casting-awareness.md +46 -0
- package/patterns/context-tag-extends.md +84 -0
- package/patterns/effect-catchall-default.md +61 -0
- package/patterns/effect-promise-vs-trypromise.md +47 -0
- package/patterns/effect-run-in-body.md +58 -0
- package/patterns/imperative-loops.md +76 -0
- package/patterns/prefer-arr-sort.md +52 -0
- package/patterns/prefer-duration-values.md +56 -0
- package/patterns/prefer-effect-fn.md +161 -0
- package/patterns/prefer-match-over-switch.md +48 -0
- package/patterns/prefer-option-over-null.md +56 -0
- package/patterns/prefer-redacted-config.md +70 -0
- package/patterns/prefer-schema-class.md +54 -0
- package/patterns/require-effect-concurrency.md +83 -0
- package/patterns/stream-large-files.md +63 -0
- package/patterns/throw-in-effect-gen.md +62 -0
- package/patterns/use-clock-service.md +45 -0
- package/patterns/use-command-executor-service.md +54 -0
- package/patterns/use-console-service.md +54 -0
- package/patterns/use-filesystem-service.md +59 -0
- package/patterns/use-http-client-service.md +77 -0
- package/patterns/use-path-service.md +53 -0
- package/patterns/use-random-service.md +45 -0
- package/patterns/use-temp-file-scoped.md +66 -0
- package/patterns/vm-in-wrong-file.md +51 -0
- package/patterns/yield-in-for-loop.md +61 -0
- package/skills/effect-ai-chat/SKILL.md +472 -0
- package/skills/effect-ai-language-model/SKILL.md +652 -0
- package/skills/effect-ai-prompt/SKILL.md +752 -0
- package/skills/effect-ai-provider/SKILL.md +668 -0
- package/skills/effect-ai-streaming/SKILL.md +418 -0
- package/skills/effect-ai-tool/SKILL.md +1132 -0
- package/skills/effect-atom-rpc/SKILL.md +488 -0
- package/skills/effect-atom-state/SKILL.md +640 -0
- package/skills/effect-batching/SKILL.md +614 -0
- package/skills/effect-cache/SKILL.md +570 -0
- package/skills/effect-cli/SKILL.md +523 -0
- package/skills/effect-command-executor/SKILL.md +675 -0
- package/skills/effect-concurrency-testing/SKILL.md +612 -0
- package/skills/effect-config/SKILL.md +580 -0
- package/skills/effect-context-witness/SKILL.md +274 -0
- package/skills/effect-domain-modeling/SKILL.md +1212 -0
- package/skills/effect-domain-predicates/SKILL.md +867 -0
- package/skills/effect-error-handling/SKILL.md +1581 -0
- package/skills/effect-fiber/SKILL.md +731 -0
- package/skills/effect-filesystem/SKILL.md +624 -0
- package/skills/effect-graph/SKILL.md +571 -0
- package/skills/effect-http-api/SKILL.md +1760 -0
- package/skills/effect-http-client/SKILL.md +989 -0
- package/skills/effect-http-server/SKILL.md +920 -0
- package/skills/effect-incremental-migration/SKILL.md +362 -0
- package/skills/effect-layer-design/SKILL.md +642 -0
- package/skills/effect-managed-runtime/SKILL.md +395 -0
- package/skills/effect-mcp-server/SKILL.md +608 -0
- package/skills/effect-observability/SKILL.md +719 -0
- package/skills/effect-optics/SKILL.md +554 -0
- package/skills/effect-parallelization/SKILL.md +668 -0
- package/skills/effect-path/SKILL.md +296 -0
- package/skills/effect-pattern-matching/SKILL.md +914 -0
- package/skills/effect-platform-abstraction/SKILL.md +1175 -0
- package/skills/effect-platform-layers/SKILL.md +514 -0
- package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
- package/skills/effect-react-composition/SKILL.md +986 -0
- package/skills/effect-react-vm/SKILL.md +675 -0
- package/skills/effect-rpc-api/SKILL.md +624 -0
- package/skills/effect-rpc-client/SKILL.md +666 -0
- package/skills/effect-rpc-cluster/SKILL.md +1623 -0
- package/skills/effect-rpc-server/SKILL.md +767 -0
- package/skills/effect-scheduling/SKILL.md +124 -0
- package/skills/effect-schema-composition/SKILL.md +975 -0
- package/skills/effect-schema-v4/SKILL.md +691 -0
- package/skills/effect-scope/SKILL.md +682 -0
- package/skills/effect-service-implementation/SKILL.md +656 -0
- package/skills/effect-socket/SKILL.md +703 -0
- package/skills/effect-sql/SKILL.md +781 -0
- package/skills/effect-stream/SKILL.md +765 -0
- package/skills/effect-testing/SKILL.md +1331 -0
- package/skills/effect-typeclass-design/SKILL.md +161 -0
- package/skills/effect-wide-events/Article.md +66 -0
- package/skills/effect-wide-events/SKILL.md +95 -0
- package/skills/effect-workflow/SKILL.md +810 -0
- package/src/agent-policy.ts +22 -0
- package/src/enforcer.ts +104 -0
- package/src/frontmatter.ts +34 -0
- package/src/guidance.ts +66 -0
- package/src/index.ts +38 -0
- package/src/pattern-catalog.ts +115 -0
- package/src/pattern-matcher.ts +178 -0
- package/src/pattern.ts +97 -0
- package/src/skills.ts +29 -0
- package/src/write-projection.ts +66 -0
|
@@ -0,0 +1,1132 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: effect-ai-tool
|
|
3
|
+
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.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Effect AI Tool Skill
|
|
7
|
+
|
|
8
|
+
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.
|
|
9
|
+
|
|
10
|
+
## Effect AI Documentation Access
|
|
11
|
+
|
|
12
|
+
For comprehensive Effect AI documentation, view the Effect v4 repository at `packages/ai/`
|
|
13
|
+
|
|
14
|
+
Reference this for:
|
|
15
|
+
|
|
16
|
+
- Tool.make, Tool.dynamic, and Tool.providerDefined APIs
|
|
17
|
+
- Toolkit.make for composing multiple tools
|
|
18
|
+
- Schema.Struct(...) for Tool.make parameters
|
|
19
|
+
- Handler implementation patterns
|
|
20
|
+
- OpenAI provider-defined tools via `@effect/ai-openai/OpenAiTool`
|
|
21
|
+
|
|
22
|
+
## Core Concepts
|
|
23
|
+
|
|
24
|
+
### Tool Anatomy
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
Effect<A, E, R>
|
|
28
|
+
↓
|
|
29
|
+
Tool<Name, Config, Requirements>
|
|
30
|
+
|
|
31
|
+
Config := {
|
|
32
|
+
parameters: Schema.Struct<Fields>
|
|
33
|
+
success: Schema<A>
|
|
34
|
+
failure: Schema<E>
|
|
35
|
+
failureMode: "error" | "return"
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Toolkit Flow
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
Tool₁, Tool₂, Tool₃
|
|
43
|
+
↓ Toolkit.make
|
|
44
|
+
Toolkit<{tool1: Tool₁, tool2: Tool₂, tool3: Tool₃}>
|
|
45
|
+
↓ .toLayer(handlers)
|
|
46
|
+
Layer<Handlers>
|
|
47
|
+
↓ Effect.provide
|
|
48
|
+
Effect with tool execution capability
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Production Harness Pattern: Effectful Local Tool Wrappers
|
|
52
|
+
|
|
53
|
+
`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.
|
|
54
|
+
|
|
55
|
+
This wrapper is local to your harness, not part of Effect AI itself.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { Effect } from 'effect';
|
|
59
|
+
|
|
60
|
+
export const ReadTool = defineEffect(
|
|
61
|
+
'read',
|
|
62
|
+
Effect.gen(function* () {
|
|
63
|
+
const fs = yield* AppFileSystem.Service;
|
|
64
|
+
|
|
65
|
+
const run = Effect.fn('ReadTool.execute')(function* (params: Params) {
|
|
66
|
+
return yield* fs.readFileString(params.path);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
description: 'Read a file',
|
|
71
|
+
async execute(params: Params) {
|
|
72
|
+
return Effect.runPromise(run(params));
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Use this pattern when the surrounding framework wants an async callback surface but your real implementation should stay in Effect.
|
|
80
|
+
|
|
81
|
+
## Creating User-Defined Tools
|
|
82
|
+
|
|
83
|
+
### Basic Tool Definition
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import * as Tool from 'effect/unstable/ai/Tool';
|
|
87
|
+
import * as Schema from 'effect/Schema';
|
|
88
|
+
|
|
89
|
+
const GetCurrentTime = Tool.make('GetCurrentTime', {
|
|
90
|
+
description: 'Returns the current timestamp in milliseconds',
|
|
91
|
+
success: Schema.Number
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const result = Tool.Success<typeof GetCurrentTime>;
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Key Pattern: Tool.make**
|
|
98
|
+
|
|
99
|
+
- First parameter: tool name (string literal)
|
|
100
|
+
- Second parameter: configuration object
|
|
101
|
+
- No parameters field = empty parameters `{}`
|
|
102
|
+
- Default success: `Schema.Void`
|
|
103
|
+
- Default failure: `Schema.Never`
|
|
104
|
+
|
|
105
|
+
### Tool from Domain Schema
|
|
106
|
+
|
|
107
|
+
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:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
import * as Tool from 'effect/unstable/ai/Tool';
|
|
111
|
+
import * as Schema from 'effect/Schema';
|
|
112
|
+
|
|
113
|
+
const UserResult = Schema.Struct({
|
|
114
|
+
id: Schema.String,
|
|
115
|
+
name: Schema.String
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const GetUserTool = Tool.make('GetUser', {
|
|
119
|
+
description: 'Retrieve user information by ID',
|
|
120
|
+
parameters: Schema.Struct({
|
|
121
|
+
userId: Schema.String
|
|
122
|
+
}),
|
|
123
|
+
success: UserResult
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
type Params = Tool.Parameters<typeof GetUserTool>;
|
|
127
|
+
type ParamsEncoded = Tool.ParametersEncoded<typeof GetUserTool>;
|
|
128
|
+
type Success = Tool.Success<typeof GetUserTool>;
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Key Pattern: Tool.make with domain schemas**
|
|
132
|
+
|
|
133
|
+
- Use `Tool.make` for user-defined tools
|
|
134
|
+
- Reference existing domain schemas in `parameters`, `success`, and `failure` fields
|
|
135
|
+
- Tool name is the first argument (string literal)
|
|
136
|
+
- Use `Tool.dynamic` for runtime-discovered tools and `Tool.providerDefined` / `OpenAiTool` for provider-native tools
|
|
137
|
+
- There is no `Tool.fromTaggedRequest` in v4
|
|
138
|
+
|
|
139
|
+
### Tool with Parameters
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import * as Tool from 'effect/unstable/ai/Tool';
|
|
143
|
+
import { Schema } from 'effect';
|
|
144
|
+
|
|
145
|
+
const GetWeather = Tool.make('GetWeather', {
|
|
146
|
+
description: 'Get weather information for a location',
|
|
147
|
+
parameters: Schema.Struct({
|
|
148
|
+
location: Schema.String,
|
|
149
|
+
units: Schema.optional(Schema.Literals(['celsius', 'fahrenheit']))
|
|
150
|
+
}),
|
|
151
|
+
success: Schema.Struct({
|
|
152
|
+
temperature: Schema.Number,
|
|
153
|
+
condition: Schema.String,
|
|
154
|
+
humidity: Schema.Number
|
|
155
|
+
})
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
type Params = Tool.Parameters<typeof GetWeather>;
|
|
159
|
+
type Success = Tool.Success<typeof GetWeather>;
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Key Pattern: parameters**
|
|
163
|
+
|
|
164
|
+
- `Tool.make` parameters must be an Effect `Schema.Top`
|
|
165
|
+
- Wrap field records with `Schema.Struct({...})`; `Tool.make` does not wrap raw field objects automatically
|
|
166
|
+
- `Tool.dynamic` can use either typed Effect schemas or raw JSON Schema discovered at runtime
|
|
167
|
+
- Use `Schema.optional()` for optional parameters
|
|
168
|
+
|
|
169
|
+
### Tool with Failure Handling
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
import * as Tool from 'effect/unstable/ai/Tool';
|
|
173
|
+
import { Schema } from 'effect';
|
|
174
|
+
|
|
175
|
+
class UserNotFound extends Schema.TaggedError<UserNotFound>()(
|
|
176
|
+
'UserNotFound',
|
|
177
|
+
{
|
|
178
|
+
userId: Schema.String
|
|
179
|
+
}
|
|
180
|
+
) {}
|
|
181
|
+
|
|
182
|
+
class DatabaseError extends Schema.TaggedError<DatabaseError>()(
|
|
183
|
+
'DatabaseError',
|
|
184
|
+
{
|
|
185
|
+
message: Schema.String
|
|
186
|
+
}
|
|
187
|
+
) {}
|
|
188
|
+
|
|
189
|
+
const FindUser = Tool.make('FindUser', {
|
|
190
|
+
description: 'Find user by ID',
|
|
191
|
+
parameters: Schema.Struct({
|
|
192
|
+
userId: Schema.String
|
|
193
|
+
}),
|
|
194
|
+
success: Schema.Struct({
|
|
195
|
+
id: Schema.String,
|
|
196
|
+
name: Schema.String,
|
|
197
|
+
email: Schema.String
|
|
198
|
+
}),
|
|
199
|
+
failure: Schema.Union([
|
|
200
|
+
Schema.instanceOf(UserNotFound),
|
|
201
|
+
Schema.instanceOf(DatabaseError)
|
|
202
|
+
]),
|
|
203
|
+
failureMode: 'error'
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
type Error = Tool.Failure<typeof FindUser>;
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Key Pattern: failureMode**
|
|
210
|
+
|
|
211
|
+
- `"error"` (default): Failures go to Effect error channel
|
|
212
|
+
- `"return"`: Failures returned as tool result (captured, not thrown)
|
|
213
|
+
|
|
214
|
+
### Tool with Service Dependencies
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
import * as Tool from 'effect/unstable/ai/Tool';
|
|
218
|
+
import * as Context from 'effect/Context';
|
|
219
|
+
import { Schema } from 'effect';
|
|
220
|
+
|
|
221
|
+
class Database extends Context.Service<
|
|
222
|
+
Database,
|
|
223
|
+
{
|
|
224
|
+
readonly query: (sql: string) => Effect.Effect<unknown>;
|
|
225
|
+
}
|
|
226
|
+
>()('Database') {}
|
|
227
|
+
|
|
228
|
+
const QueryDatabase = Tool.make('QueryDatabase', {
|
|
229
|
+
description: 'Execute a database query',
|
|
230
|
+
parameters: Schema.Struct({
|
|
231
|
+
sql: Schema.String
|
|
232
|
+
}),
|
|
233
|
+
success: Schema.Unknown,
|
|
234
|
+
dependencies: [Database]
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
type Requirements = Tool.Requirements<typeof QueryDatabase>;
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Key Pattern: dependencies**
|
|
241
|
+
|
|
242
|
+
- Array of service tags
|
|
243
|
+
- Requirements extracted at type level
|
|
244
|
+
- Must be provided when creating handlers
|
|
245
|
+
|
|
246
|
+
## Creating Toolkits
|
|
247
|
+
|
|
248
|
+
### Basic Toolkit
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
import * as Toolkit from 'effect/unstable/ai/Toolkit';
|
|
252
|
+
import * as Tool from 'effect/unstable/ai/Tool';
|
|
253
|
+
import { Effect, Schema } from 'effect';
|
|
254
|
+
|
|
255
|
+
const GetCurrentTime = Tool.make('GetCurrentTime', {
|
|
256
|
+
description: 'Get the current timestamp',
|
|
257
|
+
success: Schema.Number
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const GetWeather = Tool.make('GetWeather', {
|
|
261
|
+
description: 'Get weather for a location',
|
|
262
|
+
parameters: Schema.Struct({
|
|
263
|
+
location: Schema.String
|
|
264
|
+
}),
|
|
265
|
+
success: Schema.Struct({
|
|
266
|
+
temperature: Schema.Number,
|
|
267
|
+
condition: Schema.String
|
|
268
|
+
})
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const MyToolkit = Toolkit.make(GetCurrentTime, GetWeather);
|
|
272
|
+
|
|
273
|
+
type Tools = Toolkit.Tools<typeof MyToolkit>;
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Key Pattern: Toolkit.make**
|
|
277
|
+
|
|
278
|
+
- Accepts variadic tool arguments
|
|
279
|
+
- Returns `Toolkit<Record<Name, Tool>>`
|
|
280
|
+
- Tools indexed by their name property
|
|
281
|
+
|
|
282
|
+
### Implementing Tool Handlers
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
import { Effect } from 'effect';
|
|
286
|
+
|
|
287
|
+
const MyToolkitLayer = MyToolkit.toLayer({
|
|
288
|
+
GetCurrentTime: () => Effect.succeed(Date.now()),
|
|
289
|
+
|
|
290
|
+
GetWeather: ({ location }) =>
|
|
291
|
+
Effect.gen(function* () {
|
|
292
|
+
const data = yield* fetchWeatherData(location);
|
|
293
|
+
return {
|
|
294
|
+
temperature: data.temp,
|
|
295
|
+
condition: data.conditions
|
|
296
|
+
};
|
|
297
|
+
})
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
declare const fetchWeatherData: (location: string) => Effect.Effect<{
|
|
301
|
+
readonly temp: number;
|
|
302
|
+
readonly conditions: string;
|
|
303
|
+
}>;
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Key Pattern: toLayer**
|
|
307
|
+
|
|
308
|
+
- Object mapping tool names to handler functions
|
|
309
|
+
- Handler signature: `(params, context) => Effect<Success, Failure, Requirements>`; `context.toolCallId` exposes the provider call ID when available, and `context.preliminary(...)` emits progress updates
|
|
310
|
+
- Handler parameters are decoded `Tool.Parameters<T>` values
|
|
311
|
+
- Returns `Layer<Handlers>`
|
|
312
|
+
|
|
313
|
+
### Alternative: Handlers as Context
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
import { Effect } from 'effect';
|
|
317
|
+
|
|
318
|
+
const program = Effect.gen(function* () {
|
|
319
|
+
const handlers = yield* MyToolkit.toHandlers({
|
|
320
|
+
GetCurrentTime: () => Effect.succeed(Date.now()),
|
|
321
|
+
|
|
322
|
+
GetWeather: ({ location }) =>
|
|
323
|
+
Effect.gen(function* () {
|
|
324
|
+
const data = yield* fetchWeatherData(location);
|
|
325
|
+
return {
|
|
326
|
+
temperature: data.temp,
|
|
327
|
+
condition: data.conditions
|
|
328
|
+
};
|
|
329
|
+
})
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const result = yield* Effect.provide(myEffect, handlers);
|
|
333
|
+
return result;
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
declare const fetchWeatherData: (location: string) => Effect.Effect<{
|
|
337
|
+
readonly temp: number;
|
|
338
|
+
readonly conditions: string;
|
|
339
|
+
}>;
|
|
340
|
+
|
|
341
|
+
declare const myEffect: Effect.Effect<unknown, never, Handlers>;
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**Key Pattern: toHandlers**
|
|
345
|
+
|
|
346
|
+
- Similar to toLayer but returns a `Context` instead of Layer
|
|
347
|
+
- Use when you need direct handler context (not Layer composition)
|
|
348
|
+
- Returns `Effect<Context<Handlers>>`
|
|
349
|
+
- Provide directly to effects that require handlers
|
|
350
|
+
|
|
351
|
+
### Providing Dependencies to Handlers
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
import * as Context from 'effect/Context';
|
|
355
|
+
import { Effect } from 'effect';
|
|
356
|
+
|
|
357
|
+
interface WeatherData {
|
|
358
|
+
readonly temperature: number;
|
|
359
|
+
readonly condition: string;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
class WeatherService extends Context.Service<
|
|
363
|
+
WeatherService,
|
|
364
|
+
{
|
|
365
|
+
readonly fetch: (location: string) => Effect.Effect<WeatherData>;
|
|
366
|
+
}
|
|
367
|
+
>()('WeatherService') {}
|
|
368
|
+
|
|
369
|
+
const GetWeatherWithDeps = Tool.make('GetWeather', {
|
|
370
|
+
parameters: Schema.Struct({
|
|
371
|
+
location: Schema.String
|
|
372
|
+
}),
|
|
373
|
+
success: Schema.Struct({
|
|
374
|
+
temperature: Schema.Number,
|
|
375
|
+
condition: Schema.String
|
|
376
|
+
}),
|
|
377
|
+
dependencies: [WeatherService]
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const toolkit = Toolkit.make(GetWeatherWithDeps);
|
|
381
|
+
|
|
382
|
+
const toolkitLayer = toolkit.toLayer({
|
|
383
|
+
GetWeather: ({ location }) =>
|
|
384
|
+
Effect.gen(function* () {
|
|
385
|
+
const service = yield* WeatherService;
|
|
386
|
+
const data = yield* service.fetch(location);
|
|
387
|
+
return {
|
|
388
|
+
temperature: data.temperature,
|
|
389
|
+
condition: data.condition
|
|
390
|
+
};
|
|
391
|
+
})
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const program = Effect.gen(function* () {
|
|
395
|
+
const handlers = yield* toolkitLayer;
|
|
396
|
+
const resultStream = yield* handlers.handle('GetWeather', { location: 'NYC' });
|
|
397
|
+
return resultStream;
|
|
398
|
+
}).pipe(Effect.provide(WeatherServiceLive));
|
|
399
|
+
|
|
400
|
+
declare const WeatherServiceLive: Layer<WeatherService>;
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
**Key Pattern: Handler Context**
|
|
404
|
+
|
|
405
|
+
- Handlers run with injected dependencies
|
|
406
|
+
- Access via `yield* Tag` in Effect.gen
|
|
407
|
+
- Dependencies must be provided to final effect
|
|
408
|
+
|
|
409
|
+
## Dynamic Tools
|
|
410
|
+
|
|
411
|
+
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`.
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
const TypedDynamic = Tool.dynamic('runtimeSearch', {
|
|
415
|
+
description: 'Search with a runtime-provided typed schema',
|
|
416
|
+
parameters: Schema.Struct({ query: Schema.String }),
|
|
417
|
+
success: Schema.Array(Schema.String)
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
type TypedParams = Tool.Parameters<typeof TypedDynamic>; // { query: string }
|
|
421
|
+
|
|
422
|
+
const UntypedDynamic = Tool.dynamic('mcpTool', {
|
|
423
|
+
description: 'Runtime MCP tool with raw JSON Schema',
|
|
424
|
+
parameters: {
|
|
425
|
+
type: 'object',
|
|
426
|
+
properties: { query: { type: 'string' } },
|
|
427
|
+
required: ['query']
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
type UntypedParams = Tool.Parameters<typeof UntypedDynamic>; // unknown
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
## Registry-Owned Tool Resolution
|
|
435
|
+
|
|
436
|
+
In production agent harnesses, resolve tools through a registry service instead of scattering direct imports across prompt/session code.
|
|
437
|
+
|
|
438
|
+
The registry is the right place to:
|
|
439
|
+
|
|
440
|
+
- initialize effect-built tools once
|
|
441
|
+
- compute dynamic descriptions from permissions, config, or available subagents
|
|
442
|
+
- expose stable named handles for orchestration and tests
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
export namespace ToolRegistry {
|
|
446
|
+
export interface Interface {
|
|
447
|
+
readonly named: {
|
|
448
|
+
task: LocalToolInfo;
|
|
449
|
+
read: LocalToolInfo;
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const registry = yield* ToolRegistry.Service;
|
|
455
|
+
const task = yield* Effect.promise(() => registry.named.task.init());
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Keep static descriptions on the tool for invariant behavior. Put runtime-specific descriptions and policy shaping in the registry layer.
|
|
459
|
+
|
|
460
|
+
### Merging Toolkits
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
import * as Toolkit from 'effect/unstable/ai/Toolkit';
|
|
464
|
+
|
|
465
|
+
const mathToolkit = Toolkit.make(
|
|
466
|
+
Tool.make('add', {
|
|
467
|
+
parameters: Schema.Struct({ a: Schema.Number, b: Schema.Number }),
|
|
468
|
+
success: Schema.Number
|
|
469
|
+
}),
|
|
470
|
+
Tool.make('subtract', {
|
|
471
|
+
parameters: Schema.Struct({ a: Schema.Number, b: Schema.Number }),
|
|
472
|
+
success: Schema.Number
|
|
473
|
+
})
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
const utilityToolkit = Toolkit.make(
|
|
477
|
+
Tool.make('getCurrentTime', { success: Schema.Number }),
|
|
478
|
+
Tool.make('generateUUID', { success: Schema.String })
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
const combined = Toolkit.merge(mathToolkit, utilityToolkit);
|
|
482
|
+
|
|
483
|
+
type AllTools = Toolkit.Tools<typeof combined>;
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
**Key Pattern: Toolkit.merge**
|
|
487
|
+
|
|
488
|
+
- Combines multiple toolkits into one
|
|
489
|
+
- Later toolkits override earlier ones on name collision
|
|
490
|
+
- Type-safe union of all tools
|
|
491
|
+
- **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.
|
|
492
|
+
|
|
493
|
+
## Provider-Defined Tools
|
|
494
|
+
|
|
495
|
+
`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`).
|
|
496
|
+
|
|
497
|
+
### Basic Provider Tool
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
import * as Tool from 'effect/unstable/ai/Tool';
|
|
501
|
+
import { Schema } from 'effect';
|
|
502
|
+
|
|
503
|
+
const AnthropicBash = Tool.providerDefined({
|
|
504
|
+
id: 'anthropic.bash',
|
|
505
|
+
customName: 'Bash',
|
|
506
|
+
providerName: 'bash_20241022',
|
|
507
|
+
args: Schema.Struct({
|
|
508
|
+
command: Schema.String
|
|
509
|
+
})
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const bashTool = AnthropicBash({ command: 'ls -la' });
|
|
513
|
+
|
|
514
|
+
type ToolType = typeof bashTool;
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
**Key Pattern: Tool.providerDefined**
|
|
518
|
+
|
|
519
|
+
- Returns a function that accepts args
|
|
520
|
+
- `id`: Unique identifier `<provider>.<tool-name>`
|
|
521
|
+
- `customName`: Name used by Effect AI / Toolkit to identify this tool
|
|
522
|
+
- `providerName`: Name recognized by the AI provider
|
|
523
|
+
- `args`: Schema for provider-specific configuration arguments
|
|
524
|
+
- `requiresHandler`: Set to `true` only when your application must execute/process provider tool calls locally
|
|
525
|
+
|
|
526
|
+
### OpenAI Provider Tools
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
import { OpenAiTool } from '@effect/ai-openai';
|
|
530
|
+
|
|
531
|
+
const nativeTools = Toolkit.make(
|
|
532
|
+
OpenAiTool.WebSearch({}),
|
|
533
|
+
OpenAiTool.FileSearch({ vector_store_ids: ['vs_123'] }),
|
|
534
|
+
OpenAiTool.ImageGeneration({}),
|
|
535
|
+
OpenAiTool.Mcp({
|
|
536
|
+
server_label: 'docs',
|
|
537
|
+
server_url: 'https://mcp.example.com/mcp'
|
|
538
|
+
})
|
|
539
|
+
);
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
`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.
|
|
543
|
+
|
|
544
|
+
### Provider Tool with Handler
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
import * as Tool from 'effect/unstable/ai/Tool';
|
|
548
|
+
import { Schema } from 'effect';
|
|
549
|
+
|
|
550
|
+
const WebSearch = Tool.providerDefined({
|
|
551
|
+
id: 'custom.web_search',
|
|
552
|
+
customName: 'WebSearch',
|
|
553
|
+
providerName: 'web_search',
|
|
554
|
+
args: Schema.Struct({
|
|
555
|
+
maxResults: Schema.Number
|
|
556
|
+
}),
|
|
557
|
+
requiresHandler: true,
|
|
558
|
+
parameters: Schema.Struct({
|
|
559
|
+
query: Schema.String
|
|
560
|
+
}),
|
|
561
|
+
success: Schema.Struct({
|
|
562
|
+
results: Schema.Array(
|
|
563
|
+
Schema.Struct({
|
|
564
|
+
title: Schema.String,
|
|
565
|
+
url: Schema.String,
|
|
566
|
+
snippet: Schema.String
|
|
567
|
+
})
|
|
568
|
+
)
|
|
569
|
+
})
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
const searchTool = WebSearch({ maxResults: 10, failureMode: 'return' });
|
|
573
|
+
|
|
574
|
+
const toolkit = Toolkit.make(searchTool);
|
|
575
|
+
|
|
576
|
+
const toolkitLayer = toolkit.toLayer({
|
|
577
|
+
WebSearch: ({ query }) =>
|
|
578
|
+
Effect.gen(function* () {
|
|
579
|
+
const results = yield* performSearch(query);
|
|
580
|
+
return { results };
|
|
581
|
+
})
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
declare const performSearch: (query: string) => Effect.Effect<
|
|
585
|
+
Array<{
|
|
586
|
+
readonly title: string;
|
|
587
|
+
readonly url: string;
|
|
588
|
+
readonly snippet: string;
|
|
589
|
+
}>
|
|
590
|
+
>;
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
**Key Pattern: requiresHandler**
|
|
594
|
+
|
|
595
|
+
- `false` (default): Provider executes tool completely
|
|
596
|
+
- `true`: Your handler processes provider results
|
|
597
|
+
- Handler receives `parameters` from provider
|
|
598
|
+
|
|
599
|
+
## Tool Result Flow
|
|
600
|
+
|
|
601
|
+
### Understanding ToolCallPart and ToolResultPart
|
|
602
|
+
|
|
603
|
+
```typescript
|
|
604
|
+
import * as Prompt from 'effect/unstable/ai/Prompt';
|
|
605
|
+
|
|
606
|
+
const toolCallPart = Prompt.makePart('tool-call', {
|
|
607
|
+
id: 'call_123',
|
|
608
|
+
name: 'GetWeather',
|
|
609
|
+
params: { location: 'NYC' },
|
|
610
|
+
providerExecuted: false
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
const toolResultPart = Prompt.makePart('tool-result', {
|
|
614
|
+
id: 'call_123',
|
|
615
|
+
name: 'GetWeather',
|
|
616
|
+
result: {
|
|
617
|
+
temperature: 72,
|
|
618
|
+
condition: 'sunny'
|
|
619
|
+
},
|
|
620
|
+
isFailure: false
|
|
621
|
+
});
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
**Key Pattern: Tool Call Flow**
|
|
625
|
+
|
|
626
|
+
1. LLM generates `ToolCallPart` in response
|
|
627
|
+
2. Your code runs `yield* toolkit.handle(...)` to get a `Stream` of handler results
|
|
628
|
+
3. Preliminary results can be emitted for progress; the final result is authoritative
|
|
629
|
+
4. Create a `ToolResultPart` from the final handler result for manual loops (LanguageModel does this automatically when tool resolution is enabled)
|
|
630
|
+
5. Send the tool result back to the LLM
|
|
631
|
+
|
|
632
|
+
### Approval Flow
|
|
633
|
+
|
|
634
|
+
`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.
|
|
635
|
+
|
|
636
|
+
```typescript
|
|
637
|
+
const DeleteFile = Tool.make('DeleteFile', {
|
|
638
|
+
parameters: Schema.Struct({ path: Schema.String }),
|
|
639
|
+
success: Schema.Void,
|
|
640
|
+
needsApproval: true
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// Replace an existing tool's static or dynamic approval policy.
|
|
644
|
+
const DeleteFileWithoutApproval = DeleteFile.setNeedsApproval(false);
|
|
645
|
+
|
|
646
|
+
const DeleteOutsideWorkspace = DeleteFile.setNeedsApproval(
|
|
647
|
+
({ path }, { toolCallId, messages }) =>
|
|
648
|
+
path.startsWith('/tmp/') ||
|
|
649
|
+
(toolCallId.length > 0 && messages.length === 0)
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
const approvalResponse = Prompt.toolApprovalResponsePart({
|
|
653
|
+
approvalId: 'approval_123',
|
|
654
|
+
approved: false,
|
|
655
|
+
reason: 'User denied deletion'
|
|
656
|
+
});
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
Approved calls execute on the next model call; denied calls are converted to failed tool results with `{ type: 'execution-denied', reason }`.
|
|
660
|
+
|
|
661
|
+
`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>`.
|
|
662
|
+
|
|
663
|
+
### Executing Tool Handlers
|
|
664
|
+
|
|
665
|
+
```typescript
|
|
666
|
+
import { Effect, Stream } from 'effect';
|
|
667
|
+
|
|
668
|
+
const program = Effect.gen(function* () {
|
|
669
|
+
const toolkit = yield* MyToolkitLayer;
|
|
670
|
+
|
|
671
|
+
const resultStream = yield* toolkit.handle('GetWeather', {
|
|
672
|
+
location: 'San Francisco'
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
yield* resultStream.pipe(
|
|
676
|
+
Stream.runForEach((result) =>
|
|
677
|
+
Effect.gen(function* () {
|
|
678
|
+
yield* Effect.log(result.preliminary ? 'progress' : 'final');
|
|
679
|
+
yield* Effect.log(result.isFailure);
|
|
680
|
+
yield* Effect.log(result.result);
|
|
681
|
+
yield* Effect.log(result.encodedResult);
|
|
682
|
+
})
|
|
683
|
+
)
|
|
684
|
+
);
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
interface HandlerResult<T> {
|
|
688
|
+
readonly isFailure: boolean;
|
|
689
|
+
readonly result: Result<T>;
|
|
690
|
+
readonly encodedResult: unknown;
|
|
691
|
+
readonly preliminary: boolean;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
type Result<T> = Tool.Success<T> | Tool.Failure<T>;
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
Handlers can emit progress before the final result with `context.preliminary(...)`:
|
|
698
|
+
|
|
699
|
+
```typescript
|
|
700
|
+
const toolkitLayer = LongRunningToolkit.toLayer({
|
|
701
|
+
LongTask: (params, context) =>
|
|
702
|
+
Effect.gen(function* () {
|
|
703
|
+
yield* Effect.logDebug('handling tool call').pipe(
|
|
704
|
+
Effect.annotateLogs({ toolCallId: context.toolCallId })
|
|
705
|
+
);
|
|
706
|
+
yield* context.preliminary({ status: 'started' });
|
|
707
|
+
const result = yield* runLongTask(params);
|
|
708
|
+
return { status: 'done', result };
|
|
709
|
+
})
|
|
710
|
+
});
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
**Key Pattern: toolkit.handle**
|
|
714
|
+
|
|
715
|
+
- `toolkit.handle(name, params, toolCallId?)` accepts `Tool.ParametersEncoded<Tool>` and returns `Effect<Stream<HandlerResult<Tool>>>`
|
|
716
|
+
- `handle` decodes the encoded input with the tool's parameter schema before invoking the handler; the handler still receives `Tool.Parameters<Tool>`
|
|
717
|
+
- The optional call ID is forwarded to the handler as `context.toolCallId`
|
|
718
|
+
- `preliminary: true`: progress update; do not persist as final history
|
|
719
|
+
- `preliminary: false`: final result to send/persist
|
|
720
|
+
- `isFailure`: Whether handler failed
|
|
721
|
+
- `result`: Typed success or failure value
|
|
722
|
+
- `encodedResult`: JSON-serializable for LLM
|
|
723
|
+
|
|
724
|
+
The encoded/decoded distinction matters for transforming schemas:
|
|
725
|
+
|
|
726
|
+
```typescript
|
|
727
|
+
const Repeat = Tool.make('Repeat', {
|
|
728
|
+
parameters: Schema.Struct({ times: Schema.NumberFromString }),
|
|
729
|
+
success: Schema.Number
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
const RepeatToolkit = Toolkit.make(Repeat);
|
|
733
|
+
const RepeatLive = RepeatToolkit.toLayer({
|
|
734
|
+
Repeat: ({ times }) => Effect.succeed(times + 1) // times is number
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
const program = Effect.gen(function* () {
|
|
738
|
+
const handlers = yield* RepeatToolkit;
|
|
739
|
+
return yield* handlers.handle('Repeat', { times: '3' }); // encoded input is string
|
|
740
|
+
}).pipe(Effect.provide(RepeatLive));
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
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.
|
|
744
|
+
|
|
745
|
+
## Advanced Patterns
|
|
746
|
+
|
|
747
|
+
### Tool Annotations
|
|
748
|
+
|
|
749
|
+
```typescript
|
|
750
|
+
import * as Tool from 'effect/unstable/ai/Tool';
|
|
751
|
+
import { Schema } from 'effect';
|
|
752
|
+
|
|
753
|
+
const ReadOnlyQuery = Tool.make('query', {
|
|
754
|
+
parameters: Schema.Struct({ sql: Schema.String }),
|
|
755
|
+
success: Schema.Unknown
|
|
756
|
+
})
|
|
757
|
+
.annotate(Tool.Readonly, true)
|
|
758
|
+
.annotate(Tool.Destructive, false)
|
|
759
|
+
.annotate(Tool.Idempotent, true);
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
**Available Annotations and Defaults:**
|
|
763
|
+
|
|
764
|
+
- `Tool.Readonly`: Tool only reads data; default `false`
|
|
765
|
+
- `Tool.Destructive`: Tool performs destructive operations; default `true`
|
|
766
|
+
- `Tool.Idempotent`: Safe to call multiple times; default `false`
|
|
767
|
+
- `Tool.OpenWorld`: Can handle arbitrary external data; default `true`
|
|
768
|
+
- `Tool.Strict`: OpenAI strict JSON Schema override; default `undefined` so provider/config decides
|
|
769
|
+
- `Tool.Title`: Human-readable title
|
|
770
|
+
|
|
771
|
+
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.
|
|
772
|
+
|
|
773
|
+
### JSON Schema Generation
|
|
774
|
+
|
|
775
|
+
```typescript
|
|
776
|
+
import * as Tool from 'effect/unstable/ai/Tool';
|
|
777
|
+
|
|
778
|
+
const tool = Tool.make('example', {
|
|
779
|
+
parameters: Schema.Struct({
|
|
780
|
+
name: Schema.String,
|
|
781
|
+
age: Schema.optional(Schema.Number)
|
|
782
|
+
})
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
const jsonSchema = Tool.getJsonSchema(tool);
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
**Output:**
|
|
789
|
+
|
|
790
|
+
```json
|
|
791
|
+
{
|
|
792
|
+
"type": "object",
|
|
793
|
+
"properties": {
|
|
794
|
+
"name": { "type": "string" },
|
|
795
|
+
"age": { "type": "number" }
|
|
796
|
+
},
|
|
797
|
+
"required": ["name"],
|
|
798
|
+
"additionalProperties": false
|
|
799
|
+
}
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
### Tool Guards
|
|
803
|
+
|
|
804
|
+
```typescript
|
|
805
|
+
import * as Tool from 'effect/unstable/ai/Tool';
|
|
806
|
+
|
|
807
|
+
const userTool = Tool.make('example');
|
|
808
|
+
const providerTool = Tool.providerDefined({
|
|
809
|
+
id: 'provider.tool',
|
|
810
|
+
customName: 'Example',
|
|
811
|
+
providerName: 'example',
|
|
812
|
+
args: Schema.Struct({})
|
|
813
|
+
})({});
|
|
814
|
+
|
|
815
|
+
Tool.isUserDefined(userTool);
|
|
816
|
+
Tool.isProviderDefined(providerTool);
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
### Dynamic Tool Selection
|
|
820
|
+
|
|
821
|
+
```typescript
|
|
822
|
+
import { Effect, Match } from 'effect';
|
|
823
|
+
|
|
824
|
+
const executeTool = (toolName: string, params: unknown) =>
|
|
825
|
+
Effect.gen(function* () {
|
|
826
|
+
const toolkit = yield* MyToolkitLayer;
|
|
827
|
+
|
|
828
|
+
const handler = Match.value(toolName).pipe(
|
|
829
|
+
Match.when('GetWeather', () =>
|
|
830
|
+
toolkit.handle('GetWeather', params)
|
|
831
|
+
),
|
|
832
|
+
Match.when('GetCurrentTime', () =>
|
|
833
|
+
toolkit.handle('GetCurrentTime', params)
|
|
834
|
+
),
|
|
835
|
+
Match.orElse(() =>
|
|
836
|
+
Effect.fail(new Error(`Unknown tool: ${toolName}`))
|
|
837
|
+
)
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
return yield* handler;
|
|
841
|
+
});
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
## Complete Example
|
|
845
|
+
|
|
846
|
+
```typescript
|
|
847
|
+
import * as Tool from 'effect/unstable/ai/Tool';
|
|
848
|
+
import * as Toolkit from 'effect/unstable/ai/Toolkit';
|
|
849
|
+
import { Effect, Schema, Layer, Stream } from 'effect';
|
|
850
|
+
|
|
851
|
+
class UserNotFound extends Schema.TaggedError<UserNotFound>()(
|
|
852
|
+
'UserNotFound',
|
|
853
|
+
{
|
|
854
|
+
userId: Schema.String
|
|
855
|
+
}
|
|
856
|
+
) {}
|
|
857
|
+
|
|
858
|
+
class Database extends Context.Service<
|
|
859
|
+
Database,
|
|
860
|
+
{
|
|
861
|
+
readonly query: (sql: string) => Effect.Effect<unknown>;
|
|
862
|
+
}
|
|
863
|
+
>()('Database') {}
|
|
864
|
+
|
|
865
|
+
const GetUser = Tool.make('GetUser', {
|
|
866
|
+
description: 'Retrieve user information by ID',
|
|
867
|
+
parameters: Schema.Struct({
|
|
868
|
+
userId: Schema.String
|
|
869
|
+
}),
|
|
870
|
+
success: Schema.Struct({
|
|
871
|
+
id: Schema.String,
|
|
872
|
+
name: Schema.String,
|
|
873
|
+
email: Schema.String
|
|
874
|
+
}),
|
|
875
|
+
failure: Schema.instanceOf(UserNotFound),
|
|
876
|
+
failureMode: 'error',
|
|
877
|
+
dependencies: [Database]
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
const CreateUser = Tool.make('CreateUser', {
|
|
881
|
+
description: 'Create a new user',
|
|
882
|
+
parameters: Schema.Struct({
|
|
883
|
+
name: Schema.String,
|
|
884
|
+
email: Schema.String
|
|
885
|
+
}),
|
|
886
|
+
success: Schema.Struct({
|
|
887
|
+
id: Schema.String,
|
|
888
|
+
name: Schema.String,
|
|
889
|
+
email: Schema.String
|
|
890
|
+
}),
|
|
891
|
+
dependencies: [Database]
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
const GetCurrentTime = Tool.make('GetCurrentTime', {
|
|
895
|
+
description: 'Get the current Unix timestamp',
|
|
896
|
+
success: Schema.Number
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
const UserToolkit = Toolkit.make(GetUser, CreateUser, GetCurrentTime);
|
|
900
|
+
|
|
901
|
+
const UserToolkitLive = UserToolkit.toLayer({
|
|
902
|
+
GetUser: ({ userId }) =>
|
|
903
|
+
Effect.gen(function* () {
|
|
904
|
+
const db = yield* Database;
|
|
905
|
+
const user = yield* db.query(
|
|
906
|
+
`SELECT * FROM users WHERE id = ?`,
|
|
907
|
+
userId
|
|
908
|
+
);
|
|
909
|
+
|
|
910
|
+
if (!user) {
|
|
911
|
+
return yield* Effect.fail(new UserNotFound({ userId }));
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
return user as { id: string; name: string; email: string };
|
|
915
|
+
}),
|
|
916
|
+
|
|
917
|
+
CreateUser: ({ name, email }) =>
|
|
918
|
+
Effect.gen(function* () {
|
|
919
|
+
const db = yield* Database;
|
|
920
|
+
const id = crypto.randomUUID();
|
|
921
|
+
|
|
922
|
+
yield* db.query(
|
|
923
|
+
`INSERT INTO users (id, name, email) VALUES (?, ?, ?)`,
|
|
924
|
+
id,
|
|
925
|
+
name,
|
|
926
|
+
email
|
|
927
|
+
);
|
|
928
|
+
|
|
929
|
+
return { id, name, email };
|
|
930
|
+
}),
|
|
931
|
+
|
|
932
|
+
GetCurrentTime: () => Effect.succeed(Date.now())
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
const DatabaseLive = Layer.succeed(Database, {
|
|
936
|
+
query: (sql: string, ...params: ReadonlyArray<unknown>) =>
|
|
937
|
+
Effect.logInfo(`Query: ${sql}`).pipe(Effect.as({}))
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
const program = Effect.gen(function* () {
|
|
941
|
+
const toolkit = yield* UserToolkitLive;
|
|
942
|
+
|
|
943
|
+
const createStream = yield* toolkit.handle('CreateUser', {
|
|
944
|
+
name: 'Alice',
|
|
945
|
+
email: 'alice@example.com'
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
yield* createStream.pipe(
|
|
949
|
+
Stream.runForEach((result) => Effect.log('Created user:', result.result))
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
const getStream = yield* toolkit.handle('GetUser', {
|
|
953
|
+
userId: 'user-123'
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
yield* getStream.pipe(
|
|
957
|
+
Stream.runForEach((result) => Effect.log('Retrieved user:', result.result))
|
|
958
|
+
);
|
|
959
|
+
|
|
960
|
+
const timeStream = yield* toolkit.handle('GetCurrentTime', {});
|
|
961
|
+
|
|
962
|
+
yield* timeStream.pipe(
|
|
963
|
+
Stream.runForEach((result) => Effect.log('Current time:', result.result))
|
|
964
|
+
);
|
|
965
|
+
}).pipe(Effect.provide(DatabaseLive));
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
## Import Patterns
|
|
969
|
+
|
|
970
|
+
**CRITICAL**: Always use namespace imports:
|
|
971
|
+
|
|
972
|
+
```typescript
|
|
973
|
+
import * as Tool from 'effect/unstable/ai/Tool';
|
|
974
|
+
import * as Toolkit from 'effect/unstable/ai/Toolkit';
|
|
975
|
+
import * as Prompt from 'effect/unstable/ai/Prompt';
|
|
976
|
+
import { Schema, Effect, Data, Context, Layer } from 'effect';
|
|
977
|
+
|
|
978
|
+
const myTool = Tool.make('example');
|
|
979
|
+
const myToolkit = Toolkit.make(myTool);
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
**NEVER** do this:
|
|
983
|
+
|
|
984
|
+
```typescript
|
|
985
|
+
import { make } from 'effect/unstable/ai/Tool';
|
|
986
|
+
import { make as makeToolkit } from 'effect/unstable/ai/Toolkit';
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
## Quality Checklist
|
|
990
|
+
|
|
991
|
+
### Mandatory - Every Tool
|
|
992
|
+
|
|
993
|
+
- [ ] Tool name is descriptive and unique
|
|
994
|
+
- [ ] Description explains what the tool does
|
|
995
|
+
- [ ] Parameters use `Schema.Struct(...)` or another `Schema.Top` schema (not raw field objects)
|
|
996
|
+
- [ ] Success schema matches handler return type
|
|
997
|
+
- [ ] Failure schema includes all tagged errors
|
|
998
|
+
- [ ] failureMode matches recovery strategy
|
|
999
|
+
- [ ] Dependencies declared if accessing services
|
|
1000
|
+
- [ ] Handler implements correct signature
|
|
1001
|
+
- [ ] Type signatures use Tool.Parameters, Tool.Success, Tool.Failure
|
|
1002
|
+
|
|
1003
|
+
### Conditional - Include When Appropriate
|
|
1004
|
+
|
|
1005
|
+
- [ ] Tool.Readonly annotation for read-only tools
|
|
1006
|
+
- [ ] Tool.Destructive annotation for mutating operations
|
|
1007
|
+
- [ ] Tool.Idempotent annotation for safe retries
|
|
1008
|
+
- [ ] Custom annotations via Tool.annotate
|
|
1009
|
+
- [ ] Provider-defined tools for native provider features
|
|
1010
|
+
- [ ] Toolkit.make with all tools (preferred in v4) or Toolkit.merge for combining tool collections
|
|
1011
|
+
- [ ] Error handling with catchTag in handlers
|
|
1012
|
+
|
|
1013
|
+
## Common Patterns
|
|
1014
|
+
|
|
1015
|
+
### Validation in Handlers
|
|
1016
|
+
|
|
1017
|
+
```typescript
|
|
1018
|
+
const ValidatedTool = Tool.make('validate', {
|
|
1019
|
+
parameters: Schema.Struct({
|
|
1020
|
+
input: Schema.String
|
|
1021
|
+
}),
|
|
1022
|
+
success: Schema.Struct({
|
|
1023
|
+
valid: Schema.Boolean,
|
|
1024
|
+
errors: Schema.Array(Schema.String)
|
|
1025
|
+
})
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
const toolkit = Toolkit.make(ValidatedTool);
|
|
1029
|
+
|
|
1030
|
+
const toolkitLayer = toolkit.toLayer({
|
|
1031
|
+
validate: ({ input }) =>
|
|
1032
|
+
Effect.gen(function* () {
|
|
1033
|
+
const errors: Array<string> = [];
|
|
1034
|
+
|
|
1035
|
+
if (input.length < 3) {
|
|
1036
|
+
errors.push('Input too short');
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if (!/^[a-z]+$/.test(input)) {
|
|
1040
|
+
errors.push('Input must be lowercase letters');
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
return {
|
|
1044
|
+
valid: errors.length === 0,
|
|
1045
|
+
errors
|
|
1046
|
+
};
|
|
1047
|
+
})
|
|
1048
|
+
});
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
### Async Operations in Handlers
|
|
1052
|
+
|
|
1053
|
+
```typescript
|
|
1054
|
+
const FetchTool = Tool.make('fetch', {
|
|
1055
|
+
parameters: Schema.Struct({
|
|
1056
|
+
url: Schema.String
|
|
1057
|
+
}),
|
|
1058
|
+
success: Schema.String
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
const toolkit = Toolkit.make(FetchTool);
|
|
1062
|
+
|
|
1063
|
+
const toolkitLayer = toolkit.toLayer({
|
|
1064
|
+
fetch: ({ url }) =>
|
|
1065
|
+
Effect.tryPromise({
|
|
1066
|
+
try: () => fetch(url).then((r) => r.text()),
|
|
1067
|
+
catch: (error) => new Error(`Fetch failed: ${error}`)
|
|
1068
|
+
})
|
|
1069
|
+
});
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
### Conditional Tool Execution
|
|
1073
|
+
|
|
1074
|
+
```typescript
|
|
1075
|
+
const ConditionalTool = Tool.make('process', {
|
|
1076
|
+
parameters: Schema.Struct({
|
|
1077
|
+
mode: Schema.Literals(['fast', 'thorough'])
|
|
1078
|
+
}),
|
|
1079
|
+
success: Schema.String
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
const toolkit = Toolkit.make(ConditionalTool);
|
|
1083
|
+
|
|
1084
|
+
const toolkitLayer = toolkit.toLayer({
|
|
1085
|
+
process: ({ mode }) =>
|
|
1086
|
+
mode === 'fast'
|
|
1087
|
+
? Effect.succeed('Fast result')
|
|
1088
|
+
: Effect.gen(function* () {
|
|
1089
|
+
yield* Effect.sleep('1 second');
|
|
1090
|
+
return 'Thorough result';
|
|
1091
|
+
})
|
|
1092
|
+
});
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
## When to Use This Skill
|
|
1096
|
+
|
|
1097
|
+
- Building LLM integrations with tool calling
|
|
1098
|
+
- Creating type-safe AI agent capabilities
|
|
1099
|
+
- Implementing function calling for Claude/OpenAI
|
|
1100
|
+
- Defining validated tool parameters and results
|
|
1101
|
+
- Composing multiple tools into toolkits
|
|
1102
|
+
- Managing tool handler dependencies
|
|
1103
|
+
- Integrating provider-native tools (bash, web search)
|
|
1104
|
+
|
|
1105
|
+
## Key Principles Summary
|
|
1106
|
+
|
|
1107
|
+
1. **Tool.make** - Define user tools with parameters, success, and failure schemas
|
|
1108
|
+
2. **Tool.dynamic** - Define runtime-discovered tools; raw JSON Schema params are `unknown`
|
|
1109
|
+
3. **Tool.providerDefined / OpenAiTool** - Use provider-native tools with `customName`
|
|
1110
|
+
4. **Parameters** - `Tool.make` takes `Schema.Top` schemas such as `Schema.Struct(...)`; raw JSON Schema belongs with `Tool.dynamic`
|
|
1111
|
+
5. **Toolkit.make** - Compose multiple tools together
|
|
1112
|
+
6. **toLayer** - Implement handlers returning Layer
|
|
1113
|
+
7. **toHandlers** - Implement handlers returning Context
|
|
1114
|
+
8. **toolkit.handle** - Execute tools and consume a Stream of preliminary/final results
|
|
1115
|
+
9. **ParametersEncoded** - Pass encoded schema input to `handle`; handlers receive decoded `Parameters`
|
|
1116
|
+
10. **HandlerResult** - Access typed result, encoded JSON, failure flag, and preliminary flag
|
|
1117
|
+
11. **needsApproval** - Produces approval requests until caller supplies approval responses
|
|
1118
|
+
12. **setNeedsApproval** - Clone a tool with a replacement static or effectful approval policy
|
|
1119
|
+
13. **toolCallId** - Pass the call ID through `toolkit.handle` and read it from handler context
|
|
1120
|
+
14. **failureMode** - Control error vs return failure strategy
|
|
1121
|
+
15. **dependencies** - Declare service requirements
|
|
1122
|
+
16. **Namespace imports** - Always `import * as Tool`
|
|
1123
|
+
17. **Prompt.makePart** - Create tool-call, tool-result, and approval parts with params
|
|
1124
|
+
|
|
1125
|
+
Your tool implementations should be type-safe, validated, and provide excellent developer experience with full schema support.
|
|
1126
|
+
|
|
1127
|
+
## Related Skills
|
|
1128
|
+
|
|
1129
|
+
- effect-ai-language-model - Using tools with generateText/streamText
|
|
1130
|
+
- effect-ai-prompt - Tool call/result message integration
|
|
1131
|
+
- effect-ai-streaming - Processing tool call streams
|
|
1132
|
+
- effect-ai-provider - Provider-defined tools
|