@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.
- package/dist/{AiError-CAX_48RU.d.mts → AiError-Bf1pq4BB.d.mts} +9 -4
- package/dist/{AiError-CAX_48RU.d.mts.map → AiError-Bf1pq4BB.d.mts.map} +1 -1
- package/dist/{Audio-DPlTsNdz.d.mts → Audio-CR7CSljJ.d.mts} +1 -1
- package/dist/{Audio-DPlTsNdz.d.mts.map → Audio-CR7CSljJ.d.mts.map} +1 -1
- package/dist/{Image-HNmMpMTh.d.mts → Image-BZmKfIdq.d.mts} +1 -1
- package/dist/{Image-HNmMpMTh.d.mts.map → Image-BZmKfIdq.d.mts.map} +1 -1
- package/dist/{Items-7ljaVxH_.d.mts → Items-BnIduEV2.d.mts} +7 -7
- package/dist/{Items-7ljaVxH_.d.mts.map → Items-BnIduEV2.d.mts.map} +1 -1
- package/dist/Loop-D8nWpowc.d.mts +173 -0
- package/dist/Loop-D8nWpowc.d.mts.map +1 -0
- package/dist/{SandboxError-BvKypkJm.d.mts → SandboxError-CIrVDOMV.d.mts} +2 -2
- package/dist/{SandboxError-BvKypkJm.d.mts.map → SandboxError-CIrVDOMV.d.mts.map} +1 -1
- package/dist/{StructuredFormat-BbN4dosH.d.mts → StructuredFormat-DHaUJBdd.d.mts} +2 -2
- package/dist/{StructuredFormat-BbN4dosH.d.mts.map → StructuredFormat-DHaUJBdd.d.mts.map} +1 -1
- package/dist/Tool-2lC_XMth.d.mts +284 -0
- package/dist/Tool-2lC_XMth.d.mts.map +1 -0
- package/dist/ToolEvent-cWZ2BnbS.d.mts +151 -0
- package/dist/ToolEvent-cWZ2BnbS.d.mts.map +1 -0
- package/dist/ToolResult-CL4hK_9z.d.mts +157 -0
- package/dist/ToolResult-CL4hK_9z.d.mts.map +1 -0
- package/dist/Toolkit-Dr9l_hNy.d.mts +196 -0
- package/dist/Toolkit-Dr9l_hNy.d.mts.map +1 -0
- package/dist/browser/Browser.d.mts +190 -0
- package/dist/browser/Browser.d.mts.map +1 -0
- package/dist/browser/Browser.mjs +34 -0
- package/dist/browser/Browser.mjs.map +1 -0
- package/dist/browser/BrowserError.d.mts +141 -0
- package/dist/browser/BrowserError.d.mts.map +1 -0
- package/dist/browser/BrowserError.mjs +89 -0
- package/dist/browser/BrowserError.mjs.map +1 -0
- package/dist/browser/BrowserTool.d.mts +58 -0
- package/dist/browser/BrowserTool.d.mts.map +1 -0
- package/dist/browser/BrowserTool.mjs +86 -0
- package/dist/browser/BrowserTool.mjs.map +1 -0
- package/dist/{dist-DV5ISja1.mjs → dist-B04dlFHg.mjs} +3 -3
- package/dist/{dist-DV5ISja1.mjs.map → dist-B04dlFHg.mjs.map} +1 -1
- package/dist/dist-DWgRbDN3.mjs +140 -0
- package/dist/dist-DWgRbDN3.mjs.map +1 -0
- package/dist/domain/AiError.d.mts +2 -2
- package/dist/domain/AiError.mjs +22 -3
- package/dist/domain/AiError.mjs.map +1 -1
- package/dist/domain/Audio.d.mts +1 -1
- package/dist/domain/Image.d.mts +1 -1
- package/dist/domain/Items.d.mts +1 -1
- package/dist/domain/Music.d.mts +1 -1
- package/dist/domain/Turn.d.mts +4 -4
- package/dist/domain/Turn.test.mjs +1 -1
- package/dist/embedding-model/Embedding.d.mts +1 -1
- package/dist/embedding-model/EmbeddingModel.d.mts +1 -1
- package/dist/embedding-model/EmbeddingModel.test.mjs +1 -1
- package/dist/embedding-model/EmbeddingModel.test.mjs.map +1 -1
- package/dist/index.d.mts +19 -13
- package/dist/index.mjs +15 -9
- package/dist/language-model/LanguageModel.d.mts +10 -5
- package/dist/language-model/LanguageModel.d.mts.map +1 -1
- package/dist/language-model/LanguageModel.mjs.map +1 -1
- package/dist/language-model/LanguageModel.test.mjs +1 -1
- package/dist/loop/Loop.d.mts +2 -141
- package/dist/loop/Loop.mjs +16 -20
- package/dist/loop/Loop.mjs.map +1 -1
- package/dist/loop/Loop.test.mjs +18 -1
- package/dist/loop/Loop.test.mjs.map +1 -1
- package/dist/{magic-string.es-BgIV5Mu3.mjs → magic-string.es-Dk-MMA0F.mjs} +1 -1
- package/dist/{magic-string.es-BgIV5Mu3.mjs.map → magic-string.es-Dk-MMA0F.mjs.map} +1 -1
- package/dist/music-generator/MusicGenerator.d.mts +2 -2
- package/dist/music-generator/MusicGenerator.test.mjs +1 -1
- package/dist/observability/Metrics.d.mts +116 -28
- package/dist/observability/Metrics.d.mts.map +1 -1
- package/dist/observability/Metrics.mjs +250 -38
- package/dist/observability/Metrics.mjs.map +1 -1
- package/dist/observability/Metrics.test.d.mts +1 -0
- package/dist/observability/Metrics.test.mjs +187 -0
- package/dist/observability/Metrics.test.mjs.map +1 -0
- package/dist/observability/Telemetry.d.mts +37 -0
- package/dist/observability/Telemetry.d.mts.map +1 -0
- package/dist/observability/Telemetry.mjs +57 -0
- package/dist/observability/Telemetry.mjs.map +1 -0
- package/dist/observability/Telemetry.test.d.mts +1 -0
- package/dist/observability/Telemetry.test.mjs +92 -0
- package/dist/observability/Telemetry.test.mjs.map +1 -0
- package/dist/retry/Retry.d.mts +2 -2
- package/dist/retry/Retry.test.mjs +1 -1
- package/dist/sandbox/Sandbox.d.mts +1 -1
- package/dist/sandbox/SandboxError.d.mts +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +2 -2
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +1 -1
- package/dist/streaming/JSONL.d.mts +1 -1
- package/dist/streaming/JSONL.test.mjs +1 -1
- package/dist/streaming/SSE.test.mjs +1 -1
- package/dist/structured-format/StructuredFormat.d.mts +1 -1
- package/dist/structured-format/StructuredFormat.mjs.map +1 -1
- package/dist/structured-format/StructuredFormat.test.mjs +1 -1
- package/dist/testing/MockMusicGenerator.d.mts +1 -1
- package/dist/testing/MockProvider.d.mts +1 -1
- package/dist/testing/MockSpeechSynthesizer.d.mts +1 -1
- package/dist/tool/Approval.d.mts +19 -7
- package/dist/tool/Approval.d.mts.map +1 -1
- package/dist/tool/Approval.mjs +18 -11
- package/dist/tool/Approval.mjs.map +1 -1
- package/dist/tool/Approval.test.mjs +80 -40
- package/dist/tool/Approval.test.mjs.map +1 -1
- package/dist/tool/HistoryCheck.d.mts +2 -2
- package/dist/tool/Tool.d.mts +2 -2
- package/dist/tool/Tool.mjs +190 -36
- package/dist/tool/Tool.mjs.map +1 -1
- package/dist/tool/Tool.test.mjs +41 -3
- package/dist/tool/Tool.test.mjs.map +1 -1
- package/dist/tool/ToolEvent.d.mts +2 -151
- package/dist/tool/ToolEvent.mjs +5 -0
- package/dist/tool/ToolEvent.mjs.map +1 -1
- package/dist/tool/ToolResult.d.mts +2 -138
- package/dist/tool/ToolResult.mjs +32 -8
- package/dist/tool/ToolResult.mjs.map +1 -1
- package/dist/tool/Toolkit.d.mts +2 -73
- package/dist/tool/Toolkit.mjs +153 -35
- package/dist/tool/Toolkit.mjs.map +1 -1
- package/dist/tool/Toolkit.test.mjs +271 -11
- package/dist/tool/Toolkit.test.mjs.map +1 -1
- package/dist/transcriber/Transcriber.d.mts +2 -2
- package/dist/transcriber/Transcriber.test.mjs +1 -1
- package/dist/web-read/WebRead.d.mts +44 -0
- package/dist/web-read/WebRead.d.mts.map +1 -0
- package/dist/web-read/WebRead.mjs +18 -0
- package/dist/web-read/WebRead.mjs.map +1 -0
- package/dist/web-read/WebReadTool.d.mts +57 -0
- package/dist/web-read/WebReadTool.d.mts.map +1 -0
- package/dist/web-read/WebReadTool.mjs +59 -0
- package/dist/web-read/WebReadTool.mjs.map +1 -0
- package/dist/web-search/WebSearch.d.mts +1 -1
- package/dist/web-search/WebSearchTool.d.mts +12 -6
- package/dist/web-search/WebSearchTool.d.mts.map +1 -1
- package/dist/web-search/WebSearchTool.mjs +10 -5
- package/dist/web-search/WebSearchTool.mjs.map +1 -1
- package/package.json +21 -1
- package/src/browser/Browser.ts +268 -0
- package/src/browser/BrowserError.ts +145 -0
- package/src/browser/BrowserTool.ts +161 -0
- package/src/domain/AiError.ts +27 -1
- package/src/embedding-model/EmbeddingModel.test.ts +4 -0
- package/src/index.ts +6 -0
- package/src/language-model/LanguageModel.ts +7 -2
- package/src/loop/Loop.test.ts +22 -0
- package/src/loop/Loop.ts +27 -4
- package/src/observability/Metrics.test.ts +266 -0
- package/src/observability/Metrics.ts +505 -65
- package/src/observability/Telemetry.test.ts +123 -0
- package/src/observability/Telemetry.ts +96 -0
- package/src/structured-format/StructuredFormat.ts +4 -0
- package/src/tool/Approval.test.ts +77 -26
- package/src/tool/Approval.ts +72 -27
- package/src/tool/Tool.test.ts +55 -2
- package/src/tool/Tool.ts +335 -70
- package/src/tool/ToolEvent.ts +5 -0
- package/src/tool/ToolResult.ts +36 -9
- package/src/tool/Toolkit.test.ts +339 -9
- package/src/tool/Toolkit.ts +470 -120
- package/src/web-read/WebRead.ts +70 -0
- package/src/web-read/WebReadTool.ts +96 -0
- package/src/web-search/WebSearchTool.ts +11 -5
- package/dist/Tool-BKiieFXk.d.mts +0 -102
- package/dist/Tool-BKiieFXk.d.mts.map +0 -1
- package/dist/loop/Loop.d.mts.map +0 -1
- package/dist/tool/ToolEvent.d.mts.map +0 -1
- package/dist/tool/ToolResult.d.mts.map +0 -1
- package/dist/tool/Toolkit.d.mts.map +0 -1
package/src/tool/Toolkit.test.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
109
|
-
|
|
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(
|
|
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
|
+
})
|