@effect-uai/core 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/dist/{AiError-CAX_48RU.d.mts → AiError-Bf1pq4BB.d.mts} +9 -4
  2. package/dist/{AiError-CAX_48RU.d.mts.map → AiError-Bf1pq4BB.d.mts.map} +1 -1
  3. package/dist/{Audio-DPlTsNdz.d.mts → Audio-CR7CSljJ.d.mts} +1 -1
  4. package/dist/{Audio-DPlTsNdz.d.mts.map → Audio-CR7CSljJ.d.mts.map} +1 -1
  5. package/dist/{Image-HNmMpMTh.d.mts → Image-BZmKfIdq.d.mts} +1 -1
  6. package/dist/{Image-HNmMpMTh.d.mts.map → Image-BZmKfIdq.d.mts.map} +1 -1
  7. package/dist/{Items-7ljaVxH_.d.mts → Items-BnIduEV2.d.mts} +7 -7
  8. package/dist/{Items-7ljaVxH_.d.mts.map → Items-BnIduEV2.d.mts.map} +1 -1
  9. package/dist/Loop-D8nWpowc.d.mts +173 -0
  10. package/dist/Loop-D8nWpowc.d.mts.map +1 -0
  11. package/dist/{SandboxError-BvKypkJm.d.mts → SandboxError-CIrVDOMV.d.mts} +2 -2
  12. package/dist/{SandboxError-BvKypkJm.d.mts.map → SandboxError-CIrVDOMV.d.mts.map} +1 -1
  13. package/dist/{StructuredFormat-BbN4dosH.d.mts → StructuredFormat-DHaUJBdd.d.mts} +2 -2
  14. package/dist/{StructuredFormat-BbN4dosH.d.mts.map → StructuredFormat-DHaUJBdd.d.mts.map} +1 -1
  15. package/dist/Tool-2lC_XMth.d.mts +284 -0
  16. package/dist/Tool-2lC_XMth.d.mts.map +1 -0
  17. package/dist/ToolEvent-cWZ2BnbS.d.mts +151 -0
  18. package/dist/ToolEvent-cWZ2BnbS.d.mts.map +1 -0
  19. package/dist/ToolResult-CL4hK_9z.d.mts +157 -0
  20. package/dist/ToolResult-CL4hK_9z.d.mts.map +1 -0
  21. package/dist/Toolkit-Dr9l_hNy.d.mts +196 -0
  22. package/dist/Toolkit-Dr9l_hNy.d.mts.map +1 -0
  23. package/dist/browser/Browser.d.mts +190 -0
  24. package/dist/browser/Browser.d.mts.map +1 -0
  25. package/dist/browser/Browser.mjs +34 -0
  26. package/dist/browser/Browser.mjs.map +1 -0
  27. package/dist/browser/BrowserError.d.mts +141 -0
  28. package/dist/browser/BrowserError.d.mts.map +1 -0
  29. package/dist/browser/BrowserError.mjs +89 -0
  30. package/dist/browser/BrowserError.mjs.map +1 -0
  31. package/dist/browser/BrowserTool.d.mts +58 -0
  32. package/dist/browser/BrowserTool.d.mts.map +1 -0
  33. package/dist/browser/BrowserTool.mjs +86 -0
  34. package/dist/browser/BrowserTool.mjs.map +1 -0
  35. package/dist/{dist-DV5ISja1.mjs → dist-B04dlFHg.mjs} +3 -3
  36. package/dist/{dist-DV5ISja1.mjs.map → dist-B04dlFHg.mjs.map} +1 -1
  37. package/dist/dist-DWgRbDN3.mjs +140 -0
  38. package/dist/dist-DWgRbDN3.mjs.map +1 -0
  39. package/dist/domain/AiError.d.mts +2 -2
  40. package/dist/domain/AiError.mjs +22 -3
  41. package/dist/domain/AiError.mjs.map +1 -1
  42. package/dist/domain/Audio.d.mts +1 -1
  43. package/dist/domain/Image.d.mts +1 -1
  44. package/dist/domain/Items.d.mts +1 -1
  45. package/dist/domain/Music.d.mts +1 -1
  46. package/dist/domain/Turn.d.mts +4 -4
  47. package/dist/domain/Turn.test.mjs +1 -1
  48. package/dist/embedding-model/Embedding.d.mts +1 -1
  49. package/dist/embedding-model/EmbeddingModel.d.mts +1 -1
  50. package/dist/embedding-model/EmbeddingModel.test.mjs +1 -1
  51. package/dist/embedding-model/EmbeddingModel.test.mjs.map +1 -1
  52. package/dist/index.d.mts +19 -13
  53. package/dist/index.mjs +15 -9
  54. package/dist/language-model/LanguageModel.d.mts +10 -5
  55. package/dist/language-model/LanguageModel.d.mts.map +1 -1
  56. package/dist/language-model/LanguageModel.mjs.map +1 -1
  57. package/dist/language-model/LanguageModel.test.mjs +1 -1
  58. package/dist/loop/Loop.d.mts +2 -141
  59. package/dist/loop/Loop.mjs +16 -20
  60. package/dist/loop/Loop.mjs.map +1 -1
  61. package/dist/loop/Loop.test.mjs +18 -1
  62. package/dist/loop/Loop.test.mjs.map +1 -1
  63. package/dist/{magic-string.es-BgIV5Mu3.mjs → magic-string.es-Dk-MMA0F.mjs} +1 -1
  64. package/dist/{magic-string.es-BgIV5Mu3.mjs.map → magic-string.es-Dk-MMA0F.mjs.map} +1 -1
  65. package/dist/music-generator/MusicGenerator.d.mts +2 -2
  66. package/dist/music-generator/MusicGenerator.test.mjs +1 -1
  67. package/dist/observability/Metrics.d.mts +116 -28
  68. package/dist/observability/Metrics.d.mts.map +1 -1
  69. package/dist/observability/Metrics.mjs +250 -38
  70. package/dist/observability/Metrics.mjs.map +1 -1
  71. package/dist/observability/Metrics.test.d.mts +1 -0
  72. package/dist/observability/Metrics.test.mjs +187 -0
  73. package/dist/observability/Metrics.test.mjs.map +1 -0
  74. package/dist/observability/Telemetry.d.mts +37 -0
  75. package/dist/observability/Telemetry.d.mts.map +1 -0
  76. package/dist/observability/Telemetry.mjs +57 -0
  77. package/dist/observability/Telemetry.mjs.map +1 -0
  78. package/dist/observability/Telemetry.test.d.mts +1 -0
  79. package/dist/observability/Telemetry.test.mjs +92 -0
  80. package/dist/observability/Telemetry.test.mjs.map +1 -0
  81. package/dist/retry/Retry.d.mts +2 -2
  82. package/dist/retry/Retry.test.mjs +1 -1
  83. package/dist/sandbox/Sandbox.d.mts +1 -1
  84. package/dist/sandbox/SandboxError.d.mts +1 -1
  85. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +2 -2
  86. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +1 -1
  87. package/dist/streaming/JSONL.d.mts +1 -1
  88. package/dist/streaming/JSONL.test.mjs +1 -1
  89. package/dist/streaming/SSE.test.mjs +1 -1
  90. package/dist/structured-format/StructuredFormat.d.mts +1 -1
  91. package/dist/structured-format/StructuredFormat.mjs.map +1 -1
  92. package/dist/structured-format/StructuredFormat.test.mjs +1 -1
  93. package/dist/testing/MockMusicGenerator.d.mts +1 -1
  94. package/dist/testing/MockProvider.d.mts +1 -1
  95. package/dist/testing/MockSpeechSynthesizer.d.mts +1 -1
  96. package/dist/tool/Approval.d.mts +19 -7
  97. package/dist/tool/Approval.d.mts.map +1 -1
  98. package/dist/tool/Approval.mjs +18 -11
  99. package/dist/tool/Approval.mjs.map +1 -1
  100. package/dist/tool/Approval.test.mjs +80 -40
  101. package/dist/tool/Approval.test.mjs.map +1 -1
  102. package/dist/tool/HistoryCheck.d.mts +2 -2
  103. package/dist/tool/Tool.d.mts +2 -2
  104. package/dist/tool/Tool.mjs +190 -36
  105. package/dist/tool/Tool.mjs.map +1 -1
  106. package/dist/tool/Tool.test.mjs +41 -3
  107. package/dist/tool/Tool.test.mjs.map +1 -1
  108. package/dist/tool/ToolEvent.d.mts +2 -151
  109. package/dist/tool/ToolEvent.mjs +5 -0
  110. package/dist/tool/ToolEvent.mjs.map +1 -1
  111. package/dist/tool/ToolResult.d.mts +2 -138
  112. package/dist/tool/ToolResult.mjs +32 -8
  113. package/dist/tool/ToolResult.mjs.map +1 -1
  114. package/dist/tool/Toolkit.d.mts +2 -73
  115. package/dist/tool/Toolkit.mjs +153 -35
  116. package/dist/tool/Toolkit.mjs.map +1 -1
  117. package/dist/tool/Toolkit.test.mjs +271 -11
  118. package/dist/tool/Toolkit.test.mjs.map +1 -1
  119. package/dist/transcriber/Transcriber.d.mts +2 -2
  120. package/dist/transcriber/Transcriber.test.mjs +1 -1
  121. package/dist/web-read/WebRead.d.mts +44 -0
  122. package/dist/web-read/WebRead.d.mts.map +1 -0
  123. package/dist/web-read/WebRead.mjs +18 -0
  124. package/dist/web-read/WebRead.mjs.map +1 -0
  125. package/dist/web-read/WebReadTool.d.mts +57 -0
  126. package/dist/web-read/WebReadTool.d.mts.map +1 -0
  127. package/dist/web-read/WebReadTool.mjs +59 -0
  128. package/dist/web-read/WebReadTool.mjs.map +1 -0
  129. package/dist/web-search/WebSearch.d.mts +1 -1
  130. package/dist/web-search/WebSearchTool.d.mts +12 -6
  131. package/dist/web-search/WebSearchTool.d.mts.map +1 -1
  132. package/dist/web-search/WebSearchTool.mjs +10 -5
  133. package/dist/web-search/WebSearchTool.mjs.map +1 -1
  134. package/package.json +21 -1
  135. package/src/browser/Browser.ts +268 -0
  136. package/src/browser/BrowserError.ts +145 -0
  137. package/src/browser/BrowserTool.ts +161 -0
  138. package/src/domain/AiError.ts +27 -1
  139. package/src/embedding-model/EmbeddingModel.test.ts +4 -0
  140. package/src/index.ts +6 -0
  141. package/src/language-model/LanguageModel.ts +7 -2
  142. package/src/loop/Loop.test.ts +22 -0
  143. package/src/loop/Loop.ts +27 -4
  144. package/src/observability/Metrics.test.ts +266 -0
  145. package/src/observability/Metrics.ts +505 -65
  146. package/src/observability/Telemetry.test.ts +123 -0
  147. package/src/observability/Telemetry.ts +96 -0
  148. package/src/structured-format/StructuredFormat.ts +4 -0
  149. package/src/tool/Approval.test.ts +77 -26
  150. package/src/tool/Approval.ts +72 -27
  151. package/src/tool/Tool.test.ts +55 -2
  152. package/src/tool/Tool.ts +335 -70
  153. package/src/tool/ToolEvent.ts +5 -0
  154. package/src/tool/ToolResult.ts +36 -9
  155. package/src/tool/Toolkit.test.ts +339 -9
  156. package/src/tool/Toolkit.ts +470 -120
  157. package/src/web-read/WebRead.ts +70 -0
  158. package/src/web-read/WebReadTool.ts +96 -0
  159. package/src/web-search/WebSearchTool.ts +11 -5
  160. package/dist/Tool-BKiieFXk.d.mts +0 -102
  161. package/dist/Tool-BKiieFXk.d.mts.map +0 -1
  162. package/dist/loop/Loop.d.mts.map +0 -1
  163. package/dist/tool/ToolEvent.d.mts.map +0 -1
  164. package/dist/tool/ToolResult.d.mts.map +0 -1
  165. package/dist/tool/Toolkit.d.mts.map +0 -1
