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.
Files changed (74) hide show
  1. package/README.md +6 -5
  2. package/docs/effect-4.0.0-changelog.md +3213 -0
  3. package/docs/effect-4.0.0.md +110 -0
  4. package/guidance/effect-first-development.md +8 -6
  5. package/guidance/progressive-disclosure-guidance.md +15 -7
  6. package/package.json +6 -2
  7. package/patterns/avoid-any.md +2 -2
  8. package/patterns/avoid-direct-json.md +6 -6
  9. package/patterns/avoid-native-fetch.md +8 -6
  10. package/patterns/avoid-node-imports.md +2 -2
  11. package/patterns/avoid-non-null-assertion.md +2 -2
  12. package/patterns/avoid-object-type.md +2 -2
  13. package/patterns/avoid-platform-coupling.md +1 -1
  14. package/patterns/avoid-process-env.md +3 -4
  15. package/patterns/avoid-ts-ignore.md +1 -1
  16. package/patterns/context-tag-extends.md +11 -8
  17. package/patterns/effect-promise-vs-trypromise.md +6 -7
  18. package/patterns/prefer-arr-sort.md +1 -1
  19. package/patterns/prefer-effect-fn.md +21 -65
  20. package/patterns/prefer-schema-class.md +3 -3
  21. package/patterns/throw-in-effect-gen.md +1 -1
  22. package/patterns/use-clock-service.md +4 -0
  23. package/patterns/use-command-executor-service.md +2 -2
  24. package/patterns/use-http-client-service.md +8 -6
  25. package/patterns/use-random-service.md +6 -7
  26. package/skills/effect-ai-chat/SKILL.md +13 -7
  27. package/skills/effect-ai-decision-model/SKILL.md +301 -0
  28. package/skills/effect-ai-decision-model/openrouter.md +70 -0
  29. package/skills/effect-ai-language-model/SKILL.md +53 -21
  30. package/skills/effect-ai-prompt/SKILL.md +25 -14
  31. package/skills/effect-ai-provider/SKILL.md +53 -22
  32. package/skills/effect-ai-streaming/SKILL.md +27 -12
  33. package/skills/effect-ai-tool/SKILL.md +37 -28
  34. package/skills/effect-atom-rpc/SKILL.md +57 -36
  35. package/skills/effect-atom-state/SKILL.md +57 -19
  36. package/skills/effect-batching/SKILL.md +5 -3
  37. package/skills/effect-cache/SKILL.md +19 -7
  38. package/skills/effect-cli/SKILL.md +17 -8
  39. package/skills/effect-command-executor/SKILL.md +115 -64
  40. package/skills/effect-concurrency-testing/SKILL.md +26 -6
  41. package/skills/effect-config/SKILL.md +53 -2
  42. package/skills/effect-context-witness/SKILL.md +6 -6
  43. package/skills/effect-domain-modeling/SKILL.md +8 -1
  44. package/skills/effect-error-handling/SKILL.md +15 -2
  45. package/skills/effect-fiber/SKILL.md +20 -25
  46. package/skills/effect-filesystem/SKILL.md +69 -57
  47. package/skills/effect-http-api/SKILL.md +72 -22
  48. package/skills/effect-http-client/SKILL.md +25 -21
  49. package/skills/effect-http-server/SKILL.md +51 -21
  50. package/skills/effect-incremental-migration/SKILL.md +17 -8
  51. package/skills/effect-layer-design/SKILL.md +8 -0
  52. package/skills/effect-managed-runtime/SKILL.md +6 -0
  53. package/skills/effect-mcp-server/SKILL.md +64 -24
  54. package/skills/effect-observability/SKILL.md +61 -15
  55. package/skills/effect-parallelization/SKILL.md +24 -7
  56. package/skills/effect-path/SKILL.md +8 -2
  57. package/skills/effect-platform-abstraction/SKILL.md +88 -66
  58. package/skills/effect-platform-layers/SKILL.md +68 -67
  59. package/skills/effect-pubsub-event-bus/SKILL.md +56 -60
  60. package/skills/effect-react-composition/SKILL.md +19 -6
  61. package/skills/effect-rpc-api/SKILL.md +24 -24
  62. package/skills/effect-rpc-client/SKILL.md +33 -28
  63. package/skills/effect-rpc-cluster/SKILL.md +122 -78
  64. package/skills/effect-rpc-server/SKILL.md +56 -20
  65. package/skills/effect-scheduling/SKILL.md +29 -1
  66. package/skills/effect-schema-composition/SKILL.md +31 -13
  67. package/skills/effect-schema-v4/SKILL.md +94 -10
  68. package/skills/effect-scope/SKILL.md +13 -5
  69. package/skills/effect-service-implementation/SKILL.md +1 -1
  70. package/skills/effect-socket/SKILL.md +52 -8
  71. package/skills/effect-sql/SKILL.md +67 -33
  72. package/skills/effect-stream/SKILL.md +50 -5
  73. package/skills/effect-testing/SKILL.md +91 -2
  74. 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/unstable/ai/LanguageModel';
