@effect-uai/core 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/dist/{AiError-csR8Bhxx.d.mts → AiError-CAX_48RU.d.mts} +2 -2
  2. package/dist/{AiError-csR8Bhxx.d.mts.map → AiError-CAX_48RU.d.mts.map} +1 -1
  3. package/dist/{Image-DxyXqzAM.d.mts → Image-HNmMpMTh.d.mts} +4 -4
  4. package/dist/{Image-DxyXqzAM.d.mts.map → Image-HNmMpMTh.d.mts.map} +1 -1
  5. package/dist/{Items-Hg5AsYxl.d.mts → Items-BH8xUkoR.d.mts} +3 -3
  6. package/dist/{Items-Hg5AsYxl.d.mts.map → Items-BH8xUkoR.d.mts.map} +1 -1
  7. package/dist/{StructuredFormat-Cl41C56K.d.mts → StructuredFormat-BbN4dosH.d.mts} +11 -4
  8. package/dist/StructuredFormat-BbN4dosH.d.mts.map +1 -0
  9. package/dist/{Tool-B8B5qVEy.d.mts → Tool-87ViKCCO.d.mts} +20 -4
  10. package/dist/Tool-87ViKCCO.d.mts.map +1 -0
  11. package/dist/Turn-0CwCAyVe.d.mts +388 -0
  12. package/dist/Turn-0CwCAyVe.d.mts.map +1 -0
  13. package/dist/domain/AiError.d.mts +1 -1
  14. package/dist/domain/AiError.mjs +1 -1
  15. package/dist/domain/AiError.mjs.map +1 -1
  16. package/dist/domain/Image.d.mts +1 -1
  17. package/dist/domain/Items.d.mts +1 -1
  18. package/dist/domain/Items.mjs +1 -1
  19. package/dist/domain/Items.mjs.map +1 -1
  20. package/dist/domain/Turn.d.mts +2 -2
  21. package/dist/domain/Turn.mjs +22 -4
  22. package/dist/domain/Turn.mjs.map +1 -1
  23. package/dist/domain/Turn.test.d.mts +1 -0
  24. package/dist/domain/Turn.test.mjs +136 -0
  25. package/dist/domain/Turn.test.mjs.map +1 -0
  26. package/dist/embedding-model/Embedding.d.mts +15 -3
  27. package/dist/embedding-model/Embedding.d.mts.map +1 -1
  28. package/dist/embedding-model/Embedding.mjs.map +1 -1
  29. package/dist/embedding-model/EmbeddingModel.d.mts +33 -17
  30. package/dist/embedding-model/EmbeddingModel.d.mts.map +1 -1
  31. package/dist/embedding-model/EmbeddingModel.mjs.map +1 -1
  32. package/dist/embedding-model/EmbeddingModel.test.d.mts +1 -0
  33. package/dist/embedding-model/EmbeddingModel.test.mjs +59 -0
  34. package/dist/embedding-model/EmbeddingModel.test.mjs.map +1 -0
  35. package/dist/index.d.mts +6 -6
  36. package/dist/language-model/LanguageModel.d.mts +30 -8
  37. package/dist/language-model/LanguageModel.d.mts.map +1 -1
  38. package/dist/language-model/LanguageModel.mjs +33 -3
  39. package/dist/language-model/LanguageModel.mjs.map +1 -1
  40. package/dist/language-model/LanguageModel.test.d.mts +1 -0
  41. package/dist/language-model/LanguageModel.test.mjs +143 -0
  42. package/dist/language-model/LanguageModel.test.mjs.map +1 -0
  43. package/dist/loop/Loop.d.mts +94 -11
  44. package/dist/loop/Loop.d.mts.map +1 -1
  45. package/dist/loop/Loop.mjs +92 -26
  46. package/dist/loop/Loop.mjs.map +1 -1
  47. package/dist/loop/Loop.test.mjs +171 -3
  48. package/dist/loop/Loop.test.mjs.map +1 -1
  49. package/dist/music-generator/MusicGenerator.d.mts +1 -1
  50. package/dist/observability/Metrics.d.mts +1 -1
  51. package/dist/observability/Metrics.mjs +1 -1
  52. package/dist/observability/Metrics.mjs.map +1 -1
  53. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +1 -1
  54. package/dist/streaming/JSONL.d.mts +1 -1
  55. package/dist/streaming/JSONL.d.mts.map +1 -1
  56. package/dist/streaming/JSONL.mjs +7 -12
  57. package/dist/streaming/JSONL.mjs.map +1 -1
  58. package/dist/structured-format/StructuredFormat.d.mts +2 -2
  59. package/dist/structured-format/StructuredFormat.mjs +9 -1
  60. package/dist/structured-format/StructuredFormat.mjs.map +1 -1
  61. package/dist/structured-format/StructuredFormat.test.d.mts +1 -0
  62. package/dist/structured-format/StructuredFormat.test.mjs +70 -0
  63. package/dist/structured-format/StructuredFormat.test.mjs.map +1 -0
  64. package/dist/testing/MockMusicGenerator.d.mts.map +1 -1
  65. package/dist/testing/MockMusicGenerator.mjs +2 -2
  66. package/dist/testing/MockMusicGenerator.mjs.map +1 -1
  67. package/dist/testing/MockProvider.d.mts +23 -18
  68. package/dist/testing/MockProvider.d.mts.map +1 -1
  69. package/dist/testing/MockProvider.mjs +56 -72
  70. package/dist/testing/MockProvider.mjs.map +1 -1
  71. package/dist/testing/MockSpeechSynthesizer.d.mts.map +1 -1
  72. package/dist/testing/MockSpeechSynthesizer.mjs +2 -2
  73. package/dist/testing/MockSpeechSynthesizer.mjs.map +1 -1
  74. package/dist/testing/MockTranscriber.d.mts.map +1 -1
  75. package/dist/testing/MockTranscriber.mjs +2 -2
  76. package/dist/testing/MockTranscriber.mjs.map +1 -1
  77. package/dist/tool/HistoryCheck.d.mts +1 -1
  78. package/dist/tool/Outcome.d.mts +1 -1
  79. package/dist/tool/Resolvers.d.mts +65 -8
  80. package/dist/tool/Resolvers.d.mts.map +1 -1
  81. package/dist/tool/Resolvers.mjs +8 -12
  82. package/dist/tool/Resolvers.mjs.map +1 -1
  83. package/dist/tool/Resolvers.test.mjs +6 -5
  84. package/dist/tool/Resolvers.test.mjs.map +1 -1
  85. package/dist/tool/Tool.d.mts +2 -2
  86. package/dist/tool/Tool.mjs +18 -1
  87. package/dist/tool/Tool.mjs.map +1 -1
  88. package/dist/tool/Tool.test.d.mts +1 -0
  89. package/dist/tool/Tool.test.mjs +66 -0
  90. package/dist/tool/Tool.test.mjs.map +1 -0
  91. package/dist/tool/Toolkit.d.mts +4 -6
  92. package/dist/tool/Toolkit.d.mts.map +1 -1
  93. package/dist/tool/Toolkit.mjs +14 -43
  94. package/dist/tool/Toolkit.mjs.map +1 -1
  95. package/dist/transcriber/Transcriber.d.mts +1 -1
  96. package/package.json +1 -1
  97. package/src/domain/AiError.ts +1 -1
  98. package/src/domain/Items.ts +1 -1
  99. package/src/domain/Turn.test.ts +141 -0
  100. package/src/domain/Turn.ts +50 -43
  101. package/src/embedding-model/Embedding.ts +23 -0
  102. package/src/embedding-model/EmbeddingModel.test.ts +92 -0
  103. package/src/embedding-model/EmbeddingModel.ts +30 -20
  104. package/src/language-model/LanguageModel.test.ts +170 -0
  105. package/src/language-model/LanguageModel.ts +64 -1
  106. package/src/loop/Loop.test.ts +256 -3
  107. package/src/loop/Loop.ts +225 -49
  108. package/src/observability/Metrics.ts +1 -1
  109. package/src/streaming/JSONL.ts +9 -18
  110. package/src/structured-format/StructuredFormat.test.ts +105 -0
  111. package/src/structured-format/StructuredFormat.ts +14 -1
  112. package/src/testing/MockMusicGenerator.ts +4 -6
  113. package/src/testing/MockProvider.ts +126 -105
  114. package/src/testing/MockSpeechSynthesizer.ts +4 -6
  115. package/src/testing/MockTranscriber.ts +4 -6
  116. package/src/tool/Resolvers.test.ts +8 -5
  117. package/src/tool/Resolvers.ts +17 -19
  118. package/src/tool/Tool.test.ts +105 -0
  119. package/src/tool/Tool.ts +20 -0
  120. package/src/tool/Toolkit.ts +49 -50
  121. package/dist/StructuredFormat-Cl41C56K.d.mts.map +0 -1
  122. package/dist/Tool-B8B5qVEy.d.mts.map +0 -1
  123. package/dist/Turn-7geUcKsf.d.mts +0 -194
  124. package/dist/Turn-7geUcKsf.d.mts.map +0 -1
