@effect-uai/core 0.5.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/README.md +1 -1
  2. package/dist/{Audio-BfCTGnH3.d.mts → Audio-DPlTsNdz.d.mts} +3 -2
  3. package/dist/Audio-DPlTsNdz.d.mts.map +1 -0
  4. package/dist/{Items-BH8xUkoR.d.mts → Items-D8UaXQx_.d.mts} +15 -15
  5. package/dist/Items-D8UaXQx_.d.mts.map +1 -0
  6. package/dist/SandboxError-BvKypkJm.d.mts +158 -0
  7. package/dist/SandboxError-BvKypkJm.d.mts.map +1 -0
  8. package/dist/{Tool-87ViKCCO.d.mts → Tool-CiFXYjmR.d.mts} +11 -11
  9. package/dist/{Tool-87ViKCCO.d.mts.map → Tool-CiFXYjmR.d.mts.map} +1 -1
  10. package/dist/capabilities/Capabilities.d.mts +78 -0
  11. package/dist/capabilities/Capabilities.d.mts.map +1 -0
  12. package/dist/capabilities/Capabilities.mjs +65 -0
  13. package/dist/capabilities/Capabilities.mjs.map +1 -0
  14. package/dist/domain/Audio.d.mts +1 -1
  15. package/dist/domain/Audio.mjs.map +1 -1
  16. package/dist/domain/Items.d.mts +2 -2
  17. package/dist/domain/Items.mjs +15 -15
  18. package/dist/domain/Items.mjs.map +1 -1
  19. package/dist/domain/Music.d.mts +131 -72
  20. package/dist/domain/Music.d.mts.map +1 -1
  21. package/dist/domain/Music.mjs +36 -9
  22. package/dist/domain/Music.mjs.map +1 -1
  23. package/dist/domain/Transcript.d.mts +8 -2
  24. package/dist/domain/Transcript.d.mts.map +1 -1
  25. package/dist/domain/Transcript.mjs.map +1 -1
  26. package/dist/domain/Turn.d.mts +388 -2
  27. package/dist/domain/Turn.d.mts.map +1 -0
  28. package/dist/domain/Turn.mjs +13 -13
  29. package/dist/domain/Turn.mjs.map +1 -1
  30. package/dist/embedding-model/Embedding.d.mts +12 -2
  31. package/dist/embedding-model/Embedding.d.mts.map +1 -1
  32. package/dist/embedding-model/Embedding.mjs.map +1 -1
  33. package/dist/embedding-model/EmbeddingModel.d.mts +36 -23
  34. package/dist/embedding-model/EmbeddingModel.d.mts.map +1 -1
  35. package/dist/embedding-model/EmbeddingModel.mjs +20 -1
  36. package/dist/embedding-model/EmbeddingModel.mjs.map +1 -1
  37. package/dist/embedding-model/EmbeddingModel.test.mjs +15 -3
  38. package/dist/embedding-model/EmbeddingModel.test.mjs.map +1 -1
  39. package/dist/index.d.mts +13 -7
  40. package/dist/index.mjs +9 -3
  41. package/dist/language-model/LanguageModel.d.mts +5 -5
  42. package/dist/language-model/LanguageModel.d.mts.map +1 -1
  43. package/dist/language-model/LanguageModel.mjs.map +1 -1
  44. package/dist/loop/Loop.d.mts +37 -90
  45. package/dist/loop/Loop.d.mts.map +1 -1
  46. package/dist/loop/Loop.mjs +39 -93
  47. package/dist/loop/Loop.mjs.map +1 -1
  48. package/dist/loop/Loop.test.mjs +46 -54
  49. package/dist/loop/Loop.test.mjs.map +1 -1
  50. package/dist/music-generator/MusicGenerator.d.mts +48 -25
  51. package/dist/music-generator/MusicGenerator.d.mts.map +1 -1
  52. package/dist/music-generator/MusicGenerator.mjs +17 -9
  53. package/dist/music-generator/MusicGenerator.mjs.map +1 -1
  54. package/dist/music-generator/MusicGenerator.test.mjs +81 -56
  55. package/dist/music-generator/MusicGenerator.test.mjs.map +1 -1
  56. package/dist/sandbox/Memory.d.mts +53 -0
  57. package/dist/sandbox/Memory.d.mts.map +1 -0
  58. package/dist/sandbox/Memory.mjs +68 -0
  59. package/dist/sandbox/Memory.mjs.map +1 -0
  60. package/dist/sandbox/Sandbox.d.mts +717 -0
  61. package/dist/sandbox/Sandbox.d.mts.map +1 -0
  62. package/dist/sandbox/Sandbox.mjs +192 -0
  63. package/dist/sandbox/Sandbox.mjs.map +1 -0
  64. package/dist/sandbox/SandboxError.d.mts +2 -0
  65. package/dist/sandbox/SandboxError.mjs +89 -0
  66. package/dist/sandbox/SandboxError.mjs.map +1 -0
  67. package/dist/sandbox/SandboxImage.d.mts +28 -0
  68. package/dist/sandbox/SandboxImage.d.mts.map +1 -0
  69. package/dist/sandbox/SandboxImage.mjs +32 -0
  70. package/dist/sandbox/SandboxImage.mjs.map +1 -0
  71. package/dist/sandbox/SandboxNetwork.d.mts +34 -0
  72. package/dist/sandbox/SandboxNetwork.d.mts.map +1 -0
  73. package/dist/sandbox/SandboxNetwork.mjs +36 -0
  74. package/dist/sandbox/SandboxNetwork.mjs.map +1 -0
  75. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +84 -3
  76. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts.map +1 -1
  77. package/dist/speech-synthesizer/SpeechSynthesizer.mjs +32 -2
  78. package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -1
  79. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +59 -5
  80. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs.map +1 -1
  81. package/dist/testing/MockMusicGenerator.d.mts +12 -7
  82. package/dist/testing/MockMusicGenerator.d.mts.map +1 -1
  83. package/dist/testing/MockMusicGenerator.mjs +4 -3
  84. package/dist/testing/MockMusicGenerator.mjs.map +1 -1
  85. package/dist/testing/MockProvider.d.mts +3 -3
  86. package/dist/testing/MockProvider.d.mts.map +1 -1
  87. package/dist/testing/MockProvider.mjs.map +1 -1
  88. package/dist/testing/MockSandbox.d.mts +604 -0
  89. package/dist/testing/MockSandbox.d.mts.map +1 -0
  90. package/dist/testing/MockSandbox.mjs +160 -0
  91. package/dist/testing/MockSandbox.mjs.map +1 -0
  92. package/dist/testing/MockSpeechSynthesizer.d.mts +27 -12
  93. package/dist/testing/MockSpeechSynthesizer.d.mts.map +1 -1
  94. package/dist/testing/MockSpeechSynthesizer.mjs +75 -51
  95. package/dist/testing/MockSpeechSynthesizer.mjs.map +1 -1
  96. package/dist/tool/{Resolvers.d.mts → Approval.d.mts} +27 -27
  97. package/dist/tool/Approval.d.mts.map +1 -0
  98. package/dist/tool/{Resolvers.mjs → Approval.mjs} +18 -18
  99. package/dist/tool/Approval.mjs.map +1 -0
  100. package/dist/tool/{Resolvers.test.mjs → Approval.test.mjs} +40 -40
  101. package/dist/tool/Approval.test.mjs.map +1 -0
  102. package/dist/tool/HistoryCheck.d.mts +6 -6
  103. package/dist/tool/HistoryCheck.d.mts.map +1 -1
  104. package/dist/tool/HistoryCheck.mjs +5 -5
  105. package/dist/tool/HistoryCheck.mjs.map +1 -1
  106. package/dist/tool/Tool.d.mts +2 -2
  107. package/dist/tool/Tool.mjs +4 -4
  108. package/dist/tool/Tool.mjs.map +1 -1
  109. package/dist/tool/ToolEvent.d.mts +19 -19
  110. package/dist/tool/ToolEvent.d.mts.map +1 -1
  111. package/dist/tool/ToolEvent.mjs +8 -8
  112. package/dist/tool/ToolEvent.mjs.map +1 -1
  113. package/dist/tool/{Outcome.d.mts → ToolResult.d.mts} +29 -29
  114. package/dist/tool/ToolResult.d.mts.map +1 -0
  115. package/dist/tool/ToolResult.mjs +61 -0
  116. package/dist/tool/ToolResult.mjs.map +1 -0
  117. package/dist/tool/Toolkit.d.mts +58 -26
  118. package/dist/tool/Toolkit.d.mts.map +1 -1
  119. package/dist/tool/Toolkit.mjs +62 -34
  120. package/dist/tool/Toolkit.mjs.map +1 -1
  121. package/dist/tool/Toolkit.test.mjs +13 -13
  122. package/dist/tool/Toolkit.test.mjs.map +1 -1
  123. package/dist/transcriber/Transcriber.d.mts +11 -10
  124. package/dist/transcriber/Transcriber.d.mts.map +1 -1
  125. package/dist/transcriber/Transcriber.mjs.map +1 -1
  126. package/dist/transcriber/Transcriber.test.mjs +3 -3
  127. package/dist/transcriber/Transcriber.test.mjs.map +1 -1
  128. package/package.json +35 -7
  129. package/src/capabilities/Capabilities.ts +81 -0
  130. package/src/domain/Audio.ts +2 -1
  131. package/src/domain/Items.ts +9 -9
  132. package/src/domain/Music.ts +142 -68
  133. package/src/domain/Transcript.ts +7 -1
  134. package/src/domain/Turn.ts +16 -16
  135. package/src/embedding-model/Embedding.ts +11 -0
  136. package/src/embedding-model/EmbeddingModel.test.ts +15 -4
  137. package/src/embedding-model/EmbeddingModel.ts +48 -21
  138. package/src/index.ts +8 -2
  139. package/src/language-model/LanguageModel.ts +3 -3
  140. package/src/loop/Loop.test.ts +93 -91
  141. package/src/loop/Loop.ts +72 -170
  142. package/src/music-generator/MusicGenerator.test.ts +78 -53
  143. package/src/music-generator/MusicGenerator.ts +65 -24
  144. package/src/sandbox/Memory.ts +128 -0
  145. package/src/sandbox/Sandbox.ts +639 -0
  146. package/src/sandbox/SandboxError.ts +140 -0
  147. package/src/sandbox/SandboxImage.ts +26 -0
  148. package/src/sandbox/SandboxNetwork.ts +34 -0
  149. package/src/speech-synthesizer/SpeechSynthesizer.test.ts +92 -3
  150. package/src/speech-synthesizer/SpeechSynthesizer.ts +109 -0
  151. package/src/testing/MockMusicGenerator.ts +28 -18
  152. package/src/testing/MockProvider.ts +21 -20
  153. package/src/testing/MockSandbox.ts +342 -0
  154. package/src/testing/MockSpeechSynthesizer.ts +137 -77
  155. package/src/tool/{Resolvers.test.ts → Approval.test.ts} +45 -54
  156. package/src/tool/{Resolvers.ts → Approval.ts} +25 -25
  157. package/src/tool/HistoryCheck.ts +10 -13
  158. package/src/tool/Tool.ts +13 -15
  159. package/src/tool/ToolEvent.ts +7 -7
  160. package/src/tool/{Outcome.ts → ToolResult.ts} +20 -20
  161. package/src/tool/Toolkit.test.ts +13 -13
  162. package/src/tool/Toolkit.ts +97 -73
  163. package/src/transcriber/Transcriber.test.ts +3 -3
  164. package/src/transcriber/Transcriber.ts +10 -7
  165. package/dist/Audio-BfCTGnH3.d.mts.map +0 -1
  166. package/dist/Items-BH8xUkoR.d.mts.map +0 -1
  167. package/dist/Turn-0CwCAyVe.d.mts +0 -388
  168. package/dist/Turn-0CwCAyVe.d.mts.map +0 -1
  169. package/dist/tool/Outcome.d.mts.map +0 -1
  170. package/dist/tool/Outcome.mjs +0 -61
  171. package/dist/tool/Outcome.mjs.map +0 -1
  172. package/dist/tool/Resolvers.d.mts.map +0 -1
  173. package/dist/tool/Resolvers.mjs.map +0 -1
  174. package/dist/tool/Resolvers.test.mjs.map +0 -1
  175. /package/dist/tool/{Resolvers.test.d.mts → Approval.test.d.mts} +0 -0
