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
@@ -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)
@@ -9,7 +9,11 @@ Use this skill when implementing tools for AI language models using the Effect A
9
9
 
10
10
  ## Effect AI Documentation Access
11
11
 
12
- For comprehensive Effect AI documentation, view the Effect v4 repository at `packages/ai/`
12
+ For core AI APIs inspect `packages/effect/src/ai/` at `effect@4.0.0` in the
13
+ Effect source reference; provider implementations live under `packages/ai/`.
14
+ Keep all Effect-family packages on the same version. These APIs are tagged
15
+ `@stability unstable`, so minor releases may break them despite stable-looking
16
+ import paths.
13
17
 
14
18
  Reference this for:
15
19
 
@@ -83,7 +87,7 @@ Use this pattern when the surrounding framework wants an async callback surface
83
87
  ### Basic Tool Definition
84
88
 
85
89
  ```typescript
86
- import * as Tool from 'effect/unstable/ai/Tool';
90
+ import * as Tool from 'effect/ai/Tool';
87
91
  import * as Schema from 'effect/Schema';
88
92
 
89
93
  const GetCurrentTime = Tool.make('GetCurrentTime', {
@@ -107,7 +111,7 @@ type Result = Tool.Success<typeof GetCurrentTime>;
107
111
  If you have an existing domain schema you want to use as a tool, create the tool with `Tool.make` and reference the schema directly:
108
112
 
109
113
  ```typescript
110
- import * as Tool from 'effect/unstable/ai/Tool';
114
+ import * as Tool from 'effect/ai/Tool';
111
115
  import * as Schema from 'effect/Schema';
112
116
 