@@ -1,7 +1,7 @@
1
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, SubscriptionRef } from "effect";
4
+ import { Array, Cause, Channel, Data, Effect, Exit, Function, Match, 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,6 +24,7 @@ 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
+ loopFrom: () => loopFrom,
27
28
  loopWithState: () => loopWithState,
28
29
  next: () => next,
29
30
  nextAfter: () => nextAfter,
@@ -32,6 +33,8 @@ var Loop_exports = /* @__PURE__ */ __exportAll({
32
33
  stop: () => stop,
33
34
  stopAfter: () => stopAfter,
34
35
  stopEvent: () => stopEvent,
36
+ stopWith: () => stopWith,
37
+ stopWithAfter: () => stopWithAfter,
35
38
  value: () => value
36
39
  });
37
40
  const Event = Data.taggedEnum();
@@ -39,9 +42,20 @@ const Event = Data.taggedEnum();
39
42
  const value = (a) => Event.Value({ value: a });
40
43
  /** End the current iteration and continue with a new state. */
41
44
  const next = (state) => Event.Next({ state });
42
- /** The terminal `Stop` event. Use `stop` (the Stream) to end a loop body. */
45
+ /**
46
+ * The terminal `Stop` event with no carried state. Use `stop` (the Stream)
47
+ * to end a loop body without communicating a final state.
48
+ */
43
49
  const stopEvent = Event.Stop();
44
50
  /**
51
+ * Terminal event that ends the loop AND carries a final state. For
52
+ * `loopFrom` this is the natural "this input is done, here's the state to
53
+ * carry forward to the next input" signal — symmetric with `next(s)` but
54
+ * ending the inner loop instead of continuing it. For `loopWithState` the
55
+ * carried state is written to the `SubscriptionRef` before the loop ends.
56
+ */
57
+ const stopWith = (state) => Event.StopWith({ state });
58
+ /**
45
59
  * A single-element stream that ends the loop. Return this from a body when
46
60
  * there's nothing else to emit; equivalent to `stopAfter(Stream.empty)` but
47
61
  * named for the common case.
@@ -51,31 +65,48 @@ const stop = Stream.succeed(stopEvent);
51
65
  * Pipe a raw `Stream<A>` into the loop's emit shape, then terminate the
52
66
  * iteration with `next(state)`. Common shape for "stream this turn's
53
67
  * deltas, then continue with updated history."
68
+ *
69
+ * Dual: data-first `nextAfter(stream, state)` and data-last
70
+ * `stream.pipe(nextAfter(state))` both work.
54
71
  */
55
- const nextAfter = (stream, state) => Stream.concat(Stream.map(stream, value), Stream.fromIterable([next(state)]));
72
+ const nextAfter = Function.dual(2, (stream, state) => Stream.concat(Stream.map(stream, value), Stream.fromIterable([next(state)])));
56
73
  /**
57
74
  * Pipe a raw `Stream<A>` into the loop's emit shape, then terminate the
58
75
  * loop. Common shape for "stream this turn's deltas, then we're done."
76
+ *
77
+ * Unary on the stream — already pipe-compatible via `stream.pipe(stopAfter)`.
59
78
  */
60
79
  const stopAfter = (stream) => Stream.concat(Stream.map(stream, value), Stream.fromIterable([stopEvent]));
61
80
  /**
81
+ * Pipe a raw `Stream<A>` into the loop's emit shape, then terminate with
82
+ * `stopWith(state)`. The natural "emit final outputs, advance state, end
83
+ * this input's inner loop" shape for `loopFrom`.
84
+ *
85
+ * Dual: data-first `stopWithAfter(stream, state)` and data-last
86
+ * `stream.pipe(stopWithAfter(state))` both work.
87
+ */
88
+ const stopWithAfter = Function.dual(2, (stream, state) => Stream.concat(Stream.map(stream, value), Stream.fromIterable([stopWith(state)])));
89
+ /**
62
90
  * General `nextAfter` variant: drain `stream` to the consumer, fold elements
63
91
  * into an accumulator, and at end-of-stream emit one `next(build(finalAcc))`.
64
92
  *
65
93
  * Subsumes `nextAfter` when state is constant (`reduce: (s, _) => s`,
66
94
  * `build: (s) => s`). Used by `Toolkit.continueWith` to collect tool
67
95
  * results and build next state without exposing a Ref to recipes.
96
+ *
97
+ * Dual: data-first `nextAfterFold(stream, initial, reduce, build)` and
98
+ * data-last `stream.pipe(nextAfterFold(initial, reduce, build))` both work.
68
99
  */
69
- const nextAfterFold = (stream, initial, reduce, build) => Stream.unwrap(Effect.gen(function* () {
100
+ const nextAfterFold = Function.dual(4, (stream, initial, reduce, build) => Stream.unwrap(Effect.gen(function* () {
70
101
  const ref = yield* Ref.make(initial);
71
102
  const tapped = stream.pipe(Stream.tap((a) => Ref.update(ref, (acc) => reduce(acc, a))), Stream.map(value));
72
103
  const continuation = Stream.fromEffect(Ref.get(ref).pipe(Effect.map((acc) => next(build(acc)))));
73
104
  return tapped.pipe(Stream.concat(continuation));
74
- }));
105
+ })));
75
106
  /**
76
107
  * Lift a provider's `Stream<TurnEvent>` into a loop body's `Stream<Event<TurnEvent | A, S>>`.
77
108
  * Each delta passes through as `value(delta)` (including the terminal
78
- * `turn_complete`, so the consumer sees turn boundaries naturally). Once
109
+ * `TurnComplete`, so the consumer sees turn boundaries naturally). Once
79
110
  * the terminal arrives, `then(turn)` runs and its returned stream of loop
80
111
  * events (typically tool outputs followed by `next(state)` or `stop`) is
81
112
  * concatenated.
@@ -83,20 +114,23 @@ const nextAfterFold = (stream, initial, reduce, build) => Stream.unwrap(Effect.g
83
114
  * Pre-pipe transforms (`Stream.tap` / `Stream.map` / `Stream.filter`) on
84
115
  * the raw delta stream cover anything an `emit`-style callback would do.
85
116
  *
86
- * If the upstream ends without a `turn_complete`, the resulting stream
117
+ * If the upstream ends without a `TurnComplete`, the resulting stream
87
118
  * fails with `AiError.IncompleteTurn`. Catch it via `Stream.catchTag` if
88
119
  * you want to recover.
120
+ *
121
+ * Dual: data-first `onTurnComplete(deltas, then)` and data-last
122
+ * `deltas.pipe(onTurnComplete(then))` both work.
89
123
  */
90
- const onTurnComplete = (then) => (deltas) => Stream.unwrap(Effect.gen(function* () {
124
+ const onTurnComplete = Function.dual(2, (deltas, then) => Stream.unwrap(Effect.gen(function* () {
91
125
  const turnRef = yield* Ref.make(Option.none());
92
126
  const events = deltas.pipe(Stream.tap((delta) => isTurnComplete(delta) ? Ref.set(turnRef, Option.some(delta.turn)) : Effect.void), Stream.map(value));
93
127
  const continuation = Stream.unwrap(Effect.gen(function* () {
94
128
  const opt = yield* Ref.get(turnRef);
95
- if (Option.isNone(opt)) return yield* Effect.fail(new IncompleteTurn({}));
129
+ if (Option.isNone(opt)) return yield* new IncompleteTurn({});
96
130
  return yield* then(opt.value);
97
131
  }));
98
132
  return Stream.concat(events, continuation);
99
- }));
133
+ })));
100
134
  const isNonEmpty = (array) => array.length > 0;
101
135
  const closeBody = (current, exit) => Scope.close(current.scope, exit);
102
136
  /**
@@ -106,18 +140,10 @@ const closeBody = (current, exit) => Scope.close(current.scope, exit);
106
140
  * producing side effects may have run, but downstream never sees it.
107
141
  */
108
142
  const partitionChunk = (chunk) => {
109
- const values = [];
110
- for (let i = 0; i < chunk.length; i++) {
111
- const event = chunk[i];
112
- if (event._tag === "Value") values.push(event.value);
113
- else return {
114
- values,
115
- decision: event
116
- };
117
- }
143
+ const [valueEvents, rest] = Array.span(chunk, (e) => e._tag === "Value");
118
144
  return {
119
- values,
120
- decision: void 0
145
+ values: valueEvents.map((e) => e.value),
146
+ decision: Array.head(rest)
121
147
  };
122
148
  };
123
149
  /**
@@ -158,16 +184,53 @@ const loop = Function.dual(2, (initial, body) => Stream.scoped(Stream.fromPull(E
158
184
  return yield* Cause.done();
159
185
  }
160
186
  const { values, decision } = partitionChunk(chunk);
161
- if (decision !== void 0) {
187
+ if (Option.isSome(decision)) {
162
188
  yield* closeActive(active, Exit.void);
163
- if (decision._tag === "Stop") done = true;
164
- else if (decision._tag === "Next") state = decision.state;
189
+ if (decision.value._tag === "Stop" || decision.value._tag === "StopWith") done = true;
190
+ else if (decision.value._tag === "Next") state = decision.value.state;
165
191
  }
166
192
  if (isNonEmpty(values)) return values;
167
193
  }
168
194
  });
169
195
  }))));
170
196
  /**
197
+ * Input-driven sibling of `loop`. For each item pulled from the input
198
+ * stream, runs an inner seed-driven `loop` whose body is
199
+ * `(s) => body(s, item)`. State is threaded across input items.
200
+ *
201
+ * **Per-input semantics — the body emits standard `Event<A, S>`:**
202
+ * - `value(a)`: emit `a` downstream
203
+ * - `next(s)`: re-run the body with the SAME input and new state `s`
204
+ * (multi-turn within one input — e.g. multiple model turns + tool
205
+ * calls for one document)
206
+ * - `stop`: end this input's inner loop, advance to the next input
207
+ * (state preserved)
208
+ * - body stream ending without a decision: same as `stop` (advance)
209
+ *
210
+ * **Outer termination:** the input stream ending. To halt programmatically
211
+ * from within, end the input stream upstream (`Stream.takeWhile`, a
212
+ * `SubscriptionRef` gate, etc.). Reserving `stop` for per-item
213
+ * advancement is what makes the common "stream of documents, multi-turn
214
+ * conversation per document" shape readable.
215
+ *
216
+ * Dual: data-first `loopFrom(input, initial, body)` and data-last
217
+ * `input.pipe(loopFrom(initial, body))` both work.
218
+ */
219
+ const loopFrom = Function.dual(3, (input, initial, body) => Stream.unwrap(Effect.gen(function* () {
220
+ const stateRef = yield* Ref.make(initial);
221
+ return input.pipe(Stream.flatMap((item) => Stream.unwrap(Effect.gen(function* () {
222
+ const state = yield* Ref.get(stateRef);
223
+ const wrappedBody = (s) => {
224
+ const result = body(s, item);
225
+ return (Effect.isEffect(result) ? Stream.unwrap(result) : result).pipe(Stream.tap((event) => Match.value(event).pipe(Match.tags({
226
+ Next: (e) => Ref.set(stateRef, e.state),
227
+ StopWith: (e) => Ref.set(stateRef, e.state)
228
+ }), Match.orElse(() => Effect.void))));
229
+ };
230
+ return loop(state, wrappedBody);
231
+ }))));
232
+ })));
233
+ /**
171
234
  * Like `loop`, but exposes the current loop state as a `SubscriptionRef`
172
235
  * alongside the value stream.
173
236
  *
@@ -189,7 +252,10 @@ const loop = Function.dual(2, (initial, body) => Stream.scoped(Stream.fromPull(E
189
252
  */
190
253
  const loopWithState = (initial, body) => Effect.gen(function* () {
191
254
  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));
255
+ const tap = (stream) => stream.pipe(Stream.tap((event) => Match.value(event).pipe(Match.tags({
256
+ Next: (e) => SubscriptionRef.set(stateRef, e.state),
257
+ StopWith: (e) => SubscriptionRef.set(stateRef, e.state)
258
+ }), Match.orElse(() => Effect.void))));
193
259
  const wrappedBody = (s) => {
194
260
  const result = body(s);
195
261
  return Effect.isEffect(result) ? Effect.map(result, tap) : tap(result);
@@ -200,6 +266,6 @@ const loopWithState = (initial, body) => Effect.gen(function* () {
200
266
  };
201
267
  });
202
268
  //#endregion
203
- export { loop, loopWithState, next, nextAfter, nextAfterFold, onTurnComplete, stop, stopAfter, stopEvent, Loop_exports as t, value };
269
+ export { loop, loopFrom, loopWithState, next, nextAfter, nextAfterFold, onTurnComplete, stop, stopAfter, stopEvent, stopWith, stopWithAfter, Loop_exports as t, value };
204
270
 
205
271
  //# sourceMappingURL=Loop.mjs.map
@@ -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 {\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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"}
1
+ {"version":3,"file":"Loop.mjs","names":["Arr"],"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 Array as Arr,\n Cause,\n Channel,\n Data,\n Effect,\n Exit,\n Function,\n Match,\n Option,\n Ref,\n Result,\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 with no carried state.\n * `StopWith` also ends the loop but carries a final state that `loopFrom`\n * will thread to the next input and `loopWithState` will write to its\n * `SubscriptionRef` before the loop ends. Plain `loop` has no next\n * iteration to apply it to and treats `StopWith` like `Stop`.\n *\n * `Stop` is intentionally `{}` so the bare `stopEvent` / `stop` helpers\n * don't constrain `S` from a body's stream type — every body has a `Stop`\n * variant in its union, and forcing `S` to flow through it would break\n * inference whenever the body never uses `next` / `stopWith`.\n */\nexport type Event<A, S> = Data.TaggedEnum<{\n Value: { readonly value: A }\n Next: { readonly state: S }\n Stop: {}\n StopWith: { readonly state: S }\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/**\n * The terminal `Stop` event with no carried state. Use `stop` (the Stream)\n * to end a loop body without communicating a final state.\n */\nexport const stopEvent: Event<never, never> = Event.Stop()\n\n/**\n * Terminal event that ends the loop AND carries a final state. For\n * `loopFrom` this is the natural \"this input is done, here's the state to\n * carry forward to the next input\" signal — symmetric with `next(s)` but\n * ending the inner loop instead of continuing it. For `loopWithState` the\n * carried state is written to the `SubscriptionRef` before the loop ends.\n */\nexport const stopWith = <S>(state: S): Event<never, S> => Event.StopWith({ state })\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 *\n * Dual: data-first `nextAfter(stream, state)` and data-last\n * `stream.pipe(nextAfter(state))` both work.\n */\nexport const nextAfter: {\n <S>(state: S): <A, E, R>(stream: Stream.Stream<A, E, R>) => Stream.Stream<Event<A, S>, E, R>\n <S, A, E, R>(stream: Stream.Stream<A, E, R>, state: S): Stream.Stream<Event<A, S>, E, R>\n} = Function.dual(\n 2,\n <S, A, E, R>(stream: Stream.Stream<A, E, R>, state: S): Stream.Stream<Event<A, S>, E, R> =>\n Stream.concat(Stream.map(stream, value), Stream.fromIterable([next(state)])),\n)\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 *\n * Unary on the stream — already pipe-compatible via `stream.pipe(stopAfter)`.\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 * Pipe a raw `Stream<A>` into the loop's emit shape, then terminate with\n * `stopWith(state)`. The natural \"emit final outputs, advance state, end\n * this input's inner loop\" shape for `loopFrom`.\n *\n * Dual: data-first `stopWithAfter(stream, state)` and data-last\n * `stream.pipe(stopWithAfter(state))` both work.\n */\nexport const stopWithAfter: {\n <S>(state: S): <A, E, R>(stream: Stream.Stream<A, E, R>) => Stream.Stream<Event<A, S>, E, R>\n <S, A, E, R>(stream: Stream.Stream<A, E, R>, state: S): Stream.Stream<Event<A, S>, E, R>\n} = Function.dual(\n 2,\n <S, A, E, R>(stream: Stream.Stream<A, E, R>, state: S): Stream.Stream<Event<A, S>, E, R> =>\n Stream.concat(Stream.map(stream, value), Stream.fromIterable([stopWith(state)])),\n)\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 *\n * Dual: data-first `nextAfterFold(stream, initial, reduce, build)` and\n * data-last `stream.pipe(nextAfterFold(initial, reduce, build))` both work.\n */\nexport const nextAfterFold: {\n <A, B, S>(\n initial: B,\n reduce: (acc: B, a: A) => B,\n build: (b: B) => S,\n ): <E, R>(stream: Stream.Stream<A, E, R>) => Stream.Stream<Event<A, S>, E, R>\n <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} = Function.dual(\n 4,\n <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// ---------------------------------------------------------------------------\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 * `TurnComplete`, 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 `TurnComplete`, the resulting stream\n * fails with `AiError.IncompleteTurn`. Catch it via `Stream.catchTag` if\n * you want to recover.\n *\n * Dual: data-first `onTurnComplete(deltas, then)` and data-last\n * `deltas.pipe(onTurnComplete(then))` both work.\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 ): <E, R>(\n deltas: Stream.Stream<TurnEvent, E, R>,\n ) => Stream.Stream<Event<TurnEvent | A, S>, E | E2 | IncompleteTurn, R | R2>\n <S, A, E, R, E2 = never, R2 = never>(\n deltas: Stream.Stream<TurnEvent, E, R>,\n then: (turn: Turn) => Effect.Effect<Stream.Stream<Event<A, S>, E2, R2>, E2, R2>,\n ): Stream.Stream<Event<TurnEvent | A, S>, E | E2 | IncompleteTurn, R | R2>\n} = Function.dual(\n 2,\n <S, A, E, R, E2, R2>(\n deltas: Stream.Stream<TurnEvent, E, R>,\n then: (turn: Turn) => Effect.Effect<Stream.Stream<Event<A, S>, E2, R2>, E2, R2>,\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* new IncompleteTurn({})\n return yield* then(opt.value)\n }),\n )\n\n return Stream.concat(events, continuation)\n }),\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): {\n readonly values: ReadonlyArray<A>\n readonly decision: Option.Option<Event<A, S>>\n} => {\n const [valueEvents, rest] = Arr.span(\n chunk,\n (e): e is Event<A, S> & { _tag: \"Value\" } => e._tag === \"Value\",\n )\n return {\n values: valueEvents.map((e) => e.value),\n decision: Arr.head(rest),\n }\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 (Option.isSome(decision)) {\n yield* closeActive(active, Exit.void)\n if (decision.value._tag === \"Stop\" || decision.value._tag === \"StopWith\") {\n // `loop` has no next iteration to apply StopWith's state to;\n // the state lands in `loopFrom`'s outer ref or\n // `loopWithState`'s SubscriptionRef via their taps.\n done = true\n } else if (decision.value._tag === \"Next\") {\n state = decision.value.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` is `None` 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// loopFrom - stream-driven sibling of loop. One input item runs a full\n// multi-turn inner loop.\n// ---------------------------------------------------------------------------\n\ntype LoopFromBody<S, I, A, E, R> = (\n state: S,\n input: I,\n) => Stream.Stream<Event<A, S>, E, R> | Effect.Effect<Stream.Stream<Event<A, S>, E, R>, E, R>\n\n/**\n * Input-driven sibling of `loop`. For each item pulled from the input\n * stream, runs an inner seed-driven `loop` whose body is\n * `(s) => body(s, item)`. State is threaded across input items.\n *\n * **Per-input semantics — the body emits standard `Event<A, S>`:**\n * - `value(a)`: emit `a` downstream\n * - `next(s)`: re-run the body with the SAME input and new state `s`\n * (multi-turn within one input — e.g. multiple model turns + tool\n * calls for one document)\n * - `stop`: end this input's inner loop, advance to the next input\n * (state preserved)\n * - body stream ending without a decision: same as `stop` (advance)\n *\n * **Outer termination:** the input stream ending. To halt programmatically\n * from within, end the input stream upstream (`Stream.takeWhile`, a\n * `SubscriptionRef` gate, etc.). Reserving `stop` for per-item\n * advancement is what makes the common \"stream of documents, multi-turn\n * conversation per document\" shape readable.\n *\n * Dual: data-first `loopFrom(input, initial, body)` and data-last\n * `input.pipe(loopFrom(initial, body))` both work.\n */\nexport const loopFrom: {\n <S, I, A, E, R>(\n initial: S,\n body: LoopFromBody<S, I, A, E, R>,\n ): <EI, RI>(input: Stream.Stream<I, EI, RI>) => Stream.Stream<A, E | EI, R | RI>\n <S, I, A, E, R, EI, RI>(\n input: Stream.Stream<I, EI, RI>,\n initial: S,\n body: LoopFromBody<S, I, A, E, R>,\n ): Stream.Stream<A, E | EI, R | RI>\n} = Function.dual(\n 3,\n <S, I, A, E, R, EI, RI>(\n input: Stream.Stream<I, EI, RI>,\n initial: S,\n body: LoopFromBody<S, I, A, E, R>,\n ): Stream.Stream<A, E | EI, R | RI> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const stateRef = yield* Ref.make<S>(initial)\n return input.pipe(\n Stream.flatMap((item) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const state = yield* Ref.get(stateRef)\n // Capture Next states (and stopWith's final state) into the\n // outer ref so the LAST state seen in this input's inner\n // loop is what the next input starts from.\n const wrappedBody = (s: S) => {\n const result = body(s, item)\n const stream = Effect.isEffect(result) ? Stream.unwrap(result) : result\n return stream.pipe(\n Stream.tap((event) =>\n Match.value(event).pipe(\n Match.tags({\n Next: (e) => Ref.set(stateRef, e.state),\n StopWith: (e) => Ref.set(stateRef, e.state),\n }),\n Match.orElse(() => Effect.void),\n ),\n ),\n )\n }\n return loop(state, wrappedBody)\n }),\n ),\n ),\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 Match.value(event).pipe(\n Match.tags({\n Next: (e) => SubscriptionRef.set(stateRef, e.state),\n StopWith: (e) => SubscriptionRef.set(stateRef, e.state),\n }),\n Match.orElse(() => Effect.void),\n ),\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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"}
@@ -1,6 +1,8 @@
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";
1
+ import { RateLimited } from "../domain/AiError.mjs";
2
+ import { TurnEvent } from "../domain/Turn.mjs";
3
+ import { loop, loopFrom, loopWithState, next, nextAfter, onTurnComplete, stop, stopAfter, stopEvent, stopWith, value } from "./Loop.mjs";
4
+ import { i as it, n as globalExpect, r as describe, t as import_dist } from "../dist-DV5ISja1.mjs";
5
+ import { Deferred, Effect, Fiber, Latch, Ref, Stream, SubscriptionRef, pipe } from "effect";
4
6
  //#region src/loop/Loop.test.ts
5
7
  describe("Loop.loop", () => {
6
8
  it("threads state across iterations and emits each iteration's substream in order", async () => {
@@ -59,6 +61,47 @@ describe("Loop.loop", () => {
59
61
  2
60
62
  ]);
61
63
  });
64
+ it("type: data-last (pipe) form preserves the body's E channel", () => {
65
+ pipe({ count: 0 }, loop((_state) => Stream.fail(new RateLimited({
66
+ provider: "test",
67
+ raw: null
68
+ }))));
69
+ (0, import_dist.expectTypeOf)().toEqualTypeOf();
70
+ });
71
+ it("type: data-first form preserves the body's E channel", () => {
72
+ loop({ count: 0 }, (_state) => Stream.fail(new RateLimited({
73
+ provider: "test",
74
+ raw: null
75
+ })));
76
+ (0, import_dist.expectTypeOf)().toEqualTypeOf();
77
+ });
78
+ it("type: onTurnComplete inside loop infers S and A from the handler without annotation", () => {
79
+ pipe({ turns: 0 }, loop((state) => Effect.gen(function* () {
80
+ return Stream.empty.pipe(onTurnComplete(() => Effect.sync(() => state.turns >= 1 ? stop : nextAfter(Stream.succeed({
81
+ _tag: "tool",
82
+ name: "x"
83
+ }), { turns: state.turns + 1 }))));
84
+ })));
85
+ (0, import_dist.expectTypeOf)().toEqualTypeOf();
86
+ });
87
+ it("onTurnComplete: data-first form (Function.dual) works at runtime", async () => {
88
+ const turnComplete = TurnEvent.TurnComplete({ turn: {
89
+ items: [],
90
+ usage: {
91
+ input_tokens: 0,
92
+ output_tokens: 0
93
+ },
94
+ stop_reason: "stop"
95
+ } });
96
+ const textDelta = TurnEvent.TextDelta({ text: "hi" });
97
+ const deltas = Stream.fromIterable([textDelta, turnComplete]);
98
+ const dataFirst = onTurnComplete(deltas, () => Effect.sync(() => stop));
99
+ const dataLast = deltas.pipe(onTurnComplete(() => Effect.sync(() => stop)));
100
+ const a = await Effect.runPromise(Stream.runCollect(dataFirst));
101
+ const b = await Effect.runPromise(Stream.runCollect(dataLast));
102
+ globalExpect(a.length).toBe(3);
103
+ globalExpect(b.length).toBe(3);
104
+ });
62
105
  it("is stack-safe and linear-time across many iterations", async () => {
63
106
  const N = 1e5;
64
107
  const stream = loop(0, (n) => n >= N ? Stream.fromIterable([value(n), stopEvent]) : Stream.fromIterable([value(n), next(n + 1)]));
@@ -405,6 +448,131 @@ describe("Loop.loopWithState", () => {
405
448
  ]);
406
449
  });
407
450
  });
451
+ describe("Loop.loopFrom", () => {
452
+ 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(loopFrom(0, (turns, input) => {
454
+ if (turns >= 2 * (input === "a" ? 1 : 2)) return Stream.fromIterable([stopEvent]);
455
+ return Stream.fromIterable([value(`${input}:${turns}`), next(turns + 1)]);
456
+ }), Stream.runCollect))).toEqual([
457
+ "a:0",
458
+ "a:1",
459
+ "b:2",
460
+ "b:3"
461
+ ]);
462
+ });
463
+ it("threads state across inputs (audio-pipeline shape)", async () => {
464
+ globalExpect(await Effect.runPromise(Stream.fromIterable([
465
+ "x",
466
+ "y",
467
+ "z"
468
+ ]).pipe(loopFrom([], (history, input) => Stream.fromIterable([value([...history, input].join(",")), stopWith([...history, input])])), Stream.runCollect))).toEqual([
469
+ "x",
470
+ "x,y",
471
+ "x,y,z"
472
+ ]);
473
+ });
474
+ it("simulates a stream of documents with multi-turn tool calls per document", async () => {
475
+ globalExpect(await Effect.runPromise(Stream.fromIterable(["doc1", "doc2"]).pipe(loopFrom({
476
+ turn: 0,
477
+ totalTurns: 0
478
+ }, (state, doc) => {
479
+ if (state.turn === 0) return Stream.fromIterable([value({
480
+ kind: "text",
481
+ doc,
482
+ text: "thinking"
483
+ }), next({
484
+ turn: 1,
485
+ totalTurns: state.totalTurns + 1
486
+ })]);
487
+ if (state.turn === 1) return Stream.fromIterable([value({
488
+ kind: "tool",
489
+ doc,
490
+ tool: "search"
491
+ }), next({
492
+ turn: 2,
493
+ totalTurns: state.totalTurns + 1
494
+ })]);
495
+ return Stream.fromIterable([value({
496
+ kind: "text",
497
+ doc,
498
+ text: "final"
499
+ }), stopWith({
500
+ turn: 0,
501
+ totalTurns: state.totalTurns + 1
502
+ })]);
503
+ }), Stream.runCollect))).toEqual([
504
+ {
505
+ kind: "text",
506
+ doc: "doc1",
507
+ text: "thinking"
508
+ },
509
+ {
510
+ kind: "tool",
511
+ doc: "doc1",
512
+ tool: "search"
513
+ },
514
+ {
515
+ kind: "text",
516
+ doc: "doc1",
517
+ text: "final"
518
+ },
519
+ {
520
+ kind: "text",
521
+ doc: "doc2",
522
+ text: "thinking"
523
+ },
524
+ {
525
+ kind: "tool",
526
+ doc: "doc2",
527
+ tool: "search"
528
+ },
529
+ {
530
+ kind: "text",
531
+ doc: "doc2",
532
+ text: "final"
533
+ }
534
+ ]);
535
+ });
536
+ it("ends cleanly when the input stream ends mid-conversation", async () => {
537
+ globalExpect(await Effect.runPromise(Stream.fromIterable(["only"]).pipe(loopFrom(0, (turns, input) => turns >= 2 ? Stream.fromIterable([stopEvent]) : Stream.fromIterable([value(`${input}:${turns}`), next(turns + 1)])), Stream.runCollect))).toEqual(["only:0", "only:1"]);
538
+ });
539
+ it("body's `stop` advances to the next input (does NOT halt the whole stream)", async () => {
540
+ globalExpect(await Effect.runPromise(Stream.fromIterable([
541
+ 1,
542
+ 2,
543
+ 3
544
+ ]).pipe(loopFrom(0, (_state, input) => Stream.fromIterable([value(input * 10), stopEvent])), Stream.runCollect))).toEqual([
545
+ 10,
546
+ 20,
547
+ 30
548
+ ]);
549
+ });
550
+ it("data-first form (Function.dual) runs identically to data-last", async () => {
551
+ const inputs = Stream.fromIterable([1, 2]);
552
+ globalExpect(await Effect.runPromise(Stream.runCollect(loopFrom(inputs, 0, (state, input) => state >= input ? Stream.fromIterable([stopEvent]) : Stream.fromIterable([value(state + input), next(state + 1)]))))).toEqual([1, 3]);
553
+ });
554
+ it("supports Effect-returning bodies (parity with loop)", async () => {
555
+ globalExpect(await Effect.runPromise(Stream.fromIterable(["a"]).pipe(loopFrom(0, (turns, input) => Effect.gen(function* () {
556
+ const cur = yield* Effect.succeed(turns);
557
+ if (cur >= 2) return Stream.fromIterable([stopEvent]);
558
+ return Stream.fromIterable([value(`${input}:${cur}`), next(cur + 1)]);
559
+ })), Stream.runCollect))).toEqual(["a:0", "a:1"]);
560
+ });
561
+ it("type: data-last (pipe) form preserves the body's E channel", () => {
562
+ pipe(Stream.fromIterable([1]), loopFrom(0, (_state, _input) => Stream.fail(new RateLimited({
563
+ provider: "test",
564
+ raw: null
565
+ }))));
566
+ (0, import_dist.expectTypeOf)().toEqualTypeOf();
567
+ });
568
+ it("type: data-first form preserves the body's E channel and unifies with input's E", () => {
569
+ loopFrom(Stream.fail(/* @__PURE__ */ new Error("boom")), 0, (_state, _i) => Stream.fail(new RateLimited({
570
+ provider: "test",
571
+ raw: null
572
+ })));
573
+ (0, import_dist.expectTypeOf)().toEqualTypeOf();
574
+ });
575
+ });
408
576
  //#endregion
409
577
  export {};
410
578