opencode-effect-enforcer 0.2.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 (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +278 -0
  3. package/guidance/effect-first-development.md +1247 -0
  4. package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
  5. package/guidance/post__parse-dont-validate.md +109 -0
  6. package/guidance/progressive-disclosure-guidance.md +38 -0
  7. package/package.json +63 -0
  8. package/patterns/avoid-any.md +37 -0
  9. package/patterns/avoid-data-tagged-error.md +34 -0
  10. package/patterns/avoid-direct-json.md +51 -0
  11. package/patterns/avoid-direct-tag-checks.md +54 -0
  12. package/patterns/avoid-expect-in-if.md +52 -0
  13. package/patterns/avoid-mutable-state.md +70 -0
  14. package/patterns/avoid-native-fetch.md +61 -0
  15. package/patterns/avoid-node-imports.md +86 -0
  16. package/patterns/avoid-non-null-assertion.md +44 -0
  17. package/patterns/avoid-object-type.md +46 -0
  18. package/patterns/avoid-option-getorthrow.md +39 -0
  19. package/patterns/avoid-platform-coupling.md +43 -0
  20. package/patterns/avoid-process-env.md +43 -0
  21. package/patterns/avoid-react-hooks.md +73 -0
  22. package/patterns/avoid-schema-suffix.md +45 -0
  23. package/patterns/avoid-sync-fs.md +68 -0
  24. package/patterns/avoid-try-catch.md +47 -0
  25. package/patterns/avoid-ts-ignore.md +38 -0
  26. package/patterns/avoid-untagged-errors.md +67 -0
  27. package/patterns/avoid-yield-ref.md +46 -0
  28. package/patterns/casting-awareness.md +46 -0
  29. package/patterns/context-tag-extends.md +84 -0
  30. package/patterns/effect-catchall-default.md +61 -0
  31. package/patterns/effect-promise-vs-trypromise.md +47 -0
  32. package/patterns/effect-run-in-body.md +58 -0
  33. package/patterns/imperative-loops.md +76 -0
  34. package/patterns/prefer-arr-sort.md +52 -0
  35. package/patterns/prefer-duration-values.md +56 -0
  36. package/patterns/prefer-effect-fn.md +161 -0
  37. package/patterns/prefer-match-over-switch.md +48 -0
  38. package/patterns/prefer-option-over-null.md +56 -0
  39. package/patterns/prefer-redacted-config.md +70 -0
  40. package/patterns/prefer-schema-class.md +54 -0
  41. package/patterns/require-effect-concurrency.md +83 -0
  42. package/patterns/stream-large-files.md +63 -0
  43. package/patterns/throw-in-effect-gen.md +62 -0
  44. package/patterns/use-clock-service.md +45 -0
  45. package/patterns/use-command-executor-service.md +54 -0
  46. package/patterns/use-console-service.md +54 -0
  47. package/patterns/use-filesystem-service.md +59 -0
  48. package/patterns/use-http-client-service.md +77 -0
  49. package/patterns/use-path-service.md +53 -0
  50. package/patterns/use-random-service.md +45 -0
  51. package/patterns/use-temp-file-scoped.md +66 -0
  52. package/patterns/vm-in-wrong-file.md +51 -0
  53. package/patterns/yield-in-for-loop.md +61 -0
  54. package/skills/effect-ai-chat/SKILL.md +472 -0
  55. package/skills/effect-ai-language-model/SKILL.md +652 -0
  56. package/skills/effect-ai-prompt/SKILL.md +752 -0
  57. package/skills/effect-ai-provider/SKILL.md +668 -0
  58. package/skills/effect-ai-streaming/SKILL.md +418 -0
  59. package/skills/effect-ai-tool/SKILL.md +1132 -0
  60. package/skills/effect-atom-rpc/SKILL.md +488 -0
  61. package/skills/effect-atom-state/SKILL.md +640 -0
  62. package/skills/effect-batching/SKILL.md +614 -0
  63. package/skills/effect-cache/SKILL.md +570 -0
  64. package/skills/effect-cli/SKILL.md +523 -0
  65. package/skills/effect-command-executor/SKILL.md +675 -0
  66. package/skills/effect-concurrency-testing/SKILL.md +612 -0
  67. package/skills/effect-config/SKILL.md +580 -0
  68. package/skills/effect-context-witness/SKILL.md +274 -0
  69. package/skills/effect-domain-modeling/SKILL.md +1212 -0
  70. package/skills/effect-domain-predicates/SKILL.md +867 -0
  71. package/skills/effect-error-handling/SKILL.md +1581 -0
  72. package/skills/effect-fiber/SKILL.md +731 -0
  73. package/skills/effect-filesystem/SKILL.md +624 -0
  74. package/skills/effect-graph/SKILL.md +571 -0
  75. package/skills/effect-http-api/SKILL.md +1760 -0
  76. package/skills/effect-http-client/SKILL.md +989 -0
  77. package/skills/effect-http-server/SKILL.md +920 -0
  78. package/skills/effect-incremental-migration/SKILL.md +362 -0
  79. package/skills/effect-layer-design/SKILL.md +642 -0
  80. package/skills/effect-managed-runtime/SKILL.md +395 -0
  81. package/skills/effect-mcp-server/SKILL.md +608 -0
  82. package/skills/effect-observability/SKILL.md +719 -0
  83. package/skills/effect-optics/SKILL.md +554 -0
  84. package/skills/effect-parallelization/SKILL.md +668 -0
  85. package/skills/effect-path/SKILL.md +296 -0
  86. package/skills/effect-pattern-matching/SKILL.md +914 -0
  87. package/skills/effect-platform-abstraction/SKILL.md +1175 -0
  88. package/skills/effect-platform-layers/SKILL.md +514 -0
  89. package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
  90. package/skills/effect-react-composition/SKILL.md +986 -0
  91. package/skills/effect-react-vm/SKILL.md +675 -0
  92. package/skills/effect-rpc-api/SKILL.md +624 -0
  93. package/skills/effect-rpc-client/SKILL.md +666 -0
  94. package/skills/effect-rpc-cluster/SKILL.md +1623 -0
  95. package/skills/effect-rpc-server/SKILL.md +767 -0
  96. package/skills/effect-scheduling/SKILL.md +124 -0
  97. package/skills/effect-schema-composition/SKILL.md +975 -0
  98. package/skills/effect-schema-v4/SKILL.md +691 -0
  99. package/skills/effect-scope/SKILL.md +682 -0
  100. package/skills/effect-service-implementation/SKILL.md +656 -0
  101. package/skills/effect-socket/SKILL.md +703 -0
  102. package/skills/effect-sql/SKILL.md +781 -0
  103. package/skills/effect-stream/SKILL.md +765 -0
  104. package/skills/effect-testing/SKILL.md +1331 -0
  105. package/skills/effect-typeclass-design/SKILL.md +161 -0
  106. package/skills/effect-wide-events/Article.md +66 -0
  107. package/skills/effect-wide-events/SKILL.md +95 -0
  108. package/skills/effect-workflow/SKILL.md +810 -0
  109. package/src/agent-policy.ts +22 -0
  110. package/src/enforcer.ts +104 -0
  111. package/src/frontmatter.ts +34 -0
  112. package/src/guidance.ts +66 -0
  113. package/src/index.ts +38 -0
  114. package/src/pattern-catalog.ts +115 -0
  115. package/src/pattern-matcher.ts +178 -0
  116. package/src/pattern.ts +97 -0
  117. package/src/skills.ts +29 -0
  118. package/src/write-projection.ts +66 -0
@@ -0,0 +1,418 @@
1
+ ---
2
+ name: effect-ai-streaming
3
+ description: Master Effect AI streaming response patterns including start/delta/end protocol, accumulation strategies, resource-safe consumption, and history management with SubscriptionRef.
4
+ ---
5
+
6
+ # Effect AI Streaming
7
+
8
+ ## When to Use This Skill
9
+
10
+ - Real-time streaming responses from language models
11
+ - Building chat interfaces with incremental updates
12
+ - Managing conversation history with streaming
13
+ - Protecting concurrent stream operations
14
+ - Accumulating stream parts with side effects
15
+ - Converting stream responses to prompt history
16
+
17
+ ## Import Patterns
18
+
19
+ **CRITICAL**: Always use namespace imports:
20
+
21
+ ```typescript
22
+ import * as Stream from 'effect/Stream';
23
+ import * as Effect from 'effect/Effect';
24
+ import * as Channel from 'effect/Channel';
25
+ import * as SubscriptionRef from 'effect/SubscriptionRef';
26
+ import * as Match from 'effect/Match';
27
+ import * as Response from 'effect/unstable/ai/Response';
28
+ ```
29
+
30
+ ## StreamPart Protocol
31
+
32
+ stream := start → delta\* → end
33
+
34
+ StreamPart lifecycle for each content type follows a three-phase protocol:
35
+
36
+ ```haskell
37
+ text :: text-start → text-delta* → text-end
38
+ reasoning :: reasoning-start → reasoning-delta* → reasoning-end
39
+ toolParam :: tool-params-start → tool-params-delta* → tool-params-end
40
+ finish :: { type: "finish", reason: FinishReason, usage: Usage }
41
+ ```
42
+
43
+ Each streaming sequence has a unique `id` field that links start/delta/end parts.
44
+
45
+ ## Part Type Matching
46
+
47
+ Stream parts use a `type` field (not `_tag`), so use `Match.when` with `type` checks:
48
+
49
+ ```typescript
50
+ import * as Match from 'effect/Match';
51
+ import * as Effect from 'effect/Effect';
52
+
53
+ const processPart = (part: StreamPart) =>
54
+ Match.value(part).pipe(
55
+ Match.when({ type: 'text-delta' }, ({ delta }) =>
56
+ Effect.sync(() => console.log(delta))
57
+ ),
58
+ Match.when({ type: 'reasoning-delta' }, ({ delta }) =>
59
+ Effect.sync(() => logReasoning(delta))
60
+ ),
61
+ Match.when({ type: 'finish' }, ({ usage, reason }) =>
62
+ Effect.sync(() => recordUsage(usage, reason))
63
+ ),
64
+ Match.orElse(() => Effect.void)
65
+ );
66
+ ```
67
+
68
+ Direct `type` checks also work well for simple branching:
69
+
70
+ ```typescript
71
+ if (part.type === 'text-delta') {
72
+ console.log(part.delta);
73
+ }
74
+ ```
75
+
76
+ ## Accumulation Pattern
77
+
78
+ Accumulate stream parts incrementally using mutable state for efficiency:
79
+
80
+ ```typescript
81
+ import * as Stream from 'effect/Stream';
82
+ import * as Effect from 'effect/Effect';
83
+ import * as Prompt from 'effect/unstable/ai/Prompt';
84
+
85
+ const accumulated: Array<StreamPart> = [];
86
+ let combined = Prompt.empty;
87
+
88
+ stream.pipe(
89
+ Stream.mapChunksEffect(
90
+ Effect.fnUntraced(function* (chunk) {
91
+ const parts = Array.from(chunk);
92
+
93
+ // Append to mutable accumulator
94
+ accumulated.push(...parts);
95
+
96
+ // Fold the accumulated response so start/delta/end IDs are visible together
97
+ combined = Prompt.fromResponseParts(accumulated);
98
+
99
+ // Update history incrementally
100
+ yield* SubscriptionRef.set(
101
+ history,
102
+ Prompt.concat(checkpoint, combined)
103
+ );
104
+
105
+ return chunk;
106
+ })
107
+ )
108
+ );
109
+ ```
110
+
111
+ Key insight: `Stream.mapChunksEffect` enables side-effectful accumulation while preserving stream semantics.
112
+
113
+ ## Resource-Safe Streaming
114
+
115
+ Prevent concurrent stream operations using semaphore protection:
116
+
117
+ ```typescript
118
+ import * as Channel from 'effect/Channel';
119
+ import * as Semaphore from 'effect/Semaphore';
120
+ import * as Stream from 'effect/Stream';
121
+
122
+ const streamWithProtection = Stream.fromChannel(
123
+ Channel.acquireUseRelease(
124
+ // Acquire: Take semaphore, get checkpoint
125
+ semaphore.take(1).pipe(
126
+ Effect.zipRight(SubscriptionRef.get(history)),
127
+ Effect.map((hist) => Prompt.concat(hist, newPrompt)),
128
+ Effect.tap((checkpoint) => SubscriptionRef.set(history, checkpoint))
129
+ ),
130
+
131
+ // Use: Stream with accumulation
132
+ (checkpoint) =>
133
+ LanguageModel.streamText({ prompt: checkpoint }).pipe(
134
+ Stream.mapChunksEffect(accumulateAndUpdate),
135
+ Stream.toChannel
136
+ ),
137
+
138
+ // Release: Always release semaphore
139
+ () => semaphore.release(1)
140
+ )
141
+ );
142
+ ```
143
+
144
+ Resource acquisition order:
145
+
146
+ 1. Take semaphore (exclusive access)
147
+ 2. Get current history snapshot
148
+ 3. Merge with new prompt
149
+ 4. Update history with checkpoint
150
+ 5. Stream response (with incremental updates)
151
+ 6. Release semaphore (guaranteed via `acquireUseRelease`)
152
+
153
+ ## Consumption Patterns
154
+
155
+ runForEach :: (A → Effect<R, E>) → Stream<A, E, R> → Effect<Unit, E, R>
156
+ runDrain :: Stream<A, E, R> → Effect<Unit, E, R>
157
+ runLast :: Stream<A, E, R> → Effect<Option<A>, E, R>
158
+
159
+ ```typescript
160
+ // Process each part with side effects
161
+ stream.pipe(
162
+ Stream.runForEach((part) =>
163
+ Match.value(part).pipe(
164
+ Match.when({ type: 'text-delta' }, ({ delta }) => updateUI(delta)),
165
+ Match.when({ type: 'finish' }, ({ usage }) => recordMetrics(usage)),
166
+ Match.orElse(() => Effect.void)
167
+ )
168
+ )
169
+ );
170
+
171
+ // Consume without collecting (memory efficient)
172
+ stream.pipe(Stream.tap(logPart), Stream.runDrain);
173
+
174
+ // Get final accumulated value
175
+ stream.pipe(
176
+ Stream.runFold(initialState, (acc, part) => merge(acc, part)),
177
+ Effect.map(Option.some)
178
+ );
179
+ ```
180
+
181
+ ## History Update Pattern
182
+
183
+ Incremental merge strategy for conversation history:
184
+
185
+ ```typescript
186
+ Prompt.concat :: Prompt → Prompt → Prompt
187
+ Prompt.fromResponseParts :: Array<StreamPart> → Prompt
188
+
189
+ // Pattern: checkpoint + accumulated response fold
190
+ const accumulated: Array<StreamPart> = []
191
+ let combined = Prompt.empty
192
+
193
+ Stream.mapChunksEffect(function* (chunk) {
194
+ const parts = Array.from(chunk)
195
+ accumulated.push(...parts)
196
+
197
+ // Fold accumulated parts, not only this chunk, so start/delta/end IDs align
198
+ combined = Prompt.fromResponseParts(accumulated)
199
+
200
+ // Update history: base checkpoint + accumulated response
201
+ yield* SubscriptionRef.set(
202
+ history,
203
+ Prompt.concat(filteredCheckpoint, combined)
204
+ )
205
+
206
+ return chunk
207
+ })
208
+ ```
209
+
210
+ Why checkpoint-based merging:
211
+
212
+ - Prevents re-merging entire history on each chunk
213
+ - Separates base state (checkpoint) from streaming accumulation (combined)
214
+ - Enables atomic history updates via SubscriptionRef
215
+ - Ensures `Prompt.fromResponseParts` sees matching start/delta/end parts for each `id`
216
+
217
+ ## Tool Streaming, Finish, and Approvals
218
+
219
+ - With automatic framework tool resolution enabled, `finish` is deferred until tool handler streams complete so emitted tool results appear before finish.
220
+ - `tool-result` parts can be preliminary or final. Use preliminary results for progress updates only; `Prompt.fromResponseParts` skips preliminary results and persists final results.
221
+ - `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.
222
+ - 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.
223
+ - In OpenAI-specific SSE code, unknown future events decode through `OpenAiSchema.ResponseStreamEvent` and are ignored by `OpenAiLanguageModel`; malformed known events still fail decoding.
224
+
225
+ ## Complete Example
226
+
227
+ ```typescript
228
+ import * as Prompt from 'effect/unstable/ai/Prompt';
229
+ import * as Response from 'effect/unstable/ai/Response';
230
+ import * as LanguageModel from 'effect/unstable/ai/LanguageModel';
231
+ import * as Stream from 'effect/Stream';
232
+ import * as Effect from 'effect/Effect';
233
+ import * as SubscriptionRef from 'effect/SubscriptionRef';
234
+ import * as Semaphore from 'effect/Semaphore';
235
+ import * as Match from 'effect/Match';
236
+
237
+ const Chat = Effect.gen(function* () {
238
+ const history = yield* SubscriptionRef.make(Prompt.empty);
239
+ const semaphore = yield* Semaphore.make(1);
240
+
241
+ const streamText = (prompt: string) =>
242
+ Stream.fromChannel(
243
+ Channel.acquireUseRelease(
244
+ // Acquire
245
+ semaphore.take(1).pipe(
246
+ Effect.zipRight(SubscriptionRef.get(history)),
247
+ Effect.map((hist) =>
248
+ Prompt.concat(hist, Prompt.make(prompt))
249
+ ),
250
+ Effect.tap((checkpoint) => {
251
+ combined = Prompt.empty;
252
+ return SubscriptionRef.set(history, checkpoint);
253
+ })
254
+ ),
255
+
256
+ // Use
257
+ (checkpoint) => {
258
+ let combined = Prompt.empty;
259
+ const accumulated: Array<Response.StreamPart> = [];
260
+
261
+ return LanguageModel.streamText({
262
+ prompt: checkpoint
263
+ }).pipe(
264
+ Stream.mapChunksEffect(
265
+ Effect.fnUntraced(function* (chunk) {
266
+ const parts = Array.from(chunk);
267
+ accumulated.push(...parts);
268
+
269
+ combined = Prompt.fromResponseParts(accumulated);
270
+
271
+ yield* SubscriptionRef.set(
272
+ history,
273
+ Prompt.concat(checkpoint, combined)
274
+ );
275
+
276
+ return chunk;
277
+ })
278
+ ),
279
+ Stream.toChannel
280
+ );
281
+ },
282
+
283
+ // Release
284
+ () => semaphore.release(1)
285
+ )
286
+ );
287
+
288
+ return { streamText };
289
+ });
290
+
291
+ // Consume stream
292
+ chat.streamText('Hello').pipe(
293
+ Stream.runForEach((part) =>
294
+ Match.value(part).pipe(
295
+ Match.when({ type: 'text-delta' }, ({ delta }) =>
296
+ Effect.sync(() => console.log(delta))
297
+ ),
298
+ Match.when({ type: 'finish' }, ({ usage }) =>
299
+ Effect.sync(() => console.log(usage))
300
+ ),
301
+ Match.orElse(() => Effect.void)
302
+ )
303
+ )
304
+ );
305
+ ```
306
+
307
+ ## Anti-Patterns
308
+
309
+ ```typescript
310
+ // ❌ Avoid Effect.either for pattern matching
311
+ Effect.either(effect).pipe(
312
+ Effect.map((result) => result._tag === "Left" ? ... : ...)
313
+ )
314
+
315
+ // ✓ Use Effect.match
316
+ effect.pipe(
317
+ Effect.match({
318
+ onFailure: (error) => ...,
319
+ onSuccess: (value) => ...
320
+ })
321
+ )
322
+
323
+ // ❌ Using Match.tag on stream parts (stream parts use `type`, not `_tag`)
324
+ Match.value(part).pipe(Match.tag("text-delta", handler))
325
+
326
+ // ✓ Use Match.when with type checks (stream parts have `type` field, not `_tag`)
327
+ Match.value(part).pipe(Match.when({ type: "text-delta" }, handler))
328
+
329
+ // ✓ Direct type checks are also correct
330
+ if (part.type === "text-delta") { handler(part) }
331
+
332
+ // ❌ Accumulating in Stream.map (loses effects)
333
+ Stream.map((chunk) => {
334
+ accumulated.push(...chunk) // side effect ignored
335
+ return chunk
336
+ })
337
+
338
+ // ✓ Use Stream.mapChunksEffect
339
+ Stream.mapChunksEffect(Effect.fnUntraced(function* (chunk) {
340
+ accumulated.push(...chunk)
341
+ yield* updateHistory()
342
+ return chunk
343
+ }))
344
+ ```
345
+
346
+ ## Additional Stream Part Types
347
+
348
+ ### File Parts
349
+
350
+ ```typescript
351
+ { type: "file", mediaType: "image/png", data: Uint8Array }
352
+ ```
353
+
354
+ ### Source Parts
355
+
356
+ ```typescript
357
+ { type: "document-source", id: string, title?: string }
358
+ { type: "url-source", url: string, title?: string }
359
+ ```
360
+
361
+ ### Metadata Parts
362
+
363
+ ```typescript
364
+ { type: "response-metadata", id: string, modelId: string, timestamp: Date }
365
+ ```
366
+
367
+ ### Error Parts
368
+
369
+ ```typescript
370
+ { type: "error", error: AiError }
371
+ // Handle with:
372
+ Match.when({ type: "error" }, ({ error }) => Effect.fail(error))
373
+ ```
374
+
375
+ ## ExecutionPlan Streaming Nuances
376
+
377
+ `Stream.withExecutionPlan(plan, { onEvent })` emits the same ordered `AttemptStart` / `AttemptSuccess` / `AttemptFailure` lifecycle as the Effect combinator. A downstream consumer that stops pulling early reports `AttemptSuccess`, because the consumer ended the attempt rather than the source failing.
378
+
379
+ Set `preventFallbackOnPartialStream: true` when a provider failure after emitted chunks must fail the stream rather than append fallback-provider output to the partial response. Lifecycle observer defects are ignored and cannot change stream attempt outcomes.
380
+
381
+ ## Quality Checklist
382
+
383
+ - [ ] Use start/delta/end protocol for streaming content
384
+ - [ ] Match stream parts with `Match.when({ type: ... })` or direct `part.type` checks (NOT `Match.tag` — parts use `type`, not `_tag`)
385
+ - [ ] Accumulate using Stream.mapChunksEffect (not Stream.map)
386
+ - [ ] Use SubscriptionRef for reactive history updates
387
+ - [ ] Protect concurrent streams with Semaphore
388
+ - [ ] Use Channel.acquireUseRelease for resource safety
389
+ - [ ] Handle error parts appropriately
390
+ - [ ] Checkpoint history before streaming
391
+
392
+ ## Related Skills
393
+
394
+ - effect-ai-language-model - streamText method that produces these streams
395
+ - effect-ai-prompt - Converting stream responses to history with fromResponseParts
396
+ - effect-ai-tool - Tool call streaming parts
397
+ - effect-ai-provider - Provider-specific streaming behavior
398
+
399
+ ## Reference
400
+
401
+ StreamPart types:
402
+
403
+ - `text-start`, `text-delta`, `text-end` - Text content streaming
404
+ - `reasoning-start`, `reasoning-delta`, `reasoning-end` - Chain-of-thought streaming
405
+ - `tool-params-start`, `tool-params-delta`, `tool-params-end` - Tool parameter streaming
406
+ - `tool-call` - Complete tool invocation (non-streaming)
407
+ - `tool-result` - Tool execution result
408
+ - `finish` - Stream completion with usage stats
409
+ - `error` - Error part
410
+
411
+ Key modules:
412
+
413
+ - `effect/unstable/ai/Response` - Response part schemas and constructors
414
+ - `effect/unstable/ai/Prompt` - Prompt construction and merging
415
+ - `effect/Stream` - Stream combinators (`mapChunksEffect`, `runForEach`, `runDrain`)
416
+ - `effect/Channel` - Low-level resource management (`acquireUseRelease`)
417
+ - `effect/SubscriptionRef` - Reactive shared state
418
+ - `effect/Match` - Pattern matching (use `Match.when({ type: ... })` for stream parts)