opencode-effect-enforcer 0.2.8 → 0.3.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/README.md +3 -3
- package/docs/effect-4.0.0-changelog.md +3213 -0
- package/docs/effect-4.0.0.md +110 -0
- package/guidance/effect-first-development.md +8 -6
- package/guidance/progressive-disclosure-guidance.md +15 -7
- package/package.json +2 -2
- package/patterns/avoid-any.md +2 -2
- package/patterns/avoid-direct-json.md +6 -6
- package/patterns/avoid-native-fetch.md +8 -6
- package/patterns/avoid-node-imports.md +2 -2
- package/patterns/avoid-non-null-assertion.md +2 -2
- package/patterns/avoid-object-type.md +2 -2
- package/patterns/avoid-platform-coupling.md +1 -1
- package/patterns/avoid-process-env.md +3 -4
- package/patterns/avoid-ts-ignore.md +1 -1
- package/patterns/context-tag-extends.md +11 -8
- package/patterns/effect-promise-vs-trypromise.md +6 -7
- package/patterns/prefer-arr-sort.md +1 -1
- package/patterns/prefer-effect-fn.md +21 -65
- package/patterns/prefer-schema-class.md +3 -3
- package/patterns/throw-in-effect-gen.md +1 -1
- package/patterns/use-clock-service.md +4 -0
- package/patterns/use-command-executor-service.md +2 -2
- package/patterns/use-http-client-service.md +8 -6
- package/patterns/use-random-service.md +6 -7
- package/skills/effect-ai-chat/SKILL.md +13 -7
- package/skills/effect-ai-language-model/SKILL.md +50 -21
- package/skills/effect-ai-prompt/SKILL.md +25 -14
- package/skills/effect-ai-provider/SKILL.md +50 -22
- package/skills/effect-ai-streaming/SKILL.md +27 -12
- package/skills/effect-ai-tool/SKILL.md +37 -28
- package/skills/effect-atom-rpc/SKILL.md +57 -36
- package/skills/effect-atom-state/SKILL.md +57 -19
- package/skills/effect-batching/SKILL.md +5 -3
- package/skills/effect-cache/SKILL.md +19 -7
- package/skills/effect-cli/SKILL.md +17 -8
- package/skills/effect-command-executor/SKILL.md +115 -64
- package/skills/effect-concurrency-testing/SKILL.md +26 -6
- package/skills/effect-config/SKILL.md +53 -2
- package/skills/effect-context-witness/SKILL.md +6 -6
- package/skills/effect-domain-modeling/SKILL.md +8 -1
- package/skills/effect-error-handling/SKILL.md +15 -2
- package/skills/effect-fiber/SKILL.md +20 -25
- package/skills/effect-filesystem/SKILL.md +69 -57
- package/skills/effect-http-api/SKILL.md +72 -22
- package/skills/effect-http-client/SKILL.md +25 -21
- package/skills/effect-http-server/SKILL.md +51 -21
- package/skills/effect-incremental-migration/SKILL.md +17 -8
- package/skills/effect-layer-design/SKILL.md +8 -0
- package/skills/effect-managed-runtime/SKILL.md +6 -0
- package/skills/effect-mcp-server/SKILL.md +64 -24
- package/skills/effect-observability/SKILL.md +61 -15
- package/skills/effect-parallelization/SKILL.md +24 -7
- package/skills/effect-path/SKILL.md +8 -2
- package/skills/effect-platform-abstraction/SKILL.md +88 -66
- package/skills/effect-platform-layers/SKILL.md +68 -67
- package/skills/effect-pubsub-event-bus/SKILL.md +56 -60
- package/skills/effect-react-composition/SKILL.md +19 -6
- package/skills/effect-rpc-api/SKILL.md +24 -24
- package/skills/effect-rpc-client/SKILL.md +33 -28
- package/skills/effect-rpc-cluster/SKILL.md +122 -78
- package/skills/effect-rpc-server/SKILL.md +56 -20
- package/skills/effect-scheduling/SKILL.md +29 -1
- package/skills/effect-schema-composition/SKILL.md +31 -13
- package/skills/effect-schema-v4/SKILL.md +94 -10
- package/skills/effect-scope/SKILL.md +13 -5
- package/skills/effect-service-implementation/SKILL.md +1 -1
- package/skills/effect-socket/SKILL.md +52 -8
- package/skills/effect-sql/SKILL.md +67 -33
- package/skills/effect-stream/SKILL.md +50 -5
- package/skills/effect-testing/SKILL.md +91 -2
- package/skills/effect-workflow/SKILL.md +76 -39
|
@@ -35,7 +35,7 @@ ChildProcessSpawner :: Effect a ChildProcessSpawner -- typed I/O, scoped lifeti
|
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
```haskell
|
|
38
|
-
-- Pattern (Effect v4 — effect/
|
|
38
|
+
-- Pattern (Effect v4 — effect/process)
|
|
39
39
|
bad :: () → IO String
|
|
40
40
|
bad = exec "git" ["status"] (cb) -- callback, untyped, no cancellation
|
|
41
41
|
|
|
@@ -46,7 +46,7 @@ good = do
|
|
|
46
46
|
-- typed output, error channel, scoped lifetime
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
Direct `child_process` imports give you callback APIs, manual lifecycle, and no error channel. Use `ChildProcessSpawner` and `ChildProcess` from `effect/
|
|
49
|
+
Direct `child_process` imports give you callback APIs, manual lifecycle, and no error channel. Use `ChildProcessSpawner` and `ChildProcess` from `effect/process` for typed errors, scoped resource lifetime, and platform-agnostic process spawning.
|
|
50
50
|
|
|
51
51
|
**Exceptions:**
|
|
52
52
|
|
|
@@ -49,26 +49,28 @@ good url = pipe
|
|
|
49
49
|
|
|
50
50
|
```haskell
|
|
51
51
|
-- Composable request building
|
|
52
|
-
request ::
|
|
52
|
+
request :: HttpClientRequest
|
|
53
53
|
request = pipe
|
|
54
|
-
(HttpClientRequest.
|
|
55
|
-
(HttpClientRequest.
|
|
54
|
+
(HttpClientRequest.get "https://api.example.com/users")
|
|
55
|
+
(HttpClientRequest.setHeader "accept" "application/json")
|
|
56
56
|
|
|
57
57
|
-- With retry, timeout, tracing
|
|
58
58
|
resilient :: Effect Response (HttpClient | HttpBodyError)
|
|
59
59
|
resilient = pipe
|
|
60
|
-
request
|
|
61
|
-
(Effect.flatMap HttpClient.execute)
|
|
60
|
+
(HttpClient.execute request)
|
|
62
61
|
(Effect.retry (Schedule.recurs 3))
|
|
63
62
|
(Effect.timeout (Duration.seconds 10))
|
|
64
63
|
|
|
65
64
|
-- Provide platform layer at entry point
|
|
66
65
|
main = program
|
|
67
|
-
& provide BunHttpClient.layer -- or NodeHttpClient.
|
|
66
|
+
& provide BunHttpClient.layer -- or NodeHttpClient.layerUndici
|
|
68
67
|
```
|
|
69
68
|
|
|
70
69
|
Direct `http` / `https` imports give you callback APIs, manual TLS plumbing, and no error channel. Use Effect's `HttpClient`, `HttpClientRequest`, and `HttpClientResponse` for typed errors, composable request building, declarative retry/timeout, and testability via layer substitution.
|
|
71
70
|
|
|
71
|
+
Import these modules from `effect/http`. The retry example assumes an idempotent
|
|
72
|
+
GET; only retry mutations when their idempotency is established by the contract.
|
|
73
|
+
|
|
72
74
|
**Exceptions:**
|
|
73
75
|
|
|
74
76
|
- Platform-specific layers that implement `HttpClient.HttpClient`
|
|
@@ -34,12 +34,11 @@ bad = floor (Math.random * 100) -- R = ∅, untestable
|
|
|
34
34
|
good :: Effect Int Random
|
|
35
35
|
good = Random.nextIntBetween 0 100 -- R ⊃ Random, deterministic in tests
|
|
36
36
|
|
|
37
|
-
--
|
|
38
|
-
|
|
39
|
-
test = do
|
|
40
|
-
TestRandom.feedInts [42, 7, 13] -- deterministic sequence
|
|
41
|
-
result ← good
|
|
42
|
-
assert (result == 42)
|
|
37
|
+
-- Repeatable sequence in tests
|
|
38
|
+
seeded = good & Random.withSeed "example-seed"
|
|
43
39
|
```
|
|
44
40
|
|
|
45
|
-
`Math.random()`
|
|
41
|
+
`Math.random()` bypasses Effect's random service. Use `Random.withSeed` for
|
|
42
|
+
repeatable sequences in tests, or provide a controlled `Random.Random` service
|
|
43
|
+
when the exact generated values matter. `Random.nextIntBetween(min, max)` includes
|
|
44
|
+
both endpoints by default; pass `{ halfOpen: true }` to exclude the upper bound.
|
|
@@ -8,14 +8,17 @@ You are an Effect TypeScript expert specializing in the `Chat` module for statef
|
|
|
8
8
|
## Effect Source Reference
|
|
9
9
|
|
|
10
10
|
The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
|
|
11
|
-
|
|
11
|
+
Use `git show effect@4.0.0:<path>` in that checkout for this skill's baseline;
|
|
12
|
+
main may be ahead. Keep `effect` and `@effect/*` packages on the same version.
|
|
13
|
+
The `effect/ai` APIs are tagged `@stability unstable`: minor releases may break
|
|
14
|
+
them even though the import path no longer contains `unstable`.
|
|
12
15
|
|
|
13
16
|
Reference this for:
|
|
14
17
|
|
|
15
|
-
- Chat module source: `packages/effect/src/
|
|
18
|
+
- Chat module source: `packages/effect/src/ai/Chat.ts`
|
|
16
19
|
- Chat usage examples: `ai-docs/src/71_ai/30_chat.ts`
|
|
17
20
|
- Tool integration examples: `ai-docs/src/71_ai/20_tools.ts`
|
|
18
|
-
- Prompt construction: `packages/effect/src/
|
|
21
|
+
- Prompt construction: `packages/effect/src/ai/Prompt.ts`
|
|
19
22
|
|
|
20
23
|
## Core Imports
|
|
21
24
|
|
|
@@ -28,7 +31,7 @@ import {
|
|
|
28
31
|
Tool,
|
|
29
32
|
Toolkit,
|
|
30
33
|
AiError
|
|
31
|
-
} from 'effect/
|
|
34
|
+
} from 'effect/ai';
|
|
32
35
|
```
|
|
33
36
|
|
|
34
37
|
## What Chat Provides
|
|
@@ -122,7 +125,10 @@ yield* session.generateText({ prompt: [] });
|
|
|
122
125
|
|
|
123
126
|
## Streaming Text
|
|
124
127
|
|
|
125
|
-
`streamText` returns a `Stream` of `Response.StreamPart` values. History is updated
|
|
128
|
+
`streamText` returns a `Stream` of `Response.StreamPart` values. History is updated
|
|
129
|
+
when the stream finalizes by folding the parts received so far. Consume to
|
|
130
|
+
completion to preserve the full response: interrupted text/reasoning sequences
|
|
131
|
+
without an end marker are not folded into history.
|
|
126
132
|
|
|
127
133
|
```ts
|
|
128
134
|
yield*
|
|
@@ -215,7 +221,7 @@ const restored = yield* Chat.fromExport(data);
|
|
|
215
221
|
For automatic persistence (save after every generation), use `Chat.Persistence`:
|
|
216
222
|
|
|
217
223
|
```ts
|
|
218
|
-
import { Persistence } from 'effect/
|
|
224
|
+
import { Persistence } from 'effect/persistence';
|
|
219
225
|
|
|
220
226
|
// Create a persistence layer and provide a BackingPersistence implementation
|
|
221
227
|
const PersistenceLayer = Chat.layerPersisted({ storeId: 'my-chats' }).pipe(
|
|
@@ -459,7 +465,7 @@ Each `Chat` instance uses an internal semaphore with 1 permit, ensuring that onl
|
|
|
459
465
|
|
|
460
466
|
1. **Always provide `LanguageModel.LanguageModel`** — `generateText`, `streamText`, and `generateObject` all require it in context. Provide via `Effect.provide(modelLayer)`.
|
|
461
467
|
2. **Use `prompt: []` in agentic loops** — After the initial prompt, pass an empty prompt to let the model respond based on accumulated history including tool results.
|
|
462
|
-
3. **Import from `effect/
|
|
468
|
+
3. **Import from `effect/ai`** — Chat, Prompt, Tool, Toolkit, LanguageModel, and AiError all come from this path.
|
|
463
469
|
4. **One session = one conversation** — Create separate `Chat` instances for independent conversations. Don't share a session across unrelated threads.
|
|
464
470
|
5. **Export before shutdown** — Use `exportJson` to persist state. Restore with `Chat.fromJson`.
|
|
465
471
|
6. **Provide toolkit handlers** — When using tools, the toolkit's handler layer must be provided (e.g., `Layer.provide(ToolsLayer)`).
|
|
@@ -7,16 +7,20 @@ description: Master the Effect AI LanguageModel service for text generation, str
|
|
|
7
7
|
|
|
8
8
|
Pattern guide for working with the LanguageModel service from Effect AI for type-safe LLM interactions with Effect's functional patterns.
|
|
9
9
|
|
|
10
|
+
Baseline: `effect@4.0.0`. Inspect that tag in the Effect source reference, not
|
|
11
|
+
unreleased main. Keep Effect-family packages on the same version. `effect/ai`
|
|
12
|
+
APIs carry `@stability unstable` and may change incompatibly in minor releases.
|
|
13
|
+
|
|
10
14
|
## Import Patterns
|
|
11
15
|
|
|
12
16
|
**CRITICAL**: Always use namespace imports:
|
|
13
17
|
|
|
14
18
|
```typescript
|
|
15
|
-
import * as LanguageModel from 'effect/
|
|
16
|
-
import * as Prompt from 'effect/
|
|
17
|
-
import * as Response from 'effect/
|
|
18
|
-
import * as Toolkit from 'effect/
|
|
19
|
-
import * as Tool from 'effect/
|
|
19
|
+
import * as LanguageModel from 'effect/ai/LanguageModel';
|
|
20
|
+
import * as Prompt from 'effect/ai/Prompt';
|
|
21
|
+
import * as Response from 'effect/ai/Response';
|
|
22
|
+
import * as Toolkit from 'effect/ai/Toolkit';
|
|
23
|
+
import * as Tool from 'effect/ai/Tool';
|
|
20
24
|
import * as Effect from 'effect/Effect';
|
|
21
25
|
import * as Stream from 'effect/Stream';
|
|
22
26
|
import * as Schema from 'effect/Schema';
|
|
@@ -56,7 +60,7 @@ LanguageModel ∈ R → Effect.gen(function*() {
|
|
|
56
60
|
Basic text generation with optional tool calling:
|
|
57
61
|
|
|
58
62
|
```typescript
|
|
59
|
-
import * as LanguageModel from 'effect/
|
|
63
|
+
import * as LanguageModel from 'effect/ai/LanguageModel';
|
|
60
64
|
import * as Effect from 'effect/Effect';
|
|
61
65
|
|
|
62
66
|
// Simple text generation
|
|
@@ -104,6 +108,12 @@ For `Schema.NumberFromString`, manual calls contain `{ count: '3' }`.
|
|
|
104
108
|
|
|
105
109
|
Pass those encoded params directly to `toolkit.handle(name, params, toolCallId)` when resolving manually. `Toolkit.handle` now accepts `Tool.ParametersEncoded<T>` and performs the decode before the handler receives `Tool.Parameters<T>`.
|
|
106
110
|
|
|
111
|
+
Provide `Tool.HandlerServices<T>` to both the outer `handle` Effect and the
|
|
112
|
+
returned Stream; parameter decoding can need services before streaming starts.
|
|
113
|
+
The outer Effect can fail with `AiError`, and the Stream can fail with
|
|
114
|
+
`Tool.HandlerError<T> | AiError`, including in `failureMode: 'return'` when
|
|
115
|
+
result encoding fails.
|
|
116
|
+
|
|
107
117
|
### Response Accessors
|
|
108
118
|
|
|
109
119
|
```typescript
|
|
@@ -129,7 +139,7 @@ When converting `response.content` back into history, `Prompt.fromResponseParts`
|
|
|
129
139
|
Force schema-validated output from the model:
|
|
130
140
|
|
|
131
141
|
```typescript
|
|
132
|
-
import * as LanguageModel from 'effect/
|
|
142
|
+
import * as LanguageModel from 'effect/ai/LanguageModel';
|
|
133
143
|
import * as Schema from 'effect/Schema';
|
|
134
144
|
import * as Effect from 'effect/Effect';
|
|
135
145
|
|
|
@@ -178,8 +188,9 @@ const extractEvent = LanguageModel.generateObject({
|
|
|
178
188
|
|
|
179
189
|
Real-time streaming text generation:
|
|
180
190
|
|
|
191
|
+
<!-- typecheck -->
|
|
181
192
|
```typescript
|
|
182
|
-
import * as LanguageModel from 'effect/
|
|
193
|
+
import * as LanguageModel from 'effect/ai/LanguageModel';
|
|
183
194
|
import * as Stream from 'effect/Stream';
|
|
184
195
|
import * as Effect from 'effect/Effect';
|
|
185
196
|
import * as Console from 'effect/Console';
|
|
@@ -196,7 +207,7 @@ const program = streamStory.pipe(
|
|
|
196
207
|
return Console.log(part.delta);
|
|
197
208
|
}
|
|
198
209
|
if (part.type === 'tool-params-delta') {
|
|
199
|
-
return Console.log('Tool params:', part.
|
|
210
|
+
return Console.log('Tool params:', part.delta);
|
|
200
211
|
}
|
|
201
212
|
return Effect.void;
|
|
202
213
|
})
|
|
@@ -214,7 +225,7 @@ Common StreamPart shapes include (non-exhaustive):
|
|
|
214
225
|
| { type: "reasoning-delta", id, delta }
|
|
215
226
|
| { type: "reasoning-end", id }
|
|
216
227
|
| { type: "tool-params-start", id, name }
|
|
217
|
-
| { type: "tool-params-delta", id,
|
|
228
|
+
| { type: "tool-params-delta", id, delta }
|
|
218
229
|
| { type: "tool-params-end", id }
|
|
219
230
|
| { type: "tool-call", id, name, params }
|
|
220
231
|
| { type: "tool-result", id, name, result, isFailure, preliminary? }
|
|
@@ -289,7 +300,7 @@ toolChoice: {
|
|
|
289
300
|
## Error Handling
|
|
290
301
|
|
|
291
302
|
```typescript
|
|
292
|
-
import * as AiError from 'effect/
|
|
303
|
+
import * as AiError from 'effect/ai/AiError';
|
|
293
304
|
|
|
294
305
|
const robust = LanguageModel.generateText({
|
|
295
306
|
prompt: 'Analyze this...'
|
|
@@ -311,10 +322,22 @@ remain explicit and provider/validation failures use `AiError`. Classification
|
|
|
311
322
|
requires at least two labels, rating requires at least two distinct ordered
|
|
312
323
|
levels, and a definition requires at least one decision.
|
|
313
324
|
|
|
325
|
+
`Decision.probability({ instructions })` estimates the probability of `true`.
|
|
326
|
+
Its `criteria` is optional; when supplied it must describe both `false` and
|
|
327
|
+
`true`. Guard `decision.criteria` before reading outcome descriptions.
|
|
328
|
+
|
|
329
|
+
Custom adapters can set `DecisionModel.make({ decide, probabilityPrecision })`
|
|
330
|
+
to accept rounding drift in classification/rating distributions. At precision
|
|
331
|
+
`p`, the allowed sum drift is half a unit of the last decimal place per label
|
|
332
|
+
(plus the core numerical tolerance); accepted drift is rescaled to sum to one.
|
|
333
|
+
Zero sums, invalid probabilities, and larger drift still fail with
|
|
334
|
+
`InvalidOutputError`. Omit the option for strict sum validation. OpenRouter and
|
|
335
|
+
TypeSafe adapters use precision `2`.
|
|
336
|
+
|
|
314
337
|
<!-- typecheck -->
|
|
315
338
|
```ts
|
|
316
339
|
import * as Schema from 'effect/Schema';
|
|
317
|
-
import { Decision, DecisionModel } from 'effect/
|
|
340
|
+
import { Decision, DecisionModel } from 'effect/ai';
|
|
318
341
|
|
|
319
342
|
class Ticket extends Schema.Class<Ticket>('Ticket')({ body: Schema.String }) {}
|
|
320
343
|
const triage = Decision.make({
|
|
@@ -323,7 +346,8 @@ const triage = Decision.make({
|
|
|
323
346
|
team: Decision.classify({
|
|
324
347
|
instructions: 'Choose the team responsible for this request',
|
|
325
348
|
criteria: { billing: 'Payments and invoices', support: 'Product support' }
|
|
326
|
-
})
|
|
349
|
+
}),
|
|
350
|
+
urgent: Decision.probability({ instructions: 'The ticket needs immediate attention' })
|
|
327
351
|
}
|
|
328
352
|
});
|
|
329
353
|
const result = DecisionModel.decide(triage, { input: new Ticket({ body: 'Invoice question' }) });
|
|
@@ -332,7 +356,7 @@ const result = DecisionModel.decide(triage, { input: new Ticket({ body: 'Invoice
|
|
|
332
356
|
## Type Extraction Utilities
|
|
333
357
|
|
|
334
358
|
```typescript
|
|
335
|
-
import type * as LanguageModel from 'effect/
|
|
359
|
+
import type * as LanguageModel from 'effect/ai/LanguageModel';
|
|
336
360
|
|
|
337
361
|
// Extract error types from options
|
|
338
362
|
type MyError = LanguageModel.ExtractError<typeof options>;
|
|
@@ -360,9 +384,14 @@ make :: ConstructorParams → Effect Service
|
|
|
360
384
|
|
|
361
385
|
When implementing a custom LanguageModel provider, return encoded parts: `Array<Response.PartEncoded>` for `generateText` and `Stream<Response.StreamPartEncoded>` for `streamText`. If you emit `response-metadata`, encode timestamps as ISO strings. Providers that support provider-side conversations should honor `ProviderOptions.previousResponseId` and `ProviderOptions.incrementalPrompt`; providers that cannot should intentionally ignore them.
|
|
362
386
|
|
|
387
|
+
`ProviderOptions.incrementalFallback` is `true` when the core retries a rejected
|
|
388
|
+
incremental request with the full prompt. The fallback clears
|
|
389
|
+
`previousResponseId` and `incrementalPrompt`; use that signal to reset adapter
|
|
390
|
+
turn state without unnecessarily closing a healthy connection.
|
|
391
|
+
|
|
363
392
|
```typescript
|
|
364
|
-
import * as LanguageModel from 'effect/
|
|
365
|
-
import * as Response from 'effect/
|
|
393
|
+
import * as LanguageModel from 'effect/ai/LanguageModel';
|
|
394
|
+
import * as Response from 'effect/ai/Response';
|
|
366
395
|
|
|
367
396
|
const makeCustomProvider = Effect.gen(function* () {
|
|
368
397
|
const service = yield* LanguageModel.make({
|
|
@@ -655,7 +684,7 @@ const generated = LanguageModel.generateText({ prompt: '...' }).pipe(
|
|
|
655
684
|
Inside an effect that runs with a language model provider, you can retrieve the current provider name:
|
|
656
685
|
|
|
657
686
|
```typescript
|
|
658
|
-
import * as Model from 'effect/
|
|
687
|
+
import * as Model from 'effect/ai/Model';
|
|
659
688
|
|
|
660
689
|
const program = Effect.gen(function* () {
|
|
661
690
|
const providerName = yield* Model.ProviderName;
|
|
@@ -672,7 +701,7 @@ const program = Effect.gen(function* () {
|
|
|
672
701
|
|
|
673
702
|
## References
|
|
674
703
|
|
|
675
|
-
- Source: `packages/effect/src/
|
|
676
|
-
- Chat integration: `packages/effect/src/
|
|
677
|
-
- Response types: `effect/
|
|
678
|
-
- Tool system: `effect/
|
|
704
|
+
- Source: `packages/effect/src/ai/LanguageModel.ts`
|
|
705
|
+
- Chat integration: `packages/effect/src/ai/Chat.ts`
|
|
706
|
+
- Response types: `effect/ai/Response`
|
|
707
|
+
- Tool system: `effect/ai/Tool`, `effect/ai/Toolkit`
|
|
@@ -7,13 +7,17 @@ description: Build prompts for Effect AI using messages, parts, and composition
|
|
|
7
7
|
|
|
8
8
|
Master the Effect AI Prompt API for building type-safe conversations with language models.
|
|
9
9
|
|
|
10
|
+
Baseline: `effect@4.0.0`; inspect that tag in the Effect source reference.
|
|
11
|
+
`effect/ai` APIs remain `@stability unstable` and can break in minor releases.
|
|
12
|
+
Keep provider packages on the same version as `effect`.
|
|
13
|
+
|
|
10
14
|
## Import Patterns
|
|
11
15
|
|
|
12
16
|
**CRITICAL**: Always use namespace imports:
|
|
13
17
|
|
|
14
18
|
```typescript
|
|
15
|
-
import * as Prompt from 'effect/
|
|
16
|
-
import * as Response from 'effect/
|
|
19
|
+
import * as Prompt from 'effect/ai/Prompt';
|
|
20
|
+
import * as Response from 'effect/ai/Response';
|
|
17
21
|
import { pipe } from 'effect';
|
|
18
22
|
```
|
|
19
23
|
|
|
@@ -54,12 +58,13 @@ The Effect v4 module also exports runtime schemas for every part and each role-s
|
|
|
54
58
|
|
|
55
59
|
## Message Types
|
|
56
60
|
|
|
57
|
-
Each message has `role` and `content`.
|
|
61
|
+
Each message has `role` and `content`. System content is a string; other roles
|
|
62
|
+
contain arrays of the parts allowed for that role.
|
|
58
63
|
|
|
59
64
|
### System Messages
|
|
60
65
|
|
|
61
66
|
```typescript
|
|
62
|
-
import * as Prompt from 'effect/
|
|
67
|
+
import * as Prompt from 'effect/ai/Prompt';
|
|
63
68
|
|
|
64
69
|
// String content only
|
|
65
70
|
const system = Prompt.makeMessage('system', {
|
|
@@ -368,7 +373,7 @@ const appended = pipe(prompt, Prompt.appendSystem(' Be concise.'));
|
|
|
368
373
|
### Convert AI Response to Prompt
|
|
369
374
|
|
|
370
375
|
```typescript
|
|
371
|
-
import * as Response from 'effect/
|
|
376
|
+
import * as Response from 'effect/ai/Response';
|
|
372
377
|
|
|
373
378
|
const responseParts: ReadonlyArray<Response.AnyPart> = [
|
|
374
379
|
Response.makePart('text-start', { id: 'text_1' }),
|
|
@@ -400,7 +405,13 @@ const responseParts: ReadonlyArray<Response.AnyPart> = [
|
|
|
400
405
|
const historyPrompt = Prompt.fromResponseParts(responseParts);
|
|
401
406
|
```
|
|
402
407
|
|
|
403
|
-
`Prompt.fromResponseParts` folds streaming text/reasoning
|
|
408
|
+
`Prompt.fromResponseParts` folds streaming text/reasoning by matching IDs and
|
|
409
|
+
emits the accumulated part on its end marker. Pass complete start/delta/end
|
|
410
|
+
sequences rather than isolated deltas. It places tool calls and approval requests
|
|
411
|
+
in assistant messages and skips preliminary tool results. For final tool results
|
|
412
|
+
it always uses `encodedResult`: framework-executed results
|
|
413
|
+
(`providerExecuted: false`) become tool messages, while provider-executed results
|
|
414
|
+
(`providerExecuted: true`) remain in the assistant message with that flag preserved.
|
|
404
415
|
|
|
405
416
|
This distinction matters for hosted tools such as provider web search or code execution. Moving their results into a tool message changes the conversation shape expected by the provider.
|
|
406
417
|
|
|
@@ -426,7 +437,7 @@ This keeps prompt assembly inside the Effect graph instead of forcing Promise is
|
|
|
426
437
|
```typescript
|
|
427
438
|
import { Effect } from 'effect';
|
|
428
439
|
import * as SubscriptionRef from 'effect/SubscriptionRef';
|
|
429
|
-
import * as LanguageModel from 'effect/
|
|
440
|
+
import * as LanguageModel from 'effect/ai/LanguageModel';
|
|
430
441
|
|
|
431
442
|
const chat = Effect.gen(function* () {
|
|
432
443
|
const history = yield* SubscriptionRef.make(Prompt.empty);
|
|
@@ -456,7 +467,7 @@ const chat = Effect.gen(function* () {
|
|
|
456
467
|
### Generate Text
|
|
457
468
|
|
|
458
469
|
```typescript
|
|
459
|
-
import * as LanguageModel from 'effect/
|
|
470
|
+
import * as LanguageModel from 'effect/ai/LanguageModel';
|
|
460
471
|
import { Effect } from 'effect';
|
|
461
472
|
|
|
462
473
|
const program = Effect.gen(function* () {
|
|
@@ -474,7 +485,7 @@ const program = Effect.gen(function* () {
|
|
|
474
485
|
### Stream Text
|
|
475
486
|
|
|
476
487
|
```typescript
|
|
477
|
-
import * as LanguageModel from 'effect/
|
|
488
|
+
import * as LanguageModel from 'effect/ai/LanguageModel';
|
|
478
489
|
import { Effect, Stream } from 'effect';
|
|
479
490
|
|
|
480
491
|
const program = Effect.gen(function* () {
|
|
@@ -493,7 +504,7 @@ const program = Effect.gen(function* () {
|
|
|
493
504
|
### Generate Object
|
|
494
505
|
|
|
495
506
|
```typescript
|
|
496
|
-
import * as LanguageModel from 'effect/
|
|
507
|
+
import * as LanguageModel from 'effect/ai/LanguageModel';
|
|
497
508
|
import { Effect, Schema } from 'effect';
|
|
498
509
|
|
|
499
510
|
const Contact = Schema.Struct({
|
|
@@ -525,8 +536,8 @@ may reject it. Provider config uses snake_case; Prompt metadata uses camelCase.
|
|
|
525
536
|
```typescript
|
|
526
537
|
import { OpenAiLanguageModel } from '@effect/ai-openai';
|
|
527
538
|
import { Effect } from 'effect';
|
|
528
|
-
import * as LanguageModel from 'effect/
|
|
529
|
-
import * as Prompt from 'effect/
|
|
539
|
+
import * as LanguageModel from 'effect/ai/LanguageModel';
|
|
540
|
+
import * as Prompt from 'effect/ai/Prompt';
|
|
530
541
|
|
|
531
542
|
const program = LanguageModel.generateText({
|
|
532
543
|
prompt: Prompt.make([
|
|
@@ -552,7 +563,7 @@ OpenAI client layer at the runtime boundary (see `effect-ai-provider`).
|
|
|
552
563
|
|
|
553
564
|
```typescript
|
|
554
565
|
// Augment options interfaces via module augmentation
|
|
555
|
-
declare module 'effect/
|
|
566
|
+
declare module 'effect/ai/Prompt' {
|
|
556
567
|
interface TextPartOptions {
|
|
557
568
|
readonly anthropic?: {
|
|
558
569
|
readonly cache_control?: {
|
|
@@ -703,7 +714,7 @@ const toolInteraction = Prompt.make([
|
|
|
703
714
|
## Type Guards
|
|
704
715
|
|
|
705
716
|
```typescript
|
|
706
|
-
import * as Prompt from 'effect/
|
|
717
|
+
import * as Prompt from 'effect/ai/Prompt';
|
|
707
718
|
|
|
708
719
|
declare const value: unknown;
|
|
709
720
|
|
|
@@ -7,6 +7,11 @@ description: Configure and compose AI provider layers using @effect/ai packages.
|
|
|
7
7
|
|
|
8
8
|
Configure AI provider layers for language model integration using Effect's AI ecosystem.
|
|
9
9
|
|
|
10
|
+
Baseline: `effect@4.0.0`; inspect the matching tag in the Effect source reference.
|
|
11
|
+
Keep `effect` and every `@effect/*` package on the same version. Core AI APIs and
|
|
12
|
+
provider clients, models, and generated schemas carry `@stability unstable`:
|
|
13
|
+
minor releases may include breaking changes, even with `effect/ai` import paths.
|
|
14
|
+
|
|
10
15
|
## When to Use This Skill
|
|
11
16
|
|
|
12
17
|
Use this skill when:
|
|
@@ -34,7 +39,7 @@ import {
|
|
|
34
39
|
Stream
|
|
35
40
|
} from 'effect';
|
|
36
41
|
|
|
37
|
-
// From "effect/
|
|
42
|
+
// From "effect/ai" — namespace imports
|
|
38
43
|
import {
|
|
39
44
|
AiError,
|
|
40
45
|
Chat,
|
|
@@ -43,13 +48,13 @@ import {
|
|
|
43
48
|
Prompt,
|
|
44
49
|
Tool,
|
|
45
50
|
Toolkit
|
|
46
|
-
} from 'effect/
|
|
51
|
+
} from 'effect/ai';
|
|
47
52
|
// Or individually:
|
|
48
|
-
import * as LanguageModel from 'effect/
|
|
49
|
-
import * as Chat from 'effect/
|
|
50
|
-
import * as Model from 'effect/
|
|
51
|
-
import * as Prompt from 'effect/
|
|
52
|
-
import * as AiError from 'effect/
|
|
53
|
+
import * as LanguageModel from 'effect/ai/LanguageModel';
|
|
54
|
+
import * as Chat from 'effect/ai/Chat';
|
|
55
|
+
import * as Model from 'effect/ai/Model';
|
|
56
|
+
import * as Prompt from 'effect/ai/Prompt';
|
|
57
|
+
import * as AiError from 'effect/ai/AiError';
|
|
53
58
|
|
|
54
59
|
// Anthropic
|
|
55
60
|
import { AnthropicClient, AnthropicLanguageModel } from '@effect/ai-anthropic';
|
|
@@ -70,7 +75,7 @@ import {
|
|
|
70
75
|
} from '@effect/ai-openrouter';
|
|
71
76
|
|
|
72
77
|
// HTTP client (required by all providers)
|
|
73
|
-
import { FetchHttpClient } from 'effect/
|
|
78
|
+
import { FetchHttpClient } from 'effect/http';
|
|
74
79
|
```
|
|
75
80
|
|
|
76
81
|
## Provider Layer Pattern
|
|
@@ -96,7 +101,7 @@ ProviderClient.layerConfig :: { apiKey } → Layer Client HttpClient
|
|
|
96
101
|
```typescript
|
|
97
102
|
import { AnthropicClient, AnthropicLanguageModel } from '@effect/ai-anthropic';
|
|
98
103
|
import { Config, Layer } from 'effect';
|
|
99
|
-
import { FetchHttpClient } from 'effect/
|
|
104
|
+
import { FetchHttpClient } from 'effect/http';
|
|
100
105
|
|
|
101
106
|
// Client layer (reusable across models)
|
|
102
107
|
const AnthropicClientLayer = AnthropicClient.layerConfig({
|
|
@@ -115,6 +120,12 @@ const AnthropicLive = AnthropicLanguageModel.layer({
|
|
|
115
120
|
|
|
116
121
|
Anthropic capability detection preserves the lower output limits and structured-output support of known legacy Claude models. Unknown and newly released model identifiers default to modern capabilities: native structured outputs and `128_000` output tokens. Override capability detection with `structuredOutputs: false` (or `true`) when a model or compatible endpoint differs from that default; set `max_tokens` separately when the provider's output limit differs.
|
|
117
122
|
|
|
123
|
+
Anthropic-compatible gateways may omit `usage.inference_geo`. Malformed gateway
|
|
124
|
+
4xx error bodies are classified by HTTP status (including 429 retry delays), not
|
|
125
|
+
as `InvalidOutputError`. Direct `client.betaMessagesPost` calls expose
|
|
126
|
+
`HttpClientError` for those responses; the language-model adapter maps them to
|
|
127
|
+
`AiError` reasons.
|
|
128
|
+
|
|
118
129
|
```typescript
|
|
119
130
|
const compatibleClaude = AnthropicLanguageModel.model('future-claude-model', {
|
|
120
131
|
structuredOutputs: false,
|
|
@@ -127,7 +138,7 @@ const compatibleClaude = AnthropicLanguageModel.model('future-claude-model', {
|
|
|
127
138
|
```typescript
|
|
128
139
|
import { OpenAiClient, OpenAiLanguageModel } from '@effect/ai-openai';
|
|
129
140
|
import { Config, Layer } from 'effect';
|
|
130
|
-
import { FetchHttpClient } from 'effect/
|
|
141
|
+
import { FetchHttpClient } from 'effect/http';
|
|
131
142
|
|
|
132
143
|
const OpenAiClientLayer = OpenAiClient.layerConfig({
|
|
133
144
|
apiKey: Config.Redacted('OPENAI_API_KEY')
|
|
@@ -151,6 +162,10 @@ Public OpenAI modules:
|
|
|
151
162
|
|
|
152
163
|
`OpenAiSchema.ResponseStreamEvent` accepts both flat OpenAI error events and compatible-provider events with details nested under `error`, normalizing both to the same decoded error event shape.
|
|
153
164
|
|
|
165
|
+
For WebSocket turns, `previous_response_not_found` triggers a retry with the full
|
|
166
|
+
prompt. An open socket is retained; custom adapters can inspect
|
|
167
|
+
`ProviderOptions.incrementalFallback` to recognize the full-prompt retry.
|
|
168
|
+
|
|
154
169
|
```typescript
|
|
155
170
|
const client = yield* OpenAiClient.OpenAiClient;
|
|
156
171
|
const [body] = yield* client.createResponse({
|
|
@@ -176,14 +191,21 @@ const NativeTools = Toolkit.make(
|
|
|
176
191
|
|
|
177
192
|
Available hosted/provider tools include `WebSearch`, `CodeInterpreter`, `FileSearch`, `ImageGeneration`, and `Mcp` (`customName: "OpenAiMcp"`). MCP tool approval requests/results use this canonical `OpenAiMcp` name and the normal Effect AI approval request/response parts. Handler-required local tools such as `Shell`, `LocalShell`, and `ApplyPatch` run in your environment; provide handlers only behind explicit sandboxing, authorization, and audit policy.
|
|
178
193
|
|
|
194
|
+
Web-search calls may omit `action`. Narrow that optional field before reading
|
|
195
|
+
action details; incomplete calls are preserved as failed tool results.
|
|
196
|
+
|
|
179
197
|
## OpenAI-Compatible Providers
|
|
180
198
|
|
|
181
|
-
Use `apiUrl` with `@effect/ai-openai` for
|
|
199
|
+
Use `apiUrl` with `@effect/ai-openai` for APIs compatible with OpenAI Responses.
|
|
200
|
+
For Chat Completions-compatible endpoints use `@effect/ai-openai-compat`
|
|
201
|
+
(`OpenAiClient`, `OpenAiLanguageModel`), which preserves non-null text/tool
|
|
202
|
+
argument fragments when other streamed delta fields are null. Both adapters
|
|
203
|
+
preserve raw JSON Schema dynamic-tool arguments.
|
|
182
204
|
|
|
183
205
|
```typescript
|
|
184
206
|
import { OpenAiClient, OpenAiConfig } from '@effect/ai-openai';
|
|
185
207
|
import { Config, Layer } from 'effect';
|
|
186
|
-
import { FetchHttpClient, HttpClient, HttpClientRequest } from 'effect/
|
|
208
|
+
import { FetchHttpClient, HttpClient, HttpClientRequest } from 'effect/http';
|
|
187
209
|
|
|
188
210
|
const CompatibleClientLayer = OpenAiClient.layerConfig({
|
|
189
211
|
apiKey: Config.Redacted('OPENAI_COMPAT_API_KEY'),
|
|
@@ -208,7 +230,7 @@ import {
|
|
|
208
230
|
OpenRouterLanguageModel
|
|
209
231
|
} from '@effect/ai-openrouter';
|
|
210
232
|
import { Config, Layer } from 'effect';
|
|
211
|
-
import { FetchHttpClient } from 'effect/
|
|
233
|
+
import { FetchHttpClient } from 'effect/http';
|
|
212
234
|
|
|
213
235
|
const OpenRouterClientLayer = OpenRouterClient.layerConfig({
|
|
214
236
|
apiKey: Config.Redacted('OPENROUTER_API_KEY')
|
|
@@ -226,13 +248,13 @@ const routerModel = OpenRouterLanguageModel.model('anthropic/claude-sonnet-4');
|
|
|
226
248
|
import { AnthropicClient, AnthropicLanguageModel } from '@effect/ai-anthropic';
|
|
227
249
|
import { OpenAiClient, OpenAiLanguageModel } from '@effect/ai-openai';
|
|
228
250
|
import { Effect, ExecutionPlan, Layer } from 'effect';
|
|
229
|
-
import { LanguageModel } from 'effect/
|
|
251
|
+
import { LanguageModel } from 'effect/ai';
|
|
230
252
|
|
|
231
253
|
// Try cheaper model first, fall back to more expensive one
|
|
232
254
|
const DraftPlan = ExecutionPlan.make(
|
|
233
255
|
{
|
|
234
256
|
provide: OpenAiLanguageModel.model('gpt-5.2'),
|
|
235
|
-
attempts: 3 //
|
|
257
|
+
attempts: 3 // up to 3 total attempts before falling back
|
|
236
258
|
},
|
|
237
259
|
{
|
|
238
260
|
provide: AnthropicLanguageModel.model('claude-opus-4-6'),
|
|
@@ -278,7 +300,7 @@ Maintain conversation history with automatic context management:
|
|
|
278
300
|
|
|
279
301
|
```typescript
|
|
280
302
|
import { Effect, Ref } from 'effect';
|
|
281
|
-
import { Chat, Prompt } from 'effect/
|
|
303
|
+
import { Chat, Prompt } from 'effect/ai';
|
|
282
304
|
|
|
283
305
|
// Create with system prompt
|
|
284
306
|
const session =
|
|
@@ -361,7 +383,7 @@ OpenAI error classification distinguishes temporary rate limits from exhausted a
|
|
|
361
383
|
Wrap provider layers with metadata. Takes 3 positional arguments: `(providerName, modelId, layer)`:
|
|
362
384
|
|
|
363
385
|
```typescript
|
|
364
|
-
import { Model } from 'effect/
|
|
386
|
+
import { Model } from 'effect/ai';
|
|
365
387
|
|
|
366
388
|
// This is what ProviderLanguageModel.model() calls internally:
|
|
367
389
|
const Claude = Model.make(
|
|
@@ -437,7 +459,7 @@ Wrap `AiError` into domain-specific tagged errors:
|
|
|
437
459
|
|
|
438
460
|
```typescript
|
|
439
461
|
import { Schema } from 'effect';
|
|
440
|
-
import { AiError } from 'effect/
|
|
462
|
+
import { AiError } from 'effect/ai';
|
|
441
463
|
|
|
442
464
|
export class MyAiError extends Schema.TaggedError<MyAiError>()(
|
|
443
465
|
'MyAiError',
|
|
@@ -459,7 +481,7 @@ export class MyAiError extends Schema.TaggedError<MyAiError>()(
|
|
|
459
481
|
| ----------------------- | ------------- | ----------------------------------------------- |
|
|
460
482
|
| `@effect/ai-anthropic` | Anthropic | Claude Opus 4, Claude Sonnet 4, etc. |
|
|
461
483
|
| `@effect/ai-openai` | OpenAI | GPT-5, GPT-4.1, o-series, etc. |
|
|
462
|
-
| `@effect/ai-openai`
|
|
484
|
+
| `@effect/ai-openai-compat` | OpenAI-Compat | Chat Completions-compatible APIs via `apiUrl` |
|
|
463
485
|
| `@effect/ai-openrouter` | OpenRouter | Multi-provider proxy (any model ID) |
|
|
464
486
|
|
|
465
487
|
**Note**: There are no `@effect/ai-google` or `@effect/ai-amazon-bedrock` packages. Use OpenRouter to access Google/Bedrock models.
|
|
@@ -488,8 +510,8 @@ import {
|
|
|
488
510
|
Model,
|
|
489
511
|
Prompt,
|
|
490
512
|
type Response
|
|
491
|
-
} from 'effect/
|
|
492
|
-
import { FetchHttpClient } from 'effect/
|
|
513
|
+
} from 'effect/ai';
|
|
514
|
+
import { FetchHttpClient } from 'effect/http';
|
|
493
515
|
|
|
494
516
|
// ---------------------------------------------------------------------------
|
|
495
517
|
// Provider client layers
|
|
@@ -675,10 +697,16 @@ import { BedrockClient } from '@effect/ai-amazon-bedrock'; // Does NOT exist
|
|
|
675
697
|
## References
|
|
676
698
|
|
|
677
699
|
Provider-neutral structured decisions use `Decision` / `DecisionModel` from
|
|
678
|
-
`effect/
|
|
700
|
+
`effect/ai`. `OpenRouterDecisionModel` targets OpenRouter's alpha Decisions
|
|
679
701
|
API; `@effect/ai-typesafe` supplies a provider for TypeSafe System One. Custom
|
|
680
702
|
OpenRouter client implementations include `createDecisions`.
|
|
681
703
|
|
|
704
|
+
`OpenRouterDecisionModel` and the TypeSafe decision model configure
|
|
705
|
+
`probabilityPrecision: 2`: rounded distributions such as `0.02 / 0.93 / 0.04`
|
|
706
|
+
are accepted and rescaled. Custom `DecisionModel.make` adapters retain strict sum
|
|
707
|
+
validation unless they opt in. `Decision.probability` allows omitted `criteria`;
|
|
708
|
+
when present it must describe both boolean outcomes (see `effect-ai-language-model`).
|
|
709
|
+
|
|
682
710
|
Use each branded service's same-name type (`LanguageModel.LanguageModel`,
|
|
683
711
|
`EmbeddingModel.EmbeddingModel`, `Chat.Chat`) and its exported TypeId for custom
|
|
684
712
|
implementations. Prefer provided constructors; do not hard-code marker strings.
|