@effect-uai/core 0.17.0 → 0.19.0-dev-20261109183959
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 +5 -5
- package/dist/Content-D_Qfn8Ja.d.mts +48 -0
- package/dist/Content-D_Qfn8Ja.d.mts.map +1 -0
- package/dist/File-B923KvZ9.d.mts +46 -0
- package/dist/File-B923KvZ9.d.mts.map +1 -0
- package/dist/{Image-Cv6iBLTy.d.mts → Image-Bzc1H_UK.d.mts} +19 -8
- package/dist/Image-Bzc1H_UK.d.mts.map +1 -0
- package/dist/{Items-C7MyrYPX.d.mts → Items-Bvx8s9QH.d.mts} +109 -16
- package/dist/Items-Bvx8s9QH.d.mts.map +1 -0
- package/dist/{Loop-ChpaO-ya.d.mts → Loop-CeA0AelN.d.mts} +7 -6
- package/dist/Loop-CeA0AelN.d.mts.map +1 -0
- package/dist/Tool-Bkryw6-h.d.mts +206 -0
- package/dist/Tool-Bkryw6-h.d.mts.map +1 -0
- package/dist/ToolEvent-9inLx5cI.d.mts +22 -0
- package/dist/ToolEvent-9inLx5cI.d.mts.map +1 -0
- package/dist/ToolOutput-BNTIQgGB.d.mts +151 -0
- package/dist/ToolOutput-BNTIQgGB.d.mts.map +1 -0
- package/dist/Toolkit-D2VGeD5R.d.mts +212 -0
- package/dist/Toolkit-D2VGeD5R.d.mts.map +1 -0
- package/dist/browser/BrowserTool.d.mts +84 -22
- package/dist/browser/BrowserTool.d.mts.map +1 -1
- package/dist/browser/BrowserTool.mjs +5 -0
- package/dist/browser/BrowserTool.mjs.map +1 -1
- package/dist/capabilities/Capabilities.d.mts +1 -1
- package/dist/decision-model/Decision.test.mjs +4 -4
- package/dist/dist-B5K-y4Be.mjs +191 -0
- package/dist/dist-B5K-y4Be.mjs.map +1 -0
- package/dist/dist-CorQTQL-.mjs +1533 -0
- package/dist/dist-CorQTQL-.mjs.map +1 -0
- package/dist/dist-DNeuh84d.mjs +7 -0
- package/dist/dist-DNeuh84d.mjs.map +1 -0
- package/dist/dist-DzeSyeYF.mjs +10253 -0
- package/dist/dist-DzeSyeYF.mjs.map +1 -0
- package/dist/domain/File.d.mts +2 -0
- package/dist/domain/File.mjs +51 -0
- package/dist/domain/File.mjs.map +1 -0
- package/dist/domain/Image.d.mts +2 -2
- package/dist/domain/Image.mjs +12 -2
- package/dist/domain/Image.mjs.map +1 -1
- package/dist/domain/Items.d.mts +2 -2
- package/dist/domain/Items.mjs +65 -4
- package/dist/domain/Items.mjs.map +1 -1
- package/dist/domain/Realtime.d.mts +72 -490
- package/dist/domain/Realtime.d.mts.map +1 -1
- package/dist/domain/Research.d.mts +1 -1
- package/dist/domain/Transcript.test.mjs +3 -3
- package/dist/domain/Turn.d.mts +57 -262
- package/dist/domain/Turn.d.mts.map +1 -1
- package/dist/domain/Turn.mjs +1 -1
- package/dist/domain/Turn.test.mjs +2 -2
- package/dist/embedding-model/Embedding.d.mts +1 -1
- package/dist/embedding-model/EmbeddingModel.test.mjs +2 -2
- package/dist/embedding-model/EmbeddingModel.test.mjs.map +1 -1
- package/dist/image-generator/ImageGenerator.d.mts +10 -66
- package/dist/image-generator/ImageGenerator.d.mts.map +1 -1
- package/dist/index.C4tKZ0yW-LuFnBtmu.mjs +7042 -0
- package/dist/index.C4tKZ0yW-LuFnBtmu.mjs.map +1 -0
- package/dist/index.d.mts +10 -9
- package/dist/index.mjs +7 -6
- package/dist/language-model/LanguageModel.d.mts +2 -2
- package/dist/language-model/LanguageModel.test.mjs +2 -2
- package/dist/loop/Loop.d.mts +1 -1
- package/dist/loop/Loop.mjs.map +1 -1
- package/dist/loop/Loop.test.mjs +3 -3
- package/dist/math/Probability.test.mjs +2 -2
- package/dist/messenger/Messenger.d.mts +43 -289
- package/dist/messenger/Messenger.d.mts.map +1 -1
- package/dist/messenger/MessengerAdapter.test.mjs +3 -3
- package/dist/music-generator/MusicGenerator.test.mjs +3 -3
- package/dist/observability/Metrics.d.mts +1 -1
- package/dist/observability/Metrics.test.mjs +3 -3
- package/dist/observability/Telemetry.d.mts +1 -1
- package/dist/observability/Telemetry.mjs +1 -1
- package/dist/observability/Telemetry.mjs.map +1 -1
- package/dist/observability/Telemetry.test.mjs +4 -4
- package/dist/observability/Telemetry.test.mjs.map +1 -1
- package/dist/realtime/RealtimeSession.d.mts +1 -1
- package/dist/realtime/RealtimeSession.test.mjs +4 -4
- package/dist/retry/Retry.test.mjs +2 -2
- package/dist/sandbox/Sandbox.d.mts +32 -237
- package/dist/sandbox/Sandbox.d.mts.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +3 -3
- package/dist/streaming/JSONL.test.mjs +2 -2
- package/dist/streaming/Multipart.d.mts +1 -1
- package/dist/streaming/Multipart.mjs +3 -2
- package/dist/streaming/Multipart.mjs.map +1 -1
- package/dist/streaming/Multipart.test.mjs +28 -0
- package/dist/streaming/Multipart.test.mjs.map +1 -0
- package/dist/streaming/SSE.d.mts +1 -1
- package/dist/streaming/SSE.mjs +2 -2
- package/dist/streaming/SSE.mjs.map +1 -1
- package/dist/streaming/SSE.test.mjs +2 -2
- package/dist/streaming/Settle.test.mjs +3 -3
- package/dist/streaming/WebSocketSession.d.mts +5 -3
- package/dist/streaming/WebSocketSession.d.mts.map +1 -1
- package/dist/streaming/WebSocketSession.mjs +22 -10
- package/dist/streaming/WebSocketSession.mjs.map +1 -1
- package/dist/streaming/WebSocketSession.test.mjs +41 -5
- package/dist/streaming/WebSocketSession.test.mjs.map +1 -1
- package/dist/structured-format/StructuredFormat.mjs.map +1 -1
- package/dist/structured-format/StructuredFormat.test.mjs +2 -2
- package/dist/testing/FakeWebSocket.d.mts +5 -1
- package/dist/testing/FakeWebSocket.d.mts.map +1 -1
- package/dist/testing/FakeWebSocket.mjs +12 -6
- package/dist/testing/FakeWebSocket.mjs.map +1 -1
- package/dist/testing/MockMessenger.d.mts +19 -134
- package/dist/testing/MockMessenger.d.mts.map +1 -1
- package/dist/testing/MockProvider.d.mts +1 -1
- package/dist/testing/MockSandbox.d.mts +65 -446
- package/dist/testing/MockSandbox.d.mts.map +1 -1
- package/dist/tool/Content.d.mts +2 -0
- package/dist/tool/Content.mjs +46 -0
- package/dist/tool/Content.mjs.map +1 -0
- package/dist/tool/Content.test.d.mts +1 -0
- package/dist/tool/Content.test.mjs +48 -0
- package/dist/tool/Content.test.mjs.map +1 -0
- package/dist/tool/HistoryCheck.d.mts +6 -15
- package/dist/tool/HistoryCheck.d.mts.map +1 -1
- package/dist/tool/HistoryCheck.mjs +10 -24
- package/dist/tool/HistoryCheck.mjs.map +1 -1
- package/dist/tool/Tool.d.mts +2 -2
- package/dist/tool/Tool.mjs +55 -130
- package/dist/tool/Tool.mjs.map +1 -1
- package/dist/tool/Tool.test.mjs +222 -178
- package/dist/tool/Tool.test.mjs.map +1 -1
- package/dist/tool/ToolDecision.d.mts +164 -0
- package/dist/tool/ToolDecision.d.mts.map +1 -0
- package/dist/tool/ToolDecision.mjs +146 -0
- package/dist/tool/ToolDecision.mjs.map +1 -0
- package/dist/tool/ToolDecision.test.d.mts +1 -0
- package/dist/tool/ToolDecision.test.mjs +330 -0
- package/dist/tool/ToolDecision.test.mjs.map +1 -0
- package/dist/tool/ToolEvent.d.mts +2 -2
- package/dist/tool/ToolEvent.mjs +2 -34
- package/dist/tool/ToolEvent.mjs.map +1 -1
- package/dist/tool/ToolOutput.d.mts +2 -0
- package/dist/tool/ToolOutput.mjs +128 -0
- package/dist/tool/ToolOutput.mjs.map +1 -0
- package/dist/tool/ToolOutput.test.d.mts +1 -0
- package/dist/tool/ToolOutput.test.mjs +153 -0
- package/dist/tool/ToolOutput.test.mjs.map +1 -0
- package/dist/tool/Toolkit.d.mts +2 -2
- package/dist/tool/Toolkit.mjs +230 -165
- package/dist/tool/Toolkit.mjs.map +1 -1
- package/dist/tool/Toolkit.test.mjs +433 -391
- package/dist/tool/Toolkit.test.mjs.map +1 -1
- package/dist/transcriber/Transcriber.test.mjs +3 -3
- package/dist/web-read/WebReadTool.d.mts +2 -2
- package/dist/web-read/WebReadTool.mjs +1 -0
- package/dist/web-read/WebReadTool.mjs.map +1 -1
- package/dist/web-search/WebSearchTool.d.mts +2 -2
- package/dist/web-search/WebSearchTool.mjs +1 -0
- package/dist/web-search/WebSearchTool.mjs.map +1 -1
- package/package.json +18 -10
- package/src/browser/BrowserTool.ts +5 -0
- package/src/domain/File.ts +79 -0
- package/src/domain/Image.ts +14 -4
- package/src/domain/Items.ts +39 -3
- package/src/embedding-model/EmbeddingModel.test.ts +2 -2
- package/src/index.ts +3 -2
- package/src/loop/Loop.ts +25 -14
- package/src/observability/Telemetry.test.ts +1 -1
- package/src/observability/Telemetry.ts +2 -2
- package/src/streaming/Multipart.test.ts +26 -0
- package/src/streaming/Multipart.ts +6 -5
- package/src/streaming/SSE.ts +2 -2
- package/src/streaming/WebSocketSession.test.ts +54 -2
- package/src/streaming/WebSocketSession.ts +27 -6
- package/src/structured-format/StructuredFormat.ts +1 -1
- package/src/testing/FakeWebSocket.ts +21 -6
- package/src/tool/Content.test.ts +40 -0
- package/src/tool/Content.ts +38 -0
- package/src/tool/HistoryCheck.ts +21 -26
- package/src/tool/Tool.test.ts +240 -241
- package/src/tool/Tool.ts +204 -309
- package/src/tool/ToolDecision.test.ts +301 -0
- package/src/tool/ToolDecision.ts +360 -0
- package/src/tool/ToolEvent.ts +21 -41
- package/src/tool/ToolOutput.test.ts +132 -0
- package/src/tool/ToolOutput.ts +198 -0
- package/src/tool/Toolkit.test.ts +594 -454
- package/src/tool/Toolkit.ts +631 -356
- package/src/web-read/WebReadTool.ts +2 -1
- package/src/web-search/WebSearchTool.ts +2 -1
- package/dist/Image-Cv6iBLTy.d.mts.map +0 -1
- package/dist/Items-C7MyrYPX.d.mts.map +0 -1
- package/dist/Loop-ChpaO-ya.d.mts.map +0 -1
- package/dist/Tool-BF78QWtn.d.mts +0 -314
- package/dist/Tool-BF78QWtn.d.mts.map +0 -1
- package/dist/ToolEvent-Di4a723B.d.mts +0 -144
- package/dist/ToolEvent-Di4a723B.d.mts.map +0 -1
- package/dist/ToolResult-BsYQRjmW.d.mts +0 -152
- package/dist/ToolResult-BsYQRjmW.d.mts.map +0 -1
- package/dist/Toolkit-tlL0F2YR.d.mts +0 -193
- package/dist/Toolkit-tlL0F2YR.d.mts.map +0 -1
- package/dist/dist-BaenoU1A.mjs +0 -145
- package/dist/dist-BaenoU1A.mjs.map +0 -1
- package/dist/dist-nohcnkD6.mjs +0 -182
- package/dist/dist-nohcnkD6.mjs.map +0 -1
- package/dist/magic-string.es-CB2hOw9v.mjs +0 -1014
- package/dist/magic-string.es-CB2hOw9v.mjs.map +0 -1
- package/dist/test.DNmyFkvJ-YPzDR0Co.mjs +0 -10254
- package/dist/test.DNmyFkvJ-YPzDR0Co.mjs.map +0 -1
- package/dist/tool/Approval.d.mts +0 -113
- package/dist/tool/Approval.d.mts.map +0 -1
- package/dist/tool/Approval.mjs +0 -80
- package/dist/tool/Approval.mjs.map +0 -1
- package/dist/tool/Approval.test.mjs +0 -371
- package/dist/tool/Approval.test.mjs.map +0 -1
- package/dist/tool/ToolResult.d.mts +0 -2
- package/dist/tool/ToolResult.mjs +0 -85
- package/dist/tool/ToolResult.mjs.map +0 -1
- package/dist/utils-CiH_DjFW.mjs +0 -3918
- package/dist/utils-CiH_DjFW.mjs.map +0 -1
- package/src/tool/Approval.test.ts +0 -400
- package/src/tool/Approval.ts +0 -181
- package/src/tool/ToolResult.ts +0 -114
- /package/dist/{tool/Approval.test.d.mts → streaming/Multipart.test.d.mts} +0 -0
|
@@ -83,12 +83,12 @@ describe("embed / embedMany free exports preserve E in their return type", () =>
|
|
|
83
83
|
// `JinaEmbedding` tag.
|
|
84
84
|
// These calls exist only to assert the encoding is a compile error; they
|
|
85
85
|
// are intentionally never run, so the floating-effect check is off here.
|
|
86
|
-
// @effect-diagnostics
|
|
86
|
+
// @effect-diagnostics floatingEffect:off
|
|
87
87
|
// @ts-expect-error "multivector" is not an EmbedEncoding
|
|
88
88
|
embedMany({ inputs: ["x"], model: "m", encoding: "multivector" })
|
|
89
89
|
// @ts-expect-error "sparse" is not an EmbedEncoding
|
|
90
90
|
embed({ input: "x", model: "m", encoding: "sparse" })
|
|
91
|
-
// @effect-diagnostics
|
|
91
|
+
// @effect-diagnostics floatingEffect:error
|
|
92
92
|
})
|
|
93
93
|
|
|
94
94
|
it("when the request is widened to CommonEmbedRequest, the response is the open union", () => {
|
package/src/index.ts
CHANGED
|
@@ -46,11 +46,12 @@ export * as Settle from "./streaming/Settle.js"
|
|
|
46
46
|
export * as WebSocketSession from "./streaming/WebSocketSession.js"
|
|
47
47
|
export * as Loop from "./loop/Loop.js"
|
|
48
48
|
export * as Retry from "./retry/Retry.js"
|
|
49
|
+
export * as Content from "./tool/Content.js"
|
|
49
50
|
export * as Tool from "./tool/Tool.js"
|
|
50
51
|
export * as Toolkit from "./tool/Toolkit.js"
|
|
51
|
-
export * as ToolResult from "./tool/ToolResult.js"
|
|
52
52
|
export * as ToolEvent from "./tool/ToolEvent.js"
|
|
53
|
-
export * as
|
|
53
|
+
export * as ToolOutput from "./tool/ToolOutput.js"
|
|
54
|
+
export * as ToolDecision from "./tool/ToolDecision.js"
|
|
54
55
|
export * as HistoryCheck from "./tool/HistoryCheck.js"
|
|
55
56
|
export * as JSONL from "./streaming/JSONL.js"
|
|
56
57
|
export * as Lines from "./streaming/Lines.js"
|
package/src/loop/Loop.ts
CHANGED
|
@@ -125,26 +125,27 @@ export const stop = <S = never>(state?: S): Stream.Stream<Step<never, S>> =>
|
|
|
125
125
|
* Dual: data-first `onTurnComplete(deltas, then)` and data-last
|
|
126
126
|
* `deltas.pipe(onTurnComplete(then))` both work.
|
|
127
127
|
*/
|
|
128
|
-
|
|
128
|
+
// The Effect and the stream it returns infer their errors separately.
|
|
129
|
+
type TurnContinuation<S, A, E2, R2, E3, R3> =
|
|
129
130
|
| Stream.Stream<Step<A, S>, E2, R2>
|
|
130
|
-
| Effect.Effect<Stream.Stream<Step<A, S>, E2, R2>,
|
|
131
|
+
| Effect.Effect<Stream.Stream<Step<A, S>, E2, R2>, E3, R3>
|
|
131
132
|
|
|
132
133
|
export const onTurnComplete: {
|
|
133
|
-
<S, A, E2 = never, R2 = never>(
|
|
134
|
-
then: (turn: Turn) => TurnContinuation<S, A, E2, R2>,
|
|
134
|
+
<S, A, E2 = never, R2 = never, E3 = never, R3 = never>(
|
|
135
|
+
then: (turn: Turn) => TurnContinuation<S, A, E2, R2, E3, R3>,
|
|
135
136
|
): <E, R>(
|
|
136
137
|
deltas: Stream.Stream<TurnEvent, E, R>,
|
|
137
|
-
) => Stream.Stream<Step<TurnEvent | A, S>, E | E2 | IncompleteTurn, R | R2>
|
|
138
|
-
<S, A, E, R, E2 = never, R2 = never>(
|
|
138
|
+
) => Stream.Stream<Step<TurnEvent | A, S>, E | E2 | E3 | IncompleteTurn, R | R2 | R3>
|
|
139
|
+
<S, A, E, R, E2 = never, R2 = never, E3 = never, R3 = never>(
|
|
139
140
|
deltas: Stream.Stream<TurnEvent, E, R>,
|
|
140
|
-
then: (turn: Turn) => TurnContinuation<S, A, E2, R2>,
|
|
141
|
-
): Stream.Stream<Step<TurnEvent | A, S>, E | E2 | IncompleteTurn, R | R2>
|
|
141
|
+
then: (turn: Turn) => TurnContinuation<S, A, E2, R2, E3, R3>,
|
|
142
|
+
): Stream.Stream<Step<TurnEvent | A, S>, E | E2 | E3 | IncompleteTurn, R | R2 | R3>
|
|
142
143
|
} = Function.dual(
|
|
143
144
|
2,
|
|
144
|
-
<S, A, E, R, E2, R2>(
|
|
145
|
+
<S, A, E, R, E2, R2, E3, R3>(
|
|
145
146
|
deltas: Stream.Stream<TurnEvent, E, R>,
|
|
146
|
-
then: (turn: Turn) => TurnContinuation<S, A, E2, R2>,
|
|
147
|
-
): Stream.Stream<Step<TurnEvent | A, S>, E | E2 | IncompleteTurn, R | R2> =>
|
|
147
|
+
then: (turn: Turn) => TurnContinuation<S, A, E2, R2, E3, R3>,
|
|
148
|
+
): Stream.Stream<Step<TurnEvent | A, S>, E | E2 | E3 | IncompleteTurn, R | R2 | R3> =>
|
|
148
149
|
Stream.unwrap(
|
|
149
150
|
Effect.gen(function* () {
|
|
150
151
|
const turnRef = yield* Ref.make<Option.Option<Turn>>(Option.none())
|
|
@@ -217,6 +218,11 @@ type LoopBody<S, A, E, R> = (
|
|
|
217
218
|
state: S,
|
|
218
219
|
) => Stream.Stream<Step<A, S>, E, R> | Effect.Effect<Stream.Stream<Step<A, S>, E, R>, E, R>
|
|
219
220
|
|
|
221
|
+
// What callers pass: the Effect and its stream infer errors and requirements separately.
|
|
222
|
+
type LoopBodyIn<S, A, E, R, E2, R2> = (
|
|
223
|
+
state: S,
|
|
224
|
+
) => Stream.Stream<Step<A, S>, E, R> | Effect.Effect<Stream.Stream<Step<A, S>, E, R>, E2, R2>
|
|
225
|
+
|
|
220
226
|
/**
|
|
221
227
|
* Drive a state-threaded loop body. Each iteration runs `body(state)` to get
|
|
222
228
|
* a `Stream<Step<A, S>>`; values flow downstream, `next(s)` continues with
|
|
@@ -227,8 +233,13 @@ type LoopBody<S, A, E, R> = (
|
|
|
227
233
|
* (or `pipe(initial, loop(body))`) both work.
|
|
228
234
|
*/
|
|
229
235
|
export const loop: {
|
|
230
|
-
<S, A, E, R
|
|
231
|
-
|
|
236
|
+
<S, A, E, R, E2 = never, R2 = never>(
|
|
237
|
+
body: LoopBodyIn<S, A, E, R, E2, R2>,
|
|
238
|
+
): (initial: S) => Stream.Stream<A, E | E2, R | R2>
|
|
239
|
+
<S, A, E, R, E2 = never, R2 = never>(
|
|
240
|
+
initial: S,
|
|
241
|
+
body: LoopBodyIn<S, A, E, R, E2, R2>,
|
|
242
|
+
): Stream.Stream<A, E | E2, R | R2>
|
|
232
243
|
} = Function.dual(2, <S, A, E, R>(initial: S, body: LoopBody<S, A, E, R>): Stream.Stream<A, E, R> =>
|
|
233
244
|
Stream.scoped(
|
|
234
245
|
Stream.fromPull(
|
|
@@ -307,7 +318,7 @@ export const loop: {
|
|
|
307
318
|
|
|
308
319
|
// `Stream.fromPull` expects the generator to RETURN the pull effect,
|
|
309
320
|
// not run it; `return yield* pull` would execute a pull and break it.
|
|
310
|
-
// @effect-diagnostics-next-line
|
|
321
|
+
// @effect-diagnostics-next-line returnEffectInGen:off
|
|
311
322
|
return pull
|
|
312
323
|
}),
|
|
313
324
|
),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it } from "@effect/vitest"
|
|
2
2
|
import { Array as Arr, Duration, Effect, Layer, Metric, Option, Ref, Stream } from "effect"
|
|
3
|
-
import { type HttpBody, HttpClient, HttpClientResponse } from "effect/
|
|
3
|
+
import { type HttpBody, HttpClient, HttpClientResponse } from "effect/http"
|
|
4
4
|
import { expect } from "vitest"
|
|
5
5
|
import { type Measurement, makeEvent } from "./Metrics.js"
|
|
6
6
|
import { layerOtlp, record } from "./Telemetry.js"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Duration, Effect, Layer, Match, Metric, Stream } from "effect"
|
|
2
|
-
import type { HttpClient } from "effect/
|
|
3
|
-
import { OtlpMetrics, OtlpSerialization } from "effect/
|
|
2
|
+
import type { HttpClient } from "effect/http"
|
|
3
|
+
import { OtlpMetrics, OtlpSerialization } from "effect/observability"
|
|
4
4
|
import { isMetricEvent, type Measurement, type MetricEvent } from "./Metrics.js"
|
|
5
5
|
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, it } from "@effect/vitest"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import { HttpClientRequest } from "effect/http"
|
|
4
|
+
import { expect } from "vitest"
|
|
5
|
+
import { bodyMultipart } from "./Multipart.js"
|
|
6
|
+
|
|
7
|
+
describe("bodyMultipart", () => {
|
|
8
|
+
it.effect("sends the boundary the encoded parts are delimited by", () =>
|
|
9
|
+
Effect.gen(function* () {
|
|
10
|
+
const form = new FormData()
|
|
11
|
+
form.set("file", new Blob([new Uint8Array([1, 2, 3])], { type: "audio/ogg" }), "audio.ogg")
|
|
12
|
+
form.set("model", "whisper-1")
|
|
13
|
+
|
|
14
|
+
const request = HttpClientRequest.post("https://example.test/upload").pipe(
|
|
15
|
+
yield* bodyMultipart(form),
|
|
16
|
+
)
|
|
17
|
+
const contentType = request.headers["content-type"] ?? ""
|
|
18
|
+
const boundary = /boundary=(.+)$/.exec(contentType)?.[1]
|
|
19
|
+
|
|
20
|
+
expect(boundary).toBeDefined()
|
|
21
|
+
const body =
|
|
22
|
+
request.body._tag === "Uint8Array" ? new TextDecoder().decode(request.body.body) : ""
|
|
23
|
+
expect(body.startsWith(`--${boundary}\r\n`)).toBe(true)
|
|
24
|
+
}),
|
|
25
|
+
)
|
|
26
|
+
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
|
-
import { HttpClientRequest } from "effect/
|
|
2
|
+
import { HttpClientRequest } from "effect/http"
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Multipart body for an `HttpClientRequest`, encoded to bytes.
|
|
@@ -21,9 +21,10 @@ export const bodyMultipart = (
|
|
|
21
21
|
> =>
|
|
22
22
|
Effect.gen(function* () {
|
|
23
23
|
const encoded = new Response(form)
|
|
24
|
+
// Read before the body: once it is consumed, Bun answers `null` for this
|
|
25
|
+
// header (Node keeps it), which would drop the boundary and leave the
|
|
26
|
+
// server unable to parse the parts.
|
|
27
|
+
const contentType = encoded.headers.get("content-type") ?? "multipart/form-data"
|
|
24
28
|
const buffer = yield* Effect.tryPromise(() => encoded.arrayBuffer())
|
|
25
|
-
return HttpClientRequest.bodyUint8Array(
|
|
26
|
-
new Uint8Array(buffer),
|
|
27
|
-
encoded.headers.get("content-type") ?? "multipart/form-data",
|
|
28
|
-
)
|
|
29
|
+
return HttpClientRequest.bodyUint8Array(new Uint8Array(buffer), contentType)
|
|
29
30
|
})
|
package/src/streaming/SSE.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Stream } from "effect"
|
|
2
|
-
import { Sse } from "effect/
|
|
2
|
+
import { Sse } from "effect/encoding"
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* One Server-Sent Event. Fields per the WHATWG spec:
|
|
@@ -35,7 +35,7 @@ const makeParserState = (): ParserState => {
|
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Decode a `Stream<Uint8Array>` (e.g. an HTTP response body) into a
|
|
38
|
-
* `Stream<SSE.Event>`, using the spec parser from `effect/
|
|
38
|
+
* `Stream<SSE.Event>`, using the spec parser from `effect/encoding`.
|
|
39
39
|
* An event is dispatched only once its terminating blank line arrives, so a
|
|
40
40
|
* partial event at stream end (clean or failed) is discarded per the spec
|
|
41
41
|
* rather than surfaced as a truncated frame.
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { describe, it } from "@effect/vitest"
|
|
2
|
-
import
|
|
2
|
+
import { Effect, Exit, Pull, Queue, Schema } from "effect"
|
|
3
|
+
import * as Socket from "effect/socket/Socket"
|
|
3
4
|
import { expect } from "vitest"
|
|
4
5
|
import * as AiError from "../domain/AiError.js"
|
|
5
|
-
import
|
|
6
|
+
import * as FakeWebSocket from "../testing/FakeWebSocket.js"
|
|
7
|
+
import { open, toAiError } from "./WebSocketSession.js"
|
|
6
8
|
|
|
7
9
|
const KEY = "AIzaSyEXAMPLEKEY"
|
|
8
10
|
const URL = `wss://generativelanguage.googleapis.com/ws/BidiGenerateContent?key=${KEY}`
|
|
@@ -50,3 +52,53 @@ describe("WebSocketSession errors", () => {
|
|
|
50
52
|
expect(ended._tag).toBe("Unavailable")
|
|
51
53
|
})
|
|
52
54
|
})
|
|
55
|
+
|
|
56
|
+
const Frame = Schema.Struct({ n: Schema.Number })
|
|
57
|
+
|
|
58
|
+
const session = (server: FakeWebSocket.FakeWebSocketServer) =>
|
|
59
|
+
open({ url: "wss://example.test", provider: "test", schema: Frame }).pipe(
|
|
60
|
+
Effect.provide(server.layer),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
describe("WebSocketSession lifetime", () => {
|
|
64
|
+
it.live("writes a reply to a frame before the next one arrives", () =>
|
|
65
|
+
Effect.gen(function* () {
|
|
66
|
+
const server = yield* FakeWebSocket.make({ greeting: [{ n: 1 }] })
|
|
67
|
+
const { send, frames } = yield* session(server)
|
|
68
|
+
|
|
69
|
+
const first = yield* Queue.take(frames)
|
|
70
|
+
yield* send(JSON.stringify({ n: first.n + 1 }))
|
|
71
|
+
yield* Effect.sleep("5 millis")
|
|
72
|
+
|
|
73
|
+
expect(yield* server.sent).toEqual([{ n: 2 }])
|
|
74
|
+
}).pipe(Effect.scoped),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
it.live("ends the frames on a clean close, and refuses a send after it", () =>
|
|
78
|
+
Effect.gen(function* () {
|
|
79
|
+
const server = yield* FakeWebSocket.make({ greeting: [{ n: 1 }] })
|
|
80
|
+
const { send, frames } = yield* session(server)
|
|
81
|
+
|
|
82
|
+
yield* Queue.take(frames)
|
|
83
|
+
yield* server.close(1000)
|
|
84
|
+
const end = yield* Effect.exit(Queue.take(frames))
|
|
85
|
+
const late = yield* Effect.exit(send(JSON.stringify({ n: 2 })))
|
|
86
|
+
|
|
87
|
+
expect(Exit.isFailure(end) && Pull.isDoneCause(end.cause)).toBe(true)
|
|
88
|
+
expect(Exit.isFailure(late) && JSON.stringify(late)).toContain("Unavailable")
|
|
89
|
+
}).pipe(Effect.scoped),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
it.live("fails the frames on a dirty close", () =>
|
|
93
|
+
Effect.gen(function* () {
|
|
94
|
+
const server = yield* FakeWebSocket.make({ greeting: [{ n: 1 }] })
|
|
95
|
+
const { frames } = yield* session(server)
|
|
96
|
+
|
|
97
|
+
yield* Queue.take(frames)
|
|
98
|
+
yield* server.close(1011)
|
|
99
|
+
const end = yield* Effect.exit(Queue.take(frames))
|
|
100
|
+
|
|
101
|
+
expect(JSON.stringify(end)).toContain("Unavailable")
|
|
102
|
+
}).pipe(Effect.scoped),
|
|
103
|
+
)
|
|
104
|
+
})
|
|
@@ -14,10 +14,12 @@ import {
|
|
|
14
14
|
Option,
|
|
15
15
|
Predicate,
|
|
16
16
|
Queue,
|
|
17
|
+
Ref,
|
|
17
18
|
Schema,
|
|
18
19
|
type Scope,
|
|
20
|
+
Stream,
|
|
19
21
|
} from "effect"
|
|
20
|
-
import * as Socket from "effect/
|
|
22
|
+
import * as Socket from "effect/socket/Socket"
|
|
21
23
|
import * as AiError from "../domain/AiError.js"
|
|
22
24
|
import * as JSONL from "./JSONL.js"
|
|
23
25
|
|
|
@@ -85,6 +87,13 @@ export const toAiError =
|
|
|
85
87
|
)
|
|
86
88
|
}
|
|
87
89
|
|
|
90
|
+
/** Normal closure, going away, and no status code. */
|
|
91
|
+
const cleanCloseCodes: ReadonlySet<number> = new Set([1000, 1001, 1005])
|
|
92
|
+
|
|
93
|
+
/** Effect's `Socket` fails on every close, clean or not. */
|
|
94
|
+
export const isCleanClose = (error: Socket.SocketError): boolean =>
|
|
95
|
+
error.reason._tag === "SocketCloseError" && cleanCloseCodes.has(error.reason.code)
|
|
96
|
+
|
|
88
97
|
/**
|
|
89
98
|
* Connect for the lifetime of the surrounding `Scope`. The reader runs in a
|
|
90
99
|
* forked fiber; a dirty close reaches the consumer as a failed queue rather
|
|
@@ -97,12 +106,14 @@ export const open = <A, I>(
|
|
|
97
106
|
const asAiError = toAiError(options.provider)
|
|
98
107
|
const decode = Schema.decodeUnknownEffect(options.schema)
|
|
99
108
|
const socket = yield* Socket.makeWebSocket(options.url, {
|
|
100
|
-
// Effect's Socket treats every close code as an error by default.
|
|
101
|
-
closeCodeIsError: (code) => code !== 1000 && code !== 1001 && code !== 1005,
|
|
102
109
|
...(options.openTimeout !== undefined && { openTimeout: options.openTimeout }),
|
|
103
110
|
})
|
|
104
111
|
const frames = yield* Queue.bounded<A, AiError.AiError | Cause.Done>(options.capacity ?? 64)
|
|
105
|
-
const write = yield* socket.writer
|
|
112
|
+
const { write } = yield* socket.writer
|
|
113
|
+
// The writer waits for the next connection rather than failing, so a send
|
|
114
|
+
// after the reader ends has to be refused here.
|
|
115
|
+
const ended = yield* Ref.make(false)
|
|
116
|
+
const closed = new AiError.Unavailable({ provider: options.provider, raw: "socket closed" })
|
|
106
117
|
|
|
107
118
|
const onFrame = (raw: string) =>
|
|
108
119
|
Effect.gen(function* () {
|
|
@@ -116,8 +127,13 @@ export const open = <A, I>(
|
|
|
116
127
|
yield* Queue.offer(frames, decoded.value)
|
|
117
128
|
})
|
|
118
129
|
|
|
119
|
-
yield*
|
|
130
|
+
yield* Stream.fromPull(Socket.readerString(socket)).pipe(
|
|
131
|
+
Stream.scoped,
|
|
132
|
+
Stream.catchIf(isCleanClose, () => Stream.empty),
|
|
133
|
+
Stream.runForEach(onFrame),
|
|
120
134
|
Effect.mapError(asAiError),
|
|
135
|
+
// Set before `frames` ends, so a send after the last frame already fails.
|
|
136
|
+
Effect.ensuring(Ref.set(ended, true)),
|
|
121
137
|
// `Queue.end` on a clean close so the consumer drains what is queued;
|
|
122
138
|
// a failure must not arrive looking like the end of the conversation.
|
|
123
139
|
Effect.matchCauseEffect({
|
|
@@ -128,5 +144,10 @@ export const open = <A, I>(
|
|
|
128
144
|
Effect.forkScoped,
|
|
129
145
|
)
|
|
130
146
|
|
|
131
|
-
|
|
147
|
+
const send = (frame: string): Effect.Effect<void, AiError.AiError> =>
|
|
148
|
+
Effect.flatMap(Ref.get(ended), (isEnded) =>
|
|
149
|
+
isEnded ? Effect.fail(closed) : Effect.mapError(write(frame), asAiError),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return { send, frames }
|
|
132
153
|
})
|
|
@@ -145,7 +145,7 @@ export const parseJson =
|
|
|
145
145
|
// Validation runs through Standard Schema (Zod / Valibot / Effect), not
|
|
146
146
|
// Effect Schema, so its JSON codecs don't apply here; and JSON.parse is
|
|
147
147
|
// a separate step on purpose, to surface JsonParseError distinctly.
|
|
148
|
-
// @effect-diagnostics-next-
|
|
148
|
+
// @effect-diagnostics-next-linepreferSchemaOverJson:off
|
|
149
149
|
try: () => JSON.parse(raw),
|
|
150
150
|
catch: (cause) => new JsonParseError({ raw, cause }),
|
|
151
151
|
}),
|
|
@@ -10,12 +10,16 @@
|
|
|
10
10
|
* shim registers, since `Socket.makeWebSocket` drives it with plain callbacks.
|
|
11
11
|
* The shim is a plain object, so nothing here touches a platform global.
|
|
12
12
|
*/
|
|
13
|
-
import { Effect, Layer, Queue,
|
|
14
|
-
import * as Socket from "effect/
|
|
13
|
+
import { Deferred, Effect, Layer, Queue, type Scope, Stream, SubscriptionRef } from "effect"
|
|
14
|
+
import * as Socket from "effect/socket/Socket"
|
|
15
15
|
|
|
16
16
|
export type FakeWebSocketServer = {
|
|
17
17
|
/** Every frame the client has sent, JSON-parsed, oldest first. */
|
|
18
18
|
readonly sent: Effect.Effect<ReadonlyArray<unknown>>
|
|
19
|
+
/** Wait until the client has sent at least `count` frames. */
|
|
20
|
+
readonly awaitSent: (count: number) => Effect.Effect<void>
|
|
21
|
+
/** Wait until the client has subscribed and the greeting has landed. */
|
|
22
|
+
readonly greeted: Effect.Effect<void>
|
|
19
23
|
/** Deliver one frame to the client. */
|
|
20
24
|
readonly push: (frame: unknown) => Effect.Effect<void>
|
|
21
25
|
/** Close the socket. 1000, 1001 and 1005 are the clean codes. */
|
|
@@ -46,7 +50,8 @@ export const make = (
|
|
|
46
50
|
): Effect.Effect<FakeWebSocketServer, never, Scope.Scope> =>
|
|
47
51
|
Effect.gen(function* () {
|
|
48
52
|
const inbound = yield* Queue.unbounded<unknown>()
|
|
49
|
-
const sent = yield*
|
|
53
|
+
const sent = yield* SubscriptionRef.make<ReadonlyArray<unknown>>([])
|
|
54
|
+
const greeted = yield* Deferred.make<void>()
|
|
50
55
|
|
|
51
56
|
const wiring = {
|
|
52
57
|
listeners: {} as Record<string, ReadonlyArray<Listener>>,
|
|
@@ -73,7 +78,7 @@ export const make = (
|
|
|
73
78
|
yield* Stream.fromQueue(inbound).pipe(
|
|
74
79
|
Stream.runForEach((frame) =>
|
|
75
80
|
Effect.gen(function* () {
|
|
76
|
-
yield*
|
|
81
|
+
yield* SubscriptionRef.update(sent, (frames) => [...frames, frame])
|
|
77
82
|
const answer = yield* options?.reply?.(frame) ?? Effect.succeed([])
|
|
78
83
|
yield* deliver(answer)
|
|
79
84
|
}),
|
|
@@ -93,7 +98,10 @@ export const make = (
|
|
|
93
98
|
// The greeting waits for a subscriber before it lands.
|
|
94
99
|
if (type === "message" && !wiring.greeted) {
|
|
95
100
|
wiring.greeted = true
|
|
96
|
-
soon(() =>
|
|
101
|
+
soon(() => {
|
|
102
|
+
deliverSync(options?.greeting ?? [])
|
|
103
|
+
Deferred.doneUnsafe(greeted, Effect.void)
|
|
104
|
+
})
|
|
97
105
|
}
|
|
98
106
|
},
|
|
99
107
|
removeEventListener: (type: string, fn: Listener): void => {
|
|
@@ -111,7 +119,14 @@ export const make = (
|
|
|
111
119
|
socket as unknown as globalThis.WebSocket
|
|
112
120
|
|
|
113
121
|
return {
|
|
114
|
-
sent:
|
|
122
|
+
sent: SubscriptionRef.get(sent),
|
|
123
|
+
awaitSent: (count) =>
|
|
124
|
+
SubscriptionRef.changes(sent).pipe(
|
|
125
|
+
Stream.filter((frames) => frames.length >= count),
|
|
126
|
+
Stream.take(1),
|
|
127
|
+
Stream.runDrain,
|
|
128
|
+
),
|
|
129
|
+
greeted: Deferred.await(greeted),
|
|
115
130
|
push: (frame) => deliver([frame]),
|
|
116
131
|
close: (code = 1000) => Effect.sync(() => shutdown(code)),
|
|
117
132
|
connect,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, expectTypeOf, it } from "@effect/vitest"
|
|
2
|
+
import { Effect, Schema } from "effect"
|
|
3
|
+
import { imageBytes } from "../domain/Image.js"
|
|
4
|
+
import * as Content from "./Content.js"
|
|
5
|
+
|
|
6
|
+
const bytes = new Uint8Array([137, 80, 78, 71])
|
|
7
|
+
const encode = Schema.encodeUnknownEffect(Content.Value)
|
|
8
|
+
const decode = Schema.decodeUnknownEffect(Content.Value)
|
|
9
|
+
|
|
10
|
+
describe("Content.Value", () => {
|
|
11
|
+
it("encodes to JSON", () => {
|
|
12
|
+
expectTypeOf<typeof Content.Value.Encoded>().toExtend<Schema.Json>()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it.effect("round-trips image bytes through JSON as base64", () =>
|
|
16
|
+
Effect.gen(function* () {
|
|
17
|
+
const encoded = yield* encode(Content.image(imageBytes(bytes, "image/png")))
|
|
18
|
+
expect(encoded).toEqual({
|
|
19
|
+
_tag: "Image",
|
|
20
|
+
source: { _tag: "bytes", bytes: "iVBORw==", mimeType: "image/png" },
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const decoded = yield* decode(JSON.parse(JSON.stringify(encoded)))
|
|
24
|
+
expect(decoded).toBeInstanceOf(Content.Image)
|
|
25
|
+
expect(decoded).toEqual(Content.image(imageBytes(bytes, "image/png")))
|
|
26
|
+
}),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
it.effect("keeps plain objects plain and media as classes in a mixed array", () =>
|
|
30
|
+
Effect.gen(function* () {
|
|
31
|
+
const value = [
|
|
32
|
+
{ title: "Home" },
|
|
33
|
+
Content.file({ _tag: "url", url: "https://x/a.pdf" }, "a.pdf"),
|
|
34
|
+
]
|
|
35
|
+
const decoded = yield* decode(yield* encode(value))
|
|
36
|
+
expect(decoded).toEqual(value)
|
|
37
|
+
expect(Array.isArray(decoded) && decoded.map(Content.isMedia)).toEqual([false, true])
|
|
38
|
+
}),
|
|
39
|
+
)
|
|
40
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a tool's `run` may return without an `outputSchema`. Plain values stay
|
|
3
|
+
* plain (a string is text, anything else JSON); only media is marked, by class,
|
|
4
|
+
* so it is told apart from JSON by `instanceof`, never by shape.
|
|
5
|
+
*/
|
|
6
|
+
import { Schema } from "effect"
|
|
7
|
+
import { type FileSource, FileSourceJson } from "../domain/File.js"
|
|
8
|
+
import { type ImageSource, ImageSourceJson } from "../domain/Image.js"
|
|
9
|
+
|
|
10
|
+
export class Image extends Schema.TaggedClass<Image>()("Image", { source: ImageSourceJson }) {}
|
|
11
|
+
|
|
12
|
+
export class File extends Schema.TaggedClass<File>()("File", {
|
|
13
|
+
source: FileSourceJson,
|
|
14
|
+
filename: Schema.optionalKey(Schema.String),
|
|
15
|
+
}) {}
|
|
16
|
+
|
|
17
|
+
export const Media = Schema.Union([Image, File])
|
|
18
|
+
export type Media = typeof Media.Type
|
|
19
|
+
|
|
20
|
+
export const image = (source: ImageSource): Image => new Image({ source })
|
|
21
|
+
|
|
22
|
+
export const file = (source: FileSource, filename?: string): File =>
|
|
23
|
+
new File(filename === undefined ? { source } : { source, filename })
|
|
24
|
+
|
|
25
|
+
export const isMedia = (u: unknown): u is Media => u instanceof Image || u instanceof File
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Text, JSON, one medium, or a top-level array mixing JSON and media. Encodes to
|
|
29
|
+
* JSON (bytes as base64), so it also serves as the UI codec for tools without
|
|
30
|
+
* an `outputSchema`.
|
|
31
|
+
*/
|
|
32
|
+
export const Value = Schema.Union([
|
|
33
|
+
Schema.String,
|
|
34
|
+
Media,
|
|
35
|
+
Schema.Array(Schema.Union([Media, Schema.Json])),
|
|
36
|
+
Schema.Json,
|
|
37
|
+
])
|
|
38
|
+
export type Value = typeof Value.Type
|
package/src/tool/HistoryCheck.ts
CHANGED
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* History
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* interrupted, restarted, or branched (HITL, mid-stream abort, persisted
|
|
7
|
-
* checkpoints, stateless HTTP servers) need to detect orphans and
|
|
8
|
-
* synthesize closing outputs before submitting.
|
|
9
|
-
*
|
|
10
|
-
* Recipe author calls these at known transition points (right before the
|
|
11
|
-
* next provider request). Not invoked from inside the loop.
|
|
2
|
+
* History consistency. Providers reject a request when a `function_call` has no
|
|
3
|
+
* matching `function_call_output`; flows that can be interrupted, restarted or
|
|
4
|
+
* branched (approvals, aborts, persisted checkpoints) close orphans with these
|
|
5
|
+
* before the next model call.
|
|
12
6
|
*/
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
7
|
+
import { Effect, type Schema } from "effect"
|
|
8
|
+
import {
|
|
9
|
+
type HistoryItem,
|
|
10
|
+
isToolCall,
|
|
11
|
+
isToolCallOutput,
|
|
12
|
+
type ToolCall,
|
|
13
|
+
type ToolCallOutput,
|
|
14
|
+
} from "../domain/Items.js"
|
|
15
|
+
import * as ToolOutput from "./ToolOutput.js"
|
|
15
16
|
|
|
16
|
-
/**
|
|
17
|
-
* Return every `function_call` in `history` that does not have a matching
|
|
18
|
-
* `function_call_output` later in `history` (correlated by `call_id`).
|
|
19
|
-
* Empty result = history is provider-submittable from this invariant.
|
|
20
|
-
*/
|
|
17
|
+
/** Every `function_call` in `history` without a `function_call_output` (by `call_id`). */
|
|
21
18
|
export const findUnansweredCalls = (
|
|
22
19
|
history: ReadonlyArray<HistoryItem>,
|
|
23
20
|
): ReadonlyArray<ToolCall> => {
|
|
@@ -25,19 +22,17 @@ export const findUnansweredCalls = (
|
|
|
25
22
|
return history.filter(isToolCall).filter((c) => !answered.has(c.call_id))
|
|
26
23
|
}
|
|
27
24
|
|
|
28
|
-
/** Cheap predicate: is this history submittable to a provider? */
|
|
29
25
|
export const isReconciled = (history: ReadonlyArray<HistoryItem>): boolean =>
|
|
30
26
|
findUnansweredCalls(history).length === 0
|
|
31
27
|
|
|
32
28
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* Use when: a new user message arrives mid-approval; an approval timer
|
|
37
|
-
* fires; a persisted checkpoint contains orphans (crash recovery); a
|
|
38
|
-
* stateless HTTP server reconstructed history from a stale checkpoint.
|
|
29
|
+
* A `Cancelled` output for every unanswered call, to append before the next
|
|
30
|
+
* model call. For your own error, use `findUnansweredCalls` + `ToolOutput.failure`.
|
|
39
31
|
*/
|
|
40
32
|
export const cancelAllPending = (
|
|
41
33
|
history: ReadonlyArray<HistoryItem>,
|
|
42
|
-
reason
|
|
43
|
-
): ReadonlyArray<
|
|
34
|
+
reason = "The call was cancelled before it ran.",
|
|
35
|
+
): Effect.Effect<ReadonlyArray<ToolCallOutput>, Schema.SchemaError> =>
|
|
36
|
+
Effect.forEach(findUnansweredCalls(history), (call) =>
|
|
37
|
+
ToolOutput.failure(call, ToolOutput.Cancelled, new ToolOutput.Cancelled({ message: reason })),
|
|
38
|
+
)
|