@effect-uai/core 0.5.2 → 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 +11 -6
- package/dist/index.mjs +8 -3
- package/dist/language-model/LanguageModel.d.mts +5 -5
- package/dist/language-model/LanguageModel.d.mts.map +1 -1
- package/dist/language-model/LanguageModel.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/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.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/{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 +31 -7
- package/src/domain/Items.ts +9 -9
- package/src/domain/Turn.ts +16 -16
- package/src/index.ts +7 -2
- package/src/language-model/LanguageModel.ts +3 -3
- package/src/loop/Loop.test.ts +93 -91
- package/src/loop/Loop.ts +72 -170
- 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 +21 -20
- 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 → Approval.test.d.mts} +0 -0
package/src/loop/Loop.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pull-based `loop` for state-threaded sub-streams.
|
|
3
3
|
*
|
|
4
|
-
* Each iteration runs a body that returns a `Stream<
|
|
4
|
+
* Each iteration runs a body that returns a `Stream<Step<A, S>>`. The body
|
|
5
5
|
* emits values via `Loop.value(a)` and signals iteration control via
|
|
6
|
-
* `Loop.next(state)` (continue with new state) or `Loop.stop` (terminate).
|
|
7
|
-
* The loop unwraps `Value`
|
|
6
|
+
* `Loop.next(state)` (continue with new state) or `Loop.stop()` (terminate).
|
|
7
|
+
* The loop unwraps `Value` steps back to `A` for downstream consumers, so
|
|
8
8
|
* the resulting stream is a plain `Stream<A>`.
|
|
9
9
|
*
|
|
10
10
|
* The next body stream is only pulled when downstream pulls the outer
|
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
* scoped resources, and backpressure stay aligned with normal Stream
|
|
13
13
|
* semantics.
|
|
14
14
|
*
|
|
15
|
-
* Convention: a `Next` or `Stop`
|
|
15
|
+
* Convention: a `Next` or `Stop` step is the terminal element of a body's
|
|
16
16
|
* iteration. Values emitted in the same chunk after one are discarded
|
|
17
|
-
* (their producing side effects may already have run).
|
|
18
|
-
* `Loop.
|
|
17
|
+
* (their producing side effects may already have run). The `Loop.next` /
|
|
18
|
+
* `Loop.stop` helpers each emit a single terminal step, so concatenate
|
|
19
|
+
* your values before them (`values.pipe(Stream.concat(Loop.next(state)))`).
|
|
19
20
|
*/
|
|
20
21
|
import {
|
|
21
22
|
Array as Arr,
|
|
@@ -28,7 +29,6 @@ import {
|
|
|
28
29
|
Match,
|
|
29
30
|
Option,
|
|
30
31
|
Ref,
|
|
31
|
-
Result,
|
|
32
32
|
Scope,
|
|
33
33
|
Stream,
|
|
34
34
|
SubscriptionRef,
|
|
@@ -37,165 +37,67 @@ import { IncompleteTurn } from "../domain/AiError.js"
|
|
|
37
37
|
import { isTurnComplete, type Turn, type TurnEvent } from "../domain/Turn.js"
|
|
38
38
|
|
|
39
39
|
// ---------------------------------------------------------------------------
|
|
40
|
-
//
|
|
40
|
+
// Step type - the body's emit shape
|
|
41
41
|
// ---------------------------------------------------------------------------
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* The tagged union a body emits per pull. `Value` carries a payload that
|
|
45
45
|
* flows downstream. `Next` ends the current iteration and continues with a
|
|
46
46
|
* new state. `Stop` ends the loop entirely with no carried state.
|
|
47
|
-
* `StopWith` also ends the loop but carries a final state that `
|
|
47
|
+
* `StopWith` also ends the loop but carries a final state that `loopOver`
|
|
48
48
|
* will thread to the next input and `loopWithState` will write to its
|
|
49
49
|
* `SubscriptionRef` before the loop ends. Plain `loop` has no next
|
|
50
50
|
* iteration to apply it to and treats `StopWith` like `Stop`.
|
|
51
51
|
*
|
|
52
|
-
* `Stop` is intentionally `{}` so the bare `
|
|
53
|
-
*
|
|
52
|
+
* `Stop` is intentionally `{}` so the bare `stop()` helper doesn't
|
|
53
|
+
* constrain `S` from a body's stream type — every body has a `Stop`
|
|
54
54
|
* variant in its union, and forcing `S` to flow through it would break
|
|
55
|
-
* inference whenever the body never uses `next` / `
|
|
55
|
+
* inference whenever the body never uses `next` / `stop(state)`.
|
|
56
56
|
*/
|
|
57
|
-
export type
|
|
57
|
+
export type Step<A, S> = Data.TaggedEnum<{
|
|
58
58
|
Value: { readonly value: A }
|
|
59
59
|
Next: { readonly state: S }
|
|
60
60
|
Stop: {}
|
|
61
61
|
StopWith: { readonly state: S }
|
|
62
62
|
}>
|
|
63
63
|
|
|
64
|
-
interface
|
|
65
|
-
readonly taggedEnum:
|
|
64
|
+
interface StepDef extends Data.TaggedEnum.WithGenerics<2> {
|
|
65
|
+
readonly taggedEnum: Step<this["A"], this["B"]>
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
const
|
|
68
|
+
const Step = Data.taggedEnum<StepDef>()
|
|
69
69
|
|
|
70
70
|
/** Wrap a value so it flows through the loop to downstream consumers. */
|
|
71
|
-
export const value = <A>(a: A):
|
|
72
|
-
|
|
73
|
-
/** End the current iteration and continue with a new state. */
|
|
74
|
-
export const next = <S>(state: S): Event<never, S> => Event.Next({ state })
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* The terminal `Stop` event with no carried state. Use `stop` (the Stream)
|
|
78
|
-
* to end a loop body without communicating a final state.
|
|
79
|
-
*/
|
|
80
|
-
export const stopEvent: Event<never, never> = Event.Stop()
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Terminal event that ends the loop AND carries a final state. For
|
|
84
|
-
* `loopFrom` this is the natural "this input is done, here's the state to
|
|
85
|
-
* carry forward to the next input" signal — symmetric with `next(s)` but
|
|
86
|
-
* ending the inner loop instead of continuing it. For `loopWithState` the
|
|
87
|
-
* carried state is written to the `SubscriptionRef` before the loop ends.
|
|
88
|
-
*/
|
|
89
|
-
export const stopWith = <S>(state: S): Event<never, S> => Event.StopWith({ state })
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* A single-element stream that ends the loop. Return this from a body when
|
|
93
|
-
* there's nothing else to emit; equivalent to `stopAfter(Stream.empty)` but
|
|
94
|
-
* named for the common case.
|
|
95
|
-
*/
|
|
96
|
-
export const stop: Stream.Stream<Event<never, never>> = Stream.succeed(stopEvent)
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Pipe a raw `Stream<A>` into the loop's emit shape, then terminate the
|
|
100
|
-
* iteration with `next(state)`. Common shape for "stream this turn's
|
|
101
|
-
* deltas, then continue with updated history."
|
|
102
|
-
*
|
|
103
|
-
* Dual: data-first `nextAfter(stream, state)` and data-last
|
|
104
|
-
* `stream.pipe(nextAfter(state))` both work.
|
|
105
|
-
*/
|
|
106
|
-
export const nextAfter: {
|
|
107
|
-
<S>(state: S): <A, E, R>(stream: Stream.Stream<A, E, R>) => Stream.Stream<Event<A, S>, E, R>
|
|
108
|
-
<S, A, E, R>(stream: Stream.Stream<A, E, R>, state: S): Stream.Stream<Event<A, S>, E, R>
|
|
109
|
-
} = Function.dual(
|
|
110
|
-
2,
|
|
111
|
-
<S, A, E, R>(stream: Stream.Stream<A, E, R>, state: S): Stream.Stream<Event<A, S>, E, R> =>
|
|
112
|
-
Stream.concat(Stream.map(stream, value), Stream.fromIterable([next(state)])),
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Pipe a raw `Stream<A>` into the loop's emit shape, then terminate the
|
|
117
|
-
* loop. Common shape for "stream this turn's deltas, then we're done."
|
|
118
|
-
*
|
|
119
|
-
* Unary on the stream — already pipe-compatible via `stream.pipe(stopAfter)`.
|
|
120
|
-
*/
|
|
121
|
-
export const stopAfter = <A, E, R>(
|
|
122
|
-
stream: Stream.Stream<A, E, R>,
|
|
123
|
-
): Stream.Stream<Event<A, never>, E, R> =>
|
|
124
|
-
Stream.concat(Stream.map(stream, value), Stream.fromIterable([stopEvent]))
|
|
71
|
+
export const value = <A>(a: A): Step<A, never> => Step.Value({ value: a })
|
|
125
72
|
|
|
126
73
|
/**
|
|
127
|
-
*
|
|
128
|
-
* `
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* Dual: data-first `stopWithAfter(stream, state)` and data-last
|
|
132
|
-
* `stream.pipe(stopWithAfter(state))` both work.
|
|
74
|
+
* End the current iteration and continue with a new state. Emits a single
|
|
75
|
+
* terminal `Next` step as a one-element stream, so it slots directly into a
|
|
76
|
+
* loop body or after a run of values: `values.pipe(Stream.concat(next(s)))`.
|
|
133
77
|
*/
|
|
134
|
-
export const
|
|
135
|
-
|
|
136
|
-
<S, A, E, R>(stream: Stream.Stream<A, E, R>, state: S): Stream.Stream<Event<A, S>, E, R>
|
|
137
|
-
} = Function.dual(
|
|
138
|
-
2,
|
|
139
|
-
<S, A, E, R>(stream: Stream.Stream<A, E, R>, state: S): Stream.Stream<Event<A, S>, E, R> =>
|
|
140
|
-
Stream.concat(Stream.map(stream, value), Stream.fromIterable([stopWith(state)])),
|
|
141
|
-
)
|
|
78
|
+
export const next = <S>(state: S): Stream.Stream<Step<never, S>> =>
|
|
79
|
+
Stream.succeed(Step.Next({ state }))
|
|
142
80
|
|
|
143
81
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* results and build next state without exposing a Ref to recipes.
|
|
150
|
-
*
|
|
151
|
-
* Dual: data-first `nextAfterFold(stream, initial, reduce, build)` and
|
|
152
|
-
* data-last `stream.pipe(nextAfterFold(initial, reduce, build))` both work.
|
|
82
|
+
* End the loop. Called with no argument it emits a bare `Stop`; called with
|
|
83
|
+
* a state it emits `StopWith(state)` — `loopOver` threads that final state
|
|
84
|
+
* to the next input and `loopWithState` writes it to its `SubscriptionRef`
|
|
85
|
+
* before the loop ends. Either way it returns a single-element stream, so
|
|
86
|
+
* return it directly from a loop body.
|
|
153
87
|
*/
|
|
154
|
-
export const
|
|
155
|
-
<
|
|
156
|
-
initial: B,
|
|
157
|
-
reduce: (acc: B, a: A) => B,
|
|
158
|
-
build: (b: B) => S,
|
|
159
|
-
): <E, R>(stream: Stream.Stream<A, E, R>) => Stream.Stream<Event<A, S>, E, R>
|
|
160
|
-
<A, B, S, E, R>(
|
|
161
|
-
stream: Stream.Stream<A, E, R>,
|
|
162
|
-
initial: B,
|
|
163
|
-
reduce: (acc: B, a: A) => B,
|
|
164
|
-
build: (b: B) => S,
|
|
165
|
-
): Stream.Stream<Event<A, S>, E, R>
|
|
166
|
-
} = Function.dual(
|
|
167
|
-
4,
|
|
168
|
-
<A, B, S, E, R>(
|
|
169
|
-
stream: Stream.Stream<A, E, R>,
|
|
170
|
-
initial: B,
|
|
171
|
-
reduce: (acc: B, a: A) => B,
|
|
172
|
-
build: (b: B) => S,
|
|
173
|
-
): Stream.Stream<Event<A, S>, E, R> =>
|
|
174
|
-
Stream.unwrap(
|
|
175
|
-
Effect.gen(function* () {
|
|
176
|
-
const ref = yield* Ref.make(initial)
|
|
177
|
-
const tapped = stream.pipe(
|
|
178
|
-
Stream.tap((a) => Ref.update(ref, (acc) => reduce(acc, a))),
|
|
179
|
-
Stream.map(value),
|
|
180
|
-
)
|
|
181
|
-
const continuation = Stream.fromEffect(
|
|
182
|
-
Ref.get(ref).pipe(Effect.map((acc) => next(build(acc)))),
|
|
183
|
-
)
|
|
184
|
-
return tapped.pipe(Stream.concat(continuation))
|
|
185
|
-
}),
|
|
186
|
-
),
|
|
187
|
-
)
|
|
88
|
+
export const stop = <S = never>(state?: S): Stream.Stream<Step<never, S>> =>
|
|
89
|
+
Stream.succeed<Step<never, S>>(state === undefined ? Step.Stop() : Step.StopWith({ state }))
|
|
188
90
|
|
|
189
91
|
// ---------------------------------------------------------------------------
|
|
190
92
|
// onTurnComplete - turn-aware stream operator for loop bodies
|
|
191
93
|
// ---------------------------------------------------------------------------
|
|
192
94
|
|
|
193
95
|
/**
|
|
194
|
-
* Lift a provider's `Stream<TurnEvent>` into a loop body's `Stream<
|
|
96
|
+
* Lift a provider's `Stream<TurnEvent>` into a loop body's `Stream<Step<TurnEvent | A, S>>`.
|
|
195
97
|
* Each delta passes through as `value(delta)` (including the terminal
|
|
196
98
|
* `TurnComplete`, so the consumer sees turn boundaries naturally). Once
|
|
197
99
|
* the terminal arrives, `then(turn)` runs and its returned stream of loop
|
|
198
|
-
*
|
|
100
|
+
* steps (typically tool outputs followed by `next(state)` or `stop()`) is
|
|
199
101
|
* concatenated.
|
|
200
102
|
*
|
|
201
103
|
* Pre-pipe transforms (`Stream.tap` / `Stream.map` / `Stream.filter`) on
|
|
@@ -210,25 +112,25 @@ export const nextAfterFold: {
|
|
|
210
112
|
*/
|
|
211
113
|
export const onTurnComplete: {
|
|
212
114
|
<S, A, E2 = never, R2 = never>(
|
|
213
|
-
then: (turn: Turn) => Effect.Effect<Stream.Stream<
|
|
115
|
+
then: (turn: Turn) => Effect.Effect<Stream.Stream<Step<A, S>, E2, R2>, E2, R2>,
|
|
214
116
|
): <E, R>(
|
|
215
117
|
deltas: Stream.Stream<TurnEvent, E, R>,
|
|
216
|
-
) => Stream.Stream<
|
|
118
|
+
) => Stream.Stream<Step<TurnEvent | A, S>, E | E2 | IncompleteTurn, R | R2>
|
|
217
119
|
<S, A, E, R, E2 = never, R2 = never>(
|
|
218
120
|
deltas: Stream.Stream<TurnEvent, E, R>,
|
|
219
|
-
then: (turn: Turn) => Effect.Effect<Stream.Stream<
|
|
220
|
-
): Stream.Stream<
|
|
121
|
+
then: (turn: Turn) => Effect.Effect<Stream.Stream<Step<A, S>, E2, R2>, E2, R2>,
|
|
122
|
+
): Stream.Stream<Step<TurnEvent | A, S>, E | E2 | IncompleteTurn, R | R2>
|
|
221
123
|
} = Function.dual(
|
|
222
124
|
2,
|
|
223
125
|
<S, A, E, R, E2, R2>(
|
|
224
126
|
deltas: Stream.Stream<TurnEvent, E, R>,
|
|
225
|
-
then: (turn: Turn) => Effect.Effect<Stream.Stream<
|
|
226
|
-
): Stream.Stream<
|
|
127
|
+
then: (turn: Turn) => Effect.Effect<Stream.Stream<Step<A, S>, E2, R2>, E2, R2>,
|
|
128
|
+
): Stream.Stream<Step<TurnEvent | A, S>, E | E2 | IncompleteTurn, R | R2> =>
|
|
227
129
|
Stream.unwrap(
|
|
228
130
|
Effect.gen(function* () {
|
|
229
131
|
const turnRef = yield* Ref.make<Option.Option<Turn>>(Option.none())
|
|
230
132
|
|
|
231
|
-
const events: Stream.Stream<
|
|
133
|
+
const events: Stream.Stream<Step<TurnEvent, S>, E, R> = deltas.pipe(
|
|
232
134
|
Stream.tap((delta) =>
|
|
233
135
|
isTurnComplete(delta) ? Ref.set(turnRef, Option.some(delta.turn)) : Effect.void,
|
|
234
136
|
),
|
|
@@ -257,7 +159,7 @@ const isNonEmpty = <A>(array: ReadonlyArray<A>): array is readonly [A, ...Array<
|
|
|
257
159
|
|
|
258
160
|
type CurrentBody<S, A, E, R> = {
|
|
259
161
|
readonly scope: Scope.Closeable
|
|
260
|
-
readonly pull: Effect.Effect<ReadonlyArray<
|
|
162
|
+
readonly pull: Effect.Effect<ReadonlyArray<Step<A, S>>, E | Cause.Done<void>, R>
|
|
261
163
|
}
|
|
262
164
|
|
|
263
165
|
const closeBody = <S, A, E, R>(
|
|
@@ -266,23 +168,23 @@ const closeBody = <S, A, E, R>(
|
|
|
266
168
|
) => Scope.close(current.scope, exit)
|
|
267
169
|
|
|
268
170
|
/**
|
|
269
|
-
* Walk a chunk of `
|
|
171
|
+
* Walk a chunk of `Step<A, S>` until a terminal `Next` or `Stop` is found.
|
|
270
172
|
* Returns the unwrapped values seen so far and (optionally) the terminal
|
|
271
|
-
*
|
|
173
|
+
* step. Anything in the chunk after the terminal is discarded - its
|
|
272
174
|
* producing side effects may have run, but downstream never sees it.
|
|
273
175
|
*/
|
|
274
176
|
const partitionChunk = <A, S>(
|
|
275
|
-
chunk: ReadonlyArray<
|
|
177
|
+
chunk: ReadonlyArray<Step<A, S>>,
|
|
276
178
|
): {
|
|
277
179
|
readonly values: ReadonlyArray<A>
|
|
278
|
-
readonly decision: Option.Option<
|
|
180
|
+
readonly decision: Option.Option<Step<A, S>>
|
|
279
181
|
} => {
|
|
280
|
-
const [
|
|
182
|
+
const [valueSteps, rest] = Arr.span(
|
|
281
183
|
chunk,
|
|
282
|
-
(e): e is
|
|
184
|
+
(e): e is Step<A, S> & { _tag: "Value" } => e._tag === "Value",
|
|
283
185
|
)
|
|
284
186
|
return {
|
|
285
|
-
values:
|
|
187
|
+
values: valueSteps.map((e) => e.value),
|
|
286
188
|
decision: Arr.head(rest),
|
|
287
189
|
}
|
|
288
190
|
}
|
|
@@ -293,12 +195,12 @@ const partitionChunk = <A, S>(
|
|
|
293
195
|
|
|
294
196
|
type LoopBody<S, A, E, R> = (
|
|
295
197
|
state: S,
|
|
296
|
-
) => Stream.Stream<
|
|
198
|
+
) => Stream.Stream<Step<A, S>, E, R> | Effect.Effect<Stream.Stream<Step<A, S>, E, R>, E, R>
|
|
297
199
|
|
|
298
200
|
/**
|
|
299
201
|
* Drive a state-threaded loop body. Each iteration runs `body(state)` to get
|
|
300
|
-
* a `Stream<
|
|
301
|
-
* a new state, `stop` ends the loop. See the file header for the full
|
|
202
|
+
* a `Stream<Step<A, S>>`; values flow downstream, `next(s)` continues with
|
|
203
|
+
* a new state, `stop()` ends the loop. See the file header for the full
|
|
302
204
|
* pull-based execution model.
|
|
303
205
|
*
|
|
304
206
|
* Dual: data-first `loop(initial, body)` and data-last `loop(body)(initial)`
|
|
@@ -352,7 +254,7 @@ export const loop: {
|
|
|
352
254
|
const chunk = yield* active.pull.pipe(
|
|
353
255
|
Effect.catchIf(Cause.isDone, () =>
|
|
354
256
|
closeActive(active, Exit.void).pipe(
|
|
355
|
-
Effect.as(undefined as ReadonlyArray<
|
|
257
|
+
Effect.as(undefined as ReadonlyArray<Step<A, S>> | undefined),
|
|
356
258
|
),
|
|
357
259
|
),
|
|
358
260
|
Effect.onError((cause) => closeActive(active, Exit.failCause(cause))),
|
|
@@ -369,7 +271,7 @@ export const loop: {
|
|
|
369
271
|
yield* closeActive(active, Exit.void)
|
|
370
272
|
if (decision.value._tag === "Stop" || decision.value._tag === "StopWith") {
|
|
371
273
|
// `loop` has no next iteration to apply StopWith's state to;
|
|
372
|
-
// the state lands in `
|
|
274
|
+
// the state lands in `loopOver`'s outer ref or
|
|
373
275
|
// `loopWithState`'s SubscriptionRef via their taps.
|
|
374
276
|
done = true
|
|
375
277
|
} else if (decision.value._tag === "Next") {
|
|
@@ -378,7 +280,7 @@ export const loop: {
|
|
|
378
280
|
}
|
|
379
281
|
|
|
380
282
|
// Emit the values seen so far if any. Chunks from a Stream pull
|
|
381
|
-
// are non-empty, so when `decision` is `None` every
|
|
283
|
+
// are non-empty, so when `decision` is `None` every step was
|
|
382
284
|
// a `Value` and `values` is non-empty here. With a decision and
|
|
383
285
|
// no preceding values, fall through to the next iteration.
|
|
384
286
|
if (isNonEmpty(values)) return values
|
|
@@ -392,54 +294,54 @@ export const loop: {
|
|
|
392
294
|
)
|
|
393
295
|
|
|
394
296
|
// ---------------------------------------------------------------------------
|
|
395
|
-
//
|
|
297
|
+
// loopOver - stream-driven sibling of loop. One input item runs a full
|
|
396
298
|
// multi-turn inner loop.
|
|
397
299
|
// ---------------------------------------------------------------------------
|
|
398
300
|
|
|
399
|
-
type
|
|
301
|
+
type LoopOverBody<S, I, A, E, R> = (
|
|
400
302
|
state: S,
|
|
401
303
|
input: I,
|
|
402
|
-
) => Stream.Stream<
|
|
304
|
+
) => Stream.Stream<Step<A, S>, E, R> | Effect.Effect<Stream.Stream<Step<A, S>, E, R>, E, R>
|
|
403
305
|
|
|
404
306
|
/**
|
|
405
307
|
* Input-driven sibling of `loop`. For each item pulled from the input
|
|
406
308
|
* stream, runs an inner seed-driven `loop` whose body is
|
|
407
309
|
* `(s) => body(s, item)`. State is threaded across input items.
|
|
408
310
|
*
|
|
409
|
-
* **Per-input semantics — the body emits standard `
|
|
311
|
+
* **Per-input semantics — the body emits standard `Step<A, S>`:**
|
|
410
312
|
* - `value(a)`: emit `a` downstream
|
|
411
313
|
* - `next(s)`: re-run the body with the SAME input and new state `s`
|
|
412
314
|
* (multi-turn within one input — e.g. multiple model turns + tool
|
|
413
315
|
* calls for one document)
|
|
414
|
-
* - `stop`: end this input's inner loop, advance to the next input
|
|
316
|
+
* - `stop()`: end this input's inner loop, advance to the next input
|
|
415
317
|
* (state preserved)
|
|
416
|
-
* - body stream ending without a decision: same as `stop` (advance)
|
|
318
|
+
* - body stream ending without a decision: same as `stop()` (advance)
|
|
417
319
|
*
|
|
418
320
|
* **Outer termination:** the input stream ending. To halt programmatically
|
|
419
321
|
* from within, end the input stream upstream (`Stream.takeWhile`, a
|
|
420
|
-
* `SubscriptionRef` gate, etc.). Reserving `stop` for per-item
|
|
322
|
+
* `SubscriptionRef` gate, etc.). Reserving `stop()` for per-item
|
|
421
323
|
* advancement is what makes the common "stream of documents, multi-turn
|
|
422
324
|
* conversation per document" shape readable.
|
|
423
325
|
*
|
|
424
|
-
* Dual: data-first `
|
|
425
|
-
* `input.pipe(
|
|
326
|
+
* Dual: data-first `loopOver(input, initial, body)` and data-last
|
|
327
|
+
* `input.pipe(loopOver(initial, body))` both work.
|
|
426
328
|
*/
|
|
427
|
-
export const
|
|
329
|
+
export const loopOver: {
|
|
428
330
|
<S, I, A, E, R>(
|
|
429
331
|
initial: S,
|
|
430
|
-
body:
|
|
332
|
+
body: LoopOverBody<S, I, A, E, R>,
|
|
431
333
|
): <EI, RI>(input: Stream.Stream<I, EI, RI>) => Stream.Stream<A, E | EI, R | RI>
|
|
432
334
|
<S, I, A, E, R, EI, RI>(
|
|
433
335
|
input: Stream.Stream<I, EI, RI>,
|
|
434
336
|
initial: S,
|
|
435
|
-
body:
|
|
337
|
+
body: LoopOverBody<S, I, A, E, R>,
|
|
436
338
|
): Stream.Stream<A, E | EI, R | RI>
|
|
437
339
|
} = Function.dual(
|
|
438
340
|
3,
|
|
439
341
|
<S, I, A, E, R, EI, RI>(
|
|
440
342
|
input: Stream.Stream<I, EI, RI>,
|
|
441
343
|
initial: S,
|
|
442
|
-
body:
|
|
344
|
+
body: LoopOverBody<S, I, A, E, R>,
|
|
443
345
|
): Stream.Stream<A, E | EI, R | RI> =>
|
|
444
346
|
Stream.unwrap(
|
|
445
347
|
Effect.gen(function* () {
|
|
@@ -449,15 +351,15 @@ export const loopFrom: {
|
|
|
449
351
|
Stream.unwrap(
|
|
450
352
|
Effect.gen(function* () {
|
|
451
353
|
const state = yield* Ref.get(stateRef)
|
|
452
|
-
// Capture Next states (and
|
|
354
|
+
// Capture Next states (and stop(state)'s final state) into the
|
|
453
355
|
// outer ref so the LAST state seen in this input's inner
|
|
454
356
|
// loop is what the next input starts from.
|
|
455
357
|
const wrappedBody = (s: S) => {
|
|
456
358
|
const result = body(s, item)
|
|
457
359
|
const stream = Effect.isEffect(result) ? Stream.unwrap(result) : result
|
|
458
360
|
return stream.pipe(
|
|
459
|
-
Stream.tap((
|
|
460
|
-
Match.value(
|
|
361
|
+
Stream.tap((step) =>
|
|
362
|
+
Match.value(step).pipe(
|
|
461
363
|
Match.tags({
|
|
462
364
|
Next: (e) => Ref.set(stateRef, e.state),
|
|
463
365
|
StopWith: (e) => Ref.set(stateRef, e.state),
|
|
@@ -485,7 +387,7 @@ export const loopFrom: {
|
|
|
485
387
|
* alongside the value stream.
|
|
486
388
|
*
|
|
487
389
|
* Allocates one `SubscriptionRef<S>` seeded with `initial`, then runs the
|
|
488
|
-
* loop with a wrapped body that taps every `Next(s)`
|
|
390
|
+
* loop with a wrapped body that taps every `Next(s)` step into the ref
|
|
489
391
|
* before forwarding it. The caller decides how to consume both channels:
|
|
490
392
|
*
|
|
491
393
|
* - **Final state**: drain the stream, then `SubscriptionRef.get(state)`
|
|
@@ -510,10 +412,10 @@ export const loopWithState = <S, A, E, R>(
|
|
|
510
412
|
Effect.gen(function* () {
|
|
511
413
|
const stateRef = yield* SubscriptionRef.make(initial)
|
|
512
414
|
|
|
513
|
-
const tap = (stream: Stream.Stream<
|
|
415
|
+
const tap = (stream: Stream.Stream<Step<A, S>, E, R>): Stream.Stream<Step<A, S>, E, R> =>
|
|
514
416
|
stream.pipe(
|
|
515
|
-
Stream.tap((
|
|
516
|
-
Match.value(
|
|
417
|
+
Stream.tap((step) =>
|
|
418
|
+
Match.value(step).pipe(
|
|
517
419
|
Match.tags({
|
|
518
420
|
Next: (e) => SubscriptionRef.set(stateRef, e.state),
|
|
519
421
|
StopWith: (e) => SubscriptionRef.set(stateRef, e.state),
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Brand } from "effect"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A memory / data size, always represented internally as a number of
|
|
5
|
+
* bytes. Mirrors the `Duration` shape from `effect` — a branded value
|
|
6
|
+
* type with a wider `Input` union for ergonomic construction at call
|
|
7
|
+
* sites.
|
|
8
|
+
*
|
|
9
|
+
* `effect` itself has no byte-count primitive (only `Duration`), so we
|
|
10
|
+
* own this here rather than leak provider-specific shapes (Deno's
|
|
11
|
+
* template-literal `Memory`, Vercel's `mb`, Modal's `gpuMemory`, …)
|
|
12
|
+
* through the cross-provider `Sandbox` request types.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import * as Memory from "@effect-uai/core/Memory"
|
|
17
|
+
*
|
|
18
|
+
* Memory.gib(2) // 2 GiB → 2_147_483_648 bytes
|
|
19
|
+
* Memory.fromInputUnsafe("1.5 GiB") // human-string parsing
|
|
20
|
+
* Memory.fromInputUnsafe(1_280_000_000) // raw bytes
|
|
21
|
+
* Memory.toBytes(m) // unwrap
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export type Memory = Brand.Branded<number, "Memory">
|
|
26
|
+
|
|
27
|
+
const make = Brand.nominal<Memory>()
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Input — what the public APIs accept.
|
|
31
|
+
//
|
|
32
|
+
// Supported string forms (case-insensitive on the unit, whitespace
|
|
33
|
+
// optional between number and unit):
|
|
34
|
+
//
|
|
35
|
+
// "<n>" bytes (no unit)
|
|
36
|
+
// "<n> B" bytes
|
|
37
|
+
// "<n> kB" 1_000 bytes (decimal)
|
|
38
|
+
// "<n> MB" 1_000_000 (decimal)
|
|
39
|
+
// "<n> GB" 1_000_000_000 (decimal)
|
|
40
|
+
// "<n> KiB" 1_024 (binary)
|
|
41
|
+
// "<n> MiB" 1_048_576 (binary)
|
|
42
|
+
// "<n> GiB" 1_073_741_824 (binary)
|
|
43
|
+
//
|
|
44
|
+
// Decimal vs binary follows the SI / IEC convention (`MB` = 10^6,
|
|
45
|
+
// `MiB` = 2^20). When in doubt, prefer the binary `*iB` units.
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
export type Input = number | Memory | string
|
|
49
|
+
|
|
50
|
+
const UNIT_BYTES: Record<string, number> = {
|
|
51
|
+
b: 1,
|
|
52
|
+
kb: 1_000,
|
|
53
|
+
mb: 1_000_000,
|
|
54
|
+
gb: 1_000_000_000,
|
|
55
|
+
tb: 1_000_000_000_000,
|
|
56
|
+
kib: 1_024,
|
|
57
|
+
mib: 1_024 * 1_024,
|
|
58
|
+
gib: 1_024 * 1_024 * 1_024,
|
|
59
|
+
tib: 1_024 * 1_024 * 1_024 * 1_024,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const PARSE_RE = /^\s*(\d+(?:\.\d+)?)\s*([a-zA-Z]*)\s*$/
|
|
63
|
+
|
|
64
|
+
const parseString = (s: string): number => {
|
|
65
|
+
const m = PARSE_RE.exec(s)
|
|
66
|
+
if (m === null) {
|
|
67
|
+
throw new Error(`Memory: cannot parse "${s}" — expected forms like "1024", "10 MB", "1GiB"`)
|
|
68
|
+
}
|
|
69
|
+
const value = Number(m[1])
|
|
70
|
+
const unit = (m[2] ?? "").toLowerCase()
|
|
71
|
+
if (unit === "") return value
|
|
72
|
+
const factor = UNIT_BYTES[unit]
|
|
73
|
+
if (factor === undefined) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Memory: unknown unit "${m[2]}" in "${s}" — supported: ${Object.keys(UNIT_BYTES).join(", ")}`,
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
return value * factor
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Coerce a {@link Input} to a {@link Memory}. Throws on malformed
|
|
83
|
+
* strings or non-finite numbers. Mirrors `Duration.fromInputUnsafe`.
|
|
84
|
+
*/
|
|
85
|
+
export const fromInputUnsafe = (input: Input): Memory => {
|
|
86
|
+
if (typeof input === "number") {
|
|
87
|
+
if (!Number.isFinite(input) || input < 0) {
|
|
88
|
+
throw new Error(`Memory: bytes must be a non-negative finite number, got ${input}`)
|
|
89
|
+
}
|
|
90
|
+
return make(Math.floor(input))
|
|
91
|
+
}
|
|
92
|
+
return make(Math.floor(parseString(input)))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Unwrap
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
/** Return the underlying byte count. */
|
|
100
|
+
export const toBytes = (m: Memory | Input): number =>
|
|
101
|
+
typeof m === "number" && (m as number) === Math.floor(m as number) && (m as number) >= 0
|
|
102
|
+
? (m as number)
|
|
103
|
+
: (fromInputUnsafe(m) as unknown as number)
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Unit constructors — terse for use at call sites.
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
/** N bytes. */
|
|
110
|
+
export const bytes = (n: number): Memory => make(Math.floor(n))
|
|
111
|
+
|
|
112
|
+
/** N KiB (binary kibibytes, `n * 1024`). */
|
|
113
|
+
export const kib = (n: number): Memory => make(Math.floor(n * 1_024))
|
|
114
|
+
|
|
115
|
+
/** N MiB (binary mebibytes, `n * 1024^2`). */
|
|
116
|
+
export const mib = (n: number): Memory => make(Math.floor(n * 1_024 * 1_024))
|
|
117
|
+
|
|
118
|
+
/** N GiB (binary gibibytes, `n * 1024^3`). */
|
|
119
|
+
export const gib = (n: number): Memory => make(Math.floor(n * 1_024 * 1_024 * 1_024))
|
|
120
|
+
|
|
121
|
+
/** N kB (decimal kilobytes, `n * 1000`). */
|
|
122
|
+
export const kb = (n: number): Memory => make(Math.floor(n * 1_000))
|
|
123
|
+
|
|
124
|
+
/** N MB (decimal megabytes, `n * 1_000_000`). */
|
|
125
|
+
export const mb = (n: number): Memory => make(Math.floor(n * 1_000_000))
|
|
126
|
+
|
|
127
|
+
/** N GB (decimal gigabytes, `n * 1_000_000_000`). */
|
|
128
|
+
export const gb = (n: number): Memory => make(Math.floor(n * 1_000_000_000))
|