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
@@ -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)`).
@@ -0,0 +1,301 @@
1
+ ---
2
+ name: effect-ai-decision-model
3
+ description: Use Effect's Decision and DecisionModel APIs for System One models such as Jev. Covers typed classify/rate/probability definitions, input encoding, answer contracts, TypeSafe and OpenRouter provider layers, errors, and test implementations.
4
+ ---
5
+
6
+ # Effect AI Decision Model
7
+
8
+ Baseline: `effect@4.0.0` and matching `@effect/*` packages. These AI APIs carry
9
+ `@stability unstable`; inspect the consuming version's source before using newer
10
+ contracts. References below are pinned to the stable tag.
11
+
12
+ ## Define and call
13
+
14
+ `Decision.make({ input, decisions })` pairs an input schema with a nonempty record
15
+ of named decisions. One `DecisionModel.decide(definition, { input })` encodes one
16
+ input and sends every decision together in one provider call. Answers retain the
17
+ decision keys and inferred label literals. Keep definitions inferred rather than
18
+ widening them to `Record<string, Decision.Any>`.
19
+
20
+ <!-- typecheck -->
21
+ ```ts
22
+ import * as Decision from 'effect/ai/Decision';
23
+ import * as DecisionModel from 'effect/ai/DecisionModel';
24
+ import * as Effect from 'effect/Effect';
25
+ import * as Schema from 'effect/Schema';
26
+
27
+ class Ticket extends Schema.Class<Ticket>('Ticket')({
28
+ subject: Schema.String,
29
+ body: Schema.String
30
+ }) {}
31
+
32
+ const Triage = Decision.make({
33
+ input: Ticket,
34
+ decisions: {
35
+ department: Decision.classify({
36
+ instructions: 'Which team should handle the ticket?',
37
+ criteria: {
38
+ billing: 'Payments, invoices, and refunds',
39
+ technical: 'Bugs, outages, and integrations',
40
+ other: 'Neither billing nor technical'
41
+ }
42
+ }),
43
+ frustration: Decision.rate({
44
+ instructions: 'How frustrated is the customer?',
45
+ criteria: ['Calm or neutral', 'Expresses frustration', 'Expresses intense anger']
46
+ }),
47
+ urgent: Decision.probability({
48
+ instructions: 'Does the ticket need immediate attention?',
49
+ criteria: { false: 'Can wait', true: 'Needs action now' }
50
+ })
51
+ }
52
+ });
53
+
54
+ type TriageAnswers = Decision.Answers<typeof Triage.decisions>;
55
+
56
+ const triage = Effect.fn('Ticket.triage')(function* (raw: unknown) {
57
+ const input = yield* Schema.decodeUnknownEffect(Ticket)(raw);
58
+ return yield* DecisionModel.decide(Triage, { input });
59
+ });
60
+ ```
61
+
62
+ ### Constructors and answers
63
+
64
+ Every constructor takes an options object; all `instructions` are strings.
65
+
66
+ | Constructor | Criteria | Answer |
67
+ | --- | --- | --- |
68
+ | `Decision.classify` | Record of at least two label keys to string descriptions | `{ label, probabilities, confidence? }`; both label and distribution keys use the inferred label union |
69
+ | `Decision.rate` | Array of at least two distinct string levels, lowest to highest | `{ rating, label, probabilities, confidence? }`; label and distribution keys are the level strings |
70
+ | `Decision.probability` | Optional `{ false: string, true: string }`; both required when supplied | `{ probability }`, the probability of true in `[0, 1]` |
71
+
72
+ - `classify` preserves the provider's chosen label, even if it differs from the
73
+ distribution's maximum. The core checks membership, not argmax consistency.
74
+ - `rate.rating` is the provider's probability-weighted zero-based position, in
75
+ `[0, criteria.length - 1]`, potentially fractional. The core range-checks it but
76
+ does not recompute it or check agreement with the distribution. `rate.label` is
77
+ derived from the highest probability, breaking ties by criteria order.
78
+ - Classify/rate confidence is optional and provider-defined in `[0, 1]`; preserve
79
+ absence (for example with `Option.fromNullishOr`). Probability answers have
80
+ neither confidence nor a distribution. Public answers have no `_tag` field.
81
+ - `classify`, `rate`, and `make` synchronously throw on the cardinality/uniqueness
82
+ violations above. Define valid static literals; parse dynamic definitions at
83
+ their boundary before construction. These throws are outside `decide`'s error
84
+ channel. Constructors retain their input objects; treat definitions as immutable.
85
+
86
+ Exported types: `Classify<Label>`, `Rate<Level>`, `Probability`, their union `Any`,
87
+ `ClassifyAnswer<Label>`, `RateAnswer<Level>`, `ProbabilityAnswer`, conditional
88
+ `Answer<D>`, mapped `Answers<Decisions>`, and `Definition<Input, Decisions>`.
89
+ `Definition` holds `input`, `decisions`, and the `Decision.TypeId` brand.
90
+
91
+ ### Execution and encoding
92
+
93
+ - Static `DecisionModel.decide` requires `DecisionModel.DecisionModel` plus
94
+ `Input["EncodingServices"]`. Alternatively, `yield* DecisionModel.DecisionModel`
95
+ once and call `model.decide(definition, { input })`; only encoding services then
96
+ remain in that call's requirements.
97
+ - `DecideOptions<Input>` contains only `input: Input["Type"]`. Pass the decoded
98
+ value, not its encoded representation. Unknown input still needs boundary
99
+ decoding; its `SchemaError` is separate from `decide`'s `AiError`.
100
+ - Encoding uses `Schema.encodeEffect(Schema.toCodecJson(definition.input))`.
101
+ Providers receive JSON **values**, not a pre-stringified document. Existing
102
+ schema transformations apply: e.g. `FiniteFromString` sends a number as a string;
103
+ derived codecs can encode nested bigint/date values. Explicit `undefined`
104
+ becomes `null`, while absent fields stay absent. Custom declarations need JSON
105
+ codec annotations or encoding fails before the provider runs.
106
+ - `DecideResponse<Decisions>` is `{ answers: Decision.Answers<Decisions>, usage }`.
107
+ `usage` is a `DecisionUsage` schema-class instance with optional finite-number
108
+ `inputTokens` / `outputTokens`. Unknown counts are `undefined`, not zero.
109
+ Answer and distribution dictionaries have null prototypes; use record helpers
110
+ rather than methods inherited from `Object.prototype`.
111
+ - This is a single-response API with no prompt/history, tools, streaming, model,
112
+ or sampling options on `decide`. Provider configuration belongs in layers or
113
+ scoped provider services. Multiple inputs require separate calls; use bounded
114
+ `Effect.forEach` concurrency. The built-in span is `DecisionModel.decide`.
115
+
116
+ ## TypeSafe / Jev provider
117
+
118
+ Install `@effect/ai-typesafe` at the same release as `effect`; for Bun also install
119
+ matching `@effect/platform-bun`. Compose the graph:
120
+ HTTP transport → TypeSafe client → decision model.
121
+
122
+ <!-- typecheck -->
123
+ ```ts
124
+ import * as TypeSafeClient from '@effect/ai-typesafe/TypeSafeClient';
125
+ import * as TypeSafeDecisionModel from '@effect/ai-typesafe/TypeSafeDecisionModel';
126
+ import * as BunHttpClient from '@effect/platform-bun/BunHttpClient';
127
+ import * as Decision from 'effect/ai/Decision';
128
+ import * as DecisionModel from 'effect/ai/DecisionModel';
129
+ import * as Effect from 'effect/Effect';
130
+ import * as Layer from 'effect/Layer';
131
+ import * as Schema from 'effect/Schema';
132
+
133
+ const JevLive = TypeSafeDecisionModel.layer({ model: 'jev-latest' }).pipe(
134
+ Layer.provide(TypeSafeClient.layerConfig()),
135
+ Layer.provide(BunHttpClient.layer)
136
+ );
137
+
138
+ const Urgency = Decision.make({
139
+ input: Schema.String,
140
+ decisions: {
141
+ urgent: Decision.probability({ instructions: 'Does this need action today?' })
142
+ }
143
+ });
144
+
145
+ const program = DecisionModel.decide(Urgency, {
146
+ input: 'My card was charged twice; please fix this today.'
147
+ }).pipe(Effect.provide(JevLive));
148
+ // Run at the application entrypoint; program has no remaining service requirements.
149
+ ```
150
+
151
+ ### Provider and client surface
152
+
153
+ | API | Contract |
154
+ | --- | --- |
155
+ | `TypeSafeDecisionModel.make({ model })` | `Effect<DecisionModel, never, TypeSafeClient>`; captures the client |
156
+ | `TypeSafeDecisionModel.layer({ model })` | `Layer<DecisionModel, never, TypeSafeClient>` |
157
+ | `TypeSafeDecisionModel.model(modelId)` | `Model.Model<"typesafe", DecisionModel, TypeSafeClient>`; usable as a layer, adding `Model.ProviderName` and `Model.ModelName` metadata |
158
+ | Model IDs | Known union: `jev-latest`, `jev-preview`, `jev-1.13.0`; arbitrary strings also accepted. Use client model discovery for current availability |
159
+ | `TypeSafeClient.make(options)` / `layer(options)` | Require `HttpClient.HttpClient`; construction error is `never` |
160
+ | `TypeSafeClient.layerConfig(options?)` | Same HTTP requirement, construction error `Config.ConfigError`; defaults to `Config.Redacted('TYPESAFE_API_KEY')` |
161
+
162
+ Client options: optional `apiKey: Redacted<string>`, `apiUrl: string`, and
163
+ `transformClient: (HttpClient) => HttpClient`. Default URL:
164
+ `https://api.typesafe.ai/v1`. Explicit `make`/`layer` options do not read the
165
+ environment. In `layerConfig`, `apiKey` and `apiUrl` are `Config` values;
166
+ `transformClient` remains a plain function.
167
+
168
+ `TypeSafeConfig.withClientTransform(effect, transform)` or
169
+ `effect.pipe(TypeSafeConfig.withClientTransform(transform))` scopes HTTP
170
+ customization at request execution time, after the constructor transform. Nested
171
+ scoped transforms replace rather than compose; compose explicitly when both are
172
+ needed. The module also exports the `TypeSafeConfig` service and its
173
+ `getOrUndefined` effect. There is no per-call TypeSafe model override; provide a
174
+ different decision-model layer.
175
+
176
+ ### Wire mapping and adapter limits
177
+
178
+ - `classify` → Choice (`choice` → `label`); `rate` → Score (`score` → `rating`,
179
+ index-keyed probabilities → level-string keys); `probability` → Noul (`noul` →
180
+ `probability`). The decision provider drops response `model` and Score `legend`.
181
+ TypeSafe's Choice/Score wire codecs require confidence even though the generic
182
+ decision answer types allow it to be absent.
183
+ - Both built-in decision providers use `probabilityPrecision: 2` for rounded
184
+ distributions. Core normalization does not recompute the supplied rating or
185
+ confidence.
186
+ - Effect 4.0.0 constructors **and `TypeSafeSchema` codecs use string instructions
187
+ and string criteria**, even though the live TypeSafe API supports structured
188
+ descriptions. Put supporting structure in schema-defined input/state and refer
189
+ to it from string instructions. For structured question descriptions, inspect a
190
+ newer matching adapter or implement a separately schema-decoded HTTP boundary;
191
+ widening types or switching to this version's low-level client is insufficient.
192
+ - Core state is `Schema.Json`, but the live TypeSafe endpoint documents top-level
193
+ string/object/array. Wrap scalar boolean/number/null state in a named input field.
194
+ Current HTTP limits (255 Choice options, 10 Score levels) are provider limits,
195
+ not constructor checks; consult the [live API](https://docs.typesafe.ai/api.md)
196
+ when building dynamic definitions.
197
+
198
+ For model discovery or wire-level access, yield
199
+ `TypeSafeClient.TypeSafeClient`: `listModels()` returns `{ models }` (entries have
200
+ `name`, optional `description` / `release_date`); `systemOne({ model, state,
201
+ questions })` returns decoded `{ model, answers, usage? }`; both fail with
202
+ `AiError`. The service also exposes its configured `client`. Low-level calls keep
203
+ wire names, discriminated `type` fields, and optional snake-case token counts;
204
+ they do not apply `DecisionModel`'s definition-relative validation/normalization.
205
+ `TypeSafeSchema` exports Choice/Score/Noul question and answer codecs, `Question`,
206
+ `Answer`, `SystemOneRequest`, `SystemOneResponse`, and `ListModelsResponse`.
207
+
208
+ For **Jev through OpenRouter**, including request overrides and raw metadata,
209
+ read [the OpenRouter adapter reference](openrouter.md).
210
+
211
+ ## Failures
212
+
213
+ `decide` fails with outer `AiError.AiError` (`_tag: 'AiError'`) containing a tagged
214
+ `reason`, `module`, and `method`. Encoding failures use `InvalidUserInputError`;
215
+ missing, mismatched, or invalid answers use `InvalidOutputError`. A provider's
216
+ `AiError` propagates unchanged. Recover by reason with
217
+ `Effect.catchReason('AiError', 'RateLimitError', handler)` or `catchReasons`;
218
+ `catchTag('RateLimitError', ...)` targets the wrong level.
219
+
220
+ TypeSafe's client maps response decoding failures to `InvalidOutputError`, body
221
+ encoding failures to `InvalidRequestError`, transport/URL/HTTP encoding failures
222
+ to `NetworkError`, 404/422 to `InvalidRequestError`, and 429 to `RateLimitError`.
223
+ Other statuses use the shared `AiError.reasonFromHttpStatus` mapping. Rate-limit
224
+ reasons preserve optional `retryAfter` as `Duration` (milliseconds header first,
225
+ then seconds or HTTP-date) and TypeSafe request/error metadata.
226
+
227
+ The Effect TypeSafe client has **no automatic retries**. Add a bounded retry
228
+ policy at the owned request boundary when appropriate, honoring retry metadata;
229
+ native TypeSafe SDK retry defaults do not apply here. See `effect-scheduling` for
230
+ policy construction and `effect-error-handling` for reason-based recovery.
231
+
232
+ ## Test implementations and custom providers
233
+
234
+ `DecisionModel.make({ decide, probabilityPrecision? })` returns
235
+ `Effect<DecisionModel>`; provide it with `Layer.effect`. Its provider callback is
236
+ `(ProviderOptions) => Effect<ProviderResponse, AiError>` with no remaining service
237
+ requirements: acquire dependencies while constructing the layer and close over
238
+ them. `ProviderOptions` contains encoded `state: Schema.Json` and all `decisions`.
239
+ `ProviderResponse` contains `answers` and a required `usage` object with
240
+ `inputTokens` / `outputTokens`, each `number | undefined`.
241
+
242
+ Provider answers use `_tag: 'Classify' | 'Rate' | 'Probability'` and the public
243
+ answer fields above, except a provider Rate answer has **no label**. The core
244
+ derives it. `ProviderAnswer` is their union; individual exports are
245
+ `ProviderClassifyAnswer`, `ProviderRateAnswer`, `ProviderProbabilityAnswer`.
246
+
247
+ <!-- typecheck -->
248
+ ```ts
249
+ import * as Decision from 'effect/ai/Decision';
250
+ import * as DecisionModel from 'effect/ai/DecisionModel';
251
+ import * as Effect from 'effect/Effect';
252
+ import * as Layer from 'effect/Layer';
253
+ import * as Schema from 'effect/Schema';
254
+
255
+ const Urgency = Decision.make({
256
+ input: Schema.String,
257
+ decisions: {
258
+ urgent: Decision.probability({ instructions: 'Does this need action today?' })
259
+ }
260
+ });
261
+
262
+ const UrgencyTest = Layer.effect(
263
+ DecisionModel.DecisionModel,
264
+ DecisionModel.make({
265
+ decide: Effect.fnUntraced(function* (_request) {
266
+ return {
267
+ answers: { urgent: { _tag: 'Probability', probability: 0.9 } },
268
+ usage: { inputTokens: undefined, outputTokens: undefined }
269
+ } satisfies DecisionModel.ProviderResponse;
270
+ })
271
+ })
272
+ );
273
+
274
+ const result = DecisionModel.decide(Urgency, { input: 'Please fix this today.' })
275
+ .pipe(Effect.provide(UrgencyTest));
276
+ ```
277
+
278
+ This fixture handles the `Urgency` definition specifically. In an `it.effect`
279
+ test, yield `result` and assert `answers.urgent.probability`; inspect the provider
280
+ request when testing encoding. Returning incomplete/wrong answers through `make`
281
+ exercises real validation rather than bypassing it with a mocked `decide` method.
282
+
283
+ Validation requires every requested answer with a matching tag, every expected
284
+ distribution key with a finite probability in `[0, 1]`, a known classify label,
285
+ finite/range-valid ratings and probabilities, and optional finite/range-valid
286
+ confidence. Extra answers, probability keys, and answer fields are discarded.
287
+ Distributions must have a positive total within `1e-6` of 1 by default. With
288
+ `probabilityPrecision: p`, permitted drift is
289
+ `labelCount * 0.5 * 10 ** -p + 1e-6`; accepted drift greater than `1e-6` is
290
+ normalized. Precision describes provider rounding, not output rounding.
291
+
292
+ ## Pinned source
293
+
294
+ Resolve these paths under the Effect reference at tag `effect@4.0.0`:
295
+
296
+ - `packages/effect/src/ai/{Decision,DecisionModel}.ts` — complete core surface.
297
+ - `packages/effect/test/ai/DecisionModel.test.ts` and `typetest/ai/DecisionModel.tst.ts`
298
+ — runtime edge cases and inference contracts.
299
+ - `packages/ai/typesafe/src/{TypeSafeDecisionModel,TypeSafeClient,TypeSafeConfig,TypeSafeSchema}.ts`
300
+ — adapter, transport, scoped customization, and wire codecs.
301
+ - `packages/ai/typesafe/test/` and `typetest/` — adapter contract tests.
@@ -0,0 +1,70 @@
1
+ # OpenRouter decision adapter
2
+
3
+ Baseline: `@effect/ai-openrouter@4.0.0`, aligned with `effect`. This adapter calls
4
+ OpenRouter's **alpha Decisions API**, not chat completions.
5
+
6
+ `OpenRouterDecisionModel.make({ model, config? })` returns
7
+ `Effect<DecisionModel, never, OpenRouterClient>`; `layer` takes the same options
8
+ and provides `DecisionModel`. `model(modelId, config?)` returns
9
+ `Model.Model<'openrouter', DecisionModel, OpenRouterClient>`, including provider
10
+ and model-name metadata. All accept a string model ID; use a currently available
11
+ decision-capable ID from OpenRouter rather than the direct TypeSafe alias.
12
+
13
+ <!-- typecheck -->
14
+ ```ts
15
+ import * as OpenRouterClient from '@effect/ai-openrouter/OpenRouterClient';
16
+ import * as OpenRouterDecisionModel from '@effect/ai-openrouter/OpenRouterDecisionModel';
17
+ import * as BunHttpClient from '@effect/platform-bun/BunHttpClient';
18
+ import * as AiError from 'effect/ai/AiError';
19
+ import * as DecisionModel from 'effect/ai/DecisionModel';
20
+ import * as Config from 'effect/Config';
21
+ import * as Effect from 'effect/Effect';
22
+ import * as Layer from 'effect/Layer';
23
+
24
+ const DecisionsLive = Layer.unwrap(
25
+ Config.String('OPENROUTER_DECISION_MODEL').pipe(
26
+ Effect.map((model) => OpenRouterDecisionModel.layer({ model }))
27
+ )
28
+ ).pipe(
29
+ Layer.provide(OpenRouterClient.layerConfig({
30
+ apiKey: Config.Redacted('OPENROUTER_API_KEY')
31
+ })),
32
+ Layer.provide(BunHttpClient.layer)
33
+ );
34
+
35
+ declare const decision: Effect.Effect<unknown, AiError.AiError, DecisionModel.DecisionModel>;
36
+
37
+ const configured = decision.pipe(
38
+ Effect.provideService(OpenRouterDecisionModel.Config, { user: 'tenant-42' }),
39
+ Effect.provide(DecisionsLive)
40
+ );
41
+ ```
42
+
43
+ - `Config` is the service exported by **OpenRouterDecisionModel**, distinct from
44
+ Effect's configuration module and `OpenRouterConfig`. Its shape is
45
+ `Pick<typeof OpenRouterSchema.DecisionsRequest.Encoded, 'provider' | 'session_id' | 'user' | 'trace'>`.
46
+ Runtime fields shallowly override constructor `config`; nested provider options
47
+ are replaced, not deep-merged. It cannot override `model`, state, or questions.
48
+ - `OpenRouterClient.layerConfig` loads only supplied config fields; unlike
49
+ `TypeSafeClient.layerConfig()`, it does not default an API-key environment name.
50
+ Client options also include `apiUrl`, `siteReferrer`, `siteTitle`, and
51
+ `transformClient`; scoped `OpenRouterConfig.withClientTransform` applies to
52
+ decision requests.
53
+ - Default endpoint: `https://openrouter.ai/api/alpha/decisions`. Custom `apiUrl`
54
+ removes a trailing `/v1` (optional trailing slash) before appending
55
+ `/alpha/decisions`, preserving proxy prefixes.
56
+ - Encoded state must be a string, object, or array; scalar null/number/boolean
57
+ fails locally with `AiError` reason `InvalidUserInputError`.
58
+ - Choice/Score **must include full distributions** for the high-level adapter;
59
+ missing distributions fail with `InvalidOutputError` even if a wire response
60
+ otherwise decodes. Score index keys map to criteria strings. The adapter uses
61
+ two-decimal probability normalization and common core answer validation.
62
+ - For response ID, cost, provider metadata, or HTTP response details, yield
63
+ `OpenRouterClient.OpenRouterClient` and call `createDecisions(request)`. It
64
+ returns `[decodedBody, HttpClientResponse]` with `AiError` failures. Its schemas
65
+ are `OpenRouterSchema.DecisionsRequest`, `DecisionsQuestion`, and
66
+ `DecisionsResponse`. High-level `decide` projects only answers and token usage.
67
+
68
+ Pinned source: `packages/ai/openrouter/src/{OpenRouterDecisionModel,OpenRouterClient,OpenRouterConfig,OpenRouterSchema}.ts`
69
+ at `effect@4.0.0`; corresponding `test/OpenRouterDecisionModel.test.ts` and
70
+ `typetest/OpenRouterDecisionModel.tst.ts` cover adapter behavior and configuration.