@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
package/src/tool/ToolEvent.ts
CHANGED
|
@@ -1,46 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* - Progress : per-element passthrough from a streaming tool's run
|
|
6
|
-
* - Output : terminal result (carries a structured ToolResult)
|
|
7
|
-
*
|
|
8
|
-
* Recipes thread `ToolEvent.Output.result` through `continueWithResults` and
|
|
9
|
-
* apply `toToolCallOutput` when appending to history.
|
|
10
|
-
*
|
|
11
|
-
* Demux note: `Progress` carries `call_id` / `tool` at the top level, while
|
|
12
|
-
* `Output` nests them inside `result` (the `ToolResult` already carries both).
|
|
13
|
-
* A consumer grouping events by call reads `e.call_id` for `Progress` and
|
|
14
|
-
* `e.result.call_id` for `Output`.
|
|
2
|
+
* What `Toolkit.run` emits, typed per tool: `tool` is the discriminant, so
|
|
3
|
+
* `e.tool === "search"` narrows `value` / `result` to that tool's types. Values
|
|
4
|
+
* are decoded (the tool's own types); `Toolkit.output` encodes them for history.
|
|
15
5
|
*/
|
|
16
|
-
import {
|
|
17
|
-
import type { ToolResult } from "./ToolResult.js"
|
|
6
|
+
import type { Result } from "effect"
|
|
18
7
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
readonly call_id: string
|
|
27
|
-
readonly tool: string
|
|
28
|
-
readonly data: unknown
|
|
29
|
-
}
|
|
30
|
-
Output: {
|
|
31
|
-
readonly result: ToolResult
|
|
32
|
-
}
|
|
33
|
-
}>
|
|
8
|
+
/** An intermediate value of a `Stream` tool. */
|
|
9
|
+
export type Partial<Name extends string = string, Output = unknown> = {
|
|
10
|
+
readonly _tag: "Partial"
|
|
11
|
+
readonly call_id: string
|
|
12
|
+
readonly tool: Name
|
|
13
|
+
readonly value: Output
|
|
14
|
+
}
|
|
34
15
|
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
export const ToolEvent = Data.taggedEnum<ToolEvent>()
|
|
16
|
+
/** The call ended: `Success` with its value, or `Failure` with a declared or core error. */
|
|
17
|
+
export type Output<Name extends string = string, Out = unknown, E = unknown> = {
|
|
18
|
+
readonly _tag: "Output"
|
|
19
|
+
readonly call_id: string
|
|
20
|
+
readonly tool: Name
|
|
21
|
+
readonly result: Result.Result<Out, E>
|
|
22
|
+
}
|
|
43
23
|
|
|
44
|
-
export
|
|
45
|
-
|
|
46
|
-
|
|
24
|
+
export type ToolEvent<Name extends string = string, Out = unknown, E = unknown> =
|
|
25
|
+
| Partial<Name, Out>
|
|
26
|
+
| Output<Name, Out, E>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, expect, it } from "@effect/vitest"
|
|
2
|
+
import { Effect, Schema, SchemaTransformation } from "effect"
|
|
3
|
+
import { imageBytes } from "../domain/Image.js"
|
|
4
|
+
import * as Content from "./Content.js"
|
|
5
|
+
import * as ToolOutput from "./ToolOutput.js"
|
|
6
|
+
|
|
7
|
+
const call = { call_id: "c1" }
|
|
8
|
+
const png = Content.image(imageBytes(new Uint8Array([1, 2, 3]), "image/png"))
|
|
9
|
+
|
|
10
|
+
describe("ToolOutput.success", () => {
|
|
11
|
+
it.effect("splits only top-level media: text, JSON and arrays without media stay one part", () =>
|
|
12
|
+
Effect.gen(function* () {
|
|
13
|
+
const outputs = yield* Effect.forEach(["hello", { hits: [1, 2] }, [1, 2], png], (value) =>
|
|
14
|
+
ToolOutput.make(call, value),
|
|
15
|
+
)
|
|
16
|
+
expect(outputs.map((o) => o.output.map((p) => p.type))).toEqual([
|
|
17
|
+
["input_text"],
|
|
18
|
+
["input_json"],
|
|
19
|
+
["input_json"],
|
|
20
|
+
["input_image"],
|
|
21
|
+
])
|
|
22
|
+
}),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
it.effect("takes JSON from the schema's encoded side and media from the value", () =>
|
|
26
|
+
Effect.gen(function* () {
|
|
27
|
+
const Shot = Schema.Tuple([
|
|
28
|
+
Schema.Struct({ title: Schema.String, at: Schema.DateFromString }),
|
|
29
|
+
Content.Image,
|
|
30
|
+
])
|
|
31
|
+
const value = [{ title: "Home", at: new Date("2026-01-01T00:00:00.000Z") }, png] as const
|
|
32
|
+
|
|
33
|
+
const out = yield* ToolOutput.success(call, Shot, value)
|
|
34
|
+
expect(out.output).toEqual([
|
|
35
|
+
{ type: "input_json", value: { title: "Home", at: "2026-01-01T00:00:00.000Z" } },
|
|
36
|
+
{ type: "input_image", source: png.source },
|
|
37
|
+
])
|
|
38
|
+
expect(out.isError).toBeUndefined()
|
|
39
|
+
|
|
40
|
+
const back = yield* ToolOutput.encoded(out.output).pipe(
|
|
41
|
+
Effect.flatMap(Schema.decodeUnknownEffect(Shot)),
|
|
42
|
+
)
|
|
43
|
+
expect(back).toEqual(value)
|
|
44
|
+
}),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
it.effect("keeps the JSON whole when the schema encodes a mixed array to another shape", () =>
|
|
48
|
+
Effect.gen(function* () {
|
|
49
|
+
const Encoded = Schema.Struct({
|
|
50
|
+
title: Schema.String,
|
|
51
|
+
shot: Schema.toEncoded(Content.Image),
|
|
52
|
+
})
|
|
53
|
+
const Reshaped = Encoded.pipe(
|
|
54
|
+
Schema.decodeTo(
|
|
55
|
+
Schema.Tuple([Schema.String, Content.Image]),
|
|
56
|
+
SchemaTransformation.transform({
|
|
57
|
+
decode: ({ title, shot }) => [title, shot] as const,
|
|
58
|
+
encode: ([title, shot]) => ({ title, shot }),
|
|
59
|
+
}),
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
const out = yield* ToolOutput.success(call, Reshaped, ["Home", png] as const)
|
|
63
|
+
expect(out.output).toHaveLength(1)
|
|
64
|
+
expect(out.output[0]).toMatchObject({
|
|
65
|
+
type: "input_json",
|
|
66
|
+
value: { title: "Home", shot: { _tag: "Image" } },
|
|
67
|
+
})
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe("ToolOutput.failure", () => {
|
|
73
|
+
it.effect("encodes the error as JSON and flags the output", () =>
|
|
74
|
+
Effect.gen(function* () {
|
|
75
|
+
const out = yield* ToolOutput.failure(
|
|
76
|
+
call,
|
|
77
|
+
ToolOutput.RunError,
|
|
78
|
+
new ToolOutput.UnknownTool({ message: "no tool named nope" }),
|
|
79
|
+
)
|
|
80
|
+
expect(out).toEqual({
|
|
81
|
+
type: "function_call_output",
|
|
82
|
+
call_id: "c1",
|
|
83
|
+
output: [
|
|
84
|
+
{ type: "input_json", value: { _tag: "UnknownTool", message: "no tool named nope" } },
|
|
85
|
+
],
|
|
86
|
+
isError: true,
|
|
87
|
+
})
|
|
88
|
+
}),
|
|
89
|
+
)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe("ToolOutput.value", () => {
|
|
93
|
+
it.effect("reads parts back as the value that made them, media as classes", () =>
|
|
94
|
+
Effect.gen(function* () {
|
|
95
|
+
const value = [{ title: "Home" }, png, "caption"]
|
|
96
|
+
const out = yield* ToolOutput.make(call, value)
|
|
97
|
+
expect(out.output.map((p) => p.type)).toEqual(["input_json", "input_image", "input_text"])
|
|
98
|
+
const back = ToolOutput.value(out.output)
|
|
99
|
+
expect(back).toEqual(value)
|
|
100
|
+
expect(Array.isArray(back) && back[1]).toBeInstanceOf(Content.Image)
|
|
101
|
+
}),
|
|
102
|
+
)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe("ToolOutput.ValueParts", () => {
|
|
106
|
+
const encode = Schema.encodeEffect(ToolOutput.ValueParts)
|
|
107
|
+
const decode = Schema.decodeUnknownEffect(ToolOutput.ValueParts)
|
|
108
|
+
|
|
109
|
+
it.effect("JSON shaped like an image stays JSON through the wire", () =>
|
|
110
|
+
Effect.gen(function* () {
|
|
111
|
+
const lookalike = { _tag: "Image", source: { _tag: "url", url: "https://x/cat.png" } }
|
|
112
|
+
const wire = yield* encode(lookalike)
|
|
113
|
+
expect(wire).toEqual([{ type: "input_json", value: lookalike }])
|
|
114
|
+
const back = yield* decode(JSON.parse(JSON.stringify(wire)))
|
|
115
|
+
expect(back).toEqual(lookalike)
|
|
116
|
+
expect(back).not.toBeInstanceOf(Content.Image)
|
|
117
|
+
}),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
it.effect("media round-trips as a class, bytes as base64 on the wire", () =>
|
|
121
|
+
Effect.gen(function* () {
|
|
122
|
+
const value = [{ title: "Home" }, png, "caption"]
|
|
123
|
+
const wire = yield* encode(value)
|
|
124
|
+
expect(wire[1]).toEqual({
|
|
125
|
+
type: "input_image",
|
|
126
|
+
source: { _tag: "bytes", bytes: "AQID", mimeType: "image/png" },
|
|
127
|
+
})
|
|
128
|
+
const back = yield* decode(JSON.parse(JSON.stringify(wire)))
|
|
129
|
+
expect(back).toEqual(value)
|
|
130
|
+
}),
|
|
131
|
+
)
|
|
132
|
+
})
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool values to history parts, and back. One rule, top level only:
|
|
3
|
+
* string → text · media → its part · array with media → one part per element ·
|
|
4
|
+
* anything else → one JSON value.
|
|
5
|
+
* Every value goes through a schema (the tool's `outputSchema`, else `Content.Value`):
|
|
6
|
+
* the decoded side decides the shape and supplies media, the encoded side the JSON.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
Array as Arr,
|
|
10
|
+
Effect,
|
|
11
|
+
Match,
|
|
12
|
+
Option,
|
|
13
|
+
Predicate,
|
|
14
|
+
Schema,
|
|
15
|
+
SchemaTransformation,
|
|
16
|
+
} from "effect"
|
|
17
|
+
import { FileSourceJson } from "../domain/File.js"
|
|
18
|
+
import { ImageSourceJson } from "../domain/Image.js"
|
|
19
|
+
import * as Items from "../domain/Items.js"
|
|
20
|
+
import type { ToolCall, ToolCallOutput, ToolOutputContent } from "../domain/Items.js"
|
|
21
|
+
import * as Content from "./Content.js"
|
|
22
|
+
|
|
23
|
+
type CallId = Pick<ToolCall, "call_id">
|
|
24
|
+
|
|
25
|
+
const toPart = Match.type<Content.Media>().pipe(
|
|
26
|
+
Match.tagsExhaustive({
|
|
27
|
+
Image: (m) => Items.image(m.source),
|
|
28
|
+
File: (m) => Items.file(m.source, m.filename),
|
|
29
|
+
}),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const fromPart = Match.type<ToolOutputContent>().pipe(
|
|
33
|
+
Match.discriminatorsExhaustive("type")({
|
|
34
|
+
input_text: (p) => p.text,
|
|
35
|
+
input_json: (p) => p.value,
|
|
36
|
+
input_image: (p) => Content.image(p.source),
|
|
37
|
+
input_file: (p) => Content.file(p.source, p.filename),
|
|
38
|
+
}),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const isMixed = (u: unknown): u is ReadonlyArray<unknown> =>
|
|
42
|
+
Array.isArray(u) && u.some(Content.isMedia)
|
|
43
|
+
|
|
44
|
+
const zipMixed = (
|
|
45
|
+
decoded: ReadonlyArray<unknown>,
|
|
46
|
+
encoded: Schema.Json,
|
|
47
|
+
): ReadonlyArray<ToolOutputContent> =>
|
|
48
|
+
Array.isArray(encoded) && encoded.length === decoded.length
|
|
49
|
+
? Arr.zipWith(decoded, encoded, (d, e) =>
|
|
50
|
+
Match.value(d).pipe(
|
|
51
|
+
Match.when(Content.isMedia, toPart),
|
|
52
|
+
Match.orElse(() => (Predicate.isString(e) ? Items.text(e) : Items.json(e))),
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
: [Items.json(encoded)] // the schema reshaped the array: keep its JSON whole
|
|
56
|
+
|
|
57
|
+
/** `decoded` decides the shape and supplies media; `encoded` supplies the JSON. */
|
|
58
|
+
export const parts = (decoded: unknown, encoded: Schema.Json): ReadonlyArray<ToolOutputContent> =>
|
|
59
|
+
Match.value(decoded).pipe(
|
|
60
|
+
Match.when(Predicate.isString, (text) => [Items.text(text)]),
|
|
61
|
+
Match.when(Content.isMedia, (media) => [toPart(media)]),
|
|
62
|
+
Match.when(isMixed, (values) => zipMixed(values, encoded)),
|
|
63
|
+
Match.orElse(() => [Items.json(encoded)]),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
/** A value, encoded by `schema`, as the call's output. */
|
|
67
|
+
export const success = <A>(
|
|
68
|
+
call: CallId,
|
|
69
|
+
schema: Schema.Codec<A, Schema.Json>,
|
|
70
|
+
value: A,
|
|
71
|
+
): Effect.Effect<ToolCallOutput, Schema.SchemaError> =>
|
|
72
|
+
Effect.map(Schema.encodeEffect(schema)(value), (encoded) => ({
|
|
73
|
+
type: "function_call_output",
|
|
74
|
+
call_id: call.call_id,
|
|
75
|
+
output: parts(value, encoded),
|
|
76
|
+
}))
|
|
77
|
+
|
|
78
|
+
/** An error, encoded by `schema`, as the call's output, flagged `isError`. */
|
|
79
|
+
export const failure = <E>(
|
|
80
|
+
call: CallId,
|
|
81
|
+
schema: Schema.Codec<E, Schema.Json>,
|
|
82
|
+
error: E,
|
|
83
|
+
): Effect.Effect<ToolCallOutput, Schema.SchemaError> =>
|
|
84
|
+
Effect.map(Schema.encodeEffect(schema)(error), (encoded) => ({
|
|
85
|
+
type: "function_call_output",
|
|
86
|
+
call_id: call.call_id,
|
|
87
|
+
output: [Items.json(encoded)],
|
|
88
|
+
isError: true,
|
|
89
|
+
}))
|
|
90
|
+
|
|
91
|
+
/** Any `Content.Value` as the call's output, for calls the app closes itself. */
|
|
92
|
+
export const make = (call: CallId, value: Content.Value) => success(call, Content.Value, value)
|
|
93
|
+
|
|
94
|
+
/** One value as itself, several (or none) as the array: how parts read back as a value. */
|
|
95
|
+
export const single = <A>(values: ReadonlyArray<A>): A | ReadonlyArray<A> =>
|
|
96
|
+
Arr.isReadonlyArrayNonEmpty(values) && values.length === 1 ? Arr.headNonEmpty(values) : values
|
|
97
|
+
|
|
98
|
+
/** A text or JSON part. */
|
|
99
|
+
export const isTextual = (p: ToolOutputContent): p is Items.InputText | Items.InputJson =>
|
|
100
|
+
p.type === "input_text" || p.type === "input_json"
|
|
101
|
+
|
|
102
|
+
/** A text part verbatim, a JSON part serialized. */
|
|
103
|
+
export const partText = (p: Items.InputText | Items.InputJson): string =>
|
|
104
|
+
p.type === "input_text" ? p.text : JSON.stringify(p.value)
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The parts as one string joined by newlines, for wires (or wire paths) whose
|
|
108
|
+
* tool outputs carry text only. `None` when a part is media.
|
|
109
|
+
*/
|
|
110
|
+
export const text = (output: ReadonlyArray<ToolOutputContent>): Option.Option<string> =>
|
|
111
|
+
Arr.every(output, isTextual) ? Option.some(output.map(partText).join("\n")) : Option.none()
|
|
112
|
+
|
|
113
|
+
/** Parts back to a `Content.Value`, read from the parts' own types. */
|
|
114
|
+
export const value = (output: ReadonlyArray<ToolOutputContent>): Content.Value =>
|
|
115
|
+
single(output.map(fromPart))
|
|
116
|
+
|
|
117
|
+
/** Parts back to the encoded form an `outputSchema` decodes (media re-encoded). */
|
|
118
|
+
export const encoded = (
|
|
119
|
+
output: ReadonlyArray<ToolOutputContent>,
|
|
120
|
+
): Effect.Effect<unknown, Schema.SchemaError> =>
|
|
121
|
+
Effect.forEach(output.map(fromPart), (v) =>
|
|
122
|
+
Content.isMedia(v) ? Schema.encodeEffect(Content.Media)(v) : Effect.succeed(v),
|
|
123
|
+
).pipe(Effect.map(single))
|
|
124
|
+
|
|
125
|
+
// --- a Content.Value as typed parts, for the wire to a UI -------------------------
|
|
126
|
+
|
|
127
|
+
const isJson = Schema.is(Schema.Json)
|
|
128
|
+
|
|
129
|
+
const elementPart = Match.type<Content.Media | Schema.Json>().pipe(
|
|
130
|
+
Match.when(Content.isMedia, toPart),
|
|
131
|
+
Match.when(Predicate.isString, Items.text),
|
|
132
|
+
Match.orElse(Items.json),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
/** A `Content.Value` as history parts, by the same rule as `parts` (its JSON is itself). */
|
|
136
|
+
export const valueParts = (v: Content.Value): ReadonlyArray<ToolOutputContent> =>
|
|
137
|
+
Match.value(v).pipe(
|
|
138
|
+
Match.when(Predicate.isString, (text) => [Items.text(text)]),
|
|
139
|
+
Match.when(Content.isMedia, (media) => [toPart(media)]),
|
|
140
|
+
Match.when(isMixed, (values) => values.map(elementPart)),
|
|
141
|
+
Match.when(isJson, (json) => [Items.json(json)]),
|
|
142
|
+
// Unreachable: a Content.Value is text, JSON, media or a mix of them.
|
|
143
|
+
Match.orElse(() => []),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
/** A history part with JSON sources (bytes as base64), as it travels to a UI. */
|
|
147
|
+
export const PartJson = Schema.Union([
|
|
148
|
+
Items.InputText,
|
|
149
|
+
Items.InputJson,
|
|
150
|
+
Schema.Struct({ type: Schema.Literal("input_image"), source: ImageSourceJson }),
|
|
151
|
+
Schema.Struct({
|
|
152
|
+
type: Schema.Literal("input_file"),
|
|
153
|
+
source: FileSourceJson,
|
|
154
|
+
filename: Schema.optionalKey(Schema.String),
|
|
155
|
+
}),
|
|
156
|
+
])
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* A `Content.Value` on the wire to a UI: typed parts instead of its plain JSON,
|
|
160
|
+
* so JSON shaped like `{ _tag: "Image", source }` can't come back as an image.
|
|
161
|
+
*/
|
|
162
|
+
export const ValueParts = Schema.Array(PartJson).pipe(
|
|
163
|
+
Schema.decodeTo(
|
|
164
|
+
Schema.toType(Content.Value),
|
|
165
|
+
SchemaTransformation.transform({ decode: value, encode: valueParts }),
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
// --- core's own failures -------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
/** The model's arguments are not JSON or fail `inputSchema`. */
|
|
172
|
+
export class InputInvalid extends Schema.TaggedError<InputInvalid>(
|
|
173
|
+
"@betalyra/effect-uai/InputInvalid",
|
|
174
|
+
)("InputInvalid", { message: Schema.String }) {}
|
|
175
|
+
|
|
176
|
+
/** `run` returned a value its `outputSchema` rejects: a bug in the tool. */
|
|
177
|
+
export class OutputInvalid extends Schema.TaggedError<OutputInvalid>(
|
|
178
|
+
"@betalyra/effect-uai/OutputInvalid",
|
|
179
|
+
)("OutputInvalid", { message: Schema.String }) {}
|
|
180
|
+
|
|
181
|
+
/** No local tool by that name (hallucinated, removed, or a declared or provider tool). */
|
|
182
|
+
export class UnknownTool extends Schema.TaggedError<UnknownTool>(
|
|
183
|
+
"@betalyra/effect-uai/UnknownTool",
|
|
184
|
+
)("UnknownTool", { message: Schema.String }) {}
|
|
185
|
+
|
|
186
|
+
/** The call was left unanswered and closed before the next model call. */
|
|
187
|
+
export class Cancelled extends Schema.TaggedError<Cancelled>("@betalyra/effect-uai/Cancelled")(
|
|
188
|
+
"Cancelled",
|
|
189
|
+
{ message: Schema.String },
|
|
190
|
+
) {}
|
|
191
|
+
|
|
192
|
+
/** What `Toolkit.run` produces itself. */
|
|
193
|
+
export type RunError = InputInvalid | OutputInvalid | UnknownTool
|
|
194
|
+
export const RunError = Schema.Union([InputInvalid, OutputInvalid, UnknownTool])
|
|
195
|
+
|
|
196
|
+
/** Every error core writes into history; what history and the UI always decode. */
|
|
197
|
+
export type CoreError = RunError | Cancelled
|
|
198
|
+
export const CoreError = Schema.Union([InputInvalid, OutputInvalid, UnknownTool, Cancelled])
|