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.
- package/LICENSE +21 -0
- package/README.md +278 -0
- package/guidance/effect-first-development.md +1247 -0
- package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
- package/guidance/post__parse-dont-validate.md +109 -0
- package/guidance/progressive-disclosure-guidance.md +38 -0
- package/package.json +63 -0
- package/patterns/avoid-any.md +37 -0
- package/patterns/avoid-data-tagged-error.md +34 -0
- package/patterns/avoid-direct-json.md +51 -0
- package/patterns/avoid-direct-tag-checks.md +54 -0
- package/patterns/avoid-expect-in-if.md +52 -0
- package/patterns/avoid-mutable-state.md +70 -0
- package/patterns/avoid-native-fetch.md +61 -0
- package/patterns/avoid-node-imports.md +86 -0
- package/patterns/avoid-non-null-assertion.md +44 -0
- package/patterns/avoid-object-type.md +46 -0
- package/patterns/avoid-option-getorthrow.md +39 -0
- package/patterns/avoid-platform-coupling.md +43 -0
- package/patterns/avoid-process-env.md +43 -0
- package/patterns/avoid-react-hooks.md +73 -0
- package/patterns/avoid-schema-suffix.md +45 -0
- package/patterns/avoid-sync-fs.md +68 -0
- package/patterns/avoid-try-catch.md +47 -0
- package/patterns/avoid-ts-ignore.md +38 -0
- package/patterns/avoid-untagged-errors.md +67 -0
- package/patterns/avoid-yield-ref.md +46 -0
- package/patterns/casting-awareness.md +46 -0
- package/patterns/context-tag-extends.md +84 -0
- package/patterns/effect-catchall-default.md +61 -0
- package/patterns/effect-promise-vs-trypromise.md +47 -0
- package/patterns/effect-run-in-body.md +58 -0
- package/patterns/imperative-loops.md +76 -0
- package/patterns/prefer-arr-sort.md +52 -0
- package/patterns/prefer-duration-values.md +56 -0
- package/patterns/prefer-effect-fn.md +161 -0
- package/patterns/prefer-match-over-switch.md +48 -0
- package/patterns/prefer-option-over-null.md +56 -0
- package/patterns/prefer-redacted-config.md +70 -0
- package/patterns/prefer-schema-class.md +54 -0
- package/patterns/require-effect-concurrency.md +83 -0
- package/patterns/stream-large-files.md +63 -0
- package/patterns/throw-in-effect-gen.md +62 -0
- package/patterns/use-clock-service.md +45 -0
- package/patterns/use-command-executor-service.md +54 -0
- package/patterns/use-console-service.md +54 -0
- package/patterns/use-filesystem-service.md +59 -0
- package/patterns/use-http-client-service.md +77 -0
- package/patterns/use-path-service.md +53 -0
- package/patterns/use-random-service.md +45 -0
- package/patterns/use-temp-file-scoped.md +66 -0
- package/patterns/vm-in-wrong-file.md +51 -0
- package/patterns/yield-in-for-loop.md +61 -0
- package/skills/effect-ai-chat/SKILL.md +472 -0
- package/skills/effect-ai-language-model/SKILL.md +652 -0
- package/skills/effect-ai-prompt/SKILL.md +752 -0
- package/skills/effect-ai-provider/SKILL.md +668 -0
- package/skills/effect-ai-streaming/SKILL.md +418 -0
- package/skills/effect-ai-tool/SKILL.md +1132 -0
- package/skills/effect-atom-rpc/SKILL.md +488 -0
- package/skills/effect-atom-state/SKILL.md +640 -0
- package/skills/effect-batching/SKILL.md +614 -0
- package/skills/effect-cache/SKILL.md +570 -0
- package/skills/effect-cli/SKILL.md +523 -0
- package/skills/effect-command-executor/SKILL.md +675 -0
- package/skills/effect-concurrency-testing/SKILL.md +612 -0
- package/skills/effect-config/SKILL.md +580 -0
- package/skills/effect-context-witness/SKILL.md +274 -0
- package/skills/effect-domain-modeling/SKILL.md +1212 -0
- package/skills/effect-domain-predicates/SKILL.md +867 -0
- package/skills/effect-error-handling/SKILL.md +1581 -0
- package/skills/effect-fiber/SKILL.md +731 -0
- package/skills/effect-filesystem/SKILL.md +624 -0
- package/skills/effect-graph/SKILL.md +571 -0
- package/skills/effect-http-api/SKILL.md +1760 -0
- package/skills/effect-http-client/SKILL.md +989 -0
- package/skills/effect-http-server/SKILL.md +920 -0
- package/skills/effect-incremental-migration/SKILL.md +362 -0
- package/skills/effect-layer-design/SKILL.md +642 -0
- package/skills/effect-managed-runtime/SKILL.md +395 -0
- package/skills/effect-mcp-server/SKILL.md +608 -0
- package/skills/effect-observability/SKILL.md +719 -0
- package/skills/effect-optics/SKILL.md +554 -0
- package/skills/effect-parallelization/SKILL.md +668 -0
- package/skills/effect-path/SKILL.md +296 -0
- package/skills/effect-pattern-matching/SKILL.md +914 -0
- package/skills/effect-platform-abstraction/SKILL.md +1175 -0
- package/skills/effect-platform-layers/SKILL.md +514 -0
- package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
- package/skills/effect-react-composition/SKILL.md +986 -0
- package/skills/effect-react-vm/SKILL.md +675 -0
- package/skills/effect-rpc-api/SKILL.md +624 -0
- package/skills/effect-rpc-client/SKILL.md +666 -0
- package/skills/effect-rpc-cluster/SKILL.md +1623 -0
- package/skills/effect-rpc-server/SKILL.md +767 -0
- package/skills/effect-scheduling/SKILL.md +124 -0
- package/skills/effect-schema-composition/SKILL.md +975 -0
- package/skills/effect-schema-v4/SKILL.md +691 -0
- package/skills/effect-scope/SKILL.md +682 -0
- package/skills/effect-service-implementation/SKILL.md +656 -0
- package/skills/effect-socket/SKILL.md +703 -0
- package/skills/effect-sql/SKILL.md +781 -0
- package/skills/effect-stream/SKILL.md +765 -0
- package/skills/effect-testing/SKILL.md +1331 -0
- package/skills/effect-typeclass-design/SKILL.md +161 -0
- package/skills/effect-wide-events/Article.md +66 -0
- package/skills/effect-wide-events/SKILL.md +95 -0
- package/skills/effect-workflow/SKILL.md +810 -0
- package/src/agent-policy.ts +22 -0
- package/src/enforcer.ts +104 -0
- package/src/frontmatter.ts +34 -0
- package/src/guidance.ts +66 -0
- package/src/index.ts +38 -0
- package/src/pattern-catalog.ts +115 -0
- package/src/pattern-matcher.ts +178 -0
- package/src/pattern.ts +97 -0
- package/src/skills.ts +29 -0
- 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)
|