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.
Files changed (72) hide show
  1. package/README.md +3 -3
  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 +2 -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-language-model/SKILL.md +50 -21
  28. package/skills/effect-ai-prompt/SKILL.md +25 -14
  29. package/skills/effect-ai-provider/SKILL.md +50 -22
  30. package/skills/effect-ai-streaming/SKILL.md +27 -12
  31. package/skills/effect-ai-tool/SKILL.md +37 -28
  32. package/skills/effect-atom-rpc/SKILL.md +57 -36
  33. package/skills/effect-atom-state/SKILL.md +57 -19
  34. package/skills/effect-batching/SKILL.md +5 -3
  35. package/skills/effect-cache/SKILL.md +19 -7
  36. package/skills/effect-cli/SKILL.md +17 -8
  37. package/skills/effect-command-executor/SKILL.md +115 -64
  38. package/skills/effect-concurrency-testing/SKILL.md +26 -6
  39. package/skills/effect-config/SKILL.md +53 -2
  40. package/skills/effect-context-witness/SKILL.md +6 -6
  41. package/skills/effect-domain-modeling/SKILL.md +8 -1
  42. package/skills/effect-error-handling/SKILL.md +15 -2
  43. package/skills/effect-fiber/SKILL.md +20 -25
  44. package/skills/effect-filesystem/SKILL.md +69 -57
  45. package/skills/effect-http-api/SKILL.md +72 -22
  46. package/skills/effect-http-client/SKILL.md +25 -21
  47. package/skills/effect-http-server/SKILL.md +51 -21
  48. package/skills/effect-incremental-migration/SKILL.md +17 -8
  49. package/skills/effect-layer-design/SKILL.md +8 -0
  50. package/skills/effect-managed-runtime/SKILL.md +6 -0
  51. package/skills/effect-mcp-server/SKILL.md +64 -24
  52. package/skills/effect-observability/SKILL.md +61 -15
  53. package/skills/effect-parallelization/SKILL.md +24 -7
  54. package/skills/effect-path/SKILL.md +8 -2
  55. package/skills/effect-platform-abstraction/SKILL.md +88 -66
  56. package/skills/effect-platform-layers/SKILL.md +68 -67
  57. package/skills/effect-pubsub-event-bus/SKILL.md +56 -60
  58. package/skills/effect-react-composition/SKILL.md +19 -6
  59. package/skills/effect-rpc-api/SKILL.md +24 -24
  60. package/skills/effect-rpc-client/SKILL.md +33 -28
  61. package/skills/effect-rpc-cluster/SKILL.md +122 -78
  62. package/skills/effect-rpc-server/SKILL.md +56 -20
  63. package/skills/effect-scheduling/SKILL.md +29 -1
  64. package/skills/effect-schema-composition/SKILL.md +31 -13
  65. package/skills/effect-schema-v4/SKILL.md +94 -10
  66. package/skills/effect-scope/SKILL.md +13 -5
  67. package/skills/effect-service-implementation/SKILL.md +1 -1
  68. package/skills/effect-socket/SKILL.md +52 -8
  69. package/skills/effect-sql/SKILL.md +67 -33
  70. package/skills/effect-stream/SKILL.md +50 -5
  71. package/skills/effect-testing/SKILL.md +91 -2
  72. 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/unstable/process)
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/unstable/process` for typed errors, scoped resource lifetime, and platform-agnostic process spawning.
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 :: Effect HttpClientRequest HttpBodyError
52
+ request :: HttpClientRequest
53
53
  request = pipe
54
- (HttpClientRequest.post "/api/users")
55
- (HttpClientRequest.bodyJson { name: "Alice" })
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.layer
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
- -- In tests
38
- test :: Effect () TestRandom
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()` is non-deterministic. Use `Random` service for reproducible randomness via `TestRandom.feed*` in tests.
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
- Browse and read files there directly to look up APIs, types, and implementations.
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/unstable/ai/Chat.ts`
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/unstable/ai/Prompt.ts`
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/unstable/ai';
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 when the stream finalizes. Consume the stream to completion if the full assistant response should become history; if the stream is interrupted early, only the parts emitted before finalization are recorded.
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/unstable/persistence';
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/unstable/ai`** — Chat, Prompt, Tool, Toolkit, LanguageModel, and AiError all come from this path.
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/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';
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/unstable/ai/LanguageModel';
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/unstable/ai/LanguageModel';
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/unstable/ai/LanguageModel';
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.paramsDelta);
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, paramsDelta }
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/unstable/ai/AiError';
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/unstable/ai';
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/unstable/ai/LanguageModel';
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/unstable/ai/LanguageModel';
365
- import * as Response from 'effect/unstable/ai/Response';
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/unstable/ai/Model';
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/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`
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/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,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/unstable/ai" — namespace imports
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/unstable/ai';
51
+ } from 'effect/ai';
47
52
  // 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';
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/unstable/http';
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/unstable/http';
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/unstable/http';
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 OpenAI-compatible APIs (Azure OpenAI, local models, etc.):
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/unstable/http';
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/unstable/http';
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/unstable/ai';
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 // retry up to 3 times before falling back
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/unstable/ai';
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/unstable/ai';
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/unstable/ai';
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` | OpenAI-Compat | Any OpenAI-compatible API via `apiUrl` |
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/unstable/ai';
492
- import { FetchHttpClient } from 'effect/unstable/http';
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/unstable/ai`. `OpenRouterDecisionModel` targets OpenRouter's alpha Decisions
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.