16
- import * as Prompt from 'effect/unstable/ai/Prompt';
17
- import * as Response from 'effect/unstable/ai/Response';
18
- import * as Toolkit from 'effect/unstable/ai/Toolkit';
19
- import * as Tool from 'effect/unstable/ai/Tool';
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/unstable/ai/LanguageModel';
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/unstable/ai/LanguageModel';
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/unstable/ai/LanguageModel';
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.paramsDelta);
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, paramsDelta }
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/unstable/ai/AiError';
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/unstable/ai';
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/unstable/ai/LanguageModel';
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/unstable/ai/LanguageModel';
365
- import * as Response from 'effect/unstable/ai/Response';
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/unstable/ai/Model';
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/unstable/ai/LanguageModel.ts`
676
- - Chat integration: `packages/effect/src/unstable/ai/Chat.ts`
677
- - Response types: `effect/unstable/ai/Response`
678
- - Tool system: `effect/unstable/ai/Tool`, `effect/unstable/ai/Toolkit`
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/unstable/ai/Prompt';
16
- import * as Response from 'effect/unstable/ai/Response';
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`. Content is an array of `Part` objects.
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/unstable/ai/Prompt';
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/unstable/ai/Response';
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 only when the matching start/delta/end parts are present in the same input, places tool calls and approval requests in assistant messages, and skips preliminary tool results. For final tool results it always uses `encodedResult`: framework-executed results (`providerExecuted: false`) become tool messages, while provider-executed results (`providerExecuted: true`) remain in the assistant message with that flag preserved.
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/unstable/ai/LanguageModel';
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/unstable/ai/LanguageModel';
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/unstable/ai/LanguageModel';
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/unstable/ai/LanguageModel';
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/unstable/ai/LanguageModel';
529
- import * as Prompt from 'effect/unstable/ai/Prompt';
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/unstable/ai/Prompt' {
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/unstable/ai/Prompt';
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/unstable/ai" — namespace imports
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/unstable/ai';
54
+ } from 'effect/ai';
47
55
  // Or individually:
48
- import * as LanguageModel from 'effect/unstable/ai/LanguageModel';
49
- import * as Chat from 'effect/unstable/ai/Chat';
50
- import * as Model from 'effect/unstable/ai/Model';
51
- import * as Prompt from 'effect/unstable/ai/Prompt';
52
- import * as AiError from 'effect/unstable/ai/AiError';
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/unstable/http';
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/unstable/http';
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/unstable/http';
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 OpenAI-compatible APIs (Azure OpenAI, local models, etc.):
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/unstable/http';
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/unstable/http';
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/unstable/ai';
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 // retry up to 3 times before falling back
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/unstable/ai';
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/unstable/ai';
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/unstable/ai';
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` | OpenAI-Compat | Any OpenAI-compatible API via `apiUrl` |
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/unstable/ai';
492
- import { FetchHttpClient } from 'effect/unstable/http';
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/unstable/ai`. `OpenRouterDecisionModel` targets OpenRouter's alpha Decisions
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/unstable/ai/Response';
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/unstable/ai/Prompt';
84
- import * as Response from 'effect/unstable/ai/Response';
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/unstable/ai/Prompt';
223
- import * as Response from 'effect/unstable/ai/Response';
224
- import * as LanguageModel from 'effect/unstable/ai/LanguageModel';
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: "document-source", id: string, title?: string }
331
- { type: "url-source", url: string, title?: string }
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: string, modelId: string, timestamp: Date }
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/unstable/ai/Response` - Response part schemas and constructors
387
- - `effect/unstable/ai/Prompt` - Prompt construction and merging
388
- - `effect/Stream` - Stream combinators (`mapChunksEffect`, `runForEach`, `runDrain`)
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)