113
117
  const UserResult = Schema.Struct({
@@ -139,7 +143,7 @@ type Success = Tool.Success<typeof GetUserTool>;
139
143
  ### Tool with Parameters
140
144
 
141
145
  ```typescript
142
- import * as Tool from 'effect/unstable/ai/Tool';
146
+ import * as Tool from 'effect/ai/Tool';
143
147
  import { Schema } from 'effect';
144
148
 
145
149
  const GetWeather = Tool.make('GetWeather', {
@@ -169,7 +173,7 @@ type Success = Tool.Success<typeof GetWeather>;
169
173
  ### Tool with Failure Handling
170
174
 
171
175
  ```typescript
172
- import * as Tool from 'effect/unstable/ai/Tool';
176
+ import * as Tool from 'effect/ai/Tool';
173
177
  import { Schema } from 'effect';
174
178
 
175
179
  class UserNotFound extends Schema.TaggedError<UserNotFound>()(
@@ -196,10 +200,7 @@ const FindUser = Tool.make('FindUser', {
196
200
  name: Schema.String,
197
201
  email: Schema.String
198
202
  }),
199
- failure: Schema.Union([
200
- Schema.instanceOf(UserNotFound),
201
- Schema.instanceOf(DatabaseError)
202
- ]),
203
+ failure: Schema.Union([UserNotFound, DatabaseError]),
203
204
  failureMode: 'error'
204
205
  });
205
206
 
@@ -222,12 +223,14 @@ annotation and `Tool.FailureOrigin` type. Keep validation, declared handler, and
222
223
  internal failures distinct when presenting or reporting them.
223
224
 
224
225
  - `"error"` (default): Failures go to Effect error channel
225
- - `"return"`: Failures returned as tool result (captured, not thrown)
226
+ - `"return"`: Parameter and handler failures become failed tool results. Result
227
+ encoding can still fail the returned Stream with `AiError`; return mode does
228
+ not make stream consumption infallible.
226
229
 
227
230
  ### Tool with Service Dependencies
228
231
 
229
232
  ```typescript
230
- import * as Tool from 'effect/unstable/ai/Tool';
233
+ import * as Tool from 'effect/ai/Tool';
231
234
  import * as Context from 'effect/Context';
232
235
  import { Schema } from 'effect';
233
236
 
@@ -261,8 +264,8 @@ type Requirements = Tool.HandlerServices<typeof QueryDatabase>;
261
264
  ### Basic Toolkit
262
265
 
263
266
  ```typescript
264
- import * as Toolkit from 'effect/unstable/ai/Toolkit';
265
- import * as Tool from 'effect/unstable/ai/Tool';
267
+ import * as Toolkit from 'effect/ai/Toolkit';
268
+ import * as Tool from 'effect/ai/Tool';
266
269
  import { Effect, Schema } from 'effect';
267
270
 
268
271
  const GetCurrentTime = Tool.make('GetCurrentTime', {
@@ -473,7 +476,7 @@ Keep static descriptions on the tool for invariant behavior. Put runtime-specifi
473
476
  ### Merging Toolkits
474
477
 
475
478
  ```typescript
476
- import * as Toolkit from 'effect/unstable/ai/Toolkit';
479
+ import * as Toolkit from 'effect/ai/Toolkit';
477
480
 
478
481
  const mathToolkit = Toolkit.make(
479
482
  Tool.make('add', {
@@ -510,7 +513,7 @@ type AllTools = Toolkit.Tools<typeof combined>;
510
513
  ### Basic Provider Tool
511
514
 
512
515
  ```typescript
513
- import * as Tool from 'effect/unstable/ai/Tool';
516
+ import * as Tool from 'effect/ai/Tool';
514
517
  import { Schema } from 'effect';
515
518
 
516
519
  const AnthropicBash = Tool.providerDefined({
@@ -557,7 +560,7 @@ const nativeTools = Toolkit.make(
557
560
  ### Provider Tool with Handler
558
561
 
559
562
  ```typescript
560
- import * as Tool from 'effect/unstable/ai/Tool';
563
+ import * as Tool from 'effect/ai/Tool';
561
564
  import { Schema } from 'effect';
562
565
 
563
566
  const WebSearch = Tool.providerDefined({
@@ -614,7 +617,7 @@ declare const performSearch: (query: string) => Effect.Effect<
614
617
  ### Understanding ToolCallPart and ToolResultPart
615
618
 
616
619
  ```typescript
617
- import * as Prompt from 'effect/unstable/ai/Prompt';
620
+ import * as Prompt from 'effect/ai/Prompt';
618
621
 
619
622
  const toolCallPart = Prompt.makePart('tool-call', {
620
623
  id: 'call_123',
@@ -725,8 +728,14 @@ const toolkitLayer = LongRunningToolkit.toLayer({
725
728
 
726
729
  **Key Pattern: toolkit.handle**
727
730
 
728
- - `toolkit.handle(name, params, toolCallId?)` accepts `Tool.ParametersEncoded<Tool>` and returns `Effect<Stream<HandlerResult<Tool>>>`
731
+ - `toolkit.handle(name, params, toolCallId?, options?)` accepts
732
+ `Tool.ParametersEncoded<T>` and optional schema parse options. Its full shape is
733
+ `Effect<Stream<HandlerResult<T>, Tool.HandlerError<T> | AiError, Tool.HandlerServices<T>>, AiError, Tool.HandlerServices<T>>`.
729
734
  - `handle` decodes the encoded input with the tool's parameter schema before invoking the handler; the handler still receives `Tool.Parameters<Tool>`
735
+ - Provide handler services to the outer Effect as well as the Stream. A
736
+ service-dependent parameter decoder runs before the Stream exists; providing
737
+ services only around `Stream.runCollect` is too late. Consuming the Stream
738
+ inside the same provided `Effect.gen` keeps both stages wired.
730
739
  - The optional call ID is forwarded to the handler as `context.toolCallId`
731
740
  - `preliminary: true`: progress update; do not persist as final history
732
741
  - `preliminary: false`: final result to send/persist
@@ -760,7 +769,7 @@ Passing the decoded `{ times: 3 }` directly to `handle` is now a type error. Use
760
769
  ### Tool Annotations
761
770
 
762
771
  ```typescript
763
- import * as Tool from 'effect/unstable/ai/Tool';
772
+ import * as Tool from 'effect/ai/Tool';
764
773
  import { Schema } from 'effect';
765
774
 
766
775
  const ReadOnlyQuery = Tool.make('query', {
@@ -786,7 +795,7 @@ MCP emits the first four annotations as tool hints. They are hints, not authoriz
786
795
  ### JSON Schema Generation
787
796
 
788
797
  ```typescript
789
- import * as Tool from 'effect/unstable/ai/Tool';
798
+ import * as Tool from 'effect/ai/Tool';
790
799
 
791
800
  const tool = Tool.make('example', {
792
801
  parameters: Schema.Struct({
@@ -815,7 +824,7 @@ const jsonSchema = Tool.getJsonSchema(tool);
815
824
  ### Tool Guards
816
825
 
817
826
  ```typescript
818
- import * as Tool from 'effect/unstable/ai/Tool';
827
+ import * as Tool from 'effect/ai/Tool';
819
828
 
820
829
  const userTool = Tool.make('example');
821
830
  const providerTool = Tool.providerDefined({
@@ -858,8 +867,8 @@ const executeTool = (toolName: string, params: unknown) =>
858
867
 
859
868
  <!-- typecheck -->
860
869
  ```typescript
861
- import * as Tool from 'effect/unstable/ai/Tool';
862
- import * as Toolkit from 'effect/unstable/ai/Toolkit';
870
+ import * as Tool from 'effect/ai/Tool';
871
+ import * as Toolkit from 'effect/ai/Toolkit';
863
872
  import * as Schema from 'effect/Schema';
864
873
  import { Clock, Context, Effect, Layer, Stream } from 'effect';
865
874
 
@@ -946,9 +955,9 @@ manual `handle` calls accept their encoded representation.
946
955
  **CRITICAL**: Always use namespace imports:
947
956
 
948
957
  ```typescript
949
- import * as Tool from 'effect/unstable/ai/Tool';
950
- import * as Toolkit from 'effect/unstable/ai/Toolkit';
951
- import * as Prompt from 'effect/unstable/ai/Prompt';
958
+ import * as Tool from 'effect/ai/Tool';
959
+ import * as Toolkit from 'effect/ai/Toolkit';
960
+ import * as Prompt from 'effect/ai/Prompt';
952
961
  import { Schema, Effect, Data, Context, Layer } from 'effect';
953
962
 
954
963
  const myTool = Tool.make('example');
@@ -958,8 +967,8 @@ const myToolkit = Toolkit.make(myTool);
958
967
  **NEVER** do this:
959
968
 
960
969
  ```typescript
961
- import { make } from 'effect/unstable/ai/Tool';
962
- import { make as makeToolkit } from 'effect/unstable/ai/Toolkit';
970
+ import { make } from 'effect/ai/Tool';
971
+ import { make as makeToolkit } from 'effect/ai/Toolkit';
963
972
  ```
964
973
 
965
974
  ## Quality Checklist
@@ -3,7 +3,11 @@ name: effect-atom-rpc
3
3
  description: Build reactive Atom-aware RPC clients for React/Atom UIs using AtomRpc. Provides cached query atoms, invalidating mutation atoms, SSR hydration, and reactivity-key invalidation on top of an RpcGroup.
4
4
  ---
5
5
 
6
- You are an Effect TypeScript expert specializing in `effect/unstable/reactivity/AtomRpc` — the reactive RPC client for Atom-driven frontends.
6
+ You are an Effect TypeScript expert specializing in `effect/reactivity/AtomRpc` — the reactive RPC client for Atom-driven frontends.
7
+
8
+ Baseline: `effect@4.0.0`; inspect that tag in the Effect source reference.
9
+ Core reactivity APIs are `@stability unstable` and may break in minor releases.
10
+ Use matching Effect-family versions; `@effect/atom-react@4.0.0` requires React 19.
7
11
 
8
12
  ## When to use this skill
9
13
 
@@ -35,13 +39,13 @@ handle them when consuming the stream. An explicit zero `timeToLive` disables
35
39
  default idle retention, so unmount/remount may dispose and refetch. Omission uses
36
40
  the registry default. HttpApi calls accept per-call `sseOptions`.
37
41
 
38
- - `packages/effect/src/unstable/reactivity/AtomRpc.ts` — the whole API (~270 lines)
42
+ - `packages/effect/src/reactivity/AtomRpc.ts` — the whole API
39
43
  - `packages/effect/test/reactivity/AtomRpc.test.ts` — minimal usage + serialization test
40
- - `packages/effect/src/unstable/reactivity/AtomHttpApi.ts` — the cousin pattern for `HttpApi` (same idea, same options)
41
- - `packages/effect/src/unstable/reactivity/AsyncResult.ts` — the `AsyncResult` type returned by `query`
42
- - `packages/effect/src/unstable/reactivity/Atom.ts` — `runtime`, `family`, `serializable`, `keepAlive`, `setIdleTTL`
43
- - `packages/effect/src/unstable/reactivity/Hydration.ts` — SSR dehydrate/hydrate helpers
44
- - `packages/effect/src/unstable/reactivity/Reactivity.ts` — the `Reactivity` service that powers reactivity keys
44
+ - `packages/effect/src/reactivity/AtomHttpApi.ts` — the cousin pattern for `HttpApi` (same idea, same options)
45
+ - `packages/effect/src/reactivity/AsyncResult.ts` — the `AsyncResult` type returned by `query`
46
+ - `packages/effect/src/reactivity/Atom.ts` — `runtime`, `family`, `serializable`, `keepAlive`, `setIdleTTL`
47
+ - `packages/effect/src/reactivity/Hydration.ts` — SSR dehydrate/hydrate helpers
48
+ - `packages/effect/src/reactivity/Reactivity.ts` — the `Reactivity` service that powers reactivity keys
45
49
 
46
50
  ## Imports
47
51
 
@@ -52,9 +56,9 @@ import {
52
56
  AtomRpc,
53
57
  Hydration,
54
58
  Reactivity
55
- } from 'effect/unstable/reactivity';
56
- import { RpcClient, RpcSerialization } from 'effect/unstable/rpc';
57
- import { FetchHttpClient } from 'effect/unstable/http';
59
+ } from 'effect/reactivity';
60
+ import { RpcClient, RpcSerialization } from 'effect/rpc';
61
+ import { FetchHttpClient } from 'effect/http';
58
62
  import { useAtomValue, useAtomSet } from '@effect/atom-react';
59
63
  ```
60
64
 
@@ -77,9 +81,9 @@ The `runtime` is what powers `query` and `mutation` — internally it builds a `
77
81
 
78
82
  ```ts
79
83
  import { Layer } from 'effect';
80
- import { AtomRpc } from 'effect/unstable/reactivity';
81
- import { FetchHttpClient } from 'effect/unstable/http';
82
- import { RpcClient, RpcSerialization } from 'effect/unstable/rpc';
84
+ import { AtomRpc } from 'effect/reactivity';
85
+ import { FetchHttpClient } from 'effect/http';
86
+ import { RpcClient, RpcSerialization } from 'effect/rpc';
83
87
  import { UsersGroup } from '../shared/users-rpc.ts';
84
88
 
85
89
  export class UsersClient extends AtomRpc.Service<UsersClient>()('UsersClient', {
@@ -124,7 +128,7 @@ AtomRpc.Service<UsersClient>()('UsersClient', {
124
128
  disableTracing: false,
125
129
  generateRequestId: customRequestIdFactory, // for deterministic IDs in tests
126
130
  makeEffect: undefined, // override the default RpcClient.make
127
- runtime: Atom.runtime // override the runtime factory (e.g., Atom.runtime.withReactivity)
131
+ runtime: Atom.runtime // override with another factory, e.g. Atom.context()
128
132
  });
129
133
  ```
130
134
 
@@ -149,6 +153,11 @@ client.query(tag, payload, options?) =>
149
153
 
150
154
  Query atoms are **cached by request key** — calling `query('GetUser', { id: '1' })` from many components returns the *same* atom, so the rpc fires once and shares the result.
151
155
 
156
+ The key also includes headers, reactivity keys, TTL, and serialization key.
157
+ Use matching options to share a cache entry. Union-of-tag arguments retain the
158
+ selected RPCs' payload/result types; they no longer collapse to `never`. Narrow
159
+ the tag when application logic needs to pair a particular payload with its result.
160
+
152
161
  Effect v4 query wrappers preserve serialization and retention metadata while adding reactivity. It is therefore safe to combine `reactivityKeys`, `serializationKey`, and `timeToLive`; hydration identity and idle retention are not discarded by the reactive wrapper.
153
162
 
154
163
  ```ts
@@ -184,7 +193,11 @@ client.query(tag, payload, {
184
193
  ```
185
194
 
186
195
  - **`reactivityKeys`** — keys this atom listens to. When a `mutation` (or any `Reactivity.invalidate`) fires with overlapping keys, this atom re-fetches.
187
- - **`timeToLive`** — `Duration.Input | "infinity"`. With a finite duration, the atom is removed from the cache after that long without subscribers (`Atom.setIdleTTL`). With `Duration.infinity`, the atom is `Atom.keepAlive`'d (never evicted). Without `timeToLive`, the atom resets when the last subscriber unmounts.
196
+ - **`timeToLive`** — idle retention via `Atom.setIdleTTL`. A finite duration permits
197
+ disposal after inactivity; `Duration.infinity` uses `Atom.keepAlive` until the
198
+ registry is reset/disposed. Omission uses the registry default, while explicit
199
+ zero bypasses default idle retention. Freshness is separate: use `Atom.swr` for
200
+ stale-while-revalidate behavior (see `effect-atom-state`).
188
201
  - **`serializationKey`** — for non-stream queries, a stable key opts the atom into `Atom.serializable` keyed by `AtomRpc:${tag}:${serializationKey}`. Without a `serializationKey`, the query atom is cached for the current registry but is not serializable/hydratable, so SSR clients will fetch again. Stream rpcs cannot be serializable.
189
202
 
190
203
  ### Stream rpcs
@@ -291,25 +304,27 @@ Hydration uses `Atom.serializable` (which `query` opts into when you pass `seria
291
304
 
292
305
  ```ts
293
306
  // --- server: render then dehydrate ---
294
- import { AtomRegistry, Hydration } from 'effect/unstable/reactivity';
295
-
296
- const registry = AtomRegistry.make();
297
- const userAtom = UsersClient.query('GetUser', { id: 'u1' }, {
298
- serializationKey: 'u1'
299
- });
307
+ import { Effect } from 'effect';
308
+ import { AtomRegistry, Hydration } from 'effect/reactivity';
300
309
 
301
- const unmount = registry.mount(userAtom);
302
- yield* Effect.yieldNow;
303
- yield* Effect.yieldNow;
304
-
305
- const dehydrated = Hydration.toValues(Hydration.dehydrate(registry));
306
- unmount();
310
+ const dehydrated = yield* Effect.scoped(Effect.gen(function* () {
311
+ const registry = yield* Effect.acquireRelease(
312
+ Effect.sync(() => AtomRegistry.make()),
313
+ (registry) => Effect.sync(() => registry.dispose())
314
+ );
315
+ const userAtom = UsersClient.query('GetUser', { id: 'u1' }, {
316
+ serializationKey: 'u1'
317
+ });
318
+ yield* AtomRegistry.mount(registry, userAtom);
319
+ yield* AtomRegistry.getResult(registry, userAtom);
320
+ return Hydration.toValues(Hydration.dehydrate(registry));
321
+ }));
307
322
  // dehydrated is a JSON-safe array of { key, value } pairs to inline in the HTML
308
323
  ```
309
324
 
310
325
  ```tsx
311
326
  // --- client: hydrate before rendering ---
312
- import { AtomRegistry, Hydration } from 'effect/unstable/reactivity';
327
+ import { AtomRegistry, Hydration } from 'effect/reactivity';
313
328
  import { RegistryContext } from '@effect/atom-react';
314
329
 
315
330
  const registry = AtomRegistry.make();
@@ -327,7 +342,7 @@ Stream rpcs are not serializable — only non-stream `query` atoms with a stable
327
342
  `AtomRpc.Service` accepts a `runtime` factory option. The default is `Atom.runtime`. Override it when you want all the atoms produced by this client to share a custom reactivity scope, registry, or annotation:
328
343
 
329
344
  ```ts
330
- const myRuntime = Atom.runtime.withReactivity(['shared-namespace']);
345
+ const myRuntime = Atom.context();
331
346
 
332
347
  export class UsersClient extends AtomRpc.Service<UsersClient>()('UsersClient', {
333
348
  group: UsersGroup,
@@ -336,12 +351,17 @@ export class UsersClient extends AtomRpc.Service<UsersClient>()('UsersClient', {
336
351
  }) {}
337
352
  ```
338
353
 
354
+ `withReactivity(keys)` decorates an atom; it is not a runtime factory. Use
355
+ `reactivityKeys` on queries/mutations for invalidation. Runtime factories use
356
+ registry-scoped layer memoization by default; provide the same registry to
357
+ components that should share state.
358
+
339
359
  ## End-to-end example
340
360
 
341
361
  ```ts
342
362
  // --- shared/users-rpc.ts ---
343
363
  import { Schema } from 'effect';
344
- import { Rpc, RpcGroup } from 'effect/unstable/rpc';
364
+ import { Rpc, RpcGroup } from 'effect/rpc';
345
365
 
346
366
  export class User extends Schema.Class<User>('User')({
347
367
  id: Schema.String,
@@ -375,9 +395,9 @@ export const UsersGroup = RpcGroup.make(GetUser, ListUsers, CreateUser);
375
395
  ```ts
376
396
  // --- frontend/clients/users-client.ts ---
377
397
  import { Layer } from 'effect';
378
- import { FetchHttpClient } from 'effect/unstable/http';
379
- import { AtomRpc } from 'effect/unstable/reactivity';
380
- import { RpcClient, RpcSerialization } from 'effect/unstable/rpc';
398
+ import { FetchHttpClient } from 'effect/http';
399
+ import { AtomRpc } from 'effect/reactivity';
400
+ import { RpcClient, RpcSerialization } from 'effect/rpc';
381
401
  import { UsersGroup } from '../../shared/users-rpc.ts';
382
402
 
383
403
  export class UsersClient extends AtomRpc.Service<UsersClient>()('UsersClient', {
@@ -393,7 +413,7 @@ export class UsersClient extends AtomRpc.Service<UsersClient>()('UsersClient', {
393
413
  ```tsx
394
414
  // --- frontend/components/UserList.tsx ---
395
415
  import { useAtomValue, useAtomSet } from '@effect/atom-react';
396
- import { AsyncResult } from 'effect/unstable/reactivity';
416
+ import { AsyncResult } from 'effect/reactivity';
397
417
  import { UsersClient } from '../clients/users-client';
398
418
 
399
419
  export function UserList() {
@@ -455,7 +475,7 @@ export function UserDetail({ id }: { id: string }) {
455
475
  If your backend exposes an `HttpApi` instead of an `RpcGroup`, `AtomHttpApi.Service<Self>()(id, options)` is the same idea applied to `HttpApiGroup`/`HttpApiEndpoint`:
456
476
 
457
477
  ```ts
458
- import { AtomHttpApi } from 'effect/unstable/reactivity';
478
+ import { AtomHttpApi } from 'effect/reactivity';
459
479
 
460
480
  class ApiClient extends AtomHttpApi.Service<ApiClient>()('ApiClient', {
461
481
  api: MyHttpApi,
@@ -494,7 +514,8 @@ The `query`/`mutation` arguments differ (you pass `groupName, endpointName, requ
494
514
  - One `AtomRpc.Service` class per logical client; module-singleton, never per-component.
495
515
  - Always pass `reactivityKeys` to mutations so dependent queries refresh.
496
516
  - Pass `serializationKey` to every query you want SSR-hydratable.
497
- - Pick `timeToLive` deliberately: short (`'10 seconds'`) for hot data, long (`Duration.infinity`) for reference data, omit for "tear down on unmount".
517
+ - Pick idle `timeToLive` deliberately; omit for registry defaults, zero to bypass
518
+ default idle retention, or `Duration.infinity` to retain until registry disposal.
498
519
  - Use `AsyncResult.builder(...).onWaiting(...).onError(...).onSuccess(...)` (or `AsyncResult.matchWithWaiting`) to render — never check `.waiting` and `.error` ad-hoc. Reserve `.onFailure((cause) => ...)` for whole-`Cause` fallbacks after typed error branches.
499
520
  - For protocol layers that depend on auth tokens or other reactive state, use the `(get) => Layer` form of `protocol` so the client rebuilds when those atoms change.
500
521
  - For typed error recovery, handle declared RPC errors, RPC middleware wire errors, and `RpcClientError`; branch on `_tag` only when the relevant errors are tagged.