@effect-uai/core 0.18.0 → 0.19.0-dev-20261110065930
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 +4 -4
- 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 +1 -1
- package/dist/{dist-C8RmtoR8.mjs → dist-B5K-y4Be.mjs} +51 -32
- package/dist/dist-B5K-y4Be.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 +1 -1
- 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/embedding-model/Embedding.d.mts +1 -1
- package/dist/image-generator/ImageGenerator.d.mts +10 -66
- package/dist/image-generator/ImageGenerator.d.mts.map +1 -1
- package/dist/index.d.mts +10 -9
- package/dist/index.mjs +7 -6
- package/dist/language-model/LanguageModel.d.mts +2 -2
- package/dist/loop/Loop.d.mts +1 -1
- package/dist/loop/Loop.mjs.map +1 -1
- package/dist/messenger/Messenger.d.mts +43 -289
- package/dist/messenger/Messenger.d.mts.map +1 -1
- package/dist/messenger/MessengerAdapter.test.mjs +1 -1
- package/dist/observability/Metrics.d.mts +1 -1
- package/dist/observability/Metrics.test.mjs +1 -1
- package/dist/observability/Telemetry.test.mjs +1 -1
- package/dist/realtime/RealtimeSession.d.mts +1 -1
- package/dist/realtime/RealtimeSession.test.mjs +1 -1
- package/dist/sandbox/Sandbox.d.mts +32 -237
- package/dist/sandbox/Sandbox.d.mts.map +1 -1
- package/dist/streaming/Multipart.mjs +2 -1
- 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/Settle.test.mjs +1 -1
- package/dist/streaming/WebSocketSession.test.mjs +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 +220 -176
- 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 +431 -389
- package/dist/tool/Toolkit.test.mjs.map +1 -1
- 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 +17 -9
- 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/index.ts +3 -2
- package/src/loop/Loop.ts +24 -13
- package/src/streaming/Multipart.test.ts +26 -0
- package/src/streaming/Multipart.ts +5 -4
- 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 +203 -308
- 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-C8RmtoR8.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/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
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(
|
|
@@ -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
|
+
})
|
|
@@ -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
|
})
|
|
@@ -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
|
+
)
|