@@ -3,17 +3,13 @@ import { describe, expect, expectTypeOf, it } from "vitest"
3
3
  import * as AiError from "../domain/AiError.js"
4
4
  import { TurnEvent } from "../domain/Turn.js"
5
5
  import {
6
- type Event,
6
+ type Step,
7
7
  loop,
8
- loopFrom,
8
+ loopOver,
9
9
  loopWithState,
10
10
  next,
11
- nextAfter,
12
11
  onTurnComplete,
13
12
  stop,
14
- stopAfter,
15
- stopEvent,
16
- stopWith,
17
13
  value,
18
14
  } from "./Loop.js"
19
15
 
@@ -22,8 +18,8 @@ describe("Loop.loop", () => {
22
18
  // Each iter emits [n, n + 0.5] then continues; final iter emits [n] and stops.
23
19
  const stream = loop(0, (n: number) =>
24
20
  n >= 3
25
- ? stopAfter(Stream.fromIterable([n]))
26
- : nextAfter(Stream.fromIterable([n, n + 0.5]), n + 1),
21
+ ? Stream.fromIterable([n]).pipe(Stream.map(value), Stream.concat(stop()))
22
+ : Stream.fromIterable([n, n + 0.5]).pipe(Stream.map(value), Stream.concat(next(n + 1))),
27
23
  )
28
24
 
29
25
  const result = await Effect.runPromise(Stream.runCollect(stream))
@@ -32,9 +28,7 @@ describe("Loop.loop", () => {
32
28
 
33
29
  it("supports iterations that emit zero values and only decide", async () => {
34
30
  // Every iteration emits nothing, just bumps state; stops at 5.
35
- const stream = loop(0, (n: number) =>
36
- n >= 5 ? Stream.fromIterable([stopEvent]) : Stream.fromIterable([next(n + 1)]),
37
- )
31
+ const stream = loop(0, (n: number) => (n >= 5 ? stop() : next(n + 1)))
38
32
 
39
33
  const result = await Effect.runPromise(Stream.runCollect(stream))
40
34
  expect(result).toEqual([])
@@ -47,8 +41,8 @@ describe("Loop.loop", () => {
47
41
  Effect.gen(function* () {
48
42
  const doubled = yield* Effect.succeed(n * 2)
49
43
  return doubled >= 16
50
- ? stopAfter(Stream.fromIterable([doubled]))
51
- : nextAfter(Stream.fromIterable([doubled]), doubled)
44
+ ? Stream.fromIterable([doubled]).pipe(Stream.map(value), Stream.concat(stop()))
45
+ : Stream.fromIterable([doubled]).pipe(Stream.map(value), Stream.concat(next(doubled)))
52
46
  }),
53
47
  )
54
48
 
@@ -62,8 +56,8 @@ describe("Loop.loop", () => {
62
56
  Effect.gen(function* () {
63
57
  const doubled = yield* Effect.succeed(n * 2)
64
58
  return doubled >= 4
65
- ? stopAfter(Stream.fromIterable([doubled]))
66
- : nextAfter(Stream.fromIterable([doubled]), doubled)
59
+ ? Stream.fromIterable([doubled]).pipe(Stream.map(value), Stream.concat(stop()))
60
+ : Stream.fromIterable([doubled]).pipe(Stream.map(value), Stream.concat(next(doubled)))
67
61
  }),
68
62
  ),
69
63
  )
@@ -76,8 +70,8 @@ describe("Loop.loop", () => {
76
70
  const boom = new Error("boom")
77
71
  const stream = loop(
78
72
  0,
79
- (n: number): Stream.Stream<Event<number, number>, Error> =>
80
- n === 2 ? Stream.fail(boom) : Stream.fromIterable([value(n), next(n + 1)]),
73
+ (n: number): Stream.Stream<Step<number, number>, Error> =>
74
+ n === 2 ? Stream.fail(boom) : Stream.succeed(value(n)).pipe(Stream.concat(next(n + 1))),
81
75
  )
82
76
 
83
77
  const result = await Effect.runPromiseExit(Stream.runCollect(stream))
@@ -93,14 +87,17 @@ describe("Loop.loop", () => {
93
87
  })
94
88
 
95
89
  it("short-circuits the body's stream when a Decision is seen", async () => {
96
- // Body emits [n, next(n+1), n+10]. Once the Decision is encountered, the
97
- // body's stream is interrupted - `n+10` is never pulled, so it never
98
- // flows to the outer stream. This is the correct behavior: a Decision
99
- // marks "I'm done with this iteration"; anything after it is dead code.
90
+ // Body emits value(n), then a Next decision, then value(n+10). Once the
91
+ // decision is encountered, the body is closed - `n+10` is never pulled, so
92
+ // it never flows to the outer stream. This is the correct behavior: a
93
+ // decision marks "I'm done with this iteration"; anything after it is dead.
100
94
  const stream = loop(0, (n: number) =>
101
95
  n >= 2
102
- ? Stream.fromIterable([value(n), stopEvent])
103
- : Stream.fromIterable([value(n), next(n + 1), value(n + 10)]),
96
+ ? Stream.succeed(value(n)).pipe(Stream.concat(stop()))
97
+ : Stream.succeed(value(n)).pipe(
98
+ Stream.concat(next(n + 1)),
99
+ Stream.concat(Stream.succeed(value(n + 10))),
100
+ ),
104
101
  )
105
102
 
106
103
  const result = await Effect.runPromise(Stream.runCollect(stream))
@@ -130,7 +127,7 @@ describe("Loop.loop", () => {
130
127
 
131
128
  it("type: onTurnComplete inside loop infers S and A from the handler without annotation", () => {
132
129
  // Regression: when piped through loop, onTurnComplete's handler return
133
- // type (Stream<Event<A, S>>) is the single source of truth for the loop
130
+ // type (Stream<Step<A, S>>) is the single source of truth for the loop
134
131
  // body's element type. The previous workaround required explicit
135
132
  // <S, A> at the call site. Now the loop's outer-return generics pull
136
133
  // them through automatically.
@@ -146,10 +143,11 @@ describe("Loop.loop", () => {
146
143
  onTurnComplete(() =>
147
144
  Effect.sync(() =>
148
145
  state.turns >= 1
149
- ? stop
150
- : nextAfter(Stream.succeed<ToolEvent>({ _tag: "tool", name: "x" }), {
151
- turns: state.turns + 1,
152
- }),
146
+ ? stop()
147
+ : Stream.succeed<ToolEvent>({ _tag: "tool", name: "x" }).pipe(
148
+ Stream.map(value),
149
+ Stream.concat(next({ turns: state.turns + 1 })),
150
+ ),
153
151
  ),
154
152
  ),
155
153
  )
@@ -170,8 +168,8 @@ describe("Loop.loop", () => {
170
168
  const textDelta: TurnEvent = TurnEvent.TextDelta({ text: "hi" })
171
169
  const deltas: Stream.Stream<TurnEvent> = Stream.fromIterable([textDelta, turnComplete])
172
170
 
173
- const dataFirst = onTurnComplete(deltas, () => Effect.sync(() => stop))
174
- const dataLast = deltas.pipe(onTurnComplete(() => Effect.sync(() => stop)))
171
+ const dataFirst = onTurnComplete(deltas, () => Effect.sync(() => stop()))
172
+ const dataLast = deltas.pipe(onTurnComplete(() => Effect.sync(() => stop())))
175
173
 
176
174
  const a = await Effect.runPromise(Stream.runCollect(dataFirst))
177
175
  const b = await Effect.runPromise(Stream.runCollect(dataLast))
@@ -186,8 +184,8 @@ describe("Loop.loop", () => {
186
184
  const N = 100_000
187
185
  const stream = loop(0, (n: number) =>
188
186
  n >= N
189
- ? Stream.fromIterable([value(n), stopEvent])
190
- : Stream.fromIterable([value(n), next(n + 1)]),
187
+ ? Stream.succeed(value(n)).pipe(Stream.concat(stop()))
188
+ : Stream.succeed(value(n)).pipe(Stream.concat(next(n + 1))),
191
189
  )
192
190
 
193
191
  const count = await Effect.runPromise(
@@ -254,7 +252,7 @@ const scriptedModel = (script: ReadonlyArray<ReadonlyArray<Delta>>): MockModel =
254
252
  * 2. flatMap projects deltas into UiEvents forwarded to the outer stream.
255
253
  * 3. Continuation reads the captured calls; if any, runs them, emits
256
254
  * tool_result events, builds the next state (with model swap if a tool
257
- * asked for one), and emits `next(state)`. Otherwise `stop`.
255
+ * asked for one), and emits `next(state)`. Otherwise `stop()`.
258
256
  */
259
257
  const conversationLoop = (initial: State, runTool: ToolRunner) =>
260
258
  loop(initial, (state) =>
@@ -265,7 +263,7 @@ const conversationLoop = (initial: State, runTool: ToolRunner) =>
265
263
  ReadonlyArray<{ readonly id: string; readonly name: string }>
266
264
  >([])
267
265
 
268
- const deltas: Stream.Stream<Event<UiEvent, State>> = state.model
266
+ const deltas: Stream.Stream<Step<UiEvent, State>> = state.model
269
267
  .streamTurn(state.history)
270
268
  .pipe(
271
269
  Stream.tap((d) =>
@@ -274,7 +272,7 @@ const conversationLoop = (initial: State, runTool: ToolRunner) =>
274
272
  : Ref.update(toolCallsRef, (t) => [...t, { id: d.id, name: d.name }]),
275
273
  ),
276
274
  Stream.flatMap(
277
- (d): Stream.Stream<Event<UiEvent, State>> =>
275
+ (d): Stream.Stream<Step<UiEvent, State>> =>
278
276
  d.type === "text"
279
277
  ? Stream.fromIterable([value<UiEvent>({ type: "text", text: d.text })])
280
278
  : Stream.fromIterable([
@@ -283,13 +281,13 @@ const conversationLoop = (initial: State, runTool: ToolRunner) =>
283
281
  ),
284
282
  )
285
283
 
286
- const continuation: Stream.Stream<Event<UiEvent, State>> = Stream.unwrap(
284
+ const continuation: Stream.Stream<Step<UiEvent, State>> = Stream.unwrap(
287
285
  Effect.gen(function* () {
288
286
  const texts = yield* Ref.get(textsRef)
289
287
  const toolCalls = yield* Ref.get(toolCallsRef)
290
288
 
291
289
  if (toolCalls.length === 0) {
292
- return stopAfter(Stream.empty)
290
+ return stop()
293
291
  }
294
292
 
295
293
  const turnItems: ReadonlyArray<HistoryItem> = [
@@ -326,7 +324,10 @@ const conversationLoop = (initial: State, runTool: ToolRunner) =>
326
324
  model: nextModel,
327
325
  }
328
326
 
329
- return nextAfter(Stream.fromIterable(events), nextState)
327
+ return Stream.fromIterable(events).pipe(
328
+ Stream.map(value),
329
+ Stream.concat(next(nextState)),
330
+ )
330
331
  }),
331
332
  )
332
333
 
@@ -419,8 +420,8 @@ describe("Loop.loop - pull-specific stream semantics", () => {
419
420
  Ref.update(callsRef, (calls) => calls + 1).pipe(
420
421
  Effect.as(
421
422
  n >= 10
422
- ? Stream.fromIterable([value(n), stopEvent])
423
- : Stream.fromIterable([value(n), next(n + 1)]),
423
+ ? Stream.succeed(value(n)).pipe(Stream.concat(stop()))
424
+ : Stream.succeed(value(n)).pipe(Stream.concat(next(n + 1))),
424
425
  ),
425
426
  ),
426
427
  ),
@@ -449,8 +450,11 @@ describe("Loop.loop - pull-specific stream semantics", () => {
449
450
  const releasesRef = yield* Ref.make<ReadonlyArray<number>>([])
450
451
  const stream = loop(0, (n: number) =>
451
452
  (n >= 1
452
- ? Stream.fromIterable([value(n), stopEvent])
453
- : Stream.fromIterable([value(n), next(n + 1), value(n + 10)])
453
+ ? Stream.succeed(value(n)).pipe(Stream.concat(stop()))
454
+ : Stream.succeed(value(n)).pipe(
455
+ Stream.concat(next(n + 1)),
456
+ Stream.concat(Stream.succeed(value(n + 10))),
457
+ )
454
458
  ).pipe(Stream.ensuring(Ref.update(releasesRef, (values) => [...values, n]))),
455
459
  )
456
460
 
@@ -468,7 +472,7 @@ describe("Loop.loop - pull-specific stream semantics", () => {
468
472
  Effect.gen(function* () {
469
473
  const started = yield* Deferred.make<void>()
470
474
  const releasesRef = yield* Ref.make(0)
471
- const body = (): Stream.Stream<Event<number, never>> =>
475
+ const body = (): Stream.Stream<Step<number, never>> =>
472
476
  Stream.concat(
473
477
  Stream.fromEffect(Deferred.succeed(started, undefined).pipe(Effect.as(value(0)))),
474
478
  Stream.never,
@@ -490,7 +494,7 @@ describe("Loop.loop - pull-specific stream semantics", () => {
490
494
  const defect = new Error("body construction failed")
491
495
  const result = await Effect.runPromiseExit(
492
496
  Stream.runCollect(
493
- loop(0, (): Stream.Stream<Event<number, never>> => {
497
+ loop(0, (): Stream.Stream<Step<number, never>> => {
494
498
  throw defect
495
499
  }),
496
500
  ),
@@ -504,7 +508,9 @@ describe("Loop.loopWithState", () => {
504
508
  it("exposes the final state in the SubscriptionRef after the stream completes", async () => {
505
509
  const program = Effect.gen(function* () {
506
510
  const { stream, state } = yield* loopWithState(0, (n: number) =>
507
- n >= 3 ? stopAfter(Stream.fromIterable([n])) : nextAfter(Stream.fromIterable([n]), n + 1),
511
+ n >= 3
512
+ ? Stream.fromIterable([n]).pipe(Stream.map(value), Stream.concat(stop()))
513
+ : Stream.fromIterable([n]).pipe(Stream.map(value), Stream.concat(next(n + 1))),
508
514
  )
509
515
  const values = yield* Stream.runCollect(stream)
510
516
  const finalState = yield* SubscriptionRef.get(state)
@@ -519,9 +525,7 @@ describe("Loop.loopWithState", () => {
519
525
 
520
526
  it("the state ref starts at `initial` and stays there if the loop stops without advancing", async () => {
521
527
  const program = Effect.gen(function* () {
522
- const { stream, state } = yield* loopWithState({ count: 7 }, () =>
523
- Stream.fromIterable([stopEvent]),
524
- )
528
+ const { stream, state } = yield* loopWithState({ count: 7 }, () => stop())
525
529
  yield* Stream.runDrain(stream)
526
530
  return yield* SubscriptionRef.get(state)
527
531
  })
@@ -535,7 +539,9 @@ describe("Loop.loopWithState", () => {
535
539
  // tracks loop state without the body needing to surface it.
536
540
  const program = Effect.gen(function* () {
537
541
  const { stream, state } = yield* loopWithState(0, (n: number) =>
538
- n >= 3 ? stopAfter(Stream.fromIterable([n])) : nextAfter(Stream.fromIterable([n]), n + 1),
542
+ n >= 3
543
+ ? Stream.fromIterable([n]).pipe(Stream.map(value), Stream.concat(stop()))
544
+ : Stream.fromIterable([n]).pipe(Stream.map(value), Stream.concat(next(n + 1))),
539
545
  )
540
546
  const seen: Array<{ value: number; stateAfter: number }> = []
541
547
  yield* Stream.runForEach(stream, (v) =>
@@ -565,7 +571,7 @@ describe("Loop.loopWithState", () => {
565
571
  const { stream, state } = yield* loopWithState(0, (n: number) =>
566
572
  Effect.gen(function* () {
567
573
  if (n === 0) yield* Latch.await(start)
568
- return n >= 3 ? stopAfter(Stream.empty) : nextAfter(Stream.empty, n + 1)
574
+ return n >= 3 ? stop() : next(n + 1)
569
575
  }),
570
576
  )
571
577
 
@@ -593,8 +599,8 @@ describe("Loop.loopWithState", () => {
593
599
  const program = Effect.gen(function* () {
594
600
  const { stream } = yield* loopWithState(0, (n: number) =>
595
601
  n >= 3
596
- ? stopAfter(Stream.fromIterable([n]))
597
- : nextAfter(Stream.fromIterable([n, n + 0.5]), n + 1),
602
+ ? Stream.fromIterable([n]).pipe(Stream.map(value), Stream.concat(stop()))
603
+ : Stream.fromIterable([n, n + 0.5]).pipe(Stream.map(value), Stream.concat(next(n + 1))),
598
604
  )
599
605
  return Array.from(yield* Stream.runCollect(stream))
600
606
  })
@@ -603,16 +609,16 @@ describe("Loop.loopWithState", () => {
603
609
  })
604
610
  })
605
611
 
606
- describe("Loop.loopFrom", () => {
612
+ describe("Loop.loopOver", () => {
607
613
  it("runs a multi-turn inner loop per input until the body emits stop", async () => {
608
614
  // Per input: emit (input + turnsSoFar) twice, then stop. State counts
609
615
  // total turns ACROSS inputs. Demonstrates that `next(s)` continues with
610
616
  // the SAME input, multiple times per input — not one body call per item.
611
617
  const result = await Effect.runPromise(
612
618
  Stream.fromIterable(["a", "b"]).pipe(
613
- loopFrom(0, (turns: number, input: string) => {
614
- if (turns >= 2 * (input === "a" ? 1 : 2)) return Stream.fromIterable([stopEvent])
615
- return Stream.fromIterable([value(`${input}:${turns}`), next(turns + 1)])
619
+ loopOver(0, (turns: number, input: string) => {
620
+ if (turns >= 2 * (input === "a" ? 1 : 2)) return stop()
621
+ return Stream.succeed(value(`${input}:${turns}`)).pipe(Stream.concat(next(turns + 1)))
616
622
  }),
617
623
  Stream.runCollect,
618
624
  ),
@@ -625,15 +631,14 @@ describe("Loop.loopFrom", () => {
625
631
 
626
632
  it("threads state across inputs (audio-pipeline shape)", async () => {
627
633
  // History accumulates across inputs. Each input emits its joined view of
628
- // history+input, then `stopWith` ends the inner loop AND carries the
634
+ // history+input, then `stop(state)` ends the inner loop AND carries the
629
635
  // updated history to the next input.
630
636
  const result = await Effect.runPromise(
631
637
  Stream.fromIterable(["x", "y", "z"]).pipe(
632
- loopFrom([] as ReadonlyArray<string>, (history: ReadonlyArray<string>, input: string) =>
633
- Stream.fromIterable([
634
- value([...history, input].join(",")),
635
- stopWith([...history, input]),
636
- ]),
638
+ loopOver([] as ReadonlyArray<string>, (history: ReadonlyArray<string>, input: string) =>
639
+ Stream.succeed(value([...history, input].join(","))).pipe(
640
+ Stream.concat(stop([...history, input])),
641
+ ),
637
642
  ),
638
643
  Stream.runCollect,
639
644
  ),
@@ -653,27 +658,24 @@ describe("Loop.loopFrom", () => {
653
658
 
654
659
  const result = await Effect.runPromise(
655
660
  Stream.fromIterable(["doc1", "doc2"]).pipe(
656
- loopFrom({ turn: 0, totalTurns: 0 } as State, (state, doc: string) => {
661
+ loopOver({ turn: 0, totalTurns: 0 } as State, (state, doc: string) => {
657
662
  // Each document runs three turns then stops.
658
663
  if (state.turn === 0) {
659
- return Stream.fromIterable([
660
- value<Turn>({ kind: "text", doc, text: "thinking" }),
661
- next({ turn: 1, totalTurns: state.totalTurns + 1 }),
662
- ])
664
+ return Stream.succeed(value<Turn>({ kind: "text", doc, text: "thinking" })).pipe(
665
+ Stream.concat(next({ turn: 1, totalTurns: state.totalTurns + 1 })),
666
+ )
663
667
  }
664
668
  if (state.turn === 1) {
665
- return Stream.fromIterable([
666
- value<Turn>({ kind: "tool", doc, tool: "search" }),
667
- next({ turn: 2, totalTurns: state.totalTurns + 1 }),
668
- ])
669
+ return Stream.succeed(value<Turn>({ kind: "tool", doc, tool: "search" })).pipe(
670
+ Stream.concat(next({ turn: 2, totalTurns: state.totalTurns + 1 })),
671
+ )
669
672
  }
670
- // Final turn — `stopWith` emits the final value, advances state
673
+ // Final turn — `stop(state)` emits the final value, advances state
671
674
  // (reset turn to 0 for the next document, bump totalTurns), and
672
675
  // ends this document's inner loop in one shot.
673
- return Stream.fromIterable([
674
- value<Turn>({ kind: "text", doc, text: "final" }),
675
- stopWith({ turn: 0, totalTurns: state.totalTurns + 1 }),
676
- ])
676
+ return Stream.succeed(value<Turn>({ kind: "text", doc, text: "final" })).pipe(
677
+ Stream.concat(stop({ turn: 0, totalTurns: state.totalTurns + 1 })),
678
+ )
677
679
  }),
678
680
  Stream.runCollect,
679
681
  ),
@@ -693,10 +695,10 @@ describe("Loop.loopFrom", () => {
693
695
  // Single-input case: body advances via `next` then stops cleanly.
694
696
  const result = await Effect.runPromise(
695
697
  Stream.fromIterable(["only"]).pipe(
696
- loopFrom(0, (turns: number, input: string) =>
698
+ loopOver(0, (turns: number, input: string) =>
697
699
  turns >= 2
698
- ? Stream.fromIterable([stopEvent])
699
- : Stream.fromIterable([value(`${input}:${turns}`), next(turns + 1)]),
700
+ ? stop()
701
+ : Stream.succeed(value(`${input}:${turns}`)).pipe(Stream.concat(next(turns + 1))),
700
702
  ),
701
703
  Stream.runCollect,
702
704
  ),
@@ -705,14 +707,14 @@ describe("Loop.loopFrom", () => {
705
707
  expect(result).toEqual(["only:0", "only:1"])
706
708
  })
707
709
 
708
- it("body's `stop` advances to the next input (does NOT halt the whole stream)", async () => {
710
+ it("body's `stop()` advances to the next input (does NOT halt the whole stream)", async () => {
709
711
  // Three inputs, body always stops on its first emission. All three
710
- // are processed — `stop` is per-input, not global. To halt the whole
712
+ // are processed — `stop()` is per-input, not global. To halt the whole
711
713
  // stream, end the INPUT stream upstream.
712
714
  const result = await Effect.runPromise(
713
715
  Stream.fromIterable([1, 2, 3]).pipe(
714
- loopFrom(0, (_state: number, input: number) =>
715
- Stream.fromIterable([value(input * 10), stopEvent]),
716
+ loopOver(0, (_state: number, input: number) =>
717
+ Stream.succeed(value(input * 10)).pipe(Stream.concat(stop())),
716
718
  ),
717
719
  Stream.runCollect,
718
720
  ),
@@ -725,10 +727,10 @@ describe("Loop.loopFrom", () => {
725
727
  const inputs = Stream.fromIterable([1, 2])
726
728
  const result = await Effect.runPromise(
727
729
  Stream.runCollect(
728
- loopFrom(inputs, 0, (state: number, input: number) =>
730
+ loopOver(inputs, 0, (state: number, input: number) =>
729
731
  state >= input
730
- ? Stream.fromIterable([stopEvent])
731
- : Stream.fromIterable([value(state + input), next(state + 1)]),
732
+ ? stop()
733
+ : Stream.succeed(value(state + input)).pipe(Stream.concat(next(state + 1))),
732
734
  ),
733
735
  ),
734
736
  )
@@ -741,11 +743,11 @@ describe("Loop.loopFrom", () => {
741
743
  it("supports Effect-returning bodies (parity with loop)", async () => {
742
744
  const result = await Effect.runPromise(
743
745
  Stream.fromIterable(["a"]).pipe(
744
- loopFrom(0, (turns: number, input: string) =>
746
+ loopOver(0, (turns: number, input: string) =>
745
747
  Effect.gen(function* () {
746
748
  const cur = yield* Effect.succeed(turns)
747
- if (cur >= 2) return Stream.fromIterable([stopEvent])
748
- return Stream.fromIterable([value(`${input}:${cur}`), next(cur + 1)])
749
+ if (cur >= 2) return stop()
750
+ return Stream.succeed(value(`${input}:${cur}`)).pipe(Stream.concat(next(cur + 1)))
749
751
  }),
750
752
  ),
751
753
  Stream.runCollect,
@@ -758,7 +760,7 @@ describe("Loop.loopFrom", () => {
758
760
  it("type: data-last (pipe) form preserves the body's E channel", () => {
759
761
  const result = pipe(
760
762
  Stream.fromIterable([1]),
761
- loopFrom(0, (_state: number, _input: number) =>
763
+ loopOver(0, (_state: number, _input: number) =>
762
764
  Stream.fail(new AiError.RateLimited({ provider: "test", raw: null })),
763
765
  ),
764
766
  )
@@ -768,7 +770,7 @@ describe("Loop.loopFrom", () => {
768
770
 
769
771
  it("type: data-first form preserves the body's E channel and unifies with input's E", () => {
770
772
  const input: Stream.Stream<number, Error> = Stream.fail(new Error("boom"))
771
- const result = loopFrom(input, 0, (_state: number, _i: number) =>
773
+ const result = loopOver(input, 0, (_state: number, _i: number) =>
772
774
  Stream.fail(new AiError.RateLimited({ provider: "test", raw: null })),
773
775
  )
774
776
  type E = typeof result extends Stream.Stream<unknown, infer X, unknown> ? X : never