@effect-uai/core 0.5.1 → 0.6.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/{Items-BH8xUkoR.d.mts → Items-pXR5BAgW.d.mts} +12 -12
- package/dist/Items-pXR5BAgW.d.mts.map +1 -0
- package/dist/SandboxError-BvKypkJm.d.mts +158 -0
- package/dist/SandboxError-BvKypkJm.d.mts.map +1 -0
- package/dist/{Tool-87ViKCCO.d.mts → Tool-DP-AwKOf.d.mts} +11 -11
- package/dist/{Tool-87ViKCCO.d.mts.map → Tool-DP-AwKOf.d.mts.map} +1 -1
- package/dist/domain/Items.d.mts +2 -2
- package/dist/domain/Items.mjs +15 -15
- package/dist/domain/Items.mjs.map +1 -1
- package/dist/domain/Turn.d.mts +388 -2
- package/dist/domain/Turn.d.mts.map +1 -0
- package/dist/domain/Turn.mjs +13 -13
- package/dist/domain/Turn.mjs.map +1 -1
- package/dist/index.d.mts +12 -6
- package/dist/index.mjs +9 -3
- package/dist/language-model/LanguageModel.d.mts +29 -27
- package/dist/language-model/LanguageModel.d.mts.map +1 -1
- package/dist/language-model/LanguageModel.mjs +16 -25
- package/dist/language-model/LanguageModel.mjs.map +1 -1
- package/dist/language-model/LanguageModel.test.mjs +10 -81
- package/dist/language-model/LanguageModel.test.mjs.map +1 -1
- package/dist/loop/Loop.d.mts +37 -90
- package/dist/loop/Loop.d.mts.map +1 -1
- package/dist/loop/Loop.mjs +39 -93
- package/dist/loop/Loop.mjs.map +1 -1
- package/dist/loop/Loop.test.mjs +46 -54
- package/dist/loop/Loop.test.mjs.map +1 -1
- package/dist/retry/Retry.d.mts +64 -0
- package/dist/retry/Retry.d.mts.map +1 -0
- package/dist/retry/Retry.mjs +69 -0
- package/dist/retry/Retry.mjs.map +1 -0
- package/dist/retry/Retry.test.mjs +142 -0
- package/dist/retry/Retry.test.mjs.map +1 -0
- package/dist/sandbox/Memory.d.mts +53 -0
- package/dist/sandbox/Memory.d.mts.map +1 -0
- package/dist/sandbox/Memory.mjs +68 -0
- package/dist/sandbox/Memory.mjs.map +1 -0
- package/dist/sandbox/Sandbox.d.mts +717 -0
- package/dist/sandbox/Sandbox.d.mts.map +1 -0
- package/dist/sandbox/Sandbox.mjs +192 -0
- package/dist/sandbox/Sandbox.mjs.map +1 -0
- package/dist/sandbox/SandboxError.d.mts +2 -0
- package/dist/sandbox/SandboxError.mjs +89 -0
- package/dist/sandbox/SandboxError.mjs.map +1 -0
- package/dist/sandbox/SandboxImage.d.mts +28 -0
- package/dist/sandbox/SandboxImage.d.mts.map +1 -0
- package/dist/sandbox/SandboxImage.mjs +32 -0
- package/dist/sandbox/SandboxImage.mjs.map +1 -0
- package/dist/sandbox/SandboxNetwork.d.mts +34 -0
- package/dist/sandbox/SandboxNetwork.d.mts.map +1 -0
- package/dist/sandbox/SandboxNetwork.mjs +36 -0
- package/dist/sandbox/SandboxNetwork.mjs.map +1 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +98 -2
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs +32 -2
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +56 -2
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs.map +1 -1
- package/dist/testing/MockProvider.d.mts +3 -3
- package/dist/testing/MockProvider.d.mts.map +1 -1
- package/dist/testing/MockProvider.mjs +14 -8
- package/dist/testing/MockProvider.mjs.map +1 -1
- package/dist/testing/MockSandbox.d.mts +604 -0
- package/dist/testing/MockSandbox.d.mts.map +1 -0
- package/dist/testing/MockSandbox.mjs +160 -0
- package/dist/testing/MockSandbox.mjs.map +1 -0
- package/dist/testing/MockSpeechSynthesizer.d.mts +26 -11
- package/dist/testing/MockSpeechSynthesizer.d.mts.map +1 -1
- package/dist/testing/MockSpeechSynthesizer.mjs +75 -51
- package/dist/testing/MockSpeechSynthesizer.mjs.map +1 -1
- package/dist/tool/{Resolvers.d.mts → Approval.d.mts} +27 -27
- package/dist/tool/Approval.d.mts.map +1 -0
- package/dist/tool/{Resolvers.mjs → Approval.mjs} +18 -18
- package/dist/tool/Approval.mjs.map +1 -0
- package/dist/tool/Approval.test.d.mts +1 -0
- package/dist/tool/{Resolvers.test.mjs → Approval.test.mjs} +40 -40
- package/dist/tool/Approval.test.mjs.map +1 -0
- package/dist/tool/HistoryCheck.d.mts +6 -6
- package/dist/tool/HistoryCheck.d.mts.map +1 -1
- package/dist/tool/HistoryCheck.mjs +5 -5
- package/dist/tool/HistoryCheck.mjs.map +1 -1
- package/dist/tool/Tool.d.mts +2 -2
- package/dist/tool/Tool.mjs +4 -4
- package/dist/tool/Tool.mjs.map +1 -1
- package/dist/tool/ToolEvent.d.mts +19 -19
- package/dist/tool/ToolEvent.d.mts.map +1 -1
- package/dist/tool/ToolEvent.mjs +8 -8
- package/dist/tool/ToolEvent.mjs.map +1 -1
- package/dist/tool/{Outcome.d.mts → ToolResult.d.mts} +29 -29
- package/dist/tool/ToolResult.d.mts.map +1 -0
- package/dist/tool/ToolResult.mjs +61 -0
- package/dist/tool/ToolResult.mjs.map +1 -0
- package/dist/tool/Toolkit.d.mts +58 -26
- package/dist/tool/Toolkit.d.mts.map +1 -1
- package/dist/tool/Toolkit.mjs +62 -34
- package/dist/tool/Toolkit.mjs.map +1 -1
- package/dist/tool/Toolkit.test.mjs +13 -13
- package/dist/tool/Toolkit.test.mjs.map +1 -1
- package/package.json +35 -7
- package/src/domain/Items.ts +9 -9
- package/src/domain/Turn.ts +16 -16
- package/src/index.ts +8 -2
- package/src/language-model/LanguageModel.test.ts +21 -102
- package/src/language-model/LanguageModel.ts +36 -59
- package/src/loop/Loop.test.ts +93 -91
- package/src/loop/Loop.ts +72 -170
- package/src/retry/Retry.test.ts +163 -0
- package/src/retry/Retry.ts +84 -0
- package/src/sandbox/Memory.ts +128 -0
- package/src/sandbox/Sandbox.ts +639 -0
- package/src/sandbox/SandboxError.ts +140 -0
- package/src/sandbox/SandboxImage.ts +26 -0
- package/src/sandbox/SandboxNetwork.ts +34 -0
- package/src/speech-synthesizer/SpeechSynthesizer.test.ts +89 -0
- package/src/speech-synthesizer/SpeechSynthesizer.ts +124 -0
- package/src/testing/MockProvider.ts +31 -25
- package/src/testing/MockSandbox.ts +342 -0
- package/src/testing/MockSpeechSynthesizer.ts +137 -77
- package/src/tool/{Resolvers.test.ts → Approval.test.ts} +45 -54
- package/src/tool/{Resolvers.ts → Approval.ts} +25 -25
- package/src/tool/HistoryCheck.ts +10 -13
- package/src/tool/Tool.ts +13 -15
- package/src/tool/ToolEvent.ts +7 -7
- package/src/tool/{Outcome.ts → ToolResult.ts} +20 -20
- package/src/tool/Toolkit.test.ts +13 -13
- package/src/tool/Toolkit.ts +97 -73
- package/dist/Items-BH8xUkoR.d.mts.map +0 -1
- package/dist/Turn-0CwCAyVe.d.mts +0 -388
- package/dist/Turn-0CwCAyVe.d.mts.map +0 -1
- package/dist/tool/Outcome.d.mts.map +0 -1
- package/dist/tool/Outcome.mjs +0 -61
- package/dist/tool/Outcome.mjs.map +0 -1
- package/dist/tool/Resolvers.d.mts.map +0 -1
- package/dist/tool/Resolvers.mjs.map +0 -1
- package/dist/tool/Resolvers.test.mjs.map +0 -1
- /package/dist/{tool/Resolvers.test.d.mts → retry/Retry.test.d.mts} +0 -0
package/dist/loop/Loop.test.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { RateLimited } from "../domain/AiError.mjs";
|
|
2
2
|
import { TurnEvent } from "../domain/Turn.mjs";
|
|
3
|
-
import { loop,
|
|
3
|
+
import { loop, loopOver, loopWithState, next, onTurnComplete, stop, value } from "./Loop.mjs";
|
|
4
4
|
import { i as it, n as globalExpect, r as describe, t as import_dist } from "../dist-DV5ISja1.mjs";
|
|
5
5
|
import { Deferred, Effect, Fiber, Latch, Ref, Stream, SubscriptionRef, pipe } from "effect";
|
|
6
6
|
//#region src/loop/Loop.test.ts
|
|
7
7
|
describe("Loop.loop", () => {
|
|
8
8
|
it("threads state across iterations and emits each iteration's substream in order", async () => {
|
|
9
|
-
const stream = loop(0, (n) => n >= 3 ?
|
|
9
|
+
const stream = loop(0, (n) => n >= 3 ? Stream.fromIterable([n]).pipe(Stream.map(value), Stream.concat(stop())) : Stream.fromIterable([n, n + .5]).pipe(Stream.map(value), Stream.concat(next(n + 1))));
|
|
10
10
|
globalExpect(await Effect.runPromise(Stream.runCollect(stream))).toEqual([
|
|
11
11
|
0,
|
|
12
12
|
.5,
|
|
@@ -18,13 +18,13 @@ describe("Loop.loop", () => {
|
|
|
18
18
|
]);
|
|
19
19
|
});
|
|
20
20
|
it("supports iterations that emit zero values and only decide", async () => {
|
|
21
|
-
const stream = loop(0, (n) => n >= 5 ?
|
|
21
|
+
const stream = loop(0, (n) => n >= 5 ? stop() : next(n + 1));
|
|
22
22
|
globalExpect(await Effect.runPromise(Stream.runCollect(stream))).toEqual([]);
|
|
23
23
|
});
|
|
24
24
|
it("supports Effect-returning bodies directly (no Stream.unwrap needed)", async () => {
|
|
25
25
|
const stream = loop(1, (n) => Effect.gen(function* () {
|
|
26
26
|
const doubled = yield* Effect.succeed(n * 2);
|
|
27
|
-
return doubled >= 16 ?
|
|
27
|
+
return doubled >= 16 ? Stream.fromIterable([doubled]).pipe(Stream.map(value), Stream.concat(stop())) : Stream.fromIterable([doubled]).pipe(Stream.map(value), Stream.concat(next(doubled)));
|
|
28
28
|
}));
|
|
29
29
|
globalExpect(await Effect.runPromise(Stream.runCollect(stream))).toEqual([
|
|
30
30
|
2,
|
|
@@ -36,13 +36,13 @@ describe("Loop.loop", () => {
|
|
|
36
36
|
it("still accepts Stream.unwrap-wrapped bodies for backward compatibility", async () => {
|
|
37
37
|
const stream = loop(1, (n) => Stream.unwrap(Effect.gen(function* () {
|
|
38
38
|
const doubled = yield* Effect.succeed(n * 2);
|
|
39
|
-
return doubled >= 4 ?
|
|
39
|
+
return doubled >= 4 ? Stream.fromIterable([doubled]).pipe(Stream.map(value), Stream.concat(stop())) : Stream.fromIterable([doubled]).pipe(Stream.map(value), Stream.concat(next(doubled)));
|
|
40
40
|
})));
|
|
41
41
|
globalExpect(await Effect.runPromise(Stream.runCollect(stream))).toEqual([2, 4]);
|
|
42
42
|
});
|
|
43
43
|
it("propagates errors from the body's stream", async () => {
|
|
44
44
|
const boom = /* @__PURE__ */ new Error("boom");
|
|
45
|
-
const stream = loop(0, (n) => n === 2 ? Stream.fail(boom) : Stream.
|
|
45
|
+
const stream = loop(0, (n) => n === 2 ? Stream.fail(boom) : Stream.succeed(value(n)).pipe(Stream.concat(next(n + 1))));
|
|
46
46
|
globalExpect((await Effect.runPromiseExit(Stream.runCollect(stream)))._tag).toBe("Failure");
|
|
47
47
|
});
|
|
48
48
|
it("terminates silently if the body emits no Decision (mirrors paginate's silent stop)", async () => {
|
|
@@ -50,11 +50,7 @@ describe("Loop.loop", () => {
|
|
|
50
50
|
globalExpect(await Effect.runPromise(Stream.runCollect(stream))).toEqual([0, 1]);
|
|
51
51
|
});
|
|
52
52
|
it("short-circuits the body's stream when a Decision is seen", async () => {
|
|
53
|
-
const stream = loop(0, (n) => n >= 2 ? Stream.
|
|
54
|
-
value(n),
|
|
55
|
-
next(n + 1),
|
|
56
|
-
value(n + 10)
|
|
57
|
-
]));
|
|
53
|
+
const stream = loop(0, (n) => n >= 2 ? Stream.succeed(value(n)).pipe(Stream.concat(stop())) : Stream.succeed(value(n)).pipe(Stream.concat(next(n + 1)), Stream.concat(Stream.succeed(value(n + 10)))));
|
|
58
54
|
globalExpect(await Effect.runPromise(Stream.runCollect(stream))).toEqual([
|
|
59
55
|
0,
|
|
60
56
|
1,
|
|
@@ -77,10 +73,10 @@ describe("Loop.loop", () => {
|
|
|
77
73
|
});
|
|
78
74
|
it("type: onTurnComplete inside loop infers S and A from the handler without annotation", () => {
|
|
79
75
|
pipe({ turns: 0 }, loop((state) => Effect.gen(function* () {
|
|
80
|
-
return Stream.empty.pipe(onTurnComplete(() => Effect.sync(() => state.turns >= 1 ? stop :
|
|
76
|
+
return Stream.empty.pipe(onTurnComplete(() => Effect.sync(() => state.turns >= 1 ? stop() : Stream.succeed({
|
|
81
77
|
_tag: "tool",
|
|
82
78
|
name: "x"
|
|
83
|
-
}), { turns: state.turns + 1 }))));
|
|
79
|
+
}).pipe(Stream.map(value), Stream.concat(next({ turns: state.turns + 1 }))))));
|
|
84
80
|
})));
|
|
85
81
|
(0, import_dist.expectTypeOf)().toEqualTypeOf();
|
|
86
82
|
});
|
|
@@ -95,8 +91,8 @@ describe("Loop.loop", () => {
|
|
|
95
91
|
} });
|
|
96
92
|
const textDelta = TurnEvent.TextDelta({ text: "hi" });
|
|
97
93
|
const deltas = Stream.fromIterable([textDelta, turnComplete]);
|
|
98
|
-
const dataFirst = onTurnComplete(deltas, () => Effect.sync(() => stop));
|
|
99
|
-
const dataLast = deltas.pipe(onTurnComplete(() => Effect.sync(() => stop)));
|
|
94
|
+
const dataFirst = onTurnComplete(deltas, () => Effect.sync(() => stop()));
|
|
95
|
+
const dataLast = deltas.pipe(onTurnComplete(() => Effect.sync(() => stop())));
|
|
100
96
|
const a = await Effect.runPromise(Stream.runCollect(dataFirst));
|
|
101
97
|
const b = await Effect.runPromise(Stream.runCollect(dataLast));
|
|
102
98
|
globalExpect(a.length).toBe(3);
|
|
@@ -104,7 +100,7 @@ describe("Loop.loop", () => {
|
|
|
104
100
|
});
|
|
105
101
|
it("is stack-safe and linear-time across many iterations", async () => {
|
|
106
102
|
const N = 1e5;
|
|
107
|
-
const stream = loop(0, (n) => n >= N ? Stream.
|
|
103
|
+
const stream = loop(0, (n) => n >= N ? Stream.succeed(value(n)).pipe(Stream.concat(stop())) : Stream.succeed(value(n)).pipe(Stream.concat(next(n + 1))));
|
|
108
104
|
globalExpect(await Effect.runPromise(Stream.runFold(stream, () => 0, (acc) => acc + 1))).toBe(N + 1);
|
|
109
105
|
}, 1e4);
|
|
110
106
|
});
|
|
@@ -122,7 +118,7 @@ const scriptedModel = (script) => {
|
|
|
122
118
|
* 2. flatMap projects deltas into UiEvents forwarded to the outer stream.
|
|
123
119
|
* 3. Continuation reads the captured calls; if any, runs them, emits
|
|
124
120
|
* tool_result events, builds the next state (with model swap if a tool
|
|
125
|
-
* asked for one), and emits `next(state)`. Otherwise `stop`.
|
|
121
|
+
* asked for one), and emits `next(state)`. Otherwise `stop()`.
|
|
126
122
|
*/
|
|
127
123
|
const conversationLoop = (initial, runTool) => loop(initial, (state) => Stream.unwrap(Effect.gen(function* () {
|
|
128
124
|
const textsRef = yield* Ref.make([]);
|
|
@@ -141,7 +137,7 @@ const conversationLoop = (initial, runTool) => loop(initial, (state) => Stream.u
|
|
|
141
137
|
const continuation = Stream.unwrap(Effect.gen(function* () {
|
|
142
138
|
const texts = yield* Ref.get(textsRef);
|
|
143
139
|
const toolCalls = yield* Ref.get(toolCallsRef);
|
|
144
|
-
if (toolCalls.length === 0) return
|
|
140
|
+
if (toolCalls.length === 0) return stop();
|
|
145
141
|
const turnItems = [...texts.length > 0 ? [{
|
|
146
142
|
type: "assistant",
|
|
147
143
|
text: texts.join("")
|
|
@@ -173,7 +169,7 @@ const conversationLoop = (initial, runTool) => loop(initial, (state) => Stream.u
|
|
|
173
169
|
],
|
|
174
170
|
model: nextModel
|
|
175
171
|
};
|
|
176
|
-
return
|
|
172
|
+
return Stream.fromIterable(events).pipe(Stream.map(value), Stream.concat(next(nextState)));
|
|
177
173
|
}));
|
|
178
174
|
return Stream.concat(deltas, continuation);
|
|
179
175
|
})));
|
|
@@ -313,7 +309,7 @@ describe("Loop.loop - pull-specific stream semantics", () => {
|
|
|
313
309
|
it("does not start the next iteration when downstream only takes the first value", async () => {
|
|
314
310
|
globalExpect(await Effect.runPromise(Effect.gen(function* () {
|
|
315
311
|
const callsRef = yield* Ref.make(0);
|
|
316
|
-
yield* loop(0, (n) => Stream.unwrap(Ref.update(callsRef, (calls) => calls + 1).pipe(Effect.as(n >= 10 ? Stream.
|
|
312
|
+
yield* loop(0, (n) => Stream.unwrap(Ref.update(callsRef, (calls) => calls + 1).pipe(Effect.as(n >= 10 ? Stream.succeed(value(n)).pipe(Stream.concat(stop())) : Stream.succeed(value(n)).pipe(Stream.concat(next(n + 1))))))).pipe(Stream.take(1), Stream.runCollect);
|
|
317
313
|
return yield* Ref.get(callsRef);
|
|
318
314
|
}))).toBe(1);
|
|
319
315
|
});
|
|
@@ -325,11 +321,7 @@ describe("Loop.loop - pull-specific stream semantics", () => {
|
|
|
325
321
|
it("runs body finalizers when a Decision short-circuits the body", async () => {
|
|
326
322
|
globalExpect(await Effect.runPromise(Effect.gen(function* () {
|
|
327
323
|
const releasesRef = yield* Ref.make([]);
|
|
328
|
-
const stream = loop(0, (n) => (n >= 1 ? Stream.
|
|
329
|
-
value(n),
|
|
330
|
-
next(n + 1),
|
|
331
|
-
value(n + 10)
|
|
332
|
-
])).pipe(Stream.ensuring(Ref.update(releasesRef, (values) => [...values, n]))));
|
|
324
|
+
const stream = loop(0, (n) => (n >= 1 ? Stream.succeed(value(n)).pipe(Stream.concat(stop())) : Stream.succeed(value(n)).pipe(Stream.concat(next(n + 1)), Stream.concat(Stream.succeed(value(n + 10))))).pipe(Stream.ensuring(Ref.update(releasesRef, (values) => [...values, n]))));
|
|
333
325
|
globalExpect(yield* Stream.runCollect(stream)).toEqual([0, 1]);
|
|
334
326
|
return yield* Ref.get(releasesRef);
|
|
335
327
|
}))).toEqual([0, 1]);
|
|
@@ -356,7 +348,7 @@ describe("Loop.loop - pull-specific stream semantics", () => {
|
|
|
356
348
|
describe("Loop.loopWithState", () => {
|
|
357
349
|
it("exposes the final state in the SubscriptionRef after the stream completes", async () => {
|
|
358
350
|
const program = Effect.gen(function* () {
|
|
359
|
-
const { stream, state } = yield* loopWithState(0, (n) => n >= 3 ?
|
|
351
|
+
const { stream, state } = yield* loopWithState(0, (n) => n >= 3 ? Stream.fromIterable([n]).pipe(Stream.map(value), Stream.concat(stop())) : Stream.fromIterable([n]).pipe(Stream.map(value), Stream.concat(next(n + 1))));
|
|
360
352
|
const values = yield* Stream.runCollect(stream);
|
|
361
353
|
const finalState = yield* SubscriptionRef.get(state);
|
|
362
354
|
return {
|
|
@@ -375,7 +367,7 @@ describe("Loop.loopWithState", () => {
|
|
|
375
367
|
});
|
|
376
368
|
it("the state ref starts at `initial` and stays there if the loop stops without advancing", async () => {
|
|
377
369
|
const program = Effect.gen(function* () {
|
|
378
|
-
const { stream, state } = yield* loopWithState({ count: 7 }, () =>
|
|
370
|
+
const { stream, state } = yield* loopWithState({ count: 7 }, () => stop());
|
|
379
371
|
yield* Stream.runDrain(stream);
|
|
380
372
|
return yield* SubscriptionRef.get(state);
|
|
381
373
|
});
|
|
@@ -383,7 +375,7 @@ describe("Loop.loopWithState", () => {
|
|
|
383
375
|
});
|
|
384
376
|
it("a downstream consumer can read the live state between emitted values", async () => {
|
|
385
377
|
const program = Effect.gen(function* () {
|
|
386
|
-
const { stream, state } = yield* loopWithState(0, (n) => n >= 3 ?
|
|
378
|
+
const { stream, state } = yield* loopWithState(0, (n) => n >= 3 ? Stream.fromIterable([n]).pipe(Stream.map(value), Stream.concat(stop())) : Stream.fromIterable([n]).pipe(Stream.map(value), Stream.concat(next(n + 1))));
|
|
387
379
|
const seen = [];
|
|
388
380
|
yield* Stream.runForEach(stream, (v) => Effect.gen(function* () {
|
|
389
381
|
seen.push({
|
|
@@ -417,7 +409,7 @@ describe("Loop.loopWithState", () => {
|
|
|
417
409
|
const start = yield* Latch.make(false);
|
|
418
410
|
const { stream, state } = yield* loopWithState(0, (n) => Effect.gen(function* () {
|
|
419
411
|
if (n === 0) yield* Latch.await(start);
|
|
420
|
-
return n >= 3 ?
|
|
412
|
+
return n >= 3 ? stop() : next(n + 1);
|
|
421
413
|
}));
|
|
422
414
|
const observerFiber = yield* Effect.forkChild(SubscriptionRef.changes(state).pipe(Stream.take(4), Stream.runCollect));
|
|
423
415
|
yield* Effect.sleep("10 millis");
|
|
@@ -434,7 +426,7 @@ describe("Loop.loopWithState", () => {
|
|
|
434
426
|
});
|
|
435
427
|
it("does not interfere with the body's value stream", async () => {
|
|
436
428
|
const program = Effect.gen(function* () {
|
|
437
|
-
const { stream } = yield* loopWithState(0, (n) => n >= 3 ?
|
|
429
|
+
const { stream } = yield* loopWithState(0, (n) => n >= 3 ? Stream.fromIterable([n]).pipe(Stream.map(value), Stream.concat(stop())) : Stream.fromIterable([n, n + .5]).pipe(Stream.map(value), Stream.concat(next(n + 1))));
|
|
438
430
|
return Array.from(yield* Stream.runCollect(stream));
|
|
439
431
|
});
|
|
440
432
|
globalExpect(await Effect.runPromise(program)).toEqual([
|
|
@@ -448,11 +440,11 @@ describe("Loop.loopWithState", () => {
|
|
|
448
440
|
]);
|
|
449
441
|
});
|
|
450
442
|
});
|
|
451
|
-
describe("Loop.
|
|
443
|
+
describe("Loop.loopOver", () => {
|
|
452
444
|
it("runs a multi-turn inner loop per input until the body emits stop", async () => {
|
|
453
|
-
globalExpect(await Effect.runPromise(Stream.fromIterable(["a", "b"]).pipe(
|
|
454
|
-
if (turns >= 2 * (input === "a" ? 1 : 2)) return
|
|
455
|
-
return Stream.
|
|
445
|
+
globalExpect(await Effect.runPromise(Stream.fromIterable(["a", "b"]).pipe(loopOver(0, (turns, input) => {
|
|
446
|
+
if (turns >= 2 * (input === "a" ? 1 : 2)) return stop();
|
|
447
|
+
return Stream.succeed(value(`${input}:${turns}`)).pipe(Stream.concat(next(turns + 1)));
|
|
456
448
|
}), Stream.runCollect))).toEqual([
|
|
457
449
|
"a:0",
|
|
458
450
|
"a:1",
|
|
@@ -465,41 +457,41 @@ describe("Loop.loopFrom", () => {
|
|
|
465
457
|
"x",
|
|
466
458
|
"y",
|
|
467
459
|
"z"
|
|
468
|
-
]).pipe(
|
|
460
|
+
]).pipe(loopOver([], (history, input) => Stream.succeed(value([...history, input].join(","))).pipe(Stream.concat(stop([...history, input])))), Stream.runCollect))).toEqual([
|
|
469
461
|
"x",
|
|
470
462
|
"x,y",
|
|
471
463
|
"x,y,z"
|
|
472
464
|
]);
|
|
473
465
|
});
|
|
474
466
|
it("simulates a stream of documents with multi-turn tool calls per document", async () => {
|
|
475
|
-
globalExpect(await Effect.runPromise(Stream.fromIterable(["doc1", "doc2"]).pipe(
|
|
467
|
+
globalExpect(await Effect.runPromise(Stream.fromIterable(["doc1", "doc2"]).pipe(loopOver({
|
|
476
468
|
turn: 0,
|
|
477
469
|
totalTurns: 0
|
|
478
470
|
}, (state, doc) => {
|
|
479
|
-
if (state.turn === 0) return Stream.
|
|
471
|
+
if (state.turn === 0) return Stream.succeed(value({
|
|
480
472
|
kind: "text",
|
|
481
473
|
doc,
|
|
482
474
|
text: "thinking"
|
|
483
|
-
})
|
|
475
|
+
})).pipe(Stream.concat(next({
|
|
484
476
|
turn: 1,
|
|
485
477
|
totalTurns: state.totalTurns + 1
|
|
486
|
-
})
|
|
487
|
-
if (state.turn === 1) return Stream.
|
|
478
|
+
})));
|
|
479
|
+
if (state.turn === 1) return Stream.succeed(value({
|
|
488
480
|
kind: "tool",
|
|
489
481
|
doc,
|
|
490
482
|
tool: "search"
|
|
491
|
-
})
|
|
483
|
+
})).pipe(Stream.concat(next({
|
|
492
484
|
turn: 2,
|
|
493
485
|
totalTurns: state.totalTurns + 1
|
|
494
|
-
})
|
|
495
|
-
return Stream.
|
|
486
|
+
})));
|
|
487
|
+
return Stream.succeed(value({
|
|
496
488
|
kind: "text",
|
|
497
489
|
doc,
|
|
498
490
|
text: "final"
|
|
499
|
-
})
|
|
491
|
+
})).pipe(Stream.concat(stop({
|
|
500
492
|
turn: 0,
|
|
501
493
|
totalTurns: state.totalTurns + 1
|
|
502
|
-
})
|
|
494
|
+
})));
|
|
503
495
|
}), Stream.runCollect))).toEqual([
|
|
504
496
|
{
|
|
505
497
|
kind: "text",
|
|
@@ -534,14 +526,14 @@ describe("Loop.loopFrom", () => {
|
|
|
534
526
|
]);
|
|
535
527
|
});
|
|
536
528
|
it("ends cleanly when the input stream ends mid-conversation", async () => {
|
|
537
|
-
globalExpect(await Effect.runPromise(Stream.fromIterable(["only"]).pipe(
|
|
529
|
+
globalExpect(await Effect.runPromise(Stream.fromIterable(["only"]).pipe(loopOver(0, (turns, input) => turns >= 2 ? stop() : Stream.succeed(value(`${input}:${turns}`)).pipe(Stream.concat(next(turns + 1)))), Stream.runCollect))).toEqual(["only:0", "only:1"]);
|
|
538
530
|
});
|
|
539
|
-
it("body's `stop` advances to the next input (does NOT halt the whole stream)", async () => {
|
|
531
|
+
it("body's `stop()` advances to the next input (does NOT halt the whole stream)", async () => {
|
|
540
532
|
globalExpect(await Effect.runPromise(Stream.fromIterable([
|
|
541
533
|
1,
|
|
542
534
|
2,
|
|
543
535
|
3
|
|
544
|
-
]).pipe(
|
|
536
|
+
]).pipe(loopOver(0, (_state, input) => Stream.succeed(value(input * 10)).pipe(Stream.concat(stop()))), Stream.runCollect))).toEqual([
|
|
545
537
|
10,
|
|
546
538
|
20,
|
|
547
539
|
30
|
|
@@ -549,24 +541,24 @@ describe("Loop.loopFrom", () => {
|
|
|
549
541
|
});
|
|
550
542
|
it("data-first form (Function.dual) runs identically to data-last", async () => {
|
|
551
543
|
const inputs = Stream.fromIterable([1, 2]);
|
|
552
|
-
globalExpect(await Effect.runPromise(Stream.runCollect(
|
|
544
|
+
globalExpect(await Effect.runPromise(Stream.runCollect(loopOver(inputs, 0, (state, input) => state >= input ? stop() : Stream.succeed(value(state + input)).pipe(Stream.concat(next(state + 1))))))).toEqual([1, 3]);
|
|
553
545
|
});
|
|
554
546
|
it("supports Effect-returning bodies (parity with loop)", async () => {
|
|
555
|
-
globalExpect(await Effect.runPromise(Stream.fromIterable(["a"]).pipe(
|
|
547
|
+
globalExpect(await Effect.runPromise(Stream.fromIterable(["a"]).pipe(loopOver(0, (turns, input) => Effect.gen(function* () {
|
|
556
548
|
const cur = yield* Effect.succeed(turns);
|
|
557
|
-
if (cur >= 2) return
|
|
558
|
-
return Stream.
|
|
549
|
+
if (cur >= 2) return stop();
|
|
550
|
+
return Stream.succeed(value(`${input}:${cur}`)).pipe(Stream.concat(next(cur + 1)));
|
|
559
551
|
})), Stream.runCollect))).toEqual(["a:0", "a:1"]);
|
|
560
552
|
});
|
|
561
553
|
it("type: data-last (pipe) form preserves the body's E channel", () => {
|
|
562
|
-
pipe(Stream.fromIterable([1]),
|
|
554
|
+
pipe(Stream.fromIterable([1]), loopOver(0, (_state, _input) => Stream.fail(new RateLimited({
|
|
563
555
|
provider: "test",
|
|
564
556
|
raw: null
|
|
565
557
|
}))));
|
|
566
558
|
(0, import_dist.expectTypeOf)().toEqualTypeOf();
|
|
567
559
|
});
|
|
568
560
|
it("type: data-first form preserves the body's E channel and unifies with input's E", () => {
|
|
569
|
-
|
|
561
|
+
loopOver(Stream.fail(/* @__PURE__ */ new Error("boom")), 0, (_state, _i) => Stream.fail(new RateLimited({
|
|
570
562
|
provider: "test",
|
|
571
563
|
raw: null
|
|
572
564
|
})));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Loop.test.mjs","names":["AiError.RateLimited"],"sources":["../../src/loop/Loop.test.ts"],"sourcesContent":["import { Deferred, Effect, Fiber, Latch, pipe, Ref, Stream, SubscriptionRef } from \"effect\"\nimport { describe, expect, expectTypeOf, it } from \"vitest\"\nimport * as AiError from \"../domain/AiError.js\"\nimport { TurnEvent } from \"../domain/Turn.js\"\nimport {\n type Event,\n loop,\n loopFrom,\n loopWithState,\n next,\n nextAfter,\n onTurnComplete,\n stop,\n stopAfter,\n stopEvent,\n stopWith,\n value,\n} from \"./Loop.js\"\n\ndescribe(\"Loop.loop\", () => {\n it(\"threads state across iterations and emits each iteration's substream in order\", async () => {\n // Each iter emits [n, n + 0.5] then continues; final iter emits [n] and stops.\n const stream = loop(0, (n: number) =>\n n >= 3\n ? stopAfter(Stream.fromIterable([n]))\n : nextAfter(Stream.fromIterable([n, n + 0.5]), n + 1),\n )\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([0, 0.5, 1, 1.5, 2, 2.5, 3])\n })\n\n it(\"supports iterations that emit zero values and only decide\", async () => {\n // Every iteration emits nothing, just bumps state; stops at 5.\n const stream = loop(0, (n: number) =>\n n >= 5 ? Stream.fromIterable([stopEvent]) : Stream.fromIterable([next(n + 1)]),\n )\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([])\n })\n\n it(\"supports Effect-returning bodies directly (no Stream.unwrap needed)\", async () => {\n // Each iter yields an Effect that doubles the state, then emits it.\n // Body returns Effect<Stream> directly; loop unwraps internally.\n const stream = loop(1, (n: number) =>\n Effect.gen(function* () {\n const doubled = yield* Effect.succeed(n * 2)\n return doubled >= 16\n ? stopAfter(Stream.fromIterable([doubled]))\n : nextAfter(Stream.fromIterable([doubled]), doubled)\n }),\n )\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([2, 4, 8, 16])\n })\n\n it(\"still accepts Stream.unwrap-wrapped bodies for backward compatibility\", async () => {\n const stream = loop(1, (n: number) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const doubled = yield* Effect.succeed(n * 2)\n return doubled >= 4\n ? stopAfter(Stream.fromIterable([doubled]))\n : nextAfter(Stream.fromIterable([doubled]), doubled)\n }),\n ),\n )\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([2, 4])\n })\n\n it(\"propagates errors from the body's stream\", async () => {\n const boom = new Error(\"boom\")\n const stream = loop(\n 0,\n (n: number): Stream.Stream<Event<number, number>, Error> =>\n n === 2 ? Stream.fail(boom) : Stream.fromIterable([value(n), next(n + 1)]),\n )\n\n const result = await Effect.runPromiseExit(Stream.runCollect(stream))\n expect(result._tag).toBe(\"Failure\")\n })\n\n it(\"terminates silently if the body emits no Decision (mirrors paginate's silent stop)\", async () => {\n // No decision emitted - loop just ends after the body's stream completes.\n const stream = loop(0, (n: number) => Stream.fromIterable([value(n), value(n + 1)]))\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([0, 1])\n })\n\n it(\"short-circuits the body's stream when a Decision is seen\", async () => {\n // Body emits [n, next(n+1), n+10]. Once the Decision is encountered, the\n // body's stream is interrupted - `n+10` is never pulled, so it never\n // flows to the outer stream. This is the correct behavior: a Decision\n // marks \"I'm done with this iteration\"; anything after it is dead code.\n const stream = loop(0, (n: number) =>\n n >= 2\n ? Stream.fromIterable([value(n), stopEvent])\n : Stream.fromIterable([value(n), next(n + 1), value(n + 10)]),\n )\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([0, 1, 2])\n })\n\n it(\"type: data-last (pipe) form preserves the body's E channel\", () => {\n // Regression for the prior inference bug: when used as\n // `pipe(initial, loop(body))`, the body's E must propagate to the outer\n // stream instead of collapsing to `never`. Generics live on the outer\n // return of each overload, so neither calling form can erase them.\n const result = pipe(\n { count: 0 },\n loop((_state) => Stream.fail(new AiError.RateLimited({ provider: \"test\", raw: null }))),\n )\n type E = typeof result extends Stream.Stream<unknown, infer X, unknown> ? X : never\n expectTypeOf<E>().toEqualTypeOf<AiError.RateLimited>()\n })\n\n it(\"type: data-first form preserves the body's E channel\", () => {\n const result = loop({ count: 0 }, (_state) =>\n Stream.fail(new AiError.RateLimited({ provider: \"test\", raw: null })),\n )\n type E = typeof result extends Stream.Stream<unknown, infer X, unknown> ? X : never\n expectTypeOf<E>().toEqualTypeOf<AiError.RateLimited>()\n })\n\n it(\"type: onTurnComplete inside loop infers S and A from the handler without annotation\", () => {\n // Regression: when piped through loop, onTurnComplete's handler return\n // type (Stream<Event<A, S>>) is the single source of truth for the loop\n // body's element type. The previous workaround required explicit\n // <S, A> at the call site. Now the loop's outer-return generics pull\n // them through automatically.\n type LoopState = { readonly turns: number }\n type ToolEvent = { readonly _tag: \"tool\"; readonly name: string }\n\n const result = pipe(\n { turns: 0 } as LoopState,\n loop((state) =>\n Effect.gen(function* () {\n const deltas: Stream.Stream<TurnEvent> = Stream.empty\n return deltas.pipe(\n onTurnComplete(() =>\n Effect.sync(() =>\n state.turns >= 1\n ? stop\n : nextAfter(Stream.succeed<ToolEvent>({ _tag: \"tool\", name: \"x\" }), {\n turns: state.turns + 1,\n }),\n ),\n ),\n )\n }),\n ),\n )\n\n type Element = typeof result extends Stream.Stream<infer X, unknown, unknown> ? X : never\n expectTypeOf<Element>().toEqualTypeOf<TurnEvent | ToolEvent>()\n })\n\n it(\"onTurnComplete: data-first form (Function.dual) works at runtime\", async () => {\n // Pin both calling forms: deltas.pipe(onTurnComplete(handler)) and\n // onTurnComplete(deltas, handler). Same dispatch as loop's dual.\n const turnComplete: TurnEvent = TurnEvent.TurnComplete({\n turn: { items: [], usage: { input_tokens: 0, output_tokens: 0 }, stop_reason: \"stop\" },\n })\n const textDelta: TurnEvent = TurnEvent.TextDelta({ text: \"hi\" })\n const deltas: Stream.Stream<TurnEvent> = Stream.fromIterable([textDelta, turnComplete])\n\n const dataFirst = onTurnComplete(deltas, () => Effect.sync(() => stop))\n const dataLast = deltas.pipe(onTurnComplete(() => Effect.sync(() => stop)))\n\n const a = await Effect.runPromise(Stream.runCollect(dataFirst))\n const b = await Effect.runPromise(Stream.runCollect(dataLast))\n\n // Two value(delta) wraps + one stop sentinel from the handler.\n expect(a.length).toBe(3)\n expect(b.length).toBe(3)\n })\n\n it(\"is stack-safe and linear-time across many iterations\", async () => {\n // 100k iterations far exceeds V8's typical stack depth (~10–15k frames).\n const N = 100_000\n const stream = loop(0, (n: number) =>\n n >= N\n ? Stream.fromIterable([value(n), stopEvent])\n : Stream.fromIterable([value(n), next(n + 1)]),\n )\n\n const count = await Effect.runPromise(\n Stream.runFold(\n stream,\n (): number => 0,\n (acc) => acc + 1,\n ),\n )\n expect(count).toBe(N + 1) // 0..N inclusive\n }, 10_000)\n})\n\n// ---------------------------------------------------------------------------\n// Mock LLM scenario - proves the loop forwards deltas in real time and\n// correctly threads tool-result events between turns.\n// ---------------------------------------------------------------------------\n\ntype Delta =\n | { readonly type: \"text\"; readonly text: string }\n | { readonly type: \"tool_call\"; readonly id: string; readonly name: string }\n\ntype HistoryItem =\n | { readonly type: \"user\"; readonly text: string }\n | { readonly type: \"assistant\"; readonly text: string }\n | { readonly type: \"tool_call\"; readonly id: string; readonly name: string }\n | { readonly type: \"tool_result\"; readonly id: string; readonly output: string }\n\ntype UiEvent =\n | { readonly type: \"text\"; readonly text: string }\n | { readonly type: \"tool_started\"; readonly id: string; readonly name: string }\n | { readonly type: \"tool_result\"; readonly id: string; readonly output: string }\n\ninterface MockModel {\n readonly streamTurn: (history: ReadonlyArray<HistoryItem>) => Stream.Stream<Delta>\n}\n\ninterface State {\n readonly history: ReadonlyArray<HistoryItem>\n readonly model: MockModel\n}\n\ninterface ToolOutcome {\n readonly output: string\n readonly nextModel?: MockModel\n}\n\ntype ToolRunner = (call: { id: string; name: string }) => ToolOutcome\n\nconst scriptedModel = (script: ReadonlyArray<ReadonlyArray<Delta>>): MockModel => {\n let i = 0\n return {\n streamTurn: () => {\n const turn = script[i] ?? []\n i += 1\n return Stream.fromIterable(turn)\n },\n }\n}\n\n/**\n * Body factored out so both tests share it. Per iteration:\n * 1. Stream the model's deltas; tap captures texts + tool calls into Refs.\n * 2. flatMap projects deltas into UiEvents forwarded to the outer stream.\n * 3. Continuation reads the captured calls; if any, runs them, emits\n * tool_result events, builds the next state (with model swap if a tool\n * asked for one), and emits `next(state)`. Otherwise `stop`.\n */\nconst conversationLoop = (initial: State, runTool: ToolRunner) =>\n loop(initial, (state) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const textsRef = yield* Ref.make<ReadonlyArray<string>>([])\n const toolCallsRef = yield* Ref.make<\n ReadonlyArray<{ readonly id: string; readonly name: string }>\n >([])\n\n const deltas: Stream.Stream<Event<UiEvent, State>> = state.model\n .streamTurn(state.history)\n .pipe(\n Stream.tap((d) =>\n d.type === \"text\"\n ? Ref.update(textsRef, (t) => [...t, d.text])\n : Ref.update(toolCallsRef, (t) => [...t, { id: d.id, name: d.name }]),\n ),\n Stream.flatMap(\n (d): Stream.Stream<Event<UiEvent, State>> =>\n d.type === \"text\"\n ? Stream.fromIterable([value<UiEvent>({ type: \"text\", text: d.text })])\n : Stream.fromIterable([\n value<UiEvent>({ type: \"tool_started\", id: d.id, name: d.name }),\n ]),\n ),\n )\n\n const continuation: Stream.Stream<Event<UiEvent, State>> = Stream.unwrap(\n Effect.gen(function* () {\n const texts = yield* Ref.get(textsRef)\n const toolCalls = yield* Ref.get(toolCallsRef)\n\n if (toolCalls.length === 0) {\n return stopAfter(Stream.empty)\n }\n\n const turnItems: ReadonlyArray<HistoryItem> = [\n ...(texts.length > 0 ? [{ type: \"assistant\" as const, text: texts.join(\"\") }] : []),\n ...toolCalls.map(\n (tc): HistoryItem => ({ type: \"tool_call\", id: tc.id, name: tc.name }),\n ),\n ]\n\n const outcomes = toolCalls.map((call) => ({ call, outcome: runTool(call) }))\n\n const events: ReadonlyArray<UiEvent> = outcomes.map(({ call, outcome }) => ({\n type: \"tool_result\",\n id: call.id,\n output: outcome.output,\n }))\n\n const resultItems: ReadonlyArray<HistoryItem> = outcomes.map(\n ({ call, outcome }): HistoryItem => ({\n type: \"tool_result\",\n id: call.id,\n output: outcome.output,\n }),\n )\n\n // Last requested model wins; default to the current one.\n const nextModel = outcomes.reduce(\n (m, { outcome }) => outcome.nextModel ?? m,\n state.model,\n )\n\n const nextState: State = {\n history: [...state.history, ...turnItems, ...resultItems],\n model: nextModel,\n }\n\n return nextAfter(Stream.fromIterable(events), nextState)\n }),\n )\n\n return Stream.concat(deltas, continuation)\n }),\n ),\n )\n\ndescribe(\"Loop.loop - LLM-style scenarios\", () => {\n it(\"forwards text deltas, tool start, tool result, and post-tool text in order\", async () => {\n const m = scriptedModel([\n [\n { type: \"text\", text: \"hello\" },\n { type: \"text\", text: \" \" },\n { type: \"text\", text: \"world\" },\n { type: \"tool_call\", id: \"c1\", name: \"get_time\" },\n ],\n [\n { type: \"text\", text: \" time is \" },\n { type: \"text\", text: \"12:00\" },\n ],\n ])\n\n const runTool: ToolRunner = (call) => ({\n output: call.name === \"get_time\" ? \"12:00\" : \"?\",\n })\n\n const initial: State = {\n history: [{ type: \"user\", text: \"what time is it?\" }],\n model: m,\n }\n\n const events = await Effect.runPromise(Stream.runCollect(conversationLoop(initial, runTool)))\n\n expect(events).toEqual([\n { type: \"text\", text: \"hello\" },\n { type: \"text\", text: \" \" },\n { type: \"text\", text: \"world\" },\n { type: \"tool_started\", id: \"c1\", name: \"get_time\" },\n { type: \"tool_result\", id: \"c1\", output: \"12:00\" },\n { type: \"text\", text: \" time is \" },\n { type: \"text\", text: \"12:00\" },\n ])\n })\n\n it(\"model swap mid-stream: m1 calls upgrade, m2 finishes the response\", async () => {\n const m2 = scriptedModel([\n [\n { type: \"text\", text: \"I am m2.\" },\n { type: \"text\", text: \" The answer is 42.\" },\n ],\n ])\n\n const m1 = scriptedModel([\n [\n { type: \"text\", text: \"Hard question.\" },\n { type: \"text\", text: \" Upgrading.\" },\n { type: \"tool_call\", id: \"u1\", name: \"upgrade\" },\n ],\n ])\n\n const runTool: ToolRunner = (call) =>\n call.name === \"upgrade\" ? { output: \"ok\", nextModel: m2 } : { output: \"?\" }\n\n const initial: State = {\n history: [{ type: \"user\", text: \"what is the meaning of life?\" }],\n model: m1,\n }\n\n const events = await Effect.runPromise(Stream.runCollect(conversationLoop(initial, runTool)))\n\n expect(events).toEqual([\n { type: \"text\", text: \"Hard question.\" },\n { type: \"text\", text: \" Upgrading.\" },\n { type: \"tool_started\", id: \"u1\", name: \"upgrade\" },\n { type: \"tool_result\", id: \"u1\", output: \"ok\" },\n { type: \"text\", text: \"I am m2.\" },\n { type: \"text\", text: \" The answer is 42.\" },\n ])\n })\n})\n\ndescribe(\"Loop.loop - pull-specific stream semantics\", () => {\n it(\"does not start the next iteration when downstream only takes the first value\", async () => {\n const bodyCalls = await Effect.runPromise(\n Effect.gen(function* () {\n const callsRef = yield* Ref.make(0)\n const stream = loop(0, (n: number) =>\n Stream.unwrap(\n Ref.update(callsRef, (calls) => calls + 1).pipe(\n Effect.as(\n n >= 10\n ? Stream.fromIterable([value(n), stopEvent])\n : Stream.fromIterable([value(n), next(n + 1)]),\n ),\n ),\n ),\n )\n\n yield* stream.pipe(Stream.take(1), Stream.runCollect)\n return yield* Ref.get(callsRef)\n }),\n )\n\n expect(bodyCalls).toBe(1)\n })\n\n it(\"propagates defects from the body instead of leaving the consumer waiting\", async () => {\n const defect = new Error(\"defect\")\n const stream = loop(0, () => Stream.die(defect))\n\n const result = await Effect.runPromiseExit(Stream.runCollect(stream))\n\n expect(result._tag).toBe(\"Failure\")\n })\n\n it(\"runs body finalizers when a Decision short-circuits the body\", async () => {\n const releases = await Effect.runPromise(\n Effect.gen(function* () {\n const releasesRef = yield* Ref.make<ReadonlyArray<number>>([])\n const stream = loop(0, (n: number) =>\n (n >= 1\n ? Stream.fromIterable([value(n), stopEvent])\n : Stream.fromIterable([value(n), next(n + 1), value(n + 10)])\n ).pipe(Stream.ensuring(Ref.update(releasesRef, (values) => [...values, n]))),\n )\n\n const values = yield* Stream.runCollect(stream)\n expect(values).toEqual([0, 1])\n return yield* Ref.get(releasesRef)\n }),\n )\n\n expect(releases).toEqual([0, 1])\n })\n\n it(\"runs the active body finalizer when the downstream consumer is interrupted\", async () => {\n const releases = await Effect.runPromise(\n Effect.gen(function* () {\n const started = yield* Deferred.make<void>()\n const releasesRef = yield* Ref.make(0)\n const body = (): Stream.Stream<Event<number, never>> =>\n Stream.concat(\n Stream.fromEffect(Deferred.succeed(started, undefined).pipe(Effect.as(value(0)))),\n Stream.never,\n ).pipe(Stream.ensuring(Ref.update(releasesRef, (n) => n + 1)))\n const stream = loop(0, body)\n\n const fiber = yield* Effect.forkChild(Stream.runCollect(stream))\n yield* Deferred.await(started)\n yield* Fiber.interrupt(fiber)\n\n return yield* Ref.get(releasesRef)\n }),\n )\n\n expect(releases).toBe(1)\n })\n\n it(\"does not create a body scope if constructing the body stream defects\", async () => {\n const defect = new Error(\"body construction failed\")\n const result = await Effect.runPromiseExit(\n Stream.runCollect(\n loop(0, (): Stream.Stream<Event<number, never>> => {\n throw defect\n }),\n ),\n )\n\n expect(result._tag).toBe(\"Failure\")\n })\n})\n\ndescribe(\"Loop.loopWithState\", () => {\n it(\"exposes the final state in the SubscriptionRef after the stream completes\", async () => {\n const program = Effect.gen(function* () {\n const { stream, state } = yield* loopWithState(0, (n: number) =>\n n >= 3 ? stopAfter(Stream.fromIterable([n])) : nextAfter(Stream.fromIterable([n]), n + 1),\n )\n const values = yield* Stream.runCollect(stream)\n const finalState = yield* SubscriptionRef.get(state)\n return { values: Array.from(values), finalState }\n })\n\n const { values, finalState } = await Effect.runPromise(program)\n expect(values).toEqual([0, 1, 2, 3])\n // Last `next(state)` was `next(3)` before the iteration that emitted Stop.\n expect(finalState).toBe(3)\n })\n\n it(\"the state ref starts at `initial` and stays there if the loop stops without advancing\", async () => {\n const program = Effect.gen(function* () {\n const { stream, state } = yield* loopWithState({ count: 7 }, () =>\n Stream.fromIterable([stopEvent]),\n )\n yield* Stream.runDrain(stream)\n return yield* SubscriptionRef.get(state)\n })\n\n expect(await Effect.runPromise(program)).toEqual({ count: 7 })\n })\n\n it(\"a downstream consumer can read the live state between emitted values\", async () => {\n // Body emits one value per iteration, then advances. A `Stream.runForEach`\n // consumer reads the ref each time a value arrives — proving the ref\n // tracks loop state without the body needing to surface it.\n const program = Effect.gen(function* () {\n const { stream, state } = yield* loopWithState(0, (n: number) =>\n n >= 3 ? stopAfter(Stream.fromIterable([n])) : nextAfter(Stream.fromIterable([n]), n + 1),\n )\n const seen: Array<{ value: number; stateAfter: number }> = []\n yield* Stream.runForEach(stream, (v) =>\n Effect.gen(function* () {\n seen.push({ value: v, stateAfter: yield* SubscriptionRef.get(state) })\n }),\n )\n return seen\n })\n\n // For each iter `n`, the consumer reads the ref between values: it sees\n // the iteration's input state. The terminal iter (n=3) stops without\n // advancing, so its read still shows 3.\n expect(await Effect.runPromise(program)).toEqual([\n { value: 0, stateAfter: 0 },\n { value: 1, stateAfter: 1 },\n { value: 2, stateAfter: 2 },\n { value: 3, stateAfter: 3 },\n ])\n })\n\n it(\"SubscriptionRef.changes emits every state transition to a concurrent observer\", async () => {\n const program = Effect.gen(function* () {\n const start = yield* Latch.make(false)\n\n // Body waits on the latch in iter 0 so the observer can subscribe first.\n const { stream, state } = yield* loopWithState(0, (n: number) =>\n Effect.gen(function* () {\n if (n === 0) yield* Latch.await(start)\n return n >= 3 ? stopAfter(Stream.empty) : nextAfter(Stream.empty, n + 1)\n }),\n )\n\n // Fork the observer; take 4 distinct states (initial + 3 transitions).\n const observerFiber = yield* Effect.forkChild(\n SubscriptionRef.changes(state).pipe(Stream.take(4), Stream.runCollect),\n )\n\n // Give the observer fiber a chance to actually subscribe before the\n // loop starts advancing the ref. Without this, the loop could finish\n // before the observer's pubsub subscription is in place.\n yield* Effect.sleep(\"10 millis\")\n\n yield* Latch.open(start)\n yield* Stream.runDrain(stream)\n\n return Array.from(yield* Fiber.join(observerFiber))\n })\n\n // initial 0, then next(1), next(2), next(3) — four distinct states.\n expect(await Effect.runPromise(program)).toEqual([0, 1, 2, 3])\n })\n\n it(\"does not interfere with the body's value stream\", async () => {\n const program = Effect.gen(function* () {\n const { stream } = yield* loopWithState(0, (n: number) =>\n n >= 3\n ? stopAfter(Stream.fromIterable([n]))\n : nextAfter(Stream.fromIterable([n, n + 0.5]), n + 1),\n )\n return Array.from(yield* Stream.runCollect(stream))\n })\n\n expect(await Effect.runPromise(program)).toEqual([0, 0.5, 1, 1.5, 2, 2.5, 3])\n })\n})\n\ndescribe(\"Loop.loopFrom\", () => {\n it(\"runs a multi-turn inner loop per input until the body emits stop\", async () => {\n // Per input: emit (input + turnsSoFar) twice, then stop. State counts\n // total turns ACROSS inputs. Demonstrates that `next(s)` continues with\n // the SAME input, multiple times per input — not one body call per item.\n const result = await Effect.runPromise(\n Stream.fromIterable([\"a\", \"b\"]).pipe(\n loopFrom(0, (turns: number, input: string) => {\n if (turns >= 2 * (input === \"a\" ? 1 : 2)) return Stream.fromIterable([stopEvent])\n return Stream.fromIterable([value(`${input}:${turns}`), next(turns + 1)])\n }),\n Stream.runCollect,\n ),\n )\n\n // input=\"a\": turns 0,1 → emit \"a:0\",\"a:1\"; turns=2 → stop. State threads.\n // input=\"b\": turns 2,3 → emit \"b:2\",\"b:3\"; turns=4 → stop.\n expect(result).toEqual([\"a:0\", \"a:1\", \"b:2\", \"b:3\"])\n })\n\n it(\"threads state across inputs (audio-pipeline shape)\", async () => {\n // History accumulates across inputs. Each input emits its joined view of\n // history+input, then `stopWith` ends the inner loop AND carries the\n // updated history to the next input.\n const result = await Effect.runPromise(\n Stream.fromIterable([\"x\", \"y\", \"z\"]).pipe(\n loopFrom([] as ReadonlyArray<string>, (history: ReadonlyArray<string>, input: string) =>\n Stream.fromIterable([\n value([...history, input].join(\",\")),\n stopWith([...history, input]),\n ]),\n ),\n Stream.runCollect,\n ),\n )\n\n expect(result).toEqual([\"x\", \"x,y\", \"x,y,z\"])\n })\n\n it(\"simulates a stream of documents with multi-turn tool calls per document\", async () => {\n // Document arrives → model \"thinks\" (one text turn) → calls a tool\n // (one tool turn) → emits final text (one text turn) → done.\n // Three turns per document, two documents.\n type Turn =\n | { readonly kind: \"text\"; readonly doc: string; readonly text: string }\n | { readonly kind: \"tool\"; readonly doc: string; readonly tool: string }\n type State = { readonly turn: number; readonly totalTurns: number }\n\n const result = await Effect.runPromise(\n Stream.fromIterable([\"doc1\", \"doc2\"]).pipe(\n loopFrom({ turn: 0, totalTurns: 0 } as State, (state, doc: string) => {\n // Each document runs three turns then stops.\n if (state.turn === 0) {\n return Stream.fromIterable([\n value<Turn>({ kind: \"text\", doc, text: \"thinking\" }),\n next({ turn: 1, totalTurns: state.totalTurns + 1 }),\n ])\n }\n if (state.turn === 1) {\n return Stream.fromIterable([\n value<Turn>({ kind: \"tool\", doc, tool: \"search\" }),\n next({ turn: 2, totalTurns: state.totalTurns + 1 }),\n ])\n }\n // Final turn — `stopWith` emits the final value, advances state\n // (reset turn to 0 for the next document, bump totalTurns), and\n // ends this document's inner loop in one shot.\n return Stream.fromIterable([\n value<Turn>({ kind: \"text\", doc, text: \"final\" }),\n stopWith({ turn: 0, totalTurns: state.totalTurns + 1 }),\n ])\n }),\n Stream.runCollect,\n ),\n )\n\n expect(result).toEqual([\n { kind: \"text\", doc: \"doc1\", text: \"thinking\" },\n { kind: \"tool\", doc: \"doc1\", tool: \"search\" },\n { kind: \"text\", doc: \"doc1\", text: \"final\" },\n { kind: \"text\", doc: \"doc2\", text: \"thinking\" },\n { kind: \"tool\", doc: \"doc2\", tool: \"search\" },\n { kind: \"text\", doc: \"doc2\", text: \"final\" },\n ])\n })\n\n it(\"ends cleanly when the input stream ends mid-conversation\", async () => {\n // Single-input case: body advances via `next` then stops cleanly.\n const result = await Effect.runPromise(\n Stream.fromIterable([\"only\"]).pipe(\n loopFrom(0, (turns: number, input: string) =>\n turns >= 2\n ? Stream.fromIterable([stopEvent])\n : Stream.fromIterable([value(`${input}:${turns}`), next(turns + 1)]),\n ),\n Stream.runCollect,\n ),\n )\n\n expect(result).toEqual([\"only:0\", \"only:1\"])\n })\n\n it(\"body's `stop` advances to the next input (does NOT halt the whole stream)\", async () => {\n // Three inputs, body always stops on its first emission. All three\n // are processed — `stop` is per-input, not global. To halt the whole\n // stream, end the INPUT stream upstream.\n const result = await Effect.runPromise(\n Stream.fromIterable([1, 2, 3]).pipe(\n loopFrom(0, (_state: number, input: number) =>\n Stream.fromIterable([value(input * 10), stopEvent]),\n ),\n Stream.runCollect,\n ),\n )\n\n expect(result).toEqual([10, 20, 30])\n })\n\n it(\"data-first form (Function.dual) runs identically to data-last\", async () => {\n const inputs = Stream.fromIterable([1, 2])\n const result = await Effect.runPromise(\n Stream.runCollect(\n loopFrom(inputs, 0, (state: number, input: number) =>\n state >= input\n ? Stream.fromIterable([stopEvent])\n : Stream.fromIterable([value(state + input), next(state + 1)]),\n ),\n ),\n )\n\n // input=1: state=0 → emit 1, state→1; state=1≥1 → stop.\n // input=2: state=1 → emit 3, state→2; state=2≥2 → stop.\n expect(result).toEqual([1, 3])\n })\n\n it(\"supports Effect-returning bodies (parity with loop)\", async () => {\n const result = await Effect.runPromise(\n Stream.fromIterable([\"a\"]).pipe(\n loopFrom(0, (turns: number, input: string) =>\n Effect.gen(function* () {\n const cur = yield* Effect.succeed(turns)\n if (cur >= 2) return Stream.fromIterable([stopEvent])\n return Stream.fromIterable([value(`${input}:${cur}`), next(cur + 1)])\n }),\n ),\n Stream.runCollect,\n ),\n )\n\n expect(result).toEqual([\"a:0\", \"a:1\"])\n })\n\n it(\"type: data-last (pipe) form preserves the body's E channel\", () => {\n const result = pipe(\n Stream.fromIterable([1]),\n loopFrom(0, (_state: number, _input: number) =>\n Stream.fail(new AiError.RateLimited({ provider: \"test\", raw: null })),\n ),\n )\n type E = typeof result extends Stream.Stream<unknown, infer X, unknown> ? X : never\n expectTypeOf<E>().toEqualTypeOf<AiError.RateLimited>()\n })\n\n it(\"type: data-first form preserves the body's E channel and unifies with input's E\", () => {\n const input: Stream.Stream<number, Error> = Stream.fail(new Error(\"boom\"))\n const result = loopFrom(input, 0, (_state: number, _i: number) =>\n Stream.fail(new AiError.RateLimited({ provider: \"test\", raw: null })),\n )\n type E = typeof result extends Stream.Stream<unknown, infer X, unknown> ? X : never\n expectTypeOf<E>().toEqualTypeOf<AiError.RateLimited | Error>()\n })\n})\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Loop.test.mjs","names":["AiError.RateLimited"],"sources":["../../src/loop/Loop.test.ts"],"sourcesContent":["import { Deferred, Effect, Fiber, Latch, pipe, Ref, Stream, SubscriptionRef } from \"effect\"\nimport { describe, expect, expectTypeOf, it } from \"vitest\"\nimport * as AiError from \"../domain/AiError.js\"\nimport { TurnEvent } from \"../domain/Turn.js\"\nimport {\n type Step,\n loop,\n loopOver,\n loopWithState,\n next,\n onTurnComplete,\n stop,\n value,\n} from \"./Loop.js\"\n\ndescribe(\"Loop.loop\", () => {\n it(\"threads state across iterations and emits each iteration's substream in order\", async () => {\n // Each iter emits [n, n + 0.5] then continues; final iter emits [n] and stops.\n const stream = loop(0, (n: number) =>\n n >= 3\n ? Stream.fromIterable([n]).pipe(Stream.map(value), Stream.concat(stop()))\n : Stream.fromIterable([n, n + 0.5]).pipe(Stream.map(value), Stream.concat(next(n + 1))),\n )\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([0, 0.5, 1, 1.5, 2, 2.5, 3])\n })\n\n it(\"supports iterations that emit zero values and only decide\", async () => {\n // Every iteration emits nothing, just bumps state; stops at 5.\n const stream = loop(0, (n: number) => (n >= 5 ? stop() : next(n + 1)))\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([])\n })\n\n it(\"supports Effect-returning bodies directly (no Stream.unwrap needed)\", async () => {\n // Each iter yields an Effect that doubles the state, then emits it.\n // Body returns Effect<Stream> directly; loop unwraps internally.\n const stream = loop(1, (n: number) =>\n Effect.gen(function* () {\n const doubled = yield* Effect.succeed(n * 2)\n return doubled >= 16\n ? Stream.fromIterable([doubled]).pipe(Stream.map(value), Stream.concat(stop()))\n : Stream.fromIterable([doubled]).pipe(Stream.map(value), Stream.concat(next(doubled)))\n }),\n )\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([2, 4, 8, 16])\n })\n\n it(\"still accepts Stream.unwrap-wrapped bodies for backward compatibility\", async () => {\n const stream = loop(1, (n: number) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const doubled = yield* Effect.succeed(n * 2)\n return doubled >= 4\n ? Stream.fromIterable([doubled]).pipe(Stream.map(value), Stream.concat(stop()))\n : Stream.fromIterable([doubled]).pipe(Stream.map(value), Stream.concat(next(doubled)))\n }),\n ),\n )\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([2, 4])\n })\n\n it(\"propagates errors from the body's stream\", async () => {\n const boom = new Error(\"boom\")\n const stream = loop(\n 0,\n (n: number): Stream.Stream<Step<number, number>, Error> =>\n n === 2 ? Stream.fail(boom) : Stream.succeed(value(n)).pipe(Stream.concat(next(n + 1))),\n )\n\n const result = await Effect.runPromiseExit(Stream.runCollect(stream))\n expect(result._tag).toBe(\"Failure\")\n })\n\n it(\"terminates silently if the body emits no Decision (mirrors paginate's silent stop)\", async () => {\n // No decision emitted - loop just ends after the body's stream completes.\n const stream = loop(0, (n: number) => Stream.fromIterable([value(n), value(n + 1)]))\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([0, 1])\n })\n\n it(\"short-circuits the body's stream when a Decision is seen\", async () => {\n // Body emits value(n), then a Next decision, then value(n+10). Once the\n // decision is encountered, the body is closed - `n+10` is never pulled, so\n // it never flows to the outer stream. This is the correct behavior: a\n // decision marks \"I'm done with this iteration\"; anything after it is dead.\n const stream = loop(0, (n: number) =>\n n >= 2\n ? Stream.succeed(value(n)).pipe(Stream.concat(stop()))\n : Stream.succeed(value(n)).pipe(\n Stream.concat(next(n + 1)),\n Stream.concat(Stream.succeed(value(n + 10))),\n ),\n )\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([0, 1, 2])\n })\n\n it(\"type: data-last (pipe) form preserves the body's E channel\", () => {\n // Regression for the prior inference bug: when used as\n // `pipe(initial, loop(body))`, the body's E must propagate to the outer\n // stream instead of collapsing to `never`. Generics live on the outer\n // return of each overload, so neither calling form can erase them.\n const result = pipe(\n { count: 0 },\n loop((_state) => Stream.fail(new AiError.RateLimited({ provider: \"test\", raw: null }))),\n )\n type E = typeof result extends Stream.Stream<unknown, infer X, unknown> ? X : never\n expectTypeOf<E>().toEqualTypeOf<AiError.RateLimited>()\n })\n\n it(\"type: data-first form preserves the body's E channel\", () => {\n const result = loop({ count: 0 }, (_state) =>\n Stream.fail(new AiError.RateLimited({ provider: \"test\", raw: null })),\n )\n type E = typeof result extends Stream.Stream<unknown, infer X, unknown> ? X : never\n expectTypeOf<E>().toEqualTypeOf<AiError.RateLimited>()\n })\n\n it(\"type: onTurnComplete inside loop infers S and A from the handler without annotation\", () => {\n // Regression: when piped through loop, onTurnComplete's handler return\n // type (Stream<Step<A, S>>) is the single source of truth for the loop\n // body's element type. The previous workaround required explicit\n // <S, A> at the call site. Now the loop's outer-return generics pull\n // them through automatically.\n type LoopState = { readonly turns: number }\n type ToolEvent = { readonly _tag: \"tool\"; readonly name: string }\n\n const result = pipe(\n { turns: 0 } as LoopState,\n loop((state) =>\n Effect.gen(function* () {\n const deltas: Stream.Stream<TurnEvent> = Stream.empty\n return deltas.pipe(\n onTurnComplete(() =>\n Effect.sync(() =>\n state.turns >= 1\n ? stop()\n : Stream.succeed<ToolEvent>({ _tag: \"tool\", name: \"x\" }).pipe(\n Stream.map(value),\n Stream.concat(next({ turns: state.turns + 1 })),\n ),\n ),\n ),\n )\n }),\n ),\n )\n\n type Element = typeof result extends Stream.Stream<infer X, unknown, unknown> ? X : never\n expectTypeOf<Element>().toEqualTypeOf<TurnEvent | ToolEvent>()\n })\n\n it(\"onTurnComplete: data-first form (Function.dual) works at runtime\", async () => {\n // Pin both calling forms: deltas.pipe(onTurnComplete(handler)) and\n // onTurnComplete(deltas, handler). Same dispatch as loop's dual.\n const turnComplete: TurnEvent = TurnEvent.TurnComplete({\n turn: { items: [], usage: { input_tokens: 0, output_tokens: 0 }, stop_reason: \"stop\" },\n })\n const textDelta: TurnEvent = TurnEvent.TextDelta({ text: \"hi\" })\n const deltas: Stream.Stream<TurnEvent> = Stream.fromIterable([textDelta, turnComplete])\n\n const dataFirst = onTurnComplete(deltas, () => Effect.sync(() => stop()))\n const dataLast = deltas.pipe(onTurnComplete(() => Effect.sync(() => stop())))\n\n const a = await Effect.runPromise(Stream.runCollect(dataFirst))\n const b = await Effect.runPromise(Stream.runCollect(dataLast))\n\n // Two value(delta) wraps + one stop sentinel from the handler.\n expect(a.length).toBe(3)\n expect(b.length).toBe(3)\n })\n\n it(\"is stack-safe and linear-time across many iterations\", async () => {\n // 100k iterations far exceeds V8's typical stack depth (~10–15k frames).\n const N = 100_000\n const stream = loop(0, (n: number) =>\n n >= N\n ? Stream.succeed(value(n)).pipe(Stream.concat(stop()))\n : Stream.succeed(value(n)).pipe(Stream.concat(next(n + 1))),\n )\n\n const count = await Effect.runPromise(\n Stream.runFold(\n stream,\n (): number => 0,\n (acc) => acc + 1,\n ),\n )\n expect(count).toBe(N + 1) // 0..N inclusive\n }, 10_000)\n})\n\n// ---------------------------------------------------------------------------\n// Mock LLM scenario - proves the loop forwards deltas in real time and\n// correctly threads tool-result events between turns.\n// ---------------------------------------------------------------------------\n\ntype Delta =\n | { readonly type: \"text\"; readonly text: string }\n | { readonly type: \"tool_call\"; readonly id: string; readonly name: string }\n\ntype HistoryItem =\n | { readonly type: \"user\"; readonly text: string }\n | { readonly type: \"assistant\"; readonly text: string }\n | { readonly type: \"tool_call\"; readonly id: string; readonly name: string }\n | { readonly type: \"tool_result\"; readonly id: string; readonly output: string }\n\ntype UiEvent =\n | { readonly type: \"text\"; readonly text: string }\n | { readonly type: \"tool_started\"; readonly id: string; readonly name: string }\n | { readonly type: \"tool_result\"; readonly id: string; readonly output: string }\n\ninterface MockModel {\n readonly streamTurn: (history: ReadonlyArray<HistoryItem>) => Stream.Stream<Delta>\n}\n\ninterface State {\n readonly history: ReadonlyArray<HistoryItem>\n readonly model: MockModel\n}\n\ninterface ToolOutcome {\n readonly output: string\n readonly nextModel?: MockModel\n}\n\ntype ToolRunner = (call: { id: string; name: string }) => ToolOutcome\n\nconst scriptedModel = (script: ReadonlyArray<ReadonlyArray<Delta>>): MockModel => {\n let i = 0\n return {\n streamTurn: () => {\n const turn = script[i] ?? []\n i += 1\n return Stream.fromIterable(turn)\n },\n }\n}\n\n/**\n * Body factored out so both tests share it. Per iteration:\n * 1. Stream the model's deltas; tap captures texts + tool calls into Refs.\n * 2. flatMap projects deltas into UiEvents forwarded to the outer stream.\n * 3. Continuation reads the captured calls; if any, runs them, emits\n * tool_result events, builds the next state (with model swap if a tool\n * asked for one), and emits `next(state)`. Otherwise `stop()`.\n */\nconst conversationLoop = (initial: State, runTool: ToolRunner) =>\n loop(initial, (state) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const textsRef = yield* Ref.make<ReadonlyArray<string>>([])\n const toolCallsRef = yield* Ref.make<\n ReadonlyArray<{ readonly id: string; readonly name: string }>\n >([])\n\n const deltas: Stream.Stream<Step<UiEvent, State>> = state.model\n .streamTurn(state.history)\n .pipe(\n Stream.tap((d) =>\n d.type === \"text\"\n ? Ref.update(textsRef, (t) => [...t, d.text])\n : Ref.update(toolCallsRef, (t) => [...t, { id: d.id, name: d.name }]),\n ),\n Stream.flatMap(\n (d): Stream.Stream<Step<UiEvent, State>> =>\n d.type === \"text\"\n ? Stream.fromIterable([value<UiEvent>({ type: \"text\", text: d.text })])\n : Stream.fromIterable([\n value<UiEvent>({ type: \"tool_started\", id: d.id, name: d.name }),\n ]),\n ),\n )\n\n const continuation: Stream.Stream<Step<UiEvent, State>> = Stream.unwrap(\n Effect.gen(function* () {\n const texts = yield* Ref.get(textsRef)\n const toolCalls = yield* Ref.get(toolCallsRef)\n\n if (toolCalls.length === 0) {\n return stop()\n }\n\n const turnItems: ReadonlyArray<HistoryItem> = [\n ...(texts.length > 0 ? [{ type: \"assistant\" as const, text: texts.join(\"\") }] : []),\n ...toolCalls.map(\n (tc): HistoryItem => ({ type: \"tool_call\", id: tc.id, name: tc.name }),\n ),\n ]\n\n const outcomes = toolCalls.map((call) => ({ call, outcome: runTool(call) }))\n\n const events: ReadonlyArray<UiEvent> = outcomes.map(({ call, outcome }) => ({\n type: \"tool_result\",\n id: call.id,\n output: outcome.output,\n }))\n\n const resultItems: ReadonlyArray<HistoryItem> = outcomes.map(\n ({ call, outcome }): HistoryItem => ({\n type: \"tool_result\",\n id: call.id,\n output: outcome.output,\n }),\n )\n\n // Last requested model wins; default to the current one.\n const nextModel = outcomes.reduce(\n (m, { outcome }) => outcome.nextModel ?? m,\n state.model,\n )\n\n const nextState: State = {\n history: [...state.history, ...turnItems, ...resultItems],\n model: nextModel,\n }\n\n return Stream.fromIterable(events).pipe(\n Stream.map(value),\n Stream.concat(next(nextState)),\n )\n }),\n )\n\n return Stream.concat(deltas, continuation)\n }),\n ),\n )\n\ndescribe(\"Loop.loop - LLM-style scenarios\", () => {\n it(\"forwards text deltas, tool start, tool result, and post-tool text in order\", async () => {\n const m = scriptedModel([\n [\n { type: \"text\", text: \"hello\" },\n { type: \"text\", text: \" \" },\n { type: \"text\", text: \"world\" },\n { type: \"tool_call\", id: \"c1\", name: \"get_time\" },\n ],\n [\n { type: \"text\", text: \" time is \" },\n { type: \"text\", text: \"12:00\" },\n ],\n ])\n\n const runTool: ToolRunner = (call) => ({\n output: call.name === \"get_time\" ? \"12:00\" : \"?\",\n })\n\n const initial: State = {\n history: [{ type: \"user\", text: \"what time is it?\" }],\n model: m,\n }\n\n const events = await Effect.runPromise(Stream.runCollect(conversationLoop(initial, runTool)))\n\n expect(events).toEqual([\n { type: \"text\", text: \"hello\" },\n { type: \"text\", text: \" \" },\n { type: \"text\", text: \"world\" },\n { type: \"tool_started\", id: \"c1\", name: \"get_time\" },\n { type: \"tool_result\", id: \"c1\", output: \"12:00\" },\n { type: \"text\", text: \" time is \" },\n { type: \"text\", text: \"12:00\" },\n ])\n })\n\n it(\"model swap mid-stream: m1 calls upgrade, m2 finishes the response\", async () => {\n const m2 = scriptedModel([\n [\n { type: \"text\", text: \"I am m2.\" },\n { type: \"text\", text: \" The answer is 42.\" },\n ],\n ])\n\n const m1 = scriptedModel([\n [\n { type: \"text\", text: \"Hard question.\" },\n { type: \"text\", text: \" Upgrading.\" },\n { type: \"tool_call\", id: \"u1\", name: \"upgrade\" },\n ],\n ])\n\n const runTool: ToolRunner = (call) =>\n call.name === \"upgrade\" ? { output: \"ok\", nextModel: m2 } : { output: \"?\" }\n\n const initial: State = {\n history: [{ type: \"user\", text: \"what is the meaning of life?\" }],\n model: m1,\n }\n\n const events = await Effect.runPromise(Stream.runCollect(conversationLoop(initial, runTool)))\n\n expect(events).toEqual([\n { type: \"text\", text: \"Hard question.\" },\n { type: \"text\", text: \" Upgrading.\" },\n { type: \"tool_started\", id: \"u1\", name: \"upgrade\" },\n { type: \"tool_result\", id: \"u1\", output: \"ok\" },\n { type: \"text\", text: \"I am m2.\" },\n { type: \"text\", text: \" The answer is 42.\" },\n ])\n })\n})\n\ndescribe(\"Loop.loop - pull-specific stream semantics\", () => {\n it(\"does not start the next iteration when downstream only takes the first value\", async () => {\n const bodyCalls = await Effect.runPromise(\n Effect.gen(function* () {\n const callsRef = yield* Ref.make(0)\n const stream = loop(0, (n: number) =>\n Stream.unwrap(\n Ref.update(callsRef, (calls) => calls + 1).pipe(\n Effect.as(\n n >= 10\n ? Stream.succeed(value(n)).pipe(Stream.concat(stop()))\n : Stream.succeed(value(n)).pipe(Stream.concat(next(n + 1))),\n ),\n ),\n ),\n )\n\n yield* stream.pipe(Stream.take(1), Stream.runCollect)\n return yield* Ref.get(callsRef)\n }),\n )\n\n expect(bodyCalls).toBe(1)\n })\n\n it(\"propagates defects from the body instead of leaving the consumer waiting\", async () => {\n const defect = new Error(\"defect\")\n const stream = loop(0, () => Stream.die(defect))\n\n const result = await Effect.runPromiseExit(Stream.runCollect(stream))\n\n expect(result._tag).toBe(\"Failure\")\n })\n\n it(\"runs body finalizers when a Decision short-circuits the body\", async () => {\n const releases = await Effect.runPromise(\n Effect.gen(function* () {\n const releasesRef = yield* Ref.make<ReadonlyArray<number>>([])\n const stream = loop(0, (n: number) =>\n (n >= 1\n ? Stream.succeed(value(n)).pipe(Stream.concat(stop()))\n : Stream.succeed(value(n)).pipe(\n Stream.concat(next(n + 1)),\n Stream.concat(Stream.succeed(value(n + 10))),\n )\n ).pipe(Stream.ensuring(Ref.update(releasesRef, (values) => [...values, n]))),\n )\n\n const values = yield* Stream.runCollect(stream)\n expect(values).toEqual([0, 1])\n return yield* Ref.get(releasesRef)\n }),\n )\n\n expect(releases).toEqual([0, 1])\n })\n\n it(\"runs the active body finalizer when the downstream consumer is interrupted\", async () => {\n const releases = await Effect.runPromise(\n Effect.gen(function* () {\n const started = yield* Deferred.make<void>()\n const releasesRef = yield* Ref.make(0)\n const body = (): Stream.Stream<Step<number, never>> =>\n Stream.concat(\n Stream.fromEffect(Deferred.succeed(started, undefined).pipe(Effect.as(value(0)))),\n Stream.never,\n ).pipe(Stream.ensuring(Ref.update(releasesRef, (n) => n + 1)))\n const stream = loop(0, body)\n\n const fiber = yield* Effect.forkChild(Stream.runCollect(stream))\n yield* Deferred.await(started)\n yield* Fiber.interrupt(fiber)\n\n return yield* Ref.get(releasesRef)\n }),\n )\n\n expect(releases).toBe(1)\n })\n\n it(\"does not create a body scope if constructing the body stream defects\", async () => {\n const defect = new Error(\"body construction failed\")\n const result = await Effect.runPromiseExit(\n Stream.runCollect(\n loop(0, (): Stream.Stream<Step<number, never>> => {\n throw defect\n }),\n ),\n )\n\n expect(result._tag).toBe(\"Failure\")\n })\n})\n\ndescribe(\"Loop.loopWithState\", () => {\n it(\"exposes the final state in the SubscriptionRef after the stream completes\", async () => {\n const program = Effect.gen(function* () {\n const { stream, state } = yield* loopWithState(0, (n: number) =>\n n >= 3\n ? Stream.fromIterable([n]).pipe(Stream.map(value), Stream.concat(stop()))\n : Stream.fromIterable([n]).pipe(Stream.map(value), Stream.concat(next(n + 1))),\n )\n const values = yield* Stream.runCollect(stream)\n const finalState = yield* SubscriptionRef.get(state)\n return { values: Array.from(values), finalState }\n })\n\n const { values, finalState } = await Effect.runPromise(program)\n expect(values).toEqual([0, 1, 2, 3])\n // Last `next(state)` was `next(3)` before the iteration that emitted Stop.\n expect(finalState).toBe(3)\n })\n\n it(\"the state ref starts at `initial` and stays there if the loop stops without advancing\", async () => {\n const program = Effect.gen(function* () {\n const { stream, state } = yield* loopWithState({ count: 7 }, () => stop())\n yield* Stream.runDrain(stream)\n return yield* SubscriptionRef.get(state)\n })\n\n expect(await Effect.runPromise(program)).toEqual({ count: 7 })\n })\n\n it(\"a downstream consumer can read the live state between emitted values\", async () => {\n // Body emits one value per iteration, then advances. A `Stream.runForEach`\n // consumer reads the ref each time a value arrives — proving the ref\n // tracks loop state without the body needing to surface it.\n const program = Effect.gen(function* () {\n const { stream, state } = yield* loopWithState(0, (n: number) =>\n n >= 3\n ? Stream.fromIterable([n]).pipe(Stream.map(value), Stream.concat(stop()))\n : Stream.fromIterable([n]).pipe(Stream.map(value), Stream.concat(next(n + 1))),\n )\n const seen: Array<{ value: number; stateAfter: number }> = []\n yield* Stream.runForEach(stream, (v) =>\n Effect.gen(function* () {\n seen.push({ value: v, stateAfter: yield* SubscriptionRef.get(state) })\n }),\n )\n return seen\n })\n\n // For each iter `n`, the consumer reads the ref between values: it sees\n // the iteration's input state. The terminal iter (n=3) stops without\n // advancing, so its read still shows 3.\n expect(await Effect.runPromise(program)).toEqual([\n { value: 0, stateAfter: 0 },\n { value: 1, stateAfter: 1 },\n { value: 2, stateAfter: 2 },\n { value: 3, stateAfter: 3 },\n ])\n })\n\n it(\"SubscriptionRef.changes emits every state transition to a concurrent observer\", async () => {\n const program = Effect.gen(function* () {\n const start = yield* Latch.make(false)\n\n // Body waits on the latch in iter 0 so the observer can subscribe first.\n const { stream, state } = yield* loopWithState(0, (n: number) =>\n Effect.gen(function* () {\n if (n === 0) yield* Latch.await(start)\n return n >= 3 ? stop() : next(n + 1)\n }),\n )\n\n // Fork the observer; take 4 distinct states (initial + 3 transitions).\n const observerFiber = yield* Effect.forkChild(\n SubscriptionRef.changes(state).pipe(Stream.take(4), Stream.runCollect),\n )\n\n // Give the observer fiber a chance to actually subscribe before the\n // loop starts advancing the ref. Without this, the loop could finish\n // before the observer's pubsub subscription is in place.\n yield* Effect.sleep(\"10 millis\")\n\n yield* Latch.open(start)\n yield* Stream.runDrain(stream)\n\n return Array.from(yield* Fiber.join(observerFiber))\n })\n\n // initial 0, then next(1), next(2), next(3) — four distinct states.\n expect(await Effect.runPromise(program)).toEqual([0, 1, 2, 3])\n })\n\n it(\"does not interfere with the body's value stream\", async () => {\n const program = Effect.gen(function* () {\n const { stream } = yield* loopWithState(0, (n: number) =>\n n >= 3\n ? Stream.fromIterable([n]).pipe(Stream.map(value), Stream.concat(stop()))\n : Stream.fromIterable([n, n + 0.5]).pipe(Stream.map(value), Stream.concat(next(n + 1))),\n )\n return Array.from(yield* Stream.runCollect(stream))\n })\n\n expect(await Effect.runPromise(program)).toEqual([0, 0.5, 1, 1.5, 2, 2.5, 3])\n })\n})\n\ndescribe(\"Loop.loopOver\", () => {\n it(\"runs a multi-turn inner loop per input until the body emits stop\", async () => {\n // Per input: emit (input + turnsSoFar) twice, then stop. State counts\n // total turns ACROSS inputs. Demonstrates that `next(s)` continues with\n // the SAME input, multiple times per input — not one body call per item.\n const result = await Effect.runPromise(\n Stream.fromIterable([\"a\", \"b\"]).pipe(\n loopOver(0, (turns: number, input: string) => {\n if (turns >= 2 * (input === \"a\" ? 1 : 2)) return stop()\n return Stream.succeed(value(`${input}:${turns}`)).pipe(Stream.concat(next(turns + 1)))\n }),\n Stream.runCollect,\n ),\n )\n\n // input=\"a\": turns 0,1 → emit \"a:0\",\"a:1\"; turns=2 → stop. State threads.\n // input=\"b\": turns 2,3 → emit \"b:2\",\"b:3\"; turns=4 → stop.\n expect(result).toEqual([\"a:0\", \"a:1\", \"b:2\", \"b:3\"])\n })\n\n it(\"threads state across inputs (audio-pipeline shape)\", async () => {\n // History accumulates across inputs. Each input emits its joined view of\n // history+input, then `stop(state)` ends the inner loop AND carries the\n // updated history to the next input.\n const result = await Effect.runPromise(\n Stream.fromIterable([\"x\", \"y\", \"z\"]).pipe(\n loopOver([] as ReadonlyArray<string>, (history: ReadonlyArray<string>, input: string) =>\n Stream.succeed(value([...history, input].join(\",\"))).pipe(\n Stream.concat(stop([...history, input])),\n ),\n ),\n Stream.runCollect,\n ),\n )\n\n expect(result).toEqual([\"x\", \"x,y\", \"x,y,z\"])\n })\n\n it(\"simulates a stream of documents with multi-turn tool calls per document\", async () => {\n // Document arrives → model \"thinks\" (one text turn) → calls a tool\n // (one tool turn) → emits final text (one text turn) → done.\n // Three turns per document, two documents.\n type Turn =\n | { readonly kind: \"text\"; readonly doc: string; readonly text: string }\n | { readonly kind: \"tool\"; readonly doc: string; readonly tool: string }\n type State = { readonly turn: number; readonly totalTurns: number }\n\n const result = await Effect.runPromise(\n Stream.fromIterable([\"doc1\", \"doc2\"]).pipe(\n loopOver({ turn: 0, totalTurns: 0 } as State, (state, doc: string) => {\n // Each document runs three turns then stops.\n if (state.turn === 0) {\n return Stream.succeed(value<Turn>({ kind: \"text\", doc, text: \"thinking\" })).pipe(\n Stream.concat(next({ turn: 1, totalTurns: state.totalTurns + 1 })),\n )\n }\n if (state.turn === 1) {\n return Stream.succeed(value<Turn>({ kind: \"tool\", doc, tool: \"search\" })).pipe(\n Stream.concat(next({ turn: 2, totalTurns: state.totalTurns + 1 })),\n )\n }\n // Final turn — `stop(state)` emits the final value, advances state\n // (reset turn to 0 for the next document, bump totalTurns), and\n // ends this document's inner loop in one shot.\n return Stream.succeed(value<Turn>({ kind: \"text\", doc, text: \"final\" })).pipe(\n Stream.concat(stop({ turn: 0, totalTurns: state.totalTurns + 1 })),\n )\n }),\n Stream.runCollect,\n ),\n )\n\n expect(result).toEqual([\n { kind: \"text\", doc: \"doc1\", text: \"thinking\" },\n { kind: \"tool\", doc: \"doc1\", tool: \"search\" },\n { kind: \"text\", doc: \"doc1\", text: \"final\" },\n { kind: \"text\", doc: \"doc2\", text: \"thinking\" },\n { kind: \"tool\", doc: \"doc2\", tool: \"search\" },\n { kind: \"text\", doc: \"doc2\", text: \"final\" },\n ])\n })\n\n it(\"ends cleanly when the input stream ends mid-conversation\", async () => {\n // Single-input case: body advances via `next` then stops cleanly.\n const result = await Effect.runPromise(\n Stream.fromIterable([\"only\"]).pipe(\n loopOver(0, (turns: number, input: string) =>\n turns >= 2\n ? stop()\n : Stream.succeed(value(`${input}:${turns}`)).pipe(Stream.concat(next(turns + 1))),\n ),\n Stream.runCollect,\n ),\n )\n\n expect(result).toEqual([\"only:0\", \"only:1\"])\n })\n\n it(\"body's `stop()` advances to the next input (does NOT halt the whole stream)\", async () => {\n // Three inputs, body always stops on its first emission. All three\n // are processed — `stop()` is per-input, not global. To halt the whole\n // stream, end the INPUT stream upstream.\n const result = await Effect.runPromise(\n Stream.fromIterable([1, 2, 3]).pipe(\n loopOver(0, (_state: number, input: number) =>\n Stream.succeed(value(input * 10)).pipe(Stream.concat(stop())),\n ),\n Stream.runCollect,\n ),\n )\n\n expect(result).toEqual([10, 20, 30])\n })\n\n it(\"data-first form (Function.dual) runs identically to data-last\", async () => {\n const inputs = Stream.fromIterable([1, 2])\n const result = await Effect.runPromise(\n Stream.runCollect(\n loopOver(inputs, 0, (state: number, input: number) =>\n state >= input\n ? stop()\n : Stream.succeed(value(state + input)).pipe(Stream.concat(next(state + 1))),\n ),\n ),\n )\n\n // input=1: state=0 → emit 1, state→1; state=1≥1 → stop.\n // input=2: state=1 → emit 3, state→2; state=2≥2 → stop.\n expect(result).toEqual([1, 3])\n })\n\n it(\"supports Effect-returning bodies (parity with loop)\", async () => {\n const result = await Effect.runPromise(\n Stream.fromIterable([\"a\"]).pipe(\n loopOver(0, (turns: number, input: string) =>\n Effect.gen(function* () {\n const cur = yield* Effect.succeed(turns)\n if (cur >= 2) return stop()\n return Stream.succeed(value(`${input}:${cur}`)).pipe(Stream.concat(next(cur + 1)))\n }),\n ),\n Stream.runCollect,\n ),\n )\n\n expect(result).toEqual([\"a:0\", \"a:1\"])\n })\n\n it(\"type: data-last (pipe) form preserves the body's E channel\", () => {\n const result = pipe(\n Stream.fromIterable([1]),\n loopOver(0, (_state: number, _input: number) =>\n Stream.fail(new AiError.RateLimited({ provider: \"test\", raw: null })),\n ),\n )\n type E = typeof result extends Stream.Stream<unknown, infer X, unknown> ? X : never\n expectTypeOf<E>().toEqualTypeOf<AiError.RateLimited>()\n })\n\n it(\"type: data-first form preserves the body's E channel and unifies with input's E\", () => {\n const input: Stream.Stream<number, Error> = Stream.fail(new Error(\"boom\"))\n const result = loopOver(input, 0, (_state: number, _i: number) =>\n Stream.fail(new AiError.RateLimited({ provider: \"test\", raw: null })),\n )\n type E = typeof result extends Stream.Stream<unknown, infer X, unknown> ? X : never\n expectTypeOf<E>().toEqualTypeOf<AiError.RateLimited | Error>()\n })\n})\n"],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { d as RateLimited, m as Unavailable, p as Timeout, t as AiError } from "../AiError-CAX_48RU.mjs";
|
|
2
|
+
import { Effect, Schedule, Stream } from "effect";
|
|
3
|
+
import * as _$effect_Types0 from "effect/Types";
|
|
4
|
+
import * as _$effect_Cause0 from "effect/Cause";
|
|
5
|
+
|
|
6
|
+
//#region src/retry/Retry.d.ts
|
|
7
|
+
declare namespace Retry_d_exports {
|
|
8
|
+
export { Retryable, effect, isRetryable, stream };
|
|
9
|
+
}
|
|
10
|
+
declare const Retryable_base: new <A extends Record<string, any> = {}>(args: _$effect_Types0.VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => _$effect_Cause0.YieldableError & {
|
|
11
|
+
readonly _tag: "RetryableAi";
|
|
12
|
+
} & Readonly<A>;
|
|
13
|
+
/**
|
|
14
|
+
* Subset-aware retry helpers for `AiError`.
|
|
15
|
+
*
|
|
16
|
+
* Effect's own `Effect.retry` / `Stream.retry` apply the schedule to
|
|
17
|
+
* every failure. For AI calls that's the wrong policy — replaying a
|
|
18
|
+
* `ContentFiltered` or `InvalidRequest` won't change the outcome and
|
|
19
|
+
* just wastes quota. These helpers gate the schedule on the
|
|
20
|
+
* {@link Retryable} subset (rate limits, transport hiccups, timeouts);
|
|
21
|
+
* everything else propagates unchanged.
|
|
22
|
+
*
|
|
23
|
+
* Two carriers:
|
|
24
|
+
* - {@link stream} — for `Stream<A, AiError, R>` (e.g. `streamTurn`,
|
|
25
|
+
* `streamSynthesis`, `streamTranscriptionFrom`). Caveat: the entire
|
|
26
|
+
* stream re-runs on retry, so deltas before the failure replay on
|
|
27
|
+
* the next attempt.
|
|
28
|
+
* - {@link effect} — for `Effect<A, AiError, R>` (e.g. `turn`, `embed`,
|
|
29
|
+
* `embedMany`, `synthesize`, `transcribe`). At-most-once.
|
|
30
|
+
*
|
|
31
|
+
* Composes with `Stream.retry` / `Effect.retry` directly (no naming
|
|
32
|
+
* collision since these live under the `Retry` namespace).
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Wrapper around the retryable subset of `AiError`. Internal to the
|
|
36
|
+
* lift/retry/unlift dance — exported so callers can build their own
|
|
37
|
+
* variants if needed (e.g. a custom schedule that observes `cause`).
|
|
38
|
+
*/
|
|
39
|
+
declare class Retryable extends Retryable_base<{
|
|
40
|
+
readonly cause: RateLimited | Unavailable | Timeout;
|
|
41
|
+
}> {}
|
|
42
|
+
/** Type-narrowing predicate for the retryable subset. */
|
|
43
|
+
declare const isRetryable: (e: AiError) => e is RateLimited | Unavailable | Timeout;
|
|
44
|
+
/**
|
|
45
|
+
* Retry a `Stream<A, AiError, R>` on the retryable subset. Other
|
|
46
|
+
* failures bypass the schedule and propagate unchanged. The whole
|
|
47
|
+
* stream replays on each attempt — deltas emitted before the failure
|
|
48
|
+
* are re-emitted on the next try.
|
|
49
|
+
*/
|
|
50
|
+
declare const stream: <Out>(schedule: Schedule.Schedule<Out, Retryable>) => <A, R>(s: Stream.Stream<A, AiError, R>) => Stream.Stream<A, AiError, R>;
|
|
51
|
+
/**
|
|
52
|
+
* Retry an `Effect<A, AiError, R>` on the retryable subset. Other
|
|
53
|
+
* failures bypass the schedule and propagate unchanged. At-most-once
|
|
54
|
+
* — the underlying call only re-runs if the previous attempt failed
|
|
55
|
+
* with a retryable tag.
|
|
56
|
+
*
|
|
57
|
+
* Skips the lift/unlift dance the Stream variant needs: `Effect.retry`
|
|
58
|
+
* exposes a `while:` predicate so the error channel stays a plain
|
|
59
|
+
* `AiError` throughout.
|
|
60
|
+
*/
|
|
61
|
+
declare const effect: <Out>(schedule: Schedule.Schedule<Out, AiError>) => <A, R>(eff: Effect.Effect<A, AiError, R>) => Effect.Effect<A, AiError, R>;
|
|
62
|
+
//#endregion
|
|
63
|
+
export { Retryable, effect, isRetryable, stream, Retry_d_exports as t };
|
|
64
|
+
//# sourceMappingURL=Retry.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Retry.d.mts","names":[],"sources":["../../src/retry/Retry.ts"],"mappings":";;;;;;;;;cACoD,cAAA;;;;;;;;;;;AAAA;;;;;;;;;;;;;;;;;;cA6BvC,SAAA,SAAkB,cAAA;EAAA,SACpB,KAAA,EAAO,WAAA,GAAsB,WAAA,GAAsB,OAAA;AAAA;;cAIjD,WAAA,GACX,CAAA,EAAG,OAAA,KACF,CAAA,IAAK,WAAA,GAAsB,WAAA,GAAsB,OAAA;;;;;;;cAgBvC,MAAA,QACL,QAAA,EAAU,QAAA,CAAS,QAAA,CAAS,GAAA,EAAK,SAAA,aAChC,CAAA,EAAG,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,OAAA,EAAiB,CAAA,MAAK,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,OAAA,EAAiB,CAAA;AAzBrF;;;;;;;;;;AAAA,cAkDa,MAAA,QACL,QAAA,EAAU,QAAA,CAAS,QAAA,CAAS,GAAA,EAAK,OAAA,aAChC,GAAA,EAAK,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,OAAA,EAAiB,CAAA,MAAK,MAAA,CAAO,MAAA,CAAO,CAAA,EAAG,OAAA,EAAiB,CAAA"}
|