@@ -1,11 +1,29 @@
1
- import { Context, Effect, Layer, Schema, Stream } from "effect"
1
+ import { Context, Effect, Layer, pipe, Schema, Stream } from "effect"
2
2
  import { describe, expect, expectTypeOf, it } from "vitest"
3
+ import * as AiError from "../domain/AiError.js"
3
4
  import type { ToolCall } from "../domain/Items.js"
4
- import { isOutput } from "./ToolEvent.js"
5
- import { isOk } from "./ToolResult.js"
5
+ import { type ToolEvent, isOutput, isProgress } from "./ToolEvent.js"
6
+ import { isFailure, isOk, toToolCallOutput } from "./ToolResult.js"
6
7
  import * as Tool from "./Tool.js"
7
8
  import * as Toolkit from "./Toolkit.js"
8
9
 
10
+ const Empty = Schema.Struct({})
11
+ const toolCall = (name: string, id: string, args = "{}"): ToolCall => ({
12
+ type: "function_call",
13
+ call_id: id,
14
+ name,
15
+ arguments: args,
16
+ })
17
+ const runToArray = <T extends Toolkit.Toolkit>(
18
+ toolkit: T,
19
+ calls: ReadonlyArray<ToolCall>,
20
+ options?: Toolkit.ExecuteOptions,
21
+ ) =>
22
+ Toolkit.run(toolkit, calls, options).pipe(
23
+ Stream.runCollect,
24
+ Effect.map((events): ReadonlyArray<ToolEvent> => Array.from(events)),
25
+ )
26
+
9
27
  describe("Tool.toDescriptors", () => {
10
28
  const GetWeatherInput = Schema.Struct({ city: Schema.String })
11
29
 
@@ -92,7 +110,7 @@ describe("Toolkit.run - tools with R requirements", () => {
92
110
  })
93
111
 
94
112
  it("propagates each tool's R into the resulting Stream's requirements", () => {
95
- const stream = Toolkit.run([getWeather, getCoords], [])
113
+ const stream = Toolkit.run(Toolkit.make(getWeather, getCoords), [])
96
114
  expectTypeOf(stream).toEqualTypeOf<
97
115
  Stream.Stream<import("./ToolEvent.js").ToolEvent, never, WeatherApiKey | GeoApiKey>
98
116
  >()
@@ -104,10 +122,10 @@ describe("Toolkit.run - tools with R requirements", () => {
104
122
  Layer.succeed(GeoApiKey, { key: "geo-456" }),
105
123
  )
106
124
 
107
- const program = Toolkit.run(
108
- [getWeather, getCoords],
109
- [call("get_weather", "c1"), call("get_coords", "c2")],
110
- ).pipe(Stream.runCollect, Effect.provide(layer))
125
+ const program = Toolkit.run(Toolkit.make(getWeather, getCoords), [
126
+ call("get_weather", "c1"),
127
+ call("get_coords", "c2"),
128
+ ]).pipe(Stream.runCollect, Effect.provide(layer))
111
129
 
112
130
  const events = await Effect.runPromise(program)
113
131
  const outputs = Array.from(events).filter(isOutput)
@@ -125,6 +143,43 @@ describe("Toolkit.run - tools with R requirements", () => {
125
143
  })
126
144
  })
127
145
 
146
+ it("reports non-local kinds as non_local_tool while local calls in the same turn still run", async () => {
147
+ const escalate = Tool.signal({
148
+ name: "escalate",
149
+ description: "",
150
+ inputSchema: Tool.fromEffectSchema(Empty),
151
+ })
152
+ const localPlain = Tool.make({
153
+ name: "plain",
154
+ description: "",
155
+ inputSchema: Tool.fromEffectSchema(Empty),
156
+ run: () => Effect.succeed({ ok: true }),
157
+ })
158
+
159
+ const events = await Effect.runPromise(
160
+ Toolkit.run(Toolkit.make(localPlain, escalate), [
161
+ call("escalate", "s1"),
162
+ call("plain", "p1"),
163
+ call("ghost", "g1"),
164
+ ]).pipe(Stream.runCollect),
165
+ )
166
+ const byCall = new Map(
167
+ Array.from(events)
168
+ .filter(isOutput)
169
+ .map((e) => [e.result.call_id, e.result] as const),
170
+ )
171
+
172
+ // Signal tool: model-visible but no local executor.
173
+ const s = byCall.get("s1")
174
+ expect(s?._tag === "Failure" && s.kind).toBe("non_local_tool")
175
+ // Unknown tool stays distinct from non-local.
176
+ const g = byCall.get("g1")
177
+ expect(g?._tag === "Failure" && g.kind).toBe("unknown_tool")
178
+ // The local call still executed despite the non-local sibling.
179
+ const p = byCall.get("p1")
180
+ expect(p !== undefined && isOk(p) && p.value).toEqual({ ok: true })
181
+ })
182
+
128
183
  it("with no service-needing tools, R is never", () => {
129
184
  const plain = Tool.make({
130
185
  name: "plain",
@@ -132,9 +187,284 @@ describe("Toolkit.run - tools with R requirements", () => {
132
187
  inputSchema: Tool.fromEffectSchema(Empty),
133
188
  run: () => Effect.succeed(0),
134
189
  })
135
- const stream = Toolkit.run([plain], [])
190
+ const stream = Toolkit.run(Toolkit.make(plain), [])
136
191
  expectTypeOf(stream).toEqualTypeOf<
137
192
  Stream.Stream<import("./ToolEvent.js").ToolEvent, never, never>
138
193
  >()
139
194
  })
140
195
  })
196
+
197
+ describe("Toolkit.make / compose / namespace - uniqueness", () => {
198
+ const Empty = Schema.Struct({})
199
+ const tool = (name: string) =>
200
+ Tool.make({
201
+ name,
202
+ description: "",
203
+ inputSchema: Tool.fromEffectSchema(Empty),
204
+ run: () => Effect.succeed(0),
205
+ })
206
+
207
+ it("rejects a duplicate literal name at compile time", () => {
208
+ const a = tool("search")
209
+ const b = Tool.provider({
210
+ name: "search",
211
+ description: "",
212
+ inputSchema: Tool.fromEffectSchema(Empty),
213
+ provider: "demo",
214
+ config: {},
215
+ })
216
+ // @ts-expect-error Duplicate tool name: search
217
+ Toolkit.make(a, b)
218
+ })
219
+
220
+ it("fails compose with DuplicateToolName naming the colliding sources (dynamic)", async () => {
221
+ const github = Toolkit.fromArray([tool("search")])
222
+ const linear = Toolkit.fromArray([tool("search")])
223
+ const exit = await Effect.runPromiseExit(Toolkit.compose(github, linear))
224
+ expect(exit._tag).toBe("Failure")
225
+ if (exit._tag === "Failure") {
226
+ const json = JSON.stringify(exit.cause)
227
+ expect(json).toContain("DuplicateToolName")
228
+ expect(json).toContain("toolkit-0")
229
+ expect(json).toContain("toolkit-1")
230
+ }
231
+ })
232
+
233
+ it("namespaces names so generic names from two sources compose cleanly", async () => {
234
+ const github = Toolkit.namespace("github", Toolkit.fromArray([tool("search")]))
235
+ const linear = Toolkit.namespace("linear", Toolkit.fromArray([tool("search")]))
236
+ const composed = await Effect.runPromise(Toolkit.compose(github, linear))
237
+ expect(Object.keys(composed).sort()).toEqual(["github__search", "linear__search"])
238
+ })
239
+
240
+ it("preserves each tool's R through compose", () => {
241
+ const stream = Effect.gen(function* () {
242
+ const composed = yield* Toolkit.compose(Toolkit.make(tool("a")), Toolkit.make(tool("b")))
243
+ return Toolkit.run(composed, [])
244
+ })
245
+ // No service-needing tools here -> R stays never (compose didn't poison it).
246
+ expectTypeOf(stream).toEqualTypeOf<
247
+ Effect.Effect<
248
+ Stream.Stream<import("./ToolEvent.js").ToolEvent, never, never>,
249
+ Toolkit.DuplicateToolName
250
+ >
251
+ >()
252
+ })
253
+ })
254
+
255
+ describe("Toolkit.wrap - middleware", () => {
256
+ const Empty = Schema.Struct({})
257
+ type AuditShape = { readonly record: (name: string) => Effect.Effect<void> }
258
+ class Audit extends Context.Service<Audit, AuditShape>()("test/Audit") {}
259
+
260
+ const getWeather = Tool.make({
261
+ name: "get_weather",
262
+ description: "",
263
+ inputSchema: Tool.fromEffectSchema(Empty),
264
+ run: () => Effect.succeed({ tempC: 18 }),
265
+ })
266
+ const escalate = Tool.signal({
267
+ name: "escalate",
268
+ description: "",
269
+ inputSchema: Tool.fromEffectSchema(Empty),
270
+ })
271
+
272
+ // A middleware that needs the Audit service -> R2 = Audit.
273
+ const audited: Toolkit.Middleware<Audit> = (run, name) => (input, emit) =>
274
+ Effect.gen(function* () {
275
+ const a = yield* Audit
276
+ yield* a.record(name)
277
+ return yield* run(input, emit)
278
+ })
279
+
280
+ const call = (name: string, id: string): ToolCall => ({
281
+ type: "function_call",
282
+ call_id: id,
283
+ name,
284
+ arguments: "{}",
285
+ })
286
+
287
+ it("unions the middleware's R2 into the toolkit's requirements", () => {
288
+ const wrapped = Toolkit.wrap(Toolkit.make(getWeather), audited)
289
+ const stream = Toolkit.run(wrapped, [])
290
+ expectTypeOf(stream).toEqualTypeOf<
291
+ Stream.Stream<import("./ToolEvent.js").ToolEvent, never, Audit>
292
+ >()
293
+ })
294
+
295
+ it("runs the wrapped handler and leaves non-local kinds untouched", async () => {
296
+ const recorded: Array<string> = []
297
+ const wrapped = pipe(Toolkit.make(getWeather, escalate), Toolkit.wrap(audited))
298
+
299
+ const events = await Effect.runPromise(
300
+ Toolkit.run(wrapped, [call("get_weather", "w1"), call("escalate", "s1")]).pipe(
301
+ Stream.runCollect,
302
+ Effect.provideService(Audit, { record: (n) => Effect.sync(() => void recorded.push(n)) }),
303
+ ),
304
+ )
305
+ const byCall = new Map(
306
+ Array.from(events)
307
+ .filter(isOutput)
308
+ .map((e) => [e.result.call_id, e.result] as const),
309
+ )
310
+
311
+ // Local tool ran through the middleware (audit fired).
312
+ expect(recorded).toEqual(["get_weather"])
313
+ const w = byCall.get("w1")
314
+ expect(w !== undefined && isOk(w) && w.value).toEqual({ tempC: 18 })
315
+ // Signal kind is left alone by wrap -> still reported non-local.
316
+ const s = byCall.get("s1")
317
+ expect(s?._tag === "Failure" && s.kind).toBe("non_local_tool")
318
+ })
319
+ })
320
+
321
+ describe("Toolkit.run - executor outcomes", () => {
322
+ const plain = <N extends string>(name: N, out: unknown) =>
323
+ Tool.make({
324
+ name,
325
+ description: "",
326
+ inputSchema: Tool.fromEffectSchema(Empty),
327
+ run: () => Effect.succeed(out),
328
+ })
329
+
330
+ it("absorbs a bare string failure as a tool_failed result", async () => {
331
+ const t = Tool.make({
332
+ name: "t",
333
+ description: "",
334
+ inputSchema: Tool.fromEffectSchema(Empty),
335
+ run: () => Effect.fail("nope"),
336
+ })
337
+ const events = await Effect.runPromise(runToArray(Toolkit.make(t), [toolCall("t", "1")]))
338
+ const result = events.filter(isOutput)[0]?.result
339
+ expect(result).toMatchObject({ _tag: "Failure", kind: "tool_failed", reason: "nope" })
340
+ })
341
+
342
+ it("absorbs Tool.fail with a custom kind", async () => {
343
+ const t = Tool.make({
344
+ name: "t",
345
+ description: "",
346
+ inputSchema: Tool.fromEffectSchema(Empty),
347
+ run: () => Tool.fail("missing", { kind: "not_found" }),
348
+ })
349
+ const events = await Effect.runPromise(runToArray(Toolkit.make(t), [toolCall("t", "1")]))
350
+ const result = events.filter(isOutput)[0]?.result
351
+ expect(result).toMatchObject({ _tag: "Failure", kind: "not_found", reason: "missing" })
352
+ })
353
+
354
+ it("propagates a typed non-sentinel failure on the stream", async () => {
355
+ const t = Tool.make({
356
+ name: "t",
357
+ description: "",
358
+ inputSchema: Tool.fromEffectSchema(Empty),
359
+ run: () => Effect.fail(new AiError.RateLimited({ provider: "x", raw: null })),
360
+ })
361
+ const exit = await Effect.runPromiseExit(runToArray(Toolkit.make(t), [toolCall("t", "1")]))
362
+ expect(exit._tag).toBe("Failure")
363
+ })
364
+
365
+ it("dies on a defect instead of masking it as a result", async () => {
366
+ const t = Tool.make({
367
+ name: "t",
368
+ description: "",
369
+ inputSchema: Tool.fromEffectSchema(Empty),
370
+ run: () => Effect.die(new Error("boom")),
371
+ })
372
+ const exit = await Effect.runPromiseExit(runToArray(Toolkit.make(t), [toolCall("t", "1")]))
373
+ expect(exit._tag).toBe("Failure")
374
+ })
375
+
376
+ it("streams progress events strictly before the terminal output", async () => {
377
+ const t = Tool.make({
378
+ name: "t",
379
+ description: "",
380
+ inputSchema: Tool.fromEffectSchema(Empty),
381
+ run: (_input, emit) =>
382
+ emit("a").pipe(Effect.andThen(emit("b")), Effect.andThen(Effect.succeed("done"))),
383
+ })
384
+ const events = await Effect.runPromise(runToArray(Toolkit.make(t), [toolCall("t", "1")]))
385
+ expect(events.map((e) => e._tag)).toEqual(["Progress", "Progress", "Output"])
386
+ expect(events.filter(isProgress).map((p) => p.data)).toEqual(["a", "b"])
387
+ })
388
+
389
+ it("reports input validation failures as a result carrying the issue detail", async () => {
390
+ const t = Tool.make({
391
+ name: "t",
392
+ description: "",
393
+ inputSchema: Tool.fromEffectSchema(Schema.Struct({ x: Schema.Number })),
394
+ run: () => Effect.succeed(1),
395
+ })
396
+ const events = await Effect.runPromise(
397
+ runToArray(Toolkit.make(t), [toolCall("t", "1", '{"x":"nope"}')]),
398
+ )
399
+ const result = events.filter(isOutput)[0]?.result
400
+ expect(result).toMatchObject({ _tag: "Failure", kind: "input_validation_error" })
401
+ expect(result !== undefined && isFailure(result) && result.reason).toBeTruthy()
402
+ })
403
+
404
+ it("normalizes empty arguments to {} for a zero-arg tool", async () => {
405
+ const events = await Effect.runPromise(
406
+ runToArray(Toolkit.make(plain("t", "ok")), [toolCall("t", "1", "")]),
407
+ )
408
+ const result = events.filter(isOutput)[0]?.result
409
+ expect(result).toMatchObject({ _tag: "Ok", value: "ok" })
410
+ })
411
+
412
+ it("reports unparseable arguments as input_validation_error", async () => {
413
+ const events = await Effect.runPromise(
414
+ runToArray(Toolkit.make(plain("t", "ok")), [toolCall("t", "1", "not json")]),
415
+ )
416
+ const result = events.filter(isOutput)[0]?.result
417
+ expect(result).toMatchObject({ _tag: "Failure", kind: "input_validation_error" })
418
+ })
419
+
420
+ it("a call named constructor is unknown_tool, not non_local_tool", async () => {
421
+ const events = await Effect.runPromise(
422
+ runToArray(Toolkit.make(plain("t", "ok")), [toolCall("constructor", "1")]),
423
+ )
424
+ const result = events.filter(isOutput)[0]?.result
425
+ expect(result).toMatchObject({ _tag: "Failure", kind: "unknown_tool" })
426
+ })
427
+
428
+ it('serializes an undefined output as "null" on the wire', async () => {
429
+ const t = Tool.make({
430
+ name: "t",
431
+ description: "",
432
+ inputSchema: Tool.fromEffectSchema(Empty),
433
+ run: () => Effect.void,
434
+ })
435
+ const events = await Effect.runPromise(runToArray(Toolkit.make(t), [toolCall("t", "1")]))
436
+ const result = events.filter(isOutput)[0]?.result
437
+ expect(result !== undefined && toToolCallOutput(result).output).toBe("null")
438
+ })
439
+
440
+ it("preserves call order with concurrency 1", async () => {
441
+ const kit = Toolkit.make(plain("a", "A"), plain("b", "B"))
442
+ const events = await Effect.runPromise(
443
+ runToArray(kit, [toolCall("a", "a1"), toolCall("b", "b1")], { concurrency: 1 }),
444
+ )
445
+ expect(events.filter(isOutput).map((e) => e.result.call_id)).toEqual(["a1", "b1"])
446
+ })
447
+
448
+ it("type: run of a describeFailures toolkit has no stream error", () => {
449
+ const t = Tool.make({
450
+ name: "t",
451
+ description: "",
452
+ inputSchema: Tool.fromEffectSchema(Empty),
453
+ run: () => Effect.fail(new AiError.RateLimited({ provider: "x", raw: null })),
454
+ })
455
+ const kit = Toolkit.describeFailures(Toolkit.make(t), AiError.describe)
456
+ const stream = Toolkit.run(kit, [])
457
+ expectTypeOf(stream).toEqualTypeOf<Stream.Stream<ToolEvent, never, never>>()
458
+ })
459
+
460
+ it("type: run of a typed toolkit propagates the tool's E", () => {
461
+ const t = Tool.make({
462
+ name: "t",
463
+ description: "",
464
+ inputSchema: Tool.fromEffectSchema(Empty),
465
+ run: () => Effect.fail(new AiError.RateLimited({ provider: "x", raw: null })),
466
+ })
467
+ const stream = Toolkit.run(Toolkit.make(t), [])
468
+ expectTypeOf(stream).toEqualTypeOf<Stream.Stream<ToolEvent, AiError.RateLimited, never>>()
469
+ })
470
+ })