@effect-uai/core 0.2.0 → 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.
- package/README.md +1 -1
- package/dist/{AiError-CqmYjXyx.d.mts → AiError-csR8Bhxx.d.mts} +26 -4
- package/dist/{AiError-CqmYjXyx.d.mts.map → AiError-csR8Bhxx.d.mts.map} +1 -1
- package/dist/Audio-BfCTGnH3.d.mts +61 -0
- package/dist/Audio-BfCTGnH3.d.mts.map +1 -0
- package/dist/Image-DxyXqzAM.d.mts +61 -0
- package/dist/Image-DxyXqzAM.d.mts.map +1 -0
- package/dist/{Items-D1C2686t.d.mts → Items-Hg5AsYxl.d.mts} +132 -80
- package/dist/Items-Hg5AsYxl.d.mts.map +1 -0
- package/dist/Media-D_CpcM1Z.d.mts +57 -0
- package/dist/Media-D_CpcM1Z.d.mts.map +1 -0
- package/dist/{StructuredFormat-B5ueioNr.d.mts → StructuredFormat-Cl41C56K.d.mts} +5 -5
- package/dist/StructuredFormat-Cl41C56K.d.mts.map +1 -0
- package/dist/{Tool-5wxOCuOh.d.mts → Tool-B8B5qVEy.d.mts} +13 -13
- package/dist/Tool-B8B5qVEy.d.mts.map +1 -0
- package/dist/{Turn-Bi83du4I.d.mts → Turn-7geUcKsf.d.mts} +5 -11
- package/dist/Turn-7geUcKsf.d.mts.map +1 -0
- package/dist/{chunk-CfYAbeIz.mjs → chunk-uyGKjUfl.mjs} +2 -1
- package/dist/dist-DV5ISja1.mjs +13782 -0
- package/dist/dist-DV5ISja1.mjs.map +1 -0
- package/dist/domain/AiError.d.mts +2 -2
- package/dist/domain/AiError.mjs +19 -3
- package/dist/domain/AiError.mjs.map +1 -1
- package/dist/domain/Audio.d.mts +2 -0
- package/dist/domain/Audio.mjs +14 -0
- package/dist/domain/Audio.mjs.map +1 -0
- package/dist/domain/Image.d.mts +2 -0
- package/dist/domain/Image.mjs +58 -0
- package/dist/domain/Image.mjs.map +1 -0
- package/dist/domain/Items.d.mts +2 -2
- package/dist/domain/Items.mjs +19 -42
- package/dist/domain/Items.mjs.map +1 -1
- package/dist/domain/Media.d.mts +2 -0
- package/dist/domain/Media.mjs +14 -0
- package/dist/domain/Media.mjs.map +1 -0
- package/dist/domain/Music.d.mts +116 -0
- package/dist/domain/Music.d.mts.map +1 -0
- package/dist/domain/Music.mjs +29 -0
- package/dist/domain/Music.mjs.map +1 -0
- package/dist/domain/Transcript.d.mts +95 -0
- package/dist/domain/Transcript.d.mts.map +1 -0
- package/dist/domain/Transcript.mjs +22 -0
- package/dist/domain/Transcript.mjs.map +1 -0
- package/dist/domain/Turn.d.mts +1 -1
- package/dist/domain/Turn.mjs +1 -1
- package/dist/embedding-model/Embedding.d.mts +107 -0
- package/dist/embedding-model/Embedding.d.mts.map +1 -0
- package/dist/embedding-model/Embedding.mjs +18 -0
- package/dist/embedding-model/Embedding.mjs.map +1 -0
- package/dist/embedding-model/EmbeddingModel.d.mts +97 -0
- package/dist/embedding-model/EmbeddingModel.d.mts.map +1 -0
- package/dist/embedding-model/EmbeddingModel.mjs +17 -0
- package/dist/embedding-model/EmbeddingModel.mjs.map +1 -0
- package/dist/index.d.mts +21 -7
- package/dist/index.mjs +16 -2
- package/dist/language-model/LanguageModel.d.mts +12 -20
- package/dist/language-model/LanguageModel.d.mts.map +1 -1
- package/dist/language-model/LanguageModel.mjs +3 -20
- package/dist/language-model/LanguageModel.mjs.map +1 -1
- package/dist/loop/Loop.d.mts +31 -7
- package/dist/loop/Loop.d.mts.map +1 -1
- package/dist/loop/Loop.mjs +39 -6
- package/dist/loop/Loop.mjs.map +1 -1
- package/dist/loop/Loop.test.d.mts +1 -0
- package/dist/loop/Loop.test.mjs +411 -0
- package/dist/loop/Loop.test.mjs.map +1 -0
- package/dist/magic-string.es-BgIV5Mu3.mjs +1013 -0
- package/dist/magic-string.es-BgIV5Mu3.mjs.map +1 -0
- package/dist/math/Vector.d.mts +47 -0
- package/dist/math/Vector.d.mts.map +1 -0
- package/dist/math/Vector.mjs +117 -0
- package/dist/math/Vector.mjs.map +1 -0
- package/dist/music-generator/MusicGenerator.d.mts +77 -0
- package/dist/music-generator/MusicGenerator.d.mts.map +1 -0
- package/dist/music-generator/MusicGenerator.mjs +51 -0
- package/dist/music-generator/MusicGenerator.mjs.map +1 -0
- package/dist/music-generator/MusicGenerator.test.d.mts +1 -0
- package/dist/music-generator/MusicGenerator.test.mjs +154 -0
- package/dist/music-generator/MusicGenerator.test.mjs.map +1 -0
- package/dist/observability/Metrics.d.mts +2 -2
- package/dist/observability/Metrics.d.mts.map +1 -1
- package/dist/observability/Metrics.mjs +1 -1
- package/dist/observability/Metrics.mjs.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +96 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts.map +1 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs +48 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.test.d.mts +1 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +112 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs.map +1 -0
- package/dist/streaming/JSONL.d.mts +10 -3
- package/dist/streaming/JSONL.d.mts.map +1 -1
- package/dist/streaming/JSONL.mjs +13 -2
- package/dist/streaming/JSONL.mjs.map +1 -1
- package/dist/streaming/JSONL.test.d.mts +1 -0
- package/dist/streaming/JSONL.test.mjs +70 -0
- package/dist/streaming/JSONL.test.mjs.map +1 -0
- package/dist/streaming/Lines.mjs +1 -1
- package/dist/streaming/SSE.d.mts +2 -2
- package/dist/streaming/SSE.d.mts.map +1 -1
- package/dist/streaming/SSE.mjs +1 -1
- package/dist/streaming/SSE.mjs.map +1 -1
- package/dist/streaming/SSE.test.d.mts +1 -0
- package/dist/streaming/SSE.test.mjs +72 -0
- package/dist/streaming/SSE.test.mjs.map +1 -0
- package/dist/structured-format/StructuredFormat.d.mts +1 -1
- package/dist/structured-format/StructuredFormat.mjs +1 -1
- package/dist/structured-format/StructuredFormat.mjs.map +1 -1
- package/dist/testing/MockMusicGenerator.d.mts +39 -0
- package/dist/testing/MockMusicGenerator.d.mts.map +1 -0
- package/dist/testing/MockMusicGenerator.mjs +96 -0
- package/dist/testing/MockMusicGenerator.mjs.map +1 -0
- package/dist/testing/MockProvider.d.mts +6 -6
- package/dist/testing/MockProvider.d.mts.map +1 -1
- package/dist/testing/MockProvider.mjs.map +1 -1
- package/dist/testing/MockSpeechSynthesizer.d.mts +37 -0
- package/dist/testing/MockSpeechSynthesizer.d.mts.map +1 -0
- package/dist/testing/MockSpeechSynthesizer.mjs +95 -0
- package/dist/testing/MockSpeechSynthesizer.mjs.map +1 -0
- package/dist/testing/MockTranscriber.d.mts +37 -0
- package/dist/testing/MockTranscriber.d.mts.map +1 -0
- package/dist/testing/MockTranscriber.mjs +77 -0
- package/dist/testing/MockTranscriber.mjs.map +1 -0
- package/dist/tool/HistoryCheck.d.mts +6 -3
- package/dist/tool/HistoryCheck.d.mts.map +1 -1
- package/dist/tool/HistoryCheck.mjs +7 -1
- package/dist/tool/HistoryCheck.mjs.map +1 -1
- package/dist/tool/Outcome.d.mts +138 -2
- package/dist/tool/Outcome.d.mts.map +1 -0
- package/dist/tool/Outcome.mjs +32 -10
- package/dist/tool/Outcome.mjs.map +1 -1
- package/dist/tool/Resolvers.d.mts +11 -8
- package/dist/tool/Resolvers.d.mts.map +1 -1
- package/dist/tool/Resolvers.mjs +10 -1
- package/dist/tool/Resolvers.mjs.map +1 -1
- package/dist/tool/Resolvers.test.d.mts +1 -0
- package/dist/tool/Resolvers.test.mjs +317 -0
- package/dist/tool/Resolvers.test.mjs.map +1 -0
- package/dist/tool/Tool.d.mts +1 -1
- package/dist/tool/Tool.mjs +1 -1
- package/dist/tool/Tool.mjs.map +1 -1
- package/dist/tool/ToolEvent.d.mts +151 -2
- package/dist/tool/ToolEvent.d.mts.map +1 -0
- package/dist/tool/ToolEvent.mjs +30 -4
- package/dist/tool/ToolEvent.mjs.map +1 -1
- package/dist/tool/Toolkit.d.mts +19 -10
- package/dist/tool/Toolkit.d.mts.map +1 -1
- package/dist/tool/Toolkit.mjs +5 -5
- package/dist/tool/Toolkit.mjs.map +1 -1
- package/dist/tool/Toolkit.test.d.mts +1 -0
- package/dist/tool/Toolkit.test.mjs +113 -0
- package/dist/tool/Toolkit.test.mjs.map +1 -0
- package/dist/transcriber/Transcriber.d.mts +101 -0
- package/dist/transcriber/Transcriber.d.mts.map +1 -0
- package/dist/transcriber/Transcriber.mjs +49 -0
- package/dist/transcriber/Transcriber.mjs.map +1 -0
- package/dist/transcriber/Transcriber.test.d.mts +1 -0
- package/dist/transcriber/Transcriber.test.mjs +130 -0
- package/dist/transcriber/Transcriber.test.mjs.map +1 -0
- package/package.json +65 -13
- package/src/domain/AiError.ts +21 -0
- package/src/domain/Audio.ts +88 -0
- package/src/domain/Image.ts +75 -0
- package/src/domain/Items.ts +18 -47
- package/src/domain/Media.ts +61 -0
- package/src/domain/Music.ts +121 -0
- package/src/domain/Transcript.ts +83 -0
- package/src/embedding-model/Embedding.ts +117 -0
- package/src/embedding-model/EmbeddingModel.ts +107 -0
- package/src/index.ts +15 -1
- package/src/language-model/LanguageModel.ts +2 -22
- package/src/loop/Loop.test.ts +114 -2
- package/src/loop/Loop.ts +69 -5
- package/src/math/Vector.ts +138 -0
- package/src/music-generator/MusicGenerator.test.ts +170 -0
- package/src/music-generator/MusicGenerator.ts +123 -0
- package/src/observability/Metrics.ts +1 -1
- package/src/speech-synthesizer/SpeechSynthesizer.test.ts +141 -0
- package/src/speech-synthesizer/SpeechSynthesizer.ts +131 -0
- package/src/streaming/JSONL.ts +12 -0
- package/src/streaming/SSE.ts +1 -1
- package/src/structured-format/StructuredFormat.ts +2 -2
- package/src/testing/MockMusicGenerator.ts +170 -0
- package/src/testing/MockProvider.ts +2 -2
- package/src/testing/MockSpeechSynthesizer.ts +165 -0
- package/src/testing/MockTranscriber.ts +139 -0
- package/src/tool/HistoryCheck.ts +2 -5
- package/src/tool/Outcome.ts +36 -36
- package/src/tool/Resolvers.test.ts +11 -35
- package/src/tool/Resolvers.ts +5 -14
- package/src/tool/Tool.ts +9 -9
- package/src/tool/ToolEvent.ts +28 -24
- package/src/tool/Toolkit.test.ts +97 -2
- package/src/tool/Toolkit.ts +57 -33
- package/src/transcriber/Transcriber.test.ts +125 -0
- package/src/transcriber/Transcriber.ts +127 -0
- package/dist/Items-D1C2686t.d.mts.map +0 -1
- package/dist/Outcome-GiaNvt7i.d.mts +0 -32
- package/dist/Outcome-GiaNvt7i.d.mts.map +0 -1
- package/dist/StructuredFormat-B5ueioNr.d.mts.map +0 -1
- package/dist/Tool-5wxOCuOh.d.mts.map +0 -1
- package/dist/ToolEvent-wTMgb2GO.d.mts +0 -29
- package/dist/ToolEvent-wTMgb2GO.d.mts.map +0 -1
- package/dist/Turn-Bi83du4I.d.mts.map +0 -1
- package/dist/match/Match.d.mts +0 -16
- package/dist/match/Match.d.mts.map +0 -1
- package/dist/match/Match.mjs +0 -15
- package/dist/match/Match.mjs.map +0 -1
- package/src/match/Match.ts +0 -9
package/dist/loop/Loop.d.mts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { l as IncompleteTurn } from "../AiError-
|
|
2
|
-
import { i as TurnEvent, r as Turn } from "../Turn-
|
|
3
|
-
import { Data, Effect, Stream } from "effect";
|
|
1
|
+
import { l as IncompleteTurn } from "../AiError-csR8Bhxx.mjs";
|
|
2
|
+
import { i as TurnEvent, r as Turn } from "../Turn-7geUcKsf.mjs";
|
|
3
|
+
import { Data, Effect, Stream, SubscriptionRef } from "effect";
|
|
4
4
|
|
|
5
5
|
//#region src/loop/Loop.d.ts
|
|
6
6
|
declare namespace Loop_d_exports {
|
|
7
|
-
export { Event, loop, next, nextAfter, nextAfterFold, stop, stopAfter, stopEvent,
|
|
7
|
+
export { Event, loop, loopWithState, next, nextAfter, nextAfterFold, onTurnComplete, stop, stopAfter, stopEvent, value };
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
10
|
* The tagged union a body emits per pull. `Value` carries a payload that
|
|
@@ -48,7 +48,7 @@ declare const stopAfter: <A, E, R>(stream: Stream.Stream<A, E, R>) => Stream.Str
|
|
|
48
48
|
* into an accumulator, and at end-of-stream emit one `next(build(finalAcc))`.
|
|
49
49
|
*
|
|
50
50
|
* Subsumes `nextAfter` when state is constant (`reduce: (s, _) => s`,
|
|
51
|
-
* `build: (s) => s`). Used by `Toolkit.
|
|
51
|
+
* `build: (s) => s`). Used by `Toolkit.continueWith` to collect tool
|
|
52
52
|
* results and build next state without exposing a Ref to recipes.
|
|
53
53
|
*/
|
|
54
54
|
declare const nextAfterFold: <A, B, S, E, R>(stream: Stream.Stream<A, E, R>, initial: B, reduce: (acc: B, a: A) => B, build: (b: B) => S) => Stream.Stream<Event<A, S>, E, R>;
|
|
@@ -67,7 +67,7 @@ declare const nextAfterFold: <A, B, S, E, R>(stream: Stream.Stream<A, E, R>, ini
|
|
|
67
67
|
* fails with `AiError.IncompleteTurn`. Catch it via `Stream.catchTag` if
|
|
68
68
|
* you want to recover.
|
|
69
69
|
*/
|
|
70
|
-
declare const
|
|
70
|
+
declare const onTurnComplete: <S, A, E2 = never, R2 = never>(then: (turn: Turn) => Effect.Effect<Stream.Stream<Event<A, S>, E2, R2>, E2, R2>) => <E, R>(deltas: Stream.Stream<TurnEvent, E, R>) => Stream.Stream<Event<TurnEvent | A, S>, E | E2 | IncompleteTurn, R | R2>;
|
|
71
71
|
type LoopBody<S, A, E, R> = (state: S) => Stream.Stream<Event<A, S>, E, R> | Effect.Effect<Stream.Stream<Event<A, S>, E, R>, E, R>;
|
|
72
72
|
/**
|
|
73
73
|
* Drive a state-threaded loop body. Each iteration runs `body(state)` to get
|
|
@@ -82,6 +82,30 @@ declare const loop: {
|
|
|
82
82
|
<S, A, E, R>(body: LoopBody<S, A, E, R>): (initial: S) => Stream.Stream<A, E, R>;
|
|
83
83
|
<S, A, E, R>(initial: S, body: LoopBody<S, A, E, R>): Stream.Stream<A, E, R>;
|
|
84
84
|
};
|
|
85
|
+
/**
|
|
86
|
+
* Like `loop`, but exposes the current loop state as a `SubscriptionRef`
|
|
87
|
+
* alongside the value stream.
|
|
88
|
+
*
|
|
89
|
+
* Allocates one `SubscriptionRef<S>` seeded with `initial`, then runs the
|
|
90
|
+
* loop with a wrapped body that taps every `Next(s)` event into the ref
|
|
91
|
+
* before forwarding it. The caller decides how to consume both channels:
|
|
92
|
+
*
|
|
93
|
+
* - **Final state**: drain the stream, then `SubscriptionRef.get(state)`
|
|
94
|
+
* - the ref holds the state from the last `Next` (or `initial` if the
|
|
95
|
+
* loop ended without advancing).
|
|
96
|
+
* - **Live transitions**: `SubscriptionRef.changes(state)` is a
|
|
97
|
+
* `Stream<S>` of every state observed; subscribe alongside the value
|
|
98
|
+
* stream.
|
|
99
|
+
* - **Mid-iteration peek**: `SubscriptionRef.get(state)` at any time.
|
|
100
|
+
*
|
|
101
|
+
* The returned stream and ref are independent of each other - the ref
|
|
102
|
+
* lives outside the stream's scope, so reading it after the stream
|
|
103
|
+
* completes is safe.
|
|
104
|
+
*/
|
|
105
|
+
declare const loopWithState: <S, A, E, R>(initial: S, body: LoopBody<S, A, E, R>) => Effect.Effect<{
|
|
106
|
+
readonly stream: Stream.Stream<A, E, R>;
|
|
107
|
+
readonly state: SubscriptionRef.SubscriptionRef<S>;
|
|
108
|
+
}>;
|
|
85
109
|
//#endregion
|
|
86
|
-
export { Event, loop, next, nextAfter, nextAfterFold, stop, stopAfter, stopEvent,
|
|
110
|
+
export { Event, loop, loopWithState, next, nextAfter, nextAfterFold, onTurnComplete, stop, stopAfter, stopEvent, Loop_d_exports as t, value };
|
|
87
111
|
//# sourceMappingURL=Loop.d.mts.map
|
package/dist/loop/Loop.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Loop.d.mts","names":[],"sources":["../../src/loop/Loop.ts"],"mappings":";;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"Loop.d.mts","names":[],"sources":["../../src/loop/Loop.ts"],"mappings":"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"}
|
package/dist/loop/Loop.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as __exportAll } from "../chunk-uyGKjUfl.mjs";
|
|
2
2
|
import { IncompleteTurn } from "../domain/AiError.mjs";
|
|
3
3
|
import { isTurnComplete } from "../domain/Turn.mjs";
|
|
4
|
-
import { Cause, Channel, Data, Effect, Exit, Function, Option, Ref, Scope, Stream } from "effect";
|
|
4
|
+
import { Cause, Channel, Data, Effect, Exit, Function, Option, Ref, Scope, Stream, SubscriptionRef } from "effect";
|
|
5
5
|
//#region src/loop/Loop.ts
|
|
6
6
|
/**
|
|
7
7
|
* Pull-based `loop` for state-threaded sub-streams.
|
|
@@ -24,13 +24,14 @@ import { Cause, Channel, Data, Effect, Exit, Function, Option, Ref, Scope, Strea
|
|
|
24
24
|
*/
|
|
25
25
|
var Loop_exports = /* @__PURE__ */ __exportAll({
|
|
26
26
|
loop: () => loop,
|
|
27
|
+
loopWithState: () => loopWithState,
|
|
27
28
|
next: () => next,
|
|
28
29
|
nextAfter: () => nextAfter,
|
|
29
30
|
nextAfterFold: () => nextAfterFold,
|
|
31
|
+
onTurnComplete: () => onTurnComplete,
|
|
30
32
|
stop: () => stop,
|
|
31
33
|
stopAfter: () => stopAfter,
|
|
32
34
|
stopEvent: () => stopEvent,
|
|
33
|
-
streamUntilComplete: () => streamUntilComplete,
|
|
34
35
|
value: () => value
|
|
35
36
|
});
|
|
36
37
|
const Event = Data.taggedEnum();
|
|
@@ -62,7 +63,7 @@ const stopAfter = (stream) => Stream.concat(Stream.map(stream, value), Stream.fr
|
|
|
62
63
|
* into an accumulator, and at end-of-stream emit one `next(build(finalAcc))`.
|
|
63
64
|
*
|
|
64
65
|
* Subsumes `nextAfter` when state is constant (`reduce: (s, _) => s`,
|
|
65
|
-
* `build: (s) => s`). Used by `Toolkit.
|
|
66
|
+
* `build: (s) => s`). Used by `Toolkit.continueWith` to collect tool
|
|
66
67
|
* results and build next state without exposing a Ref to recipes.
|
|
67
68
|
*/
|
|
68
69
|
const nextAfterFold = (stream, initial, reduce, build) => Stream.unwrap(Effect.gen(function* () {
|
|
@@ -86,7 +87,7 @@ const nextAfterFold = (stream, initial, reduce, build) => Stream.unwrap(Effect.g
|
|
|
86
87
|
* fails with `AiError.IncompleteTurn`. Catch it via `Stream.catchTag` if
|
|
87
88
|
* you want to recover.
|
|
88
89
|
*/
|
|
89
|
-
const
|
|
90
|
+
const onTurnComplete = (then) => (deltas) => Stream.unwrap(Effect.gen(function* () {
|
|
90
91
|
const turnRef = yield* Ref.make(Option.none());
|
|
91
92
|
const events = deltas.pipe(Stream.tap((delta) => isTurnComplete(delta) ? Ref.set(turnRef, Option.some(delta.turn)) : Effect.void), Stream.map(value));
|
|
92
93
|
const continuation = Stream.unwrap(Effect.gen(function* () {
|
|
@@ -166,7 +167,39 @@ const loop = Function.dual(2, (initial, body) => Stream.scoped(Stream.fromPull(E
|
|
|
166
167
|
}
|
|
167
168
|
});
|
|
168
169
|
}))));
|
|
170
|
+
/**
|
|
171
|
+
* Like `loop`, but exposes the current loop state as a `SubscriptionRef`
|
|
172
|
+
* alongside the value stream.
|
|
173
|
+
*
|
|
174
|
+
* Allocates one `SubscriptionRef<S>` seeded with `initial`, then runs the
|
|
175
|
+
* loop with a wrapped body that taps every `Next(s)` event into the ref
|
|
176
|
+
* before forwarding it. The caller decides how to consume both channels:
|
|
177
|
+
*
|
|
178
|
+
* - **Final state**: drain the stream, then `SubscriptionRef.get(state)`
|
|
179
|
+
* - the ref holds the state from the last `Next` (or `initial` if the
|
|
180
|
+
* loop ended without advancing).
|
|
181
|
+
* - **Live transitions**: `SubscriptionRef.changes(state)` is a
|
|
182
|
+
* `Stream<S>` of every state observed; subscribe alongside the value
|
|
183
|
+
* stream.
|
|
184
|
+
* - **Mid-iteration peek**: `SubscriptionRef.get(state)` at any time.
|
|
185
|
+
*
|
|
186
|
+
* The returned stream and ref are independent of each other - the ref
|
|
187
|
+
* lives outside the stream's scope, so reading it after the stream
|
|
188
|
+
* completes is safe.
|
|
189
|
+
*/
|
|
190
|
+
const loopWithState = (initial, body) => Effect.gen(function* () {
|
|
191
|
+
const stateRef = yield* SubscriptionRef.make(initial);
|
|
192
|
+
const tap = (stream) => stream.pipe(Stream.tap((event) => event._tag === "Next" ? SubscriptionRef.set(stateRef, event.state) : Effect.void));
|
|
193
|
+
const wrappedBody = (s) => {
|
|
194
|
+
const result = body(s);
|
|
195
|
+
return Effect.isEffect(result) ? Effect.map(result, tap) : tap(result);
|
|
196
|
+
};
|
|
197
|
+
return {
|
|
198
|
+
stream: loop(initial, wrappedBody),
|
|
199
|
+
state: stateRef
|
|
200
|
+
};
|
|
201
|
+
});
|
|
169
202
|
//#endregion
|
|
170
|
-
export { loop, next, nextAfter, nextAfterFold, stop, stopAfter, stopEvent,
|
|
203
|
+
export { loop, loopWithState, next, nextAfter, nextAfterFold, onTurnComplete, stop, stopAfter, stopEvent, Loop_exports as t, value };
|
|
171
204
|
|
|
172
205
|
//# sourceMappingURL=Loop.mjs.map
|
package/dist/loop/Loop.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Loop.mjs","names":[],"sources":["../../src/loop/Loop.ts"],"sourcesContent":["/**\n * Pull-based `loop` for state-threaded sub-streams.\n *\n * Each iteration runs a body that returns a `Stream<Event<A, S>>`. The body\n * emits values via `Loop.value(a)` and signals iteration control via\n * `Loop.next(state)` (continue with new state) or `Loop.stop` (terminate).\n * The loop unwraps `Value` events back to `A` for downstream consumers, so\n * the resulting stream is a plain `Stream<A>`.\n *\n * The next body stream is only pulled when downstream pulls the outer\n * stream - no producer fiber, no queue buffering. Cancellation, failures,\n * scoped resources, and backpressure stay aligned with normal Stream\n * semantics.\n *\n * Convention: a `Next` or `Stop` event is the terminal element of a body's\n * iteration. Values emitted in the same chunk after one are discarded\n * (their producing side effects may already have run). Prefer the\n * `Loop.nextAfter` / `Loop.stopAfter` helpers to terminate cleanly.\n */\nimport { Cause, Channel, Data, Effect, Exit, Function, Option, Ref, Scope, Stream } from \"effect\"\nimport { IncompleteTurn } from \"../domain/AiError.js\"\nimport { isTurnComplete, type Turn, type TurnEvent } from \"../domain/Turn.js\"\n\n// ---------------------------------------------------------------------------\n// Event type - the body's emit shape\n// ---------------------------------------------------------------------------\n\n/**\n * The tagged union a body emits per pull. `Value` carries a payload that\n * flows downstream. `Next` ends the current iteration and continues with a\n * new state. `Stop` ends the loop entirely.\n */\nexport type Event<A, S> = Data.TaggedEnum<{\n Value: { readonly value: A }\n Next: { readonly state: S }\n Stop: {}\n}>\n\ninterface EventDef extends Data.TaggedEnum.WithGenerics<2> {\n readonly taggedEnum: Event<this[\"A\"], this[\"B\"]>\n}\n\nconst Event = Data.taggedEnum<EventDef>()\n\n/** Wrap a value so it flows through the loop to downstream consumers. */\nexport const value = <A>(a: A): Event<A, never> => Event.Value({ value: a })\n\n/** End the current iteration and continue with a new state. */\nexport const next = <S>(state: S): Event<never, S> => Event.Next({ state })\n\n/** The terminal `Stop` event. Use `stop` (the Stream) to end a loop body. */\nexport const stopEvent: Event<never, never> = Event.Stop()\n\n/**\n * A single-element stream that ends the loop. Return this from a body when\n * there's nothing else to emit; equivalent to `stopAfter(Stream.empty)` but\n * named for the common case.\n */\nexport const stop: Stream.Stream<Event<never, never>> = Stream.succeed(stopEvent)\n\n/**\n * Pipe a raw `Stream<A>` into the loop's emit shape, then terminate the\n * iteration with `next(state)`. Common shape for \"stream this turn's\n * deltas, then continue with updated history.\"\n */\nexport const nextAfter = <S, A, E, R>(\n stream: Stream.Stream<A, E, R>,\n state: S,\n): Stream.Stream<Event<A, S>, E, R> =>\n Stream.concat(Stream.map(stream, value), Stream.fromIterable([next(state)]))\n\n/**\n * Pipe a raw `Stream<A>` into the loop's emit shape, then terminate the\n * loop. Common shape for \"stream this turn's deltas, then we're done.\"\n */\nexport const stopAfter = <A, E, R>(\n stream: Stream.Stream<A, E, R>,\n): Stream.Stream<Event<A, never>, E, R> =>\n Stream.concat(Stream.map(stream, value), Stream.fromIterable([stopEvent]))\n\n/**\n * General `nextAfter` variant: drain `stream` to the consumer, fold elements\n * into an accumulator, and at end-of-stream emit one `next(build(finalAcc))`.\n *\n * Subsumes `nextAfter` when state is constant (`reduce: (s, _) => s`,\n * `build: (s) => s`). Used by `Toolkit.nextStateFrom` to collect tool\n * results and build next state without exposing a Ref to recipes.\n */\nexport const nextAfterFold = <A, B, S, E, R>(\n stream: Stream.Stream<A, E, R>,\n initial: B,\n reduce: (acc: B, a: A) => B,\n build: (b: B) => S,\n): Stream.Stream<Event<A, S>, E, R> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const ref = yield* Ref.make(initial)\n const tapped = stream.pipe(\n Stream.tap((a) => Ref.update(ref, (acc) => reduce(acc, a))),\n Stream.map(value),\n )\n const continuation = Stream.fromEffect(\n Ref.get(ref).pipe(Effect.map((acc) => next(build(acc)))),\n )\n return tapped.pipe(Stream.concat(continuation))\n }),\n )\n\n// ---------------------------------------------------------------------------\n// streamUntilComplete - turn-aware stream operator for loop bodies\n// ---------------------------------------------------------------------------\n\n/**\n * Lift a provider's `Stream<TurnEvent>` into a loop body's `Stream<Event<TurnEvent | A, S>>`.\n * Each delta passes through as `value(delta)` (including the terminal\n * `turn_complete`, so the consumer sees turn boundaries naturally). Once\n * the terminal arrives, `then(turn)` runs and its returned stream of loop\n * events (typically tool outputs followed by `next(state)` or `stop`) is\n * concatenated.\n *\n * Pre-pipe transforms (`Stream.tap` / `Stream.map` / `Stream.filter`) on\n * the raw delta stream cover anything an `emit`-style callback would do.\n *\n * If the upstream ends without a `turn_complete`, the resulting stream\n * fails with `AiError.IncompleteTurn`. Catch it via `Stream.catchTag` if\n * you want to recover.\n */\nexport const streamUntilComplete =\n <S, A, E2 = never, R2 = never>(\n then: (turn: Turn) => Effect.Effect<Stream.Stream<Event<A, S>, E2, R2>, E2, R2>,\n ) =>\n <E, R>(\n deltas: Stream.Stream<TurnEvent, E, R>,\n ): Stream.Stream<Event<TurnEvent | A, S>, E | E2 | IncompleteTurn, R | R2> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const turnRef = yield* Ref.make<Option.Option<Turn>>(Option.none())\n\n const events: Stream.Stream<Event<TurnEvent, S>, E, R> = deltas.pipe(\n Stream.tap((delta) =>\n isTurnComplete(delta) ? Ref.set(turnRef, Option.some(delta.turn)) : Effect.void,\n ),\n Stream.map(value),\n )\n\n const continuation = Stream.unwrap(\n Effect.gen(function* () {\n const opt = yield* Ref.get(turnRef)\n if (Option.isNone(opt)) return yield* Effect.fail(new IncompleteTurn({}))\n return yield* then(opt.value)\n }),\n )\n\n return Stream.concat(events, continuation)\n }),\n )\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nconst isNonEmpty = <A>(array: ReadonlyArray<A>): array is readonly [A, ...Array<A>] =>\n array.length > 0\n\ninterface CurrentBody<S, A, E, R> {\n readonly scope: Scope.Closeable\n readonly pull: Effect.Effect<ReadonlyArray<Event<A, S>>, E | Cause.Done<void>, R>\n}\n\nconst closeBody = <S, A, E, R>(\n current: CurrentBody<S, A, E, R>,\n exit: Exit.Exit<unknown, unknown>,\n) => Scope.close(current.scope, exit)\n\n/**\n * Walk a chunk of `Event<A, S>` until a terminal `Next` or `Stop` is found.\n * Returns the unwrapped values seen so far and (optionally) the terminal\n * event. Anything in the chunk after the terminal is discarded - its\n * producing side effects may have run, but downstream never sees it.\n */\nconst partitionChunk = <A, S>(\n chunk: ReadonlyArray<Event<A, S>>,\n): { readonly values: Array<A>; readonly decision: Event<A, S> | undefined } => {\n const values: Array<A> = []\n for (let i = 0; i < chunk.length; i++) {\n const event = chunk[i]!\n if (event._tag === \"Value\") {\n values.push(event.value)\n } else {\n return { values, decision: event }\n }\n }\n return { values, decision: undefined }\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\ntype LoopBody<S, A, E, R> = (\n state: S,\n) => Stream.Stream<Event<A, S>, E, R> | Effect.Effect<Stream.Stream<Event<A, S>, E, R>, E, R>\n\n/**\n * Drive a state-threaded loop body. Each iteration runs `body(state)` to get\n * a `Stream<Event<A, S>>`; values flow downstream, `next(s)` continues with\n * a new state, `stop` ends the loop. See the file header for the full\n * pull-based execution model.\n *\n * Dual: data-first `loop(initial, body)` and data-last `loop(body)(initial)`\n * (or `pipe(initial, loop(body))`) both work.\n */\nexport const loop: {\n <S, A, E, R>(body: LoopBody<S, A, E, R>): (initial: S) => Stream.Stream<A, E, R>\n <S, A, E, R>(initial: S, body: LoopBody<S, A, E, R>): Stream.Stream<A, E, R>\n} = Function.dual(\n 2,\n <S, A, E, R>(initial: S, body: LoopBody<S, A, E, R>): Stream.Stream<A, E, R> =>\n Stream.scoped(\n Stream.fromPull(\n Effect.gen(function* () {\n const outerScope = yield* Effect.scope\n let state = initial\n let current: CurrentBody<S, A, E, R> | undefined\n let done = false\n\n const closeActive = (\n active: CurrentBody<S, A, E, R>,\n exit: Exit.Exit<unknown, unknown>,\n ) => {\n const isActive = current === active\n if (isActive) current = undefined\n // Scope.close is idempotent. Multiple paths can race to close the\n // active body during cancellation/failure, so closing twice is safe.\n return closeBody(active, exit)\n }\n\n yield* Scope.addFinalizerExit(outerScope, (exit) =>\n current === undefined ? Effect.void : closeActive(current, exit),\n )\n\n const pull = Effect.gen(function* () {\n while (true) {\n if (done) return yield* Cause.done()\n\n if (current === undefined) {\n const result = body(state)\n const stream = Effect.isEffect(result) ? Stream.unwrap(result) : result\n const bodyScope = yield* Scope.fork(outerScope)\n const bodyPull = yield* Channel.toPullScoped(\n Stream.toChannel(stream),\n bodyScope,\n ).pipe(Effect.onError((cause) => Scope.close(bodyScope, Exit.failCause(cause))))\n current = { scope: bodyScope, pull: bodyPull }\n }\n\n const active = current\n const chunk = yield* active.pull.pipe(\n Effect.catchIf(Cause.isDone, () =>\n closeActive(active, Exit.void).pipe(\n Effect.as(undefined as ReadonlyArray<Event<A, S>> | undefined),\n ),\n ),\n Effect.onError((cause) => closeActive(active, Exit.failCause(cause))),\n )\n\n if (chunk === undefined) {\n done = true\n return yield* Cause.done()\n }\n\n const { values, decision } = partitionChunk(chunk)\n\n if (decision !== undefined) {\n yield* closeActive(active, Exit.void)\n if (decision._tag === \"Stop\") {\n done = true\n } else if (decision._tag === \"Next\") {\n state = decision.state\n }\n }\n\n // Emit the values seen so far if any. Chunks from a Stream pull\n // are non-empty, so when `decision === undefined` every event was\n // a `Value` and `values` is non-empty here. With a decision and\n // no preceding values, fall through to the next iteration.\n if (isNonEmpty(values)) return values\n }\n })\n\n return pull\n }),\n ),\n ),\n)\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Loop.mjs","names":[],"sources":["../../src/loop/Loop.ts"],"sourcesContent":["/**\n * Pull-based `loop` for state-threaded sub-streams.\n *\n * Each iteration runs a body that returns a `Stream<Event<A, S>>`. The body\n * emits values via `Loop.value(a)` and signals iteration control via\n * `Loop.next(state)` (continue with new state) or `Loop.stop` (terminate).\n * The loop unwraps `Value` events back to `A` for downstream consumers, so\n * the resulting stream is a plain `Stream<A>`.\n *\n * The next body stream is only pulled when downstream pulls the outer\n * stream - no producer fiber, no queue buffering. Cancellation, failures,\n * scoped resources, and backpressure stay aligned with normal Stream\n * semantics.\n *\n * Convention: a `Next` or `Stop` event is the terminal element of a body's\n * iteration. Values emitted in the same chunk after one are discarded\n * (their producing side effects may already have run). Prefer the\n * `Loop.nextAfter` / `Loop.stopAfter` helpers to terminate cleanly.\n */\nimport {\n Cause,\n Channel,\n Data,\n Effect,\n Exit,\n Function,\n Option,\n Ref,\n Scope,\n Stream,\n SubscriptionRef,\n} from \"effect\"\nimport { IncompleteTurn } from \"../domain/AiError.js\"\nimport { isTurnComplete, type Turn, type TurnEvent } from \"../domain/Turn.js\"\n\n// ---------------------------------------------------------------------------\n// Event type - the body's emit shape\n// ---------------------------------------------------------------------------\n\n/**\n * The tagged union a body emits per pull. `Value` carries a payload that\n * flows downstream. `Next` ends the current iteration and continues with a\n * new state. `Stop` ends the loop entirely.\n */\nexport type Event<A, S> = Data.TaggedEnum<{\n Value: { readonly value: A }\n Next: { readonly state: S }\n Stop: {}\n}>\n\ninterface EventDef extends Data.TaggedEnum.WithGenerics<2> {\n readonly taggedEnum: Event<this[\"A\"], this[\"B\"]>\n}\n\nconst Event = Data.taggedEnum<EventDef>()\n\n/** Wrap a value so it flows through the loop to downstream consumers. */\nexport const value = <A>(a: A): Event<A, never> => Event.Value({ value: a })\n\n/** End the current iteration and continue with a new state. */\nexport const next = <S>(state: S): Event<never, S> => Event.Next({ state })\n\n/** The terminal `Stop` event. Use `stop` (the Stream) to end a loop body. */\nexport const stopEvent: Event<never, never> = Event.Stop()\n\n/**\n * A single-element stream that ends the loop. Return this from a body when\n * there's nothing else to emit; equivalent to `stopAfter(Stream.empty)` but\n * named for the common case.\n */\nexport const stop: Stream.Stream<Event<never, never>> = Stream.succeed(stopEvent)\n\n/**\n * Pipe a raw `Stream<A>` into the loop's emit shape, then terminate the\n * iteration with `next(state)`. Common shape for \"stream this turn's\n * deltas, then continue with updated history.\"\n */\nexport const nextAfter = <S, A, E, R>(\n stream: Stream.Stream<A, E, R>,\n state: S,\n): Stream.Stream<Event<A, S>, E, R> =>\n Stream.concat(Stream.map(stream, value), Stream.fromIterable([next(state)]))\n\n/**\n * Pipe a raw `Stream<A>` into the loop's emit shape, then terminate the\n * loop. Common shape for \"stream this turn's deltas, then we're done.\"\n */\nexport const stopAfter = <A, E, R>(\n stream: Stream.Stream<A, E, R>,\n): Stream.Stream<Event<A, never>, E, R> =>\n Stream.concat(Stream.map(stream, value), Stream.fromIterable([stopEvent]))\n\n/**\n * General `nextAfter` variant: drain `stream` to the consumer, fold elements\n * into an accumulator, and at end-of-stream emit one `next(build(finalAcc))`.\n *\n * Subsumes `nextAfter` when state is constant (`reduce: (s, _) => s`,\n * `build: (s) => s`). Used by `Toolkit.continueWith` to collect tool\n * results and build next state without exposing a Ref to recipes.\n */\nexport const nextAfterFold = <A, B, S, E, R>(\n stream: Stream.Stream<A, E, R>,\n initial: B,\n reduce: (acc: B, a: A) => B,\n build: (b: B) => S,\n): Stream.Stream<Event<A, S>, E, R> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const ref = yield* Ref.make(initial)\n const tapped = stream.pipe(\n Stream.tap((a) => Ref.update(ref, (acc) => reduce(acc, a))),\n Stream.map(value),\n )\n const continuation = Stream.fromEffect(\n Ref.get(ref).pipe(Effect.map((acc) => next(build(acc)))),\n )\n return tapped.pipe(Stream.concat(continuation))\n }),\n )\n\n// ---------------------------------------------------------------------------\n// onTurnComplete - turn-aware stream operator for loop bodies\n// ---------------------------------------------------------------------------\n\n/**\n * Lift a provider's `Stream<TurnEvent>` into a loop body's `Stream<Event<TurnEvent | A, S>>`.\n * Each delta passes through as `value(delta)` (including the terminal\n * `turn_complete`, so the consumer sees turn boundaries naturally). Once\n * the terminal arrives, `then(turn)` runs and its returned stream of loop\n * events (typically tool outputs followed by `next(state)` or `stop`) is\n * concatenated.\n *\n * Pre-pipe transforms (`Stream.tap` / `Stream.map` / `Stream.filter`) on\n * the raw delta stream cover anything an `emit`-style callback would do.\n *\n * If the upstream ends without a `turn_complete`, the resulting stream\n * fails with `AiError.IncompleteTurn`. Catch it via `Stream.catchTag` if\n * you want to recover.\n */\nexport const onTurnComplete =\n <S, A, E2 = never, R2 = never>(\n then: (turn: Turn) => Effect.Effect<Stream.Stream<Event<A, S>, E2, R2>, E2, R2>,\n ) =>\n <E, R>(\n deltas: Stream.Stream<TurnEvent, E, R>,\n ): Stream.Stream<Event<TurnEvent | A, S>, E | E2 | IncompleteTurn, R | R2> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const turnRef = yield* Ref.make<Option.Option<Turn>>(Option.none())\n\n const events: Stream.Stream<Event<TurnEvent, S>, E, R> = deltas.pipe(\n Stream.tap((delta) =>\n isTurnComplete(delta) ? Ref.set(turnRef, Option.some(delta.turn)) : Effect.void,\n ),\n Stream.map(value),\n )\n\n const continuation = Stream.unwrap(\n Effect.gen(function* () {\n const opt = yield* Ref.get(turnRef)\n if (Option.isNone(opt)) return yield* Effect.fail(new IncompleteTurn({}))\n return yield* then(opt.value)\n }),\n )\n\n return Stream.concat(events, continuation)\n }),\n )\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nconst isNonEmpty = <A>(array: ReadonlyArray<A>): array is readonly [A, ...Array<A>] =>\n array.length > 0\n\ntype CurrentBody<S, A, E, R> = {\n readonly scope: Scope.Closeable\n readonly pull: Effect.Effect<ReadonlyArray<Event<A, S>>, E | Cause.Done<void>, R>\n}\n\nconst closeBody = <S, A, E, R>(\n current: CurrentBody<S, A, E, R>,\n exit: Exit.Exit<unknown, unknown>,\n) => Scope.close(current.scope, exit)\n\n/**\n * Walk a chunk of `Event<A, S>` until a terminal `Next` or `Stop` is found.\n * Returns the unwrapped values seen so far and (optionally) the terminal\n * event. Anything in the chunk after the terminal is discarded - its\n * producing side effects may have run, but downstream never sees it.\n */\nconst partitionChunk = <A, S>(\n chunk: ReadonlyArray<Event<A, S>>,\n): { readonly values: Array<A>; readonly decision: Event<A, S> | undefined } => {\n const values: Array<A> = []\n for (let i = 0; i < chunk.length; i++) {\n const event = chunk[i]!\n if (event._tag === \"Value\") {\n values.push(event.value)\n } else {\n return { values, decision: event }\n }\n }\n return { values, decision: undefined }\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\ntype LoopBody<S, A, E, R> = (\n state: S,\n) => Stream.Stream<Event<A, S>, E, R> | Effect.Effect<Stream.Stream<Event<A, S>, E, R>, E, R>\n\n/**\n * Drive a state-threaded loop body. Each iteration runs `body(state)` to get\n * a `Stream<Event<A, S>>`; values flow downstream, `next(s)` continues with\n * a new state, `stop` ends the loop. See the file header for the full\n * pull-based execution model.\n *\n * Dual: data-first `loop(initial, body)` and data-last `loop(body)(initial)`\n * (or `pipe(initial, loop(body))`) both work.\n */\nexport const loop: {\n <S, A, E, R>(body: LoopBody<S, A, E, R>): (initial: S) => Stream.Stream<A, E, R>\n <S, A, E, R>(initial: S, body: LoopBody<S, A, E, R>): Stream.Stream<A, E, R>\n} = Function.dual(\n 2,\n <S, A, E, R>(initial: S, body: LoopBody<S, A, E, R>): Stream.Stream<A, E, R> =>\n Stream.scoped(\n Stream.fromPull(\n Effect.gen(function* () {\n const outerScope = yield* Effect.scope\n let state = initial\n let current: CurrentBody<S, A, E, R> | undefined\n let done = false\n\n const closeActive = (\n active: CurrentBody<S, A, E, R>,\n exit: Exit.Exit<unknown, unknown>,\n ) => {\n const isActive = current === active\n if (isActive) current = undefined\n // Scope.close is idempotent. Multiple paths can race to close the\n // active body during cancellation/failure, so closing twice is safe.\n return closeBody(active, exit)\n }\n\n yield* Scope.addFinalizerExit(outerScope, (exit) =>\n current === undefined ? Effect.void : closeActive(current, exit),\n )\n\n const pull = Effect.gen(function* () {\n while (true) {\n if (done) return yield* Cause.done()\n\n if (current === undefined) {\n const result = body(state)\n const stream = Effect.isEffect(result) ? Stream.unwrap(result) : result\n const bodyScope = yield* Scope.fork(outerScope)\n const bodyPull = yield* Channel.toPullScoped(\n Stream.toChannel(stream),\n bodyScope,\n ).pipe(Effect.onError((cause) => Scope.close(bodyScope, Exit.failCause(cause))))\n current = { scope: bodyScope, pull: bodyPull }\n }\n\n const active = current\n const chunk = yield* active.pull.pipe(\n Effect.catchIf(Cause.isDone, () =>\n closeActive(active, Exit.void).pipe(\n Effect.as(undefined as ReadonlyArray<Event<A, S>> | undefined),\n ),\n ),\n Effect.onError((cause) => closeActive(active, Exit.failCause(cause))),\n )\n\n if (chunk === undefined) {\n done = true\n return yield* Cause.done()\n }\n\n const { values, decision } = partitionChunk(chunk)\n\n if (decision !== undefined) {\n yield* closeActive(active, Exit.void)\n if (decision._tag === \"Stop\") {\n done = true\n } else if (decision._tag === \"Next\") {\n state = decision.state\n }\n }\n\n // Emit the values seen so far if any. Chunks from a Stream pull\n // are non-empty, so when `decision === undefined` every event was\n // a `Value` and `values` is non-empty here. With a decision and\n // no preceding values, fall through to the next iteration.\n if (isNonEmpty(values)) return values\n }\n })\n\n return pull\n }),\n ),\n ),\n)\n\n// ---------------------------------------------------------------------------\n// loopWithState - same body protocol, plus a live state observable.\n// ---------------------------------------------------------------------------\n\n/**\n * Like `loop`, but exposes the current loop state as a `SubscriptionRef`\n * alongside the value stream.\n *\n * Allocates one `SubscriptionRef<S>` seeded with `initial`, then runs the\n * loop with a wrapped body that taps every `Next(s)` event into the ref\n * before forwarding it. The caller decides how to consume both channels:\n *\n * - **Final state**: drain the stream, then `SubscriptionRef.get(state)`\n * - the ref holds the state from the last `Next` (or `initial` if the\n * loop ended without advancing).\n * - **Live transitions**: `SubscriptionRef.changes(state)` is a\n * `Stream<S>` of every state observed; subscribe alongside the value\n * stream.\n * - **Mid-iteration peek**: `SubscriptionRef.get(state)` at any time.\n *\n * The returned stream and ref are independent of each other - the ref\n * lives outside the stream's scope, so reading it after the stream\n * completes is safe.\n */\nexport const loopWithState = <S, A, E, R>(\n initial: S,\n body: LoopBody<S, A, E, R>,\n): Effect.Effect<{\n readonly stream: Stream.Stream<A, E, R>\n readonly state: SubscriptionRef.SubscriptionRef<S>\n}> =>\n Effect.gen(function* () {\n const stateRef = yield* SubscriptionRef.make(initial)\n\n const tap = (stream: Stream.Stream<Event<A, S>, E, R>): Stream.Stream<Event<A, S>, E, R> =>\n stream.pipe(\n Stream.tap((event) =>\n event._tag === \"Next\" ? SubscriptionRef.set(stateRef, event.state) : Effect.void,\n ),\n )\n\n const wrappedBody: LoopBody<S, A, E, R> = (s) => {\n const result = body(s)\n return Effect.isEffect(result) ? Effect.map(result, tap) : tap(result)\n }\n\n return {\n stream: loop(initial, wrappedBody),\n state: stateRef,\n }\n })\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { loop, loopWithState, next, nextAfter, stopAfter, stopEvent, value } from "./Loop.mjs";
|
|
2
|
+
import { i as it, n as globalExpect, r as describe } from "../dist-DV5ISja1.mjs";
|
|
3
|
+
import { Deferred, Effect, Fiber, Latch, Ref, Stream, SubscriptionRef } from "effect";
|
|
4
|
+
//#region src/loop/Loop.test.ts
|
|
5
|
+
describe("Loop.loop", () => {
|
|
6
|
+
it("threads state across iterations and emits each iteration's substream in order", async () => {
|
|
7
|
+
const stream = loop(0, (n) => n >= 3 ? stopAfter(Stream.fromIterable([n])) : nextAfter(Stream.fromIterable([n, n + .5]), n + 1));
|
|
8
|
+
globalExpect(await Effect.runPromise(Stream.runCollect(stream))).toEqual([
|
|
9
|
+
0,
|
|
10
|
+
.5,
|
|
11
|
+
1,
|
|
12
|
+
1.5,
|
|
13
|
+
2,
|
|
14
|
+
2.5,
|
|
15
|
+
3
|
|
16
|
+
]);
|
|
17
|
+
});
|
|
18
|
+
it("supports iterations that emit zero values and only decide", async () => {
|
|
19
|
+
const stream = loop(0, (n) => n >= 5 ? Stream.fromIterable([stopEvent]) : Stream.fromIterable([next(n + 1)]));
|
|
20
|
+
globalExpect(await Effect.runPromise(Stream.runCollect(stream))).toEqual([]);
|
|
21
|
+
});
|
|
22
|
+
it("supports Effect-returning bodies directly (no Stream.unwrap needed)", async () => {
|
|
23
|
+
const stream = loop(1, (n) => Effect.gen(function* () {
|
|
24
|
+
const doubled = yield* Effect.succeed(n * 2);
|
|
25
|
+
return doubled >= 16 ? stopAfter(Stream.fromIterable([doubled])) : nextAfter(Stream.fromIterable([doubled]), doubled);
|
|
26
|
+
}));
|
|
27
|
+
globalExpect(await Effect.runPromise(Stream.runCollect(stream))).toEqual([
|
|
28
|
+
2,
|
|
29
|
+
4,
|
|
30
|
+
8,
|
|
31
|
+
16
|
|
32
|
+
]);
|
|
33
|
+
});
|
|
34
|
+
it("still accepts Stream.unwrap-wrapped bodies for backward compatibility", async () => {
|
|
35
|
+
const stream = loop(1, (n) => Stream.unwrap(Effect.gen(function* () {
|
|
36
|
+
const doubled = yield* Effect.succeed(n * 2);
|
|
37
|
+
return doubled >= 4 ? stopAfter(Stream.fromIterable([doubled])) : nextAfter(Stream.fromIterable([doubled]), doubled);
|
|
38
|
+
})));
|
|
39
|
+
globalExpect(await Effect.runPromise(Stream.runCollect(stream))).toEqual([2, 4]);
|
|
40
|
+
});
|
|
41
|
+
it("propagates errors from the body's stream", async () => {
|
|
42
|
+
const boom = /* @__PURE__ */ new Error("boom");
|
|
43
|
+
const stream = loop(0, (n) => n === 2 ? Stream.fail(boom) : Stream.fromIterable([value(n), next(n + 1)]));
|
|
44
|
+
globalExpect((await Effect.runPromiseExit(Stream.runCollect(stream)))._tag).toBe("Failure");
|
|
45
|
+
});
|
|
46
|
+
it("terminates silently if the body emits no Decision (mirrors paginate's silent stop)", async () => {
|
|
47
|
+
const stream = loop(0, (n) => Stream.fromIterable([value(n), value(n + 1)]));
|
|
48
|
+
globalExpect(await Effect.runPromise(Stream.runCollect(stream))).toEqual([0, 1]);
|
|
49
|
+
});
|
|
50
|
+
it("short-circuits the body's stream when a Decision is seen", async () => {
|
|
51
|
+
const stream = loop(0, (n) => n >= 2 ? Stream.fromIterable([value(n), stopEvent]) : Stream.fromIterable([
|
|
52
|
+
value(n),
|
|
53
|
+
next(n + 1),
|
|
54
|
+
value(n + 10)
|
|
55
|
+
]));
|
|
56
|
+
globalExpect(await Effect.runPromise(Stream.runCollect(stream))).toEqual([
|
|
57
|
+
0,
|
|
58
|
+
1,
|
|
59
|
+
2
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
it("is stack-safe and linear-time across many iterations", async () => {
|
|
63
|
+
const N = 1e5;
|
|
64
|
+
const stream = loop(0, (n) => n >= N ? Stream.fromIterable([value(n), stopEvent]) : Stream.fromIterable([value(n), next(n + 1)]));
|
|
65
|
+
globalExpect(await Effect.runPromise(Stream.runFold(stream, () => 0, (acc) => acc + 1))).toBe(N + 1);
|
|
66
|
+
}, 1e4);
|
|
67
|
+
});
|
|
68
|
+
const scriptedModel = (script) => {
|
|
69
|
+
let i = 0;
|
|
70
|
+
return { streamTurn: () => {
|
|
71
|
+
const turn = script[i] ?? [];
|
|
72
|
+
i += 1;
|
|
73
|
+
return Stream.fromIterable(turn);
|
|
74
|
+
} };
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Body factored out so both tests share it. Per iteration:
|
|
78
|
+
* 1. Stream the model's deltas; tap captures texts + tool calls into Refs.
|
|
79
|
+
* 2. flatMap projects deltas into UiEvents forwarded to the outer stream.
|
|
80
|
+
* 3. Continuation reads the captured calls; if any, runs them, emits
|
|
81
|
+
* tool_result events, builds the next state (with model swap if a tool
|
|
82
|
+
* asked for one), and emits `next(state)`. Otherwise `stop`.
|
|
83
|
+
*/
|
|
84
|
+
const conversationLoop = (initial, runTool) => loop(initial, (state) => Stream.unwrap(Effect.gen(function* () {
|
|
85
|
+
const textsRef = yield* Ref.make([]);
|
|
86
|
+
const toolCallsRef = yield* Ref.make([]);
|
|
87
|
+
const deltas = state.model.streamTurn(state.history).pipe(Stream.tap((d) => d.type === "text" ? Ref.update(textsRef, (t) => [...t, d.text]) : Ref.update(toolCallsRef, (t) => [...t, {
|
|
88
|
+
id: d.id,
|
|
89
|
+
name: d.name
|
|
90
|
+
}])), Stream.flatMap((d) => d.type === "text" ? Stream.fromIterable([value({
|
|
91
|
+
type: "text",
|
|
92
|
+
text: d.text
|
|
93
|
+
})]) : Stream.fromIterable([value({
|
|
94
|
+
type: "tool_started",
|
|
95
|
+
id: d.id,
|
|
96
|
+
name: d.name
|
|
97
|
+
})])));
|
|
98
|
+
const continuation = Stream.unwrap(Effect.gen(function* () {
|
|
99
|
+
const texts = yield* Ref.get(textsRef);
|
|
100
|
+
const toolCalls = yield* Ref.get(toolCallsRef);
|
|
101
|
+
if (toolCalls.length === 0) return stopAfter(Stream.empty);
|
|
102
|
+
const turnItems = [...texts.length > 0 ? [{
|
|
103
|
+
type: "assistant",
|
|
104
|
+
text: texts.join("")
|
|
105
|
+
}] : [], ...toolCalls.map((tc) => ({
|
|
106
|
+
type: "tool_call",
|
|
107
|
+
id: tc.id,
|
|
108
|
+
name: tc.name
|
|
109
|
+
}))];
|
|
110
|
+
const outcomes = toolCalls.map((call) => ({
|
|
111
|
+
call,
|
|
112
|
+
outcome: runTool(call)
|
|
113
|
+
}));
|
|
114
|
+
const events = outcomes.map(({ call, outcome }) => ({
|
|
115
|
+
type: "tool_result",
|
|
116
|
+
id: call.id,
|
|
117
|
+
output: outcome.output
|
|
118
|
+
}));
|
|
119
|
+
const resultItems = outcomes.map(({ call, outcome }) => ({
|
|
120
|
+
type: "tool_result",
|
|
121
|
+
id: call.id,
|
|
122
|
+
output: outcome.output
|
|
123
|
+
}));
|
|
124
|
+
const nextModel = outcomes.reduce((m, { outcome }) => outcome.nextModel ?? m, state.model);
|
|
125
|
+
const nextState = {
|
|
126
|
+
history: [
|
|
127
|
+
...state.history,
|
|
128
|
+
...turnItems,
|
|
129
|
+
...resultItems
|
|
130
|
+
],
|
|
131
|
+
model: nextModel
|
|
132
|
+
};
|
|
133
|
+
return nextAfter(Stream.fromIterable(events), nextState);
|
|
134
|
+
}));
|
|
135
|
+
return Stream.concat(deltas, continuation);
|
|
136
|
+
})));
|
|
137
|
+
describe("Loop.loop - LLM-style scenarios", () => {
|
|
138
|
+
it("forwards text deltas, tool start, tool result, and post-tool text in order", async () => {
|
|
139
|
+
const m = scriptedModel([[
|
|
140
|
+
{
|
|
141
|
+
type: "text",
|
|
142
|
+
text: "hello"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
type: "text",
|
|
146
|
+
text: " "
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
type: "text",
|
|
150
|
+
text: "world"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
type: "tool_call",
|
|
154
|
+
id: "c1",
|
|
155
|
+
name: "get_time"
|
|
156
|
+
}
|
|
157
|
+
], [{
|
|
158
|
+
type: "text",
|
|
159
|
+
text: " time is "
|
|
160
|
+
}, {
|
|
161
|
+
type: "text",
|
|
162
|
+
text: "12:00"
|
|
163
|
+
}]]);
|
|
164
|
+
const runTool = (call) => ({ output: call.name === "get_time" ? "12:00" : "?" });
|
|
165
|
+
const initial = {
|
|
166
|
+
history: [{
|
|
167
|
+
type: "user",
|
|
168
|
+
text: "what time is it?"
|
|
169
|
+
}],
|
|
170
|
+
model: m
|
|
171
|
+
};
|
|
172
|
+
globalExpect(await Effect.runPromise(Stream.runCollect(conversationLoop(initial, runTool)))).toEqual([
|
|
173
|
+
{
|
|
174
|
+
type: "text",
|
|
175
|
+
text: "hello"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
type: "text",
|
|
179
|
+
text: " "
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
type: "text",
|
|
183
|
+
text: "world"
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
type: "tool_started",
|
|
187
|
+
id: "c1",
|
|
188
|
+
name: "get_time"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
type: "tool_result",
|
|
192
|
+
id: "c1",
|
|
193
|
+
output: "12:00"
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
type: "text",
|
|
197
|
+
text: " time is "
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
type: "text",
|
|
201
|
+
text: "12:00"
|
|
202
|
+
}
|
|
203
|
+
]);
|
|
204
|
+
});
|
|
205
|
+
it("model swap mid-stream: m1 calls upgrade, m2 finishes the response", async () => {
|
|
206
|
+
const m2 = scriptedModel([[{
|
|
207
|
+
type: "text",
|
|
208
|
+
text: "I am m2."
|
|
209
|
+
}, {
|
|
210
|
+
type: "text",
|
|
211
|
+
text: " The answer is 42."
|
|
212
|
+
}]]);
|
|
213
|
+
const m1 = scriptedModel([[
|
|
214
|
+
{
|
|
215
|
+
type: "text",
|
|
216
|
+
text: "Hard question."
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
type: "text",
|
|
220
|
+
text: " Upgrading."
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
type: "tool_call",
|
|
224
|
+
id: "u1",
|
|
225
|
+
name: "upgrade"
|
|
226
|
+
}
|
|
227
|
+
]]);
|
|
228
|
+
const runTool = (call) => call.name === "upgrade" ? {
|
|
229
|
+
output: "ok",
|
|
230
|
+
nextModel: m2
|
|
231
|
+
} : { output: "?" };
|
|
232
|
+
const initial = {
|
|
233
|
+
history: [{
|
|
234
|
+
type: "user",
|
|
235
|
+
text: "what is the meaning of life?"
|
|
236
|
+
}],
|
|
237
|
+
model: m1
|
|
238
|
+
};
|
|
239
|
+
globalExpect(await Effect.runPromise(Stream.runCollect(conversationLoop(initial, runTool)))).toEqual([
|
|
240
|
+
{
|
|
241
|
+
type: "text",
|
|
242
|
+
text: "Hard question."
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
type: "text",
|
|
246
|
+
text: " Upgrading."
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
type: "tool_started",
|
|
250
|
+
id: "u1",
|
|
251
|
+
name: "upgrade"
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
type: "tool_result",
|
|
255
|
+
id: "u1",
|
|
256
|
+
output: "ok"
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
type: "text",
|
|
260
|
+
text: "I am m2."
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
type: "text",
|
|
264
|
+
text: " The answer is 42."
|
|
265
|
+
}
|
|
266
|
+
]);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
describe("Loop.loop - pull-specific stream semantics", () => {
|
|
270
|
+
it("does not start the next iteration when downstream only takes the first value", async () => {
|
|
271
|
+
globalExpect(await Effect.runPromise(Effect.gen(function* () {
|
|
272
|
+
const callsRef = yield* Ref.make(0);
|
|
273
|
+
yield* loop(0, (n) => Stream.unwrap(Ref.update(callsRef, (calls) => calls + 1).pipe(Effect.as(n >= 10 ? Stream.fromIterable([value(n), stopEvent]) : Stream.fromIterable([value(n), next(n + 1)]))))).pipe(Stream.take(1), Stream.runCollect);
|
|
274
|
+
return yield* Ref.get(callsRef);
|
|
275
|
+
}))).toBe(1);
|
|
276
|
+
});
|
|
277
|
+
it("propagates defects from the body instead of leaving the consumer waiting", async () => {
|
|
278
|
+
const defect = /* @__PURE__ */ new Error("defect");
|
|
279
|
+
const stream = loop(0, () => Stream.die(defect));
|
|
280
|
+
globalExpect((await Effect.runPromiseExit(Stream.runCollect(stream)))._tag).toBe("Failure");
|
|
281
|
+
});
|
|
282
|
+
it("runs body finalizers when a Decision short-circuits the body", async () => {
|
|
283
|
+
globalExpect(await Effect.runPromise(Effect.gen(function* () {
|
|
284
|
+
const releasesRef = yield* Ref.make([]);
|
|
285
|
+
const stream = loop(0, (n) => (n >= 1 ? Stream.fromIterable([value(n), stopEvent]) : Stream.fromIterable([
|
|
286
|
+
value(n),
|
|
287
|
+
next(n + 1),
|
|
288
|
+
value(n + 10)
|
|
289
|
+
])).pipe(Stream.ensuring(Ref.update(releasesRef, (values) => [...values, n]))));
|
|
290
|
+
globalExpect(yield* Stream.runCollect(stream)).toEqual([0, 1]);
|
|
291
|
+
return yield* Ref.get(releasesRef);
|
|
292
|
+
}))).toEqual([0, 1]);
|
|
293
|
+
});
|
|
294
|
+
it("runs the active body finalizer when the downstream consumer is interrupted", async () => {
|
|
295
|
+
globalExpect(await Effect.runPromise(Effect.gen(function* () {
|
|
296
|
+
const started = yield* Deferred.make();
|
|
297
|
+
const releasesRef = yield* Ref.make(0);
|
|
298
|
+
const body = () => Stream.concat(Stream.fromEffect(Deferred.succeed(started, void 0).pipe(Effect.as(value(0)))), Stream.never).pipe(Stream.ensuring(Ref.update(releasesRef, (n) => n + 1)));
|
|
299
|
+
const stream = loop(0, body);
|
|
300
|
+
const fiber = yield* Effect.forkChild(Stream.runCollect(stream));
|
|
301
|
+
yield* Deferred.await(started);
|
|
302
|
+
yield* Fiber.interrupt(fiber);
|
|
303
|
+
return yield* Ref.get(releasesRef);
|
|
304
|
+
}))).toBe(1);
|
|
305
|
+
});
|
|
306
|
+
it("does not create a body scope if constructing the body stream defects", async () => {
|
|
307
|
+
const defect = /* @__PURE__ */ new Error("body construction failed");
|
|
308
|
+
globalExpect((await Effect.runPromiseExit(Stream.runCollect(loop(0, () => {
|
|
309
|
+
throw defect;
|
|
310
|
+
}))))._tag).toBe("Failure");
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
describe("Loop.loopWithState", () => {
|
|
314
|
+
it("exposes the final state in the SubscriptionRef after the stream completes", async () => {
|
|
315
|
+
const program = Effect.gen(function* () {
|
|
316
|
+
const { stream, state } = yield* loopWithState(0, (n) => n >= 3 ? stopAfter(Stream.fromIterable([n])) : nextAfter(Stream.fromIterable([n]), n + 1));
|
|
317
|
+
const values = yield* Stream.runCollect(stream);
|
|
318
|
+
const finalState = yield* SubscriptionRef.get(state);
|
|
319
|
+
return {
|
|
320
|
+
values: Array.from(values),
|
|
321
|
+
finalState
|
|
322
|
+
};
|
|
323
|
+
});
|
|
324
|
+
const { values, finalState } = await Effect.runPromise(program);
|
|
325
|
+
globalExpect(values).toEqual([
|
|
326
|
+
0,
|
|
327
|
+
1,
|
|
328
|
+
2,
|
|
329
|
+
3
|
|
330
|
+
]);
|
|
331
|
+
globalExpect(finalState).toBe(3);
|
|
332
|
+
});
|
|
333
|
+
it("the state ref starts at `initial` and stays there if the loop stops without advancing", async () => {
|
|
334
|
+
const program = Effect.gen(function* () {
|
|
335
|
+
const { stream, state } = yield* loopWithState({ count: 7 }, () => Stream.fromIterable([stopEvent]));
|
|
336
|
+
yield* Stream.runDrain(stream);
|
|
337
|
+
return yield* SubscriptionRef.get(state);
|
|
338
|
+
});
|
|
339
|
+
globalExpect(await Effect.runPromise(program)).toEqual({ count: 7 });
|
|
340
|
+
});
|
|
341
|
+
it("a downstream consumer can read the live state between emitted values", async () => {
|
|
342
|
+
const program = Effect.gen(function* () {
|
|
343
|
+
const { stream, state } = yield* loopWithState(0, (n) => n >= 3 ? stopAfter(Stream.fromIterable([n])) : nextAfter(Stream.fromIterable([n]), n + 1));
|
|
344
|
+
const seen = [];
|
|
345
|
+
yield* Stream.runForEach(stream, (v) => Effect.gen(function* () {
|
|
346
|
+
seen.push({
|
|
347
|
+
value: v,
|
|
348
|
+
stateAfter: yield* SubscriptionRef.get(state)
|
|
349
|
+
});
|
|
350
|
+
}));
|
|
351
|
+
return seen;
|
|
352
|
+
});
|
|
353
|
+
globalExpect(await Effect.runPromise(program)).toEqual([
|
|
354
|
+
{
|
|
355
|
+
value: 0,
|
|
356
|
+
stateAfter: 0
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
value: 1,
|
|
360
|
+
stateAfter: 1
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
value: 2,
|
|
364
|
+
stateAfter: 2
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
value: 3,
|
|
368
|
+
stateAfter: 3
|
|
369
|
+
}
|
|
370
|
+
]);
|
|
371
|
+
});
|
|
372
|
+
it("SubscriptionRef.changes emits every state transition to a concurrent observer", async () => {
|
|
373
|
+
const program = Effect.gen(function* () {
|
|
374
|
+
const start = yield* Latch.make(false);
|
|
375
|
+
const { stream, state } = yield* loopWithState(0, (n) => Effect.gen(function* () {
|
|
376
|
+
if (n === 0) yield* Latch.await(start);
|
|
377
|
+
return n >= 3 ? stopAfter(Stream.empty) : nextAfter(Stream.empty, n + 1);
|
|
378
|
+
}));
|
|
379
|
+
const observerFiber = yield* Effect.forkChild(SubscriptionRef.changes(state).pipe(Stream.take(4), Stream.runCollect));
|
|
380
|
+
yield* Effect.sleep("10 millis");
|
|
381
|
+
yield* Latch.open(start);
|
|
382
|
+
yield* Stream.runDrain(stream);
|
|
383
|
+
return Array.from(yield* Fiber.join(observerFiber));
|
|
384
|
+
});
|
|
385
|
+
globalExpect(await Effect.runPromise(program)).toEqual([
|
|
386
|
+
0,
|
|
387
|
+
1,
|
|
388
|
+
2,
|
|
389
|
+
3
|
|
390
|
+
]);
|
|
391
|
+
});
|
|
392
|
+
it("does not interfere with the body's value stream", async () => {
|
|
393
|
+
const program = Effect.gen(function* () {
|
|
394
|
+
const { stream } = yield* loopWithState(0, (n) => n >= 3 ? stopAfter(Stream.fromIterable([n])) : nextAfter(Stream.fromIterable([n, n + .5]), n + 1));
|
|
395
|
+
return Array.from(yield* Stream.runCollect(stream));
|
|
396
|
+
});
|
|
397
|
+
globalExpect(await Effect.runPromise(program)).toEqual([
|
|
398
|
+
0,
|
|
399
|
+
.5,
|
|
400
|
+
1,
|
|
401
|
+
1.5,
|
|
402
|
+
2,
|
|
403
|
+
2.5,
|
|
404
|
+
3
|
|
405
|
+
]);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
//#endregion
|
|
409
|
+
export {};
|
|
410
|
+
|
|
411
|
+
//# sourceMappingURL=Loop.test.mjs.map
|