@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.
Files changed (218) hide show
  1. package/README.md +5 -5
  2. package/dist/Content-D_Qfn8Ja.d.mts +48 -0
  3. package/dist/Content-D_Qfn8Ja.d.mts.map +1 -0
  4. package/dist/File-B923KvZ9.d.mts +46 -0
  5. package/dist/File-B923KvZ9.d.mts.map +1 -0
  6. package/dist/{Image-Cv6iBLTy.d.mts → Image-Bzc1H_UK.d.mts} +19 -8
  7. package/dist/Image-Bzc1H_UK.d.mts.map +1 -0
  8. package/dist/{Items-C7MyrYPX.d.mts → Items-Bvx8s9QH.d.mts} +109 -16
  9. package/dist/Items-Bvx8s9QH.d.mts.map +1 -0
  10. package/dist/{Loop-ChpaO-ya.d.mts → Loop-CeA0AelN.d.mts} +7 -6
  11. package/dist/Loop-CeA0AelN.d.mts.map +1 -0
  12. package/dist/Tool-Bkryw6-h.d.mts +206 -0
  13. package/dist/Tool-Bkryw6-h.d.mts.map +1 -0
  14. package/dist/ToolEvent-9inLx5cI.d.mts +22 -0
  15. package/dist/ToolEvent-9inLx5cI.d.mts.map +1 -0
  16. package/dist/ToolOutput-BNTIQgGB.d.mts +151 -0
  17. package/dist/ToolOutput-BNTIQgGB.d.mts.map +1 -0
  18. package/dist/Toolkit-D2VGeD5R.d.mts +212 -0
  19. package/dist/Toolkit-D2VGeD5R.d.mts.map +1 -0
  20. package/dist/browser/BrowserTool.d.mts +84 -22
  21. package/dist/browser/BrowserTool.d.mts.map +1 -1
  22. package/dist/browser/BrowserTool.mjs +5 -0
  23. package/dist/browser/BrowserTool.mjs.map +1 -1
  24. package/dist/capabilities/Capabilities.d.mts +1 -1
  25. package/dist/decision-model/Decision.test.mjs +4 -4
  26. package/dist/dist-B5K-y4Be.mjs +191 -0
  27. package/dist/dist-B5K-y4Be.mjs.map +1 -0
  28. package/dist/dist-CorQTQL-.mjs +1533 -0
  29. package/dist/dist-CorQTQL-.mjs.map +1 -0
  30. package/dist/dist-DNeuh84d.mjs +7 -0
  31. package/dist/dist-DNeuh84d.mjs.map +1 -0
  32. package/dist/dist-DzeSyeYF.mjs +10253 -0
  33. package/dist/dist-DzeSyeYF.mjs.map +1 -0
  34. package/dist/domain/File.d.mts +2 -0
  35. package/dist/domain/File.mjs +51 -0
  36. package/dist/domain/File.mjs.map +1 -0
  37. package/dist/domain/Image.d.mts +2 -2
  38. package/dist/domain/Image.mjs +12 -2
  39. package/dist/domain/Image.mjs.map +1 -1
  40. package/dist/domain/Items.d.mts +2 -2
  41. package/dist/domain/Items.mjs +65 -4
  42. package/dist/domain/Items.mjs.map +1 -1
  43. package/dist/domain/Realtime.d.mts +72 -490
  44. package/dist/domain/Realtime.d.mts.map +1 -1
  45. package/dist/domain/Research.d.mts +1 -1
  46. package/dist/domain/Transcript.test.mjs +3 -3
  47. package/dist/domain/Turn.d.mts +57 -262
  48. package/dist/domain/Turn.d.mts.map +1 -1
  49. package/dist/domain/Turn.mjs +1 -1
  50. package/dist/domain/Turn.test.mjs +2 -2
  51. package/dist/embedding-model/Embedding.d.mts +1 -1
  52. package/dist/embedding-model/EmbeddingModel.test.mjs +2 -2
  53. package/dist/embedding-model/EmbeddingModel.test.mjs.map +1 -1
  54. package/dist/image-generator/ImageGenerator.d.mts +10 -66
  55. package/dist/image-generator/ImageGenerator.d.mts.map +1 -1
  56. package/dist/index.C4tKZ0yW-LuFnBtmu.mjs +7042 -0
  57. package/dist/index.C4tKZ0yW-LuFnBtmu.mjs.map +1 -0
  58. package/dist/index.d.mts +10 -9
  59. package/dist/index.mjs +7 -6
  60. package/dist/language-model/LanguageModel.d.mts +2 -2
  61. package/dist/language-model/LanguageModel.test.mjs +2 -2
  62. package/dist/loop/Loop.d.mts +1 -1
  63. package/dist/loop/Loop.mjs.map +1 -1
  64. package/dist/loop/Loop.test.mjs +3 -3
  65. package/dist/math/Probability.test.mjs +2 -2
  66. package/dist/messenger/Messenger.d.mts +43 -289
  67. package/dist/messenger/Messenger.d.mts.map +1 -1
  68. package/dist/messenger/MessengerAdapter.test.mjs +3 -3
  69. package/dist/music-generator/MusicGenerator.test.mjs +3 -3
  70. package/dist/observability/Metrics.d.mts +1 -1
  71. package/dist/observability/Metrics.test.mjs +3 -3
  72. package/dist/observability/Telemetry.d.mts +1 -1
  73. package/dist/observability/Telemetry.mjs +1 -1
  74. package/dist/observability/Telemetry.mjs.map +1 -1
  75. package/dist/observability/Telemetry.test.mjs +4 -4
  76. package/dist/observability/Telemetry.test.mjs.map +1 -1
  77. package/dist/realtime/RealtimeSession.d.mts +1 -1
  78. package/dist/realtime/RealtimeSession.test.mjs +4 -4
  79. package/dist/retry/Retry.test.mjs +2 -2
  80. package/dist/sandbox/Sandbox.d.mts +32 -237
  81. package/dist/sandbox/Sandbox.d.mts.map +1 -1
  82. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +3 -3
  83. package/dist/streaming/JSONL.test.mjs +2 -2
  84. package/dist/streaming/Multipart.d.mts +1 -1
  85. package/dist/streaming/Multipart.mjs +3 -2
  86. package/dist/streaming/Multipart.mjs.map +1 -1
  87. package/dist/streaming/Multipart.test.mjs +28 -0
  88. package/dist/streaming/Multipart.test.mjs.map +1 -0
  89. package/dist/streaming/SSE.d.mts +1 -1
  90. package/dist/streaming/SSE.mjs +2 -2
  91. package/dist/streaming/SSE.mjs.map +1 -1
  92. package/dist/streaming/SSE.test.mjs +2 -2
  93. package/dist/streaming/Settle.test.mjs +3 -3
  94. package/dist/streaming/WebSocketSession.d.mts +5 -3
  95. package/dist/streaming/WebSocketSession.d.mts.map +1 -1
  96. package/dist/streaming/WebSocketSession.mjs +22 -10
  97. package/dist/streaming/WebSocketSession.mjs.map +1 -1
  98. package/dist/streaming/WebSocketSession.test.mjs +41 -5
  99. package/dist/streaming/WebSocketSession.test.mjs.map +1 -1
  100. package/dist/structured-format/StructuredFormat.mjs.map +1 -1
  101. package/dist/structured-format/StructuredFormat.test.mjs +2 -2
  102. package/dist/testing/FakeWebSocket.d.mts +5 -1
  103. package/dist/testing/FakeWebSocket.d.mts.map +1 -1
  104. package/dist/testing/FakeWebSocket.mjs +12 -6
  105. package/dist/testing/FakeWebSocket.mjs.map +1 -1
  106. package/dist/testing/MockMessenger.d.mts +19 -134
  107. package/dist/testing/MockMessenger.d.mts.map +1 -1
  108. package/dist/testing/MockProvider.d.mts +1 -1
  109. package/dist/testing/MockSandbox.d.mts +65 -446
  110. package/dist/testing/MockSandbox.d.mts.map +1 -1
  111. package/dist/tool/Content.d.mts +2 -0
  112. package/dist/tool/Content.mjs +46 -0
  113. package/dist/tool/Content.mjs.map +1 -0
  114. package/dist/tool/Content.test.d.mts +1 -0
  115. package/dist/tool/Content.test.mjs +48 -0
  116. package/dist/tool/Content.test.mjs.map +1 -0
  117. package/dist/tool/HistoryCheck.d.mts +6 -15
  118. package/dist/tool/HistoryCheck.d.mts.map +1 -1
  119. package/dist/tool/HistoryCheck.mjs +10 -24
  120. package/dist/tool/HistoryCheck.mjs.map +1 -1
  121. package/dist/tool/Tool.d.mts +2 -2
  122. package/dist/tool/Tool.mjs +55 -130
  123. package/dist/tool/Tool.mjs.map +1 -1
  124. package/dist/tool/Tool.test.mjs +222 -178
  125. package/dist/tool/Tool.test.mjs.map +1 -1
  126. package/dist/tool/ToolDecision.d.mts +164 -0
  127. package/dist/tool/ToolDecision.d.mts.map +1 -0
  128. package/dist/tool/ToolDecision.mjs +146 -0
  129. package/dist/tool/ToolDecision.mjs.map +1 -0
  130. package/dist/tool/ToolDecision.test.d.mts +1 -0
  131. package/dist/tool/ToolDecision.test.mjs +330 -0
  132. package/dist/tool/ToolDecision.test.mjs.map +1 -0
  133. package/dist/tool/ToolEvent.d.mts +2 -2
  134. package/dist/tool/ToolEvent.mjs +2 -34
  135. package/dist/tool/ToolEvent.mjs.map +1 -1
  136. package/dist/tool/ToolOutput.d.mts +2 -0
  137. package/dist/tool/ToolOutput.mjs +128 -0
  138. package/dist/tool/ToolOutput.mjs.map +1 -0
  139. package/dist/tool/ToolOutput.test.d.mts +1 -0
  140. package/dist/tool/ToolOutput.test.mjs +153 -0
  141. package/dist/tool/ToolOutput.test.mjs.map +1 -0
  142. package/dist/tool/Toolkit.d.mts +2 -2
  143. package/dist/tool/Toolkit.mjs +230 -165
  144. package/dist/tool/Toolkit.mjs.map +1 -1
  145. package/dist/tool/Toolkit.test.mjs +433 -391
  146. package/dist/tool/Toolkit.test.mjs.map +1 -1
  147. package/dist/transcriber/Transcriber.test.mjs +3 -3
  148. package/dist/web-read/WebReadTool.d.mts +2 -2
  149. package/dist/web-read/WebReadTool.mjs +1 -0
  150. package/dist/web-read/WebReadTool.mjs.map +1 -1
  151. package/dist/web-search/WebSearchTool.d.mts +2 -2
  152. package/dist/web-search/WebSearchTool.mjs +1 -0
  153. package/dist/web-search/WebSearchTool.mjs.map +1 -1
  154. package/package.json +18 -10
  155. package/src/browser/BrowserTool.ts +5 -0
  156. package/src/domain/File.ts +79 -0
  157. package/src/domain/Image.ts +14 -4
  158. package/src/domain/Items.ts +39 -3
  159. package/src/embedding-model/EmbeddingModel.test.ts +2 -2
  160. package/src/index.ts +3 -2
  161. package/src/loop/Loop.ts +25 -14
  162. package/src/observability/Telemetry.test.ts +1 -1
  163. package/src/observability/Telemetry.ts +2 -2
  164. package/src/streaming/Multipart.test.ts +26 -0
  165. package/src/streaming/Multipart.ts +6 -5
  166. package/src/streaming/SSE.ts +2 -2
  167. package/src/streaming/WebSocketSession.test.ts +54 -2
  168. package/src/streaming/WebSocketSession.ts +27 -6
  169. package/src/structured-format/StructuredFormat.ts +1 -1
  170. package/src/testing/FakeWebSocket.ts +21 -6
  171. package/src/tool/Content.test.ts +40 -0
  172. package/src/tool/Content.ts +38 -0
  173. package/src/tool/HistoryCheck.ts +21 -26
  174. package/src/tool/Tool.test.ts +240 -241
  175. package/src/tool/Tool.ts +204 -309
  176. package/src/tool/ToolDecision.test.ts +301 -0
  177. package/src/tool/ToolDecision.ts +360 -0
  178. package/src/tool/ToolEvent.ts +21 -41
  179. package/src/tool/ToolOutput.test.ts +132 -0
  180. package/src/tool/ToolOutput.ts +198 -0
  181. package/src/tool/Toolkit.test.ts +594 -454
  182. package/src/tool/Toolkit.ts +631 -356
  183. package/src/web-read/WebReadTool.ts +2 -1
  184. package/src/web-search/WebSearchTool.ts +2 -1
  185. package/dist/Image-Cv6iBLTy.d.mts.map +0 -1
  186. package/dist/Items-C7MyrYPX.d.mts.map +0 -1
  187. package/dist/Loop-ChpaO-ya.d.mts.map +0 -1
  188. package/dist/Tool-BF78QWtn.d.mts +0 -314
  189. package/dist/Tool-BF78QWtn.d.mts.map +0 -1
  190. package/dist/ToolEvent-Di4a723B.d.mts +0 -144
  191. package/dist/ToolEvent-Di4a723B.d.mts.map +0 -1
  192. package/dist/ToolResult-BsYQRjmW.d.mts +0 -152
  193. package/dist/ToolResult-BsYQRjmW.d.mts.map +0 -1
  194. package/dist/Toolkit-tlL0F2YR.d.mts +0 -193
  195. package/dist/Toolkit-tlL0F2YR.d.mts.map +0 -1
  196. package/dist/dist-BaenoU1A.mjs +0 -145
  197. package/dist/dist-BaenoU1A.mjs.map +0 -1
  198. package/dist/dist-nohcnkD6.mjs +0 -182
  199. package/dist/dist-nohcnkD6.mjs.map +0 -1
  200. package/dist/magic-string.es-CB2hOw9v.mjs +0 -1014
  201. package/dist/magic-string.es-CB2hOw9v.mjs.map +0 -1
  202. package/dist/test.DNmyFkvJ-YPzDR0Co.mjs +0 -10254
  203. package/dist/test.DNmyFkvJ-YPzDR0Co.mjs.map +0 -1
  204. package/dist/tool/Approval.d.mts +0 -113
  205. package/dist/tool/Approval.d.mts.map +0 -1
  206. package/dist/tool/Approval.mjs +0 -80
  207. package/dist/tool/Approval.mjs.map +0 -1
  208. package/dist/tool/Approval.test.mjs +0 -371
  209. package/dist/tool/Approval.test.mjs.map +0 -1
  210. package/dist/tool/ToolResult.d.mts +0 -2
  211. package/dist/tool/ToolResult.mjs +0 -85
  212. package/dist/tool/ToolResult.mjs.map +0 -1
  213. package/dist/utils-CiH_DjFW.mjs +0 -3918
  214. package/dist/utils-CiH_DjFW.mjs.map +0 -1
  215. package/src/tool/Approval.test.ts +0 -400
  216. package/src/tool/Approval.ts +0 -181
  217. package/src/tool/ToolResult.ts +0 -114
  218. /package/dist/{tool/Approval.test.d.mts → streaming/Multipart.test.d.mts} +0 -0
@@ -1,46 +1,26 @@
1
1
  /**
2
- * The event type emitted while handling tool calls.
3
- *
4
- * - ApprovalRequested : gated calls waiting for approval
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 { Data } from "effect"
17
- import type { ToolResult } from "./ToolResult.js"
6
+ import type { Result } from "effect"
18
7
 
19
- export type ToolEvent = Data.TaggedEnum<{
20
- ApprovalRequested: {
21
- readonly call_id: string
22
- readonly tool: string
23
- readonly arguments: string
24
- }
25
- Progress: {
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
- * Namespace of constructors, type guards, and matchers for `ToolEvent`,
37
- * provided by `Data.taggedEnum`. Use `ToolEvent.Output({ result })` to build
38
- * an event, `ToolEvent.$is("Output")` for type narrowing,
39
- * `ToolEvent.$match({ ApprovalRequested, Progress, Output })` for
40
- * exhaustive pattern matching.
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 const isApprovalRequested = ToolEvent.$is("ApprovalRequested")
45
- export const isProgress = ToolEvent.$is("Progress")
46
- export const isOutput = ToolEvent.$is("Output")
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])