opencode-effect-enforcer 0.2.8 → 0.4.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 +6 -5
- 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 +6 -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-decision-model/SKILL.md +301 -0
- package/skills/effect-ai-decision-model/openrouter.md +70 -0
- package/skills/effect-ai-language-model/SKILL.md +53 -21
- package/skills/effect-ai-prompt/SKILL.md +25 -14
- package/skills/effect-ai-provider/SKILL.md +53 -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
|
@@ -7,16 +7,23 @@ 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
|
+
|
|
14
|
+
For System One classification, rating, and probability calls through
|
|
15
|
+
`DecisionModel`, use `effect-ai-decision-model`.
|
|
16
|
+
|
|
10
17
|
## Import Patterns
|
|
11
18
|
|
|
12
19
|
**CRITICAL**: Always use namespace imports:
|
|
13
20
|
|
|
14
21
|
```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/
|
|
22
|
+
import * as LanguageModel from 'effect/ai/LanguageModel';
|
|
23
|
+
import * as Prompt from 'effect/ai/Prompt';
|
|
24
|
+
import * as Response from 'effect/ai/Response';
|
|
25
|
+
import * as Toolkit from 'effect/ai/Toolkit';
|
|
26
|
+
import * as Tool from 'effect/ai/Tool';
|
|
20
27
|
import * as Effect from 'effect/Effect';
|
|
21
28
|
import * as Stream from 'effect/Stream';
|
|
22
29
|
import * as Schema from 'effect/Schema';
|
|
@@ -56,7 +63,7 @@ LanguageModel ∈ R → Effect.gen(function*() {
|
|
|
56
63
|
Basic text generation with optional tool calling:
|
|
57
64
|
|
|
58
65
|
```typescript
|
|
59
|
-
import * as LanguageModel from 'effect/
|
|
66
|
+
import * as LanguageModel from 'effect/ai/LanguageModel';
|
|
60
67
|
import * as Effect from 'effect/Effect';
|
|
61
68
|
|
|
62
69
|
// Simple text generation
|
|
@@ -104,6 +111,12 @@ For `Schema.NumberFromString`, manual calls contain `{ count: '3' }`.
|
|
|
104
111
|
|
|
105
112
|
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
113
|
|
|
114
|
+
Provide `Tool.HandlerServices<T>` to both the outer `handle` Effect and the
|
|
115
|
+
returned Stream; parameter decoding can need services before streaming starts.
|
|
116
|
+
The outer Effect can fail with `AiError`, and the Stream can fail with
|
|
117
|
+
`Tool.HandlerError<T> | AiError`, including in `failureMode: 'return'` when
|
|
118
|
+
result encoding fails.
|
|
119
|
+
|
|
107
120
|
### Response Accessors
|
|
108
121
|
|
|
109
122
|
```typescript
|
|
@@ -129,7 +142,7 @@ When converting `response.content` back into history, `Prompt.fromResponseParts`
|
|
|
129
142
|
Force schema-validated output from the model:
|
|
130
143
|
|
|
131
144
|
```typescript
|
|
132
|
-
import * as LanguageModel from 'effect/
|
|
145
|
+
import * as LanguageModel from 'effect/ai/LanguageModel';
|
|
133
146
|
import * as Schema from 'effect/Schema';
|
|
134
147
|
import * as Effect from 'effect/Effect';
|
|
135
148
|
|
|
@@ -178,8 +191,9 @@ const extractEvent = LanguageModel.generateObject({
|
|
|
178
191
|
|
|
179
192
|
Real-time streaming text generation:
|
|
180
193
|
|
|
194
|
+
<!-- typecheck -->
|
|
181
195
|
```typescript
|
|
182
|
-
import * as LanguageModel from 'effect/
|
|
196
|
+
import * as LanguageModel from 'effect/ai/LanguageModel';
|
|
183
197
|
import * as Stream from 'effect/Stream';
|
|
184
198
|
import * as Effect from 'effect/Effect';
|
|
185
199
|
import * as Console from 'effect/Console';
|
|
@@ -196,7 +210,7 @@ const program = streamStory.pipe(
|
|
|
196
210
|
return Console.log(part.delta);
|
|
197
211
|
}
|
|
198
212
|
if (part.type === 'tool-params-delta') {
|
|
199
|
-
return Console.log('Tool params:', part.
|
|
213
|
+
return Console.log('Tool params:', part.delta);
|
|
200
214
|
}
|
|
201
215
|
return Effect.void;
|
|
202
216
|
})
|
|
@@ -214,7 +228,7 @@ Common StreamPart shapes include (non-exhaustive):
|
|
|
214
228
|
| { type: "reasoning-delta", id, delta }
|
|
215
229
|
| { type: "reasoning-end", id }
|
|
216
230
|
| { type: "tool-params-start", id, name }
|
|
217
|
-
| { type: "tool-params-delta", id,
|
|
231
|
+
| { type: "tool-params-delta", id, delta }
|
|
218
232
|
| { type: "tool-params-end", id }
|
|
219
233
|
| { type: "tool-call", id, name, params }
|
|
220
234
|
| { type: "tool-result", id, name, result, isFailure, preliminary? }
|
|
@@ -289,7 +303,7 @@ toolChoice: {
|
|
|
289
303
|
## Error Handling
|
|
290
304
|
|
|
291
305
|
```typescript
|
|
292
|
-
import * as AiError from 'effect/
|
|
306
|
+
import * as AiError from 'effect/ai/AiError';
|
|
293
307
|
|
|
294
308
|
const robust = LanguageModel.generateText({
|
|
295
309
|
prompt: 'Analyze this...'
|
|
@@ -311,10 +325,22 @@ remain explicit and provider/validation failures use `AiError`. Classification
|
|
|
311
325
|
requires at least two labels, rating requires at least two distinct ordered
|
|
312
326
|
levels, and a definition requires at least one decision.
|
|
313
327
|
|
|
328
|
+
`Decision.probability({ instructions })` estimates the probability of `true`.
|
|
329
|
+
Its `criteria` is optional; when supplied it must describe both `false` and
|
|
330
|
+
`true`. Guard `decision.criteria` before reading outcome descriptions.
|
|
331
|
+
|
|
332
|
+
Custom adapters can set `DecisionModel.make({ decide, probabilityPrecision })`
|
|
333
|
+
to accept rounding drift in classification/rating distributions. At precision
|
|
334
|
+
`p`, the allowed sum drift is half a unit of the last decimal place per label
|
|
335
|
+
(plus the core numerical tolerance); accepted drift is rescaled to sum to one.
|
|
336
|
+
Zero sums, invalid probabilities, and larger drift still fail with
|
|
337
|
+
`InvalidOutputError`. Omit the option for strict sum validation. OpenRouter and
|
|
338
|
+
TypeSafe adapters use precision `2`.
|
|
339
|
+
|
|
314
340
|
<!-- typecheck -->
|
|
315
341
|
```ts
|
|
316
342
|
import * as Schema from 'effect/Schema';
|
|
317
|
-
import { Decision, DecisionModel } from 'effect/
|
|
343
|
+
import { Decision, DecisionModel } from 'effect/ai';
|
|
318
344
|
|
|
319
345
|
class Ticket extends Schema.Class<Ticket>('Ticket')({ body: Schema.String }) {}
|
|
320
346
|
const triage = Decision.make({
|
|
@@ -323,7 +349,8 @@ const triage = Decision.make({
|
|
|
323
349
|
team: Decision.classify({
|
|
324
350
|
instructions: 'Choose the team responsible for this request',
|
|
325
351
|
criteria: { billing: 'Payments and invoices', support: 'Product support' }
|
|
326
|
-
})
|
|
352
|
+
}),
|
|
353
|
+
urgent: Decision.probability({ instructions: 'The ticket needs immediate attention' })
|
|
327
354
|
}
|
|
328
355
|
});
|
|
329
356
|
const result = DecisionModel.decide(triage, { input: new Ticket({ body: 'Invoice question' }) });
|
|
@@ -332,7 +359,7 @@ const result = DecisionModel.decide(triage, { input: new Ticket({ body: 'Invoice
|
|
|
332
359
|
## Type Extraction Utilities
|
|
333
360
|
|
|
334
361
|
```typescript
|
|
335
|
-
import type * as LanguageModel from 'effect/
|
|
362
|
+
import type * as LanguageModel from 'effect/ai/LanguageModel';
|
|
336
363
|
|
|
337
364
|
// Extract error types from options
|
|
338
365
|
type MyError = LanguageModel.ExtractError<typeof options>;
|
|
@@ -360,9 +387,14 @@ make :: ConstructorParams → Effect Service
|
|
|
360
387
|
|
|
361
388
|
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
389
|
|
|
390
|
+
`ProviderOptions.incrementalFallback` is `true` when the core retries a rejected
|
|
391
|
+
incremental request with the full prompt. The fallback clears
|
|
392
|
+
`previousResponseId` and `incrementalPrompt`; use that signal to reset adapter
|
|
393
|
+
turn state without unnecessarily closing a healthy connection.
|
|
394
|
+
|
|
363
395
|
```typescript
|
|
364
|
-
import * as LanguageModel from 'effect/
|
|
365
|
-
import * as Response from 'effect/
|
|
396
|
+
import * as LanguageModel from 'effect/ai/LanguageModel';
|
|
397
|
+
import * as Response from 'effect/ai/Response';
|
|
366
398
|
|
|
367
399
|
const makeCustomProvider = Effect.gen(function* () {
|
|
368
400
|
const service = yield* LanguageModel.make({
|
|
@@ -655,7 +687,7 @@ const generated = LanguageModel.generateText({ prompt: '...' }).pipe(
|
|
|
655
687
|
Inside an effect that runs with a language model provider, you can retrieve the current provider name:
|
|
656
688
|
|
|
657
689
|
```typescript
|
|
658
|
-
import * as Model from 'effect/
|
|
690
|
+
import * as Model from 'effect/ai/Model';
|
|
659
691
|
|
|
660
692
|
const program = Effect.gen(function* () {
|
|
661
693
|
const providerName = yield* Model.ProviderName;
|
|
@@ -672,7 +704,7 @@ const program = Effect.gen(function* () {
|
|
|
672
704
|
|
|
673
705
|
## References
|
|
674
706
|
|
|
675
|
-
- Source: `packages/effect/src/
|
|
676
|
-
- Chat integration: `packages/effect/src/
|
|
677
|
-
- Response types: `effect/
|
|
678
|
-
- Tool system: `effect/
|
|
707
|
+
- Source: `packages/effect/src/ai/LanguageModel.ts`
|
|
708
|
+
- Chat integration: `packages/effect/src/ai/Chat.ts`
|
|
709
|
+
- Response types: `effect/ai/Response`
|
|
710
|
+
- 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,14 @@ 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
|
+
|
|
15
|
+
For TypeSafe/Jev and OpenRouter **decision-model** layers, use
|
|
16
|
+
`effect-ai-decision-model`; its provider contracts differ from language models.
|
|
17
|
+
|
|
10
18
|
## When to Use This Skill
|
|
11
19
|
|
|
12
20
|
Use this skill when:
|
|
@@ -34,7 +42,7 @@ import {
|
|
|
34
42
|
Stream
|
|
35
43
|
} from 'effect';
|
|
36
44
|
|
|
37
|
-
// From "effect/
|
|
45
|
+
// From "effect/ai" — namespace imports
|
|
38
46
|
import {
|
|
39
47
|
AiError,
|
|
40
48
|
Chat,
|
|
@@ -43,13 +51,13 @@ import {
|
|
|
43
51
|
Prompt,
|
|
44
52
|
Tool,
|
|
45
53
|
Toolkit
|
|
46
|
-
} from 'effect/
|
|
54
|
+
} from 'effect/ai';
|
|
47
55
|
// 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/
|
|
56
|
+
import * as LanguageModel from 'effect/ai/LanguageModel';
|
|
57
|
+
import * as Chat from 'effect/ai/Chat';
|
|
58
|
+
import * as Model from 'effect/ai/Model';
|
|
59
|
+
import * as Prompt from 'effect/ai/Prompt';
|
|
60
|
+
import * as AiError from 'effect/ai/AiError';
|
|
53
61
|
|
|
54
62
|
// Anthropic
|
|
55
63
|
import { AnthropicClient, AnthropicLanguageModel } from '@effect/ai-anthropic';
|
|
@@ -70,7 +78,7 @@ import {
|
|
|
70
78
|
} from '@effect/ai-openrouter';
|
|
71
79
|
|
|
72
80
|
// HTTP client (required by all providers)
|
|
73
|
-
import { FetchHttpClient } from 'effect/
|
|
81
|
+
import { FetchHttpClient } from 'effect/http';
|
|
74
82
|
```
|
|
75
83
|
|
|
76
84
|
## Provider Layer Pattern
|
|
@@ -96,7 +104,7 @@ ProviderClient.layerConfig :: { apiKey } → Layer Client HttpClient
|
|
|
96
104
|
```typescript
|
|
97
105
|
import { AnthropicClient, AnthropicLanguageModel } from '@effect/ai-anthropic';
|
|
98
106
|
import { Config, Layer } from 'effect';
|
|
99
|
-
import { FetchHttpClient } from 'effect/
|
|
107
|
+
import { FetchHttpClient } from 'effect/http';
|
|
100
108
|
|
|
101
109
|
// Client layer (reusable across models)
|
|
102
110
|
const AnthropicClientLayer = AnthropicClient.layerConfig({
|
|
@@ -115,6 +123,12 @@ const AnthropicLive = AnthropicLanguageModel.layer({
|
|
|
115
123
|
|
|
116
124
|
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
125
|
|
|
126
|
+
Anthropic-compatible gateways may omit `usage.inference_geo`. Malformed gateway
|
|
127
|
+
4xx error bodies are classified by HTTP status (including 429 retry delays), not
|
|
128
|
+
as `InvalidOutputError`. Direct `client.betaMessagesPost` calls expose
|
|
129
|
+
`HttpClientError` for those responses; the language-model adapter maps them to
|
|
130
|
+
`AiError` reasons.
|
|
131
|
+
|
|
118
132
|
```typescript
|
|
119
133
|
const compatibleClaude = AnthropicLanguageModel.model('future-claude-model', {
|
|
120
134
|
structuredOutputs: false,
|
|
@@ -127,7 +141,7 @@ const compatibleClaude = AnthropicLanguageModel.model('future-claude-model', {
|
|
|
127
141
|
```typescript
|
|
128
142
|
import { OpenAiClient, OpenAiLanguageModel } from '@effect/ai-openai';
|
|
129
143
|
import { Config, Layer } from 'effect';
|
|
130
|
-
import { FetchHttpClient } from 'effect/
|
|
144
|
+
import { FetchHttpClient } from 'effect/http';
|
|
131
145
|
|
|
132
146
|
const OpenAiClientLayer = OpenAiClient.layerConfig({
|
|
133
147
|
apiKey: Config.Redacted('OPENAI_API_KEY')
|
|
@@ -151,6 +165,10 @@ Public OpenAI modules:
|
|
|
151
165
|
|
|
152
166
|
`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
167
|
|
|
168
|
+
For WebSocket turns, `previous_response_not_found` triggers a retry with the full
|
|
169
|
+
prompt. An open socket is retained; custom adapters can inspect
|
|
170
|
+
`ProviderOptions.incrementalFallback` to recognize the full-prompt retry.
|
|
171
|
+
|
|
154
172
|
```typescript
|
|
155
173
|
const client = yield* OpenAiClient.OpenAiClient;
|
|
156
174
|
const [body] = yield* client.createResponse({
|
|
@@ -176,14 +194,21 @@ const NativeTools = Toolkit.make(
|
|
|
176
194
|
|
|
177
195
|
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
196
|
|
|
197
|
+
Web-search calls may omit `action`. Narrow that optional field before reading
|
|
198
|
+
action details; incomplete calls are preserved as failed tool results.
|
|
199
|
+
|
|
179
200
|
## OpenAI-Compatible Providers
|
|
180
201
|
|
|
181
|
-
Use `apiUrl` with `@effect/ai-openai` for
|
|
202
|
+
Use `apiUrl` with `@effect/ai-openai` for APIs compatible with OpenAI Responses.
|
|
203
|
+
For Chat Completions-compatible endpoints use `@effect/ai-openai-compat`
|
|
204
|
+
(`OpenAiClient`, `OpenAiLanguageModel`), which preserves non-null text/tool
|
|
205
|
+
argument fragments when other streamed delta fields are null. Both adapters
|
|
206
|
+
preserve raw JSON Schema dynamic-tool arguments.
|
|
182
207
|
|
|
183
208
|
```typescript
|
|
184
209
|
import { OpenAiClient, OpenAiConfig } from '@effect/ai-openai';
|
|
185
210
|
import { Config, Layer } from 'effect';
|
|
186
|
-
import { FetchHttpClient, HttpClient, HttpClientRequest } from 'effect/
|
|
211
|
+
import { FetchHttpClient, HttpClient, HttpClientRequest } from 'effect/http';
|
|
187
212
|
|
|
188
213
|
const CompatibleClientLayer = OpenAiClient.layerConfig({
|
|
189
214
|
apiKey: Config.Redacted('OPENAI_COMPAT_API_KEY'),
|
|
@@ -208,7 +233,7 @@ import {
|
|
|
208
233
|
OpenRouterLanguageModel
|
|
209
234
|
} from '@effect/ai-openrouter';
|
|
210
235
|
import { Config, Layer } from 'effect';
|
|
211
|
-
import { FetchHttpClient } from 'effect/
|
|
236
|
+
import { FetchHttpClient } from 'effect/http';
|
|
212
237
|
|
|
213
238
|
const OpenRouterClientLayer = OpenRouterClient.layerConfig({
|
|
214
239
|
apiKey: Config.Redacted('OPENROUTER_API_KEY')
|
|
@@ -226,13 +251,13 @@ const routerModel = OpenRouterLanguageModel.model('anthropic/claude-sonnet-4');
|
|
|
226
251
|
import { AnthropicClient, AnthropicLanguageModel } from '@effect/ai-anthropic';
|
|
227
252
|
import { OpenAiClient, OpenAiLanguageModel } from '@effect/ai-openai';
|
|
228
253
|
import { Effect, ExecutionPlan, Layer } from 'effect';
|
|
229
|
-
import { LanguageModel } from 'effect/
|
|
254
|
+
import { LanguageModel } from 'effect/ai';
|
|
230
255
|
|
|
231
256
|
// Try cheaper model first, fall back to more expensive one
|
|
232
257
|
const DraftPlan = ExecutionPlan.make(
|
|
233
258
|
{
|
|
234
259
|
provide: OpenAiLanguageModel.model('gpt-5.2'),
|
|
235
|
-
attempts: 3 //
|
|
260
|
+
attempts: 3 // up to 3 total attempts before falling back
|
|
236
261
|
},
|
|
237
262
|
{
|
|
238
263
|
provide: AnthropicLanguageModel.model('claude-opus-4-6'),
|
|
@@ -278,7 +303,7 @@ Maintain conversation history with automatic context management:
|
|
|
278
303
|
|
|
279
304
|
```typescript
|
|
280
305
|
import { Effect, Ref } from 'effect';
|
|
281
|
-
import { Chat, Prompt } from 'effect/
|
|
306
|
+
import { Chat, Prompt } from 'effect/ai';
|
|
282
307
|
|
|
283
308
|
// Create with system prompt
|
|
284
309
|
const session =
|
|
@@ -361,7 +386,7 @@ OpenAI error classification distinguishes temporary rate limits from exhausted a
|
|
|
361
386
|
Wrap provider layers with metadata. Takes 3 positional arguments: `(providerName, modelId, layer)`:
|
|
362
387
|
|
|
363
388
|
```typescript
|
|
364
|
-
import { Model } from 'effect/
|
|
389
|
+
import { Model } from 'effect/ai';
|
|
365
390
|
|
|
366
391
|
// This is what ProviderLanguageModel.model() calls internally:
|
|
367
392
|
const Claude = Model.make(
|
|
@@ -437,7 +462,7 @@ Wrap `AiError` into domain-specific tagged errors:
|
|
|
437
462
|
|
|
438
463
|
```typescript
|
|
439
464
|
import { Schema } from 'effect';
|
|
440
|
-
import { AiError } from 'effect/
|
|
465
|
+
import { AiError } from 'effect/ai';
|
|
441
466
|
|
|
442
467
|
export class MyAiError extends Schema.TaggedError<MyAiError>()(
|
|
443
468
|
'MyAiError',
|
|
@@ -459,7 +484,7 @@ export class MyAiError extends Schema.TaggedError<MyAiError>()(
|
|
|
459
484
|
| ----------------------- | ------------- | ----------------------------------------------- |
|
|
460
485
|
| `@effect/ai-anthropic` | Anthropic | Claude Opus 4, Claude Sonnet 4, etc. |
|
|
461
486
|
| `@effect/ai-openai` | OpenAI | GPT-5, GPT-4.1, o-series, etc. |
|
|
462
|
-
| `@effect/ai-openai`
|
|
487
|
+
| `@effect/ai-openai-compat` | OpenAI-Compat | Chat Completions-compatible APIs via `apiUrl` |
|
|
463
488
|
| `@effect/ai-openrouter` | OpenRouter | Multi-provider proxy (any model ID) |
|
|
464
489
|
|
|
465
490
|
**Note**: There are no `@effect/ai-google` or `@effect/ai-amazon-bedrock` packages. Use OpenRouter to access Google/Bedrock models.
|
|
@@ -488,8 +513,8 @@ import {
|
|
|
488
513
|
Model,
|
|
489
514
|
Prompt,
|
|
490
515
|
type Response
|
|
491
|
-
} from 'effect/
|
|
492
|
-
import { FetchHttpClient } from 'effect/
|
|
516
|
+
} from 'effect/ai';
|
|
517
|
+
import { FetchHttpClient } from 'effect/http';
|
|
493
518
|
|
|
494
519
|
// ---------------------------------------------------------------------------
|
|
495
520
|
// Provider client layers
|
|
@@ -675,10 +700,16 @@ import { BedrockClient } from '@effect/ai-amazon-bedrock'; // Does NOT exist
|
|
|
675
700
|
## References
|
|
676
701
|
|
|
677
702
|
Provider-neutral structured decisions use `Decision` / `DecisionModel` from
|
|
678
|
-
`effect/
|
|
703
|
+
`effect/ai`. `OpenRouterDecisionModel` targets OpenRouter's alpha Decisions
|
|
679
704
|
API; `@effect/ai-typesafe` supplies a provider for TypeSafe System One. Custom
|
|
680
705
|
OpenRouter client implementations include `createDecisions`.
|
|
681
706
|
|
|
707
|
+
`OpenRouterDecisionModel` and the TypeSafe decision model configure
|
|
708
|
+
`probabilityPrecision: 2`: rounded distributions such as `0.02 / 0.93 / 0.04`
|
|
709
|
+
are accepted and rescaled. Custom `DecisionModel.make` adapters retain strict sum
|
|
710
|
+
validation unless they opt in. `Decision.probability` allows omitted `criteria`;
|
|
711
|
+
when present it must describe both boolean outcomes (see `effect-ai-language-model`).
|
|
712
|
+
|
|
682
713
|
Use each branded service's same-name type (`LanguageModel.LanguageModel`,
|
|
683
714
|
`EmbeddingModel.EmbeddingModel`, `Chat.Chat`) and its exported TypeId for custom
|
|
684
715
|
implementations. Prefer provided constructors; do not hard-code marker strings.
|
|
@@ -5,6 +5,10 @@ description: Master Effect AI streaming response patterns including start/delta/
|
|
|
5
5
|
|
|
6
6
|
# Effect AI Streaming
|
|
7
7
|
|
|
8
|
+
Baseline: `effect@4.0.0`; inspect that tag in the Effect source reference.
|
|
9
|
+
The `effect/ai` APIs remain `@stability unstable` despite their shorter import
|
|
10
|
+
paths and may break in minor releases. Align provider package versions with `effect`.
|
|
11
|
+
|
|
8
12
|
## When to Use This Skill
|
|
9
13
|
|
|
10
14
|
- Real-time streaming responses from language models
|
|
@@ -24,7 +28,7 @@ import * as Effect from 'effect/Effect';
|
|
|
24
28
|
import * as Channel from 'effect/Channel';
|
|
25
29
|
import * as SubscriptionRef from 'effect/SubscriptionRef';
|
|
26
30
|
import * as Match from 'effect/Match';
|
|
27
|
-
import * as Response from 'effect/
|
|
31
|
+
import * as Response from 'effect/ai/Response';
|
|
28
32
|
```
|
|
29
33
|
|
|
30
34
|
## StreamPart Protocol
|
|
@@ -80,8 +84,8 @@ Accumulate stream parts incrementally using mutable state for efficiency:
|
|
|
80
84
|
```typescript
|
|
81
85
|
import * as Stream from 'effect/Stream';
|
|
82
86
|
import * as Effect from 'effect/Effect';
|
|
83
|
-
import * as Prompt from 'effect/
|
|
84
|
-
import * as Response from 'effect/
|
|
87
|
+
import * as Prompt from 'effect/ai/Prompt';
|
|
88
|
+
import * as Response from 'effect/ai/Response';
|
|
85
89
|
import * as SubscriptionRef from 'effect/SubscriptionRef';
|
|
86
90
|
|
|
87
91
|
const streamWithHistory = Stream.suspend(() => {
|
|
@@ -214,14 +218,20 @@ Why checkpoint-based merging:
|
|
|
214
218
|
- `Prompt.fromResponseParts` routes framework-executed final results into a tool message, but keeps provider-executed final results in the assistant message. It preserves `providerExecuted` and uses `encodedResult` in both cases.
|
|
215
219
|
- Tools requiring approval emit `tool-approval-request`. Append a matching `Prompt.toolApprovalResponsePart` in a tool message and call the model again; approved/denied responses are pre-resolved into final tool results before the next provider call.
|
|
216
220
|
- In OpenAI-specific SSE code, unknown future events decode through `OpenAiSchema.ResponseStreamEvent` and are ignored by `OpenAiLanguageModel`; malformed known events still fail decoding.
|
|
221
|
+
- OpenAI-compatible Chat Completions providers can send null delta fields. The
|
|
222
|
+
adapter preserves text and tool arguments from the remaining fields; custom
|
|
223
|
+
adapters should skip null fragments rather than discard the whole chunk.
|
|
224
|
+
- Manual `Toolkit.handle` execution requires handler services on both the outer
|
|
225
|
+
Effect and its returned Stream. Even return-mode streams can fail with
|
|
226
|
+
`AiError` during result encoding; keep that error channel when composing streams.
|
|
217
227
|
|
|
218
228
|
## Complete Example
|
|
219
229
|
|
|
220
230
|
<!-- typecheck -->
|
|
221
231
|
```typescript
|
|
222
|
-
import * as Prompt from 'effect/
|
|
223
|
-
import * as Response from 'effect/
|
|
224
|
-
import * as LanguageModel from 'effect/
|
|
232
|
+
import * as Prompt from 'effect/ai/Prompt';
|
|
233
|
+
import * as Response from 'effect/ai/Response';
|
|
234
|
+
import * as LanguageModel from 'effect/ai/LanguageModel';
|
|
225
235
|
import * as Stream from 'effect/Stream';
|
|
226
236
|
import * as Channel from 'effect/Channel';
|
|
227
237
|
import * as Effect from 'effect/Effect';
|
|
@@ -327,16 +337,21 @@ Stream.mapArrayEffect(Effect.fnUntraced(function* (chunk) {
|
|
|
327
337
|
### Source Parts
|
|
328
338
|
|
|
329
339
|
```typescript
|
|
330
|
-
{ type: "
|
|
331
|
-
{ type: "
|
|
340
|
+
{ type: "source", sourceType: "document", id: string, mediaType: string, title: string }
|
|
341
|
+
{ type: "source", sourceType: "url", id: string, url: URL, title: string }
|
|
332
342
|
```
|
|
333
343
|
|
|
344
|
+
The encoded URL source uses a string URL.
|
|
345
|
+
|
|
334
346
|
### Metadata Parts
|
|
335
347
|
|
|
336
348
|
```typescript
|
|
337
|
-
{ type: "response-metadata", id
|
|
349
|
+
{ type: "response-metadata", id?: string, modelId?: string, timestamp?: DateTime.Utc }
|
|
338
350
|
```
|
|
339
351
|
|
|
352
|
+
The encoded provider form uses an ISO string for `timestamp`; the decoded part
|
|
353
|
+
uses `DateTime.Utc`.
|
|
354
|
+
|
|
340
355
|
### Error Parts
|
|
341
356
|
|
|
342
357
|
```typescript
|
|
@@ -383,9 +398,9 @@ StreamPart types:
|
|
|
383
398
|
|
|
384
399
|
Key modules:
|
|
385
400
|
|
|
386
|
-
- `effect/
|
|
387
|
-
- `effect/
|
|
388
|
-
- `effect/Stream` - Stream combinators (`
|
|
401
|
+
- `effect/ai/Response` - Response part schemas and constructors
|
|
402
|
+
- `effect/ai/Prompt` - Prompt construction and merging
|
|
403
|
+
- `effect/Stream` - Stream combinators (`mapArrayEffect`, `runForEach`, `runDrain`)
|
|
389
404
|
- `effect/Channel` - Low-level resource management (`acquireUseRelease`)
|
|
390
405
|
- `effect/SubscriptionRef` - Reactive shared state
|
|
391
406
|
- `effect/Match` - Pattern matching (use `Match.when({ type: ... })` for stream parts)
|