@effect-uai/core 0.9.0 → 0.11.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-CrdR1EQi.d.mts → AiError-Bf1pq4BB.d.mts} +8 -3
- package/dist/{AiError-CrdR1EQi.d.mts.map → AiError-Bf1pq4BB.d.mts.map} +1 -1
- package/dist/{Audio-DDSKCkSe.d.mts → Audio-CR7CSljJ.d.mts} +2 -2
- package/dist/{Audio-DDSKCkSe.d.mts.map → Audio-CR7CSljJ.d.mts.map} +1 -1
- package/dist/{Image-C_Zjpwhc.d.mts → Image-C4vuBmu0.d.mts} +12 -12
- package/dist/{Image-C_Zjpwhc.d.mts.map → Image-C4vuBmu0.d.mts.map} +1 -1
- package/dist/{Items-CgKD6Npw.d.mts → Items-BWNu6VGp.d.mts} +118 -92
- package/dist/Items-BWNu6VGp.d.mts.map +1 -0
- package/dist/{Loop-B5_cqumi.d.mts → Loop-D8nWpowc.d.mts} +2 -2
- package/dist/{Loop-B5_cqumi.d.mts.map → Loop-D8nWpowc.d.mts.map} +1 -1
- package/dist/{Media-DOLyaygI.d.mts → Media-D_CpcM1Z.d.mts} +1 -1
- package/dist/{Media-DOLyaygI.d.mts.map → Media-D_CpcM1Z.d.mts.map} +1 -1
- package/dist/{StructuredFormat-CMJFD3sn.d.mts → StructuredFormat-DHaUJBdd.d.mts} +1 -1
- package/dist/{StructuredFormat-CMJFD3sn.d.mts.map → StructuredFormat-DHaUJBdd.d.mts.map} +1 -1
- package/dist/{Tool-0XUIT2A2.d.mts → Tool-CXFHCQrT.d.mts} +101 -16
- package/dist/Tool-CXFHCQrT.d.mts.map +1 -0
- package/dist/{ToolEvent-DYrAUJGs.d.mts → ToolEvent-nVijCrvm.d.mts} +2 -2
- package/dist/ToolEvent-nVijCrvm.d.mts.map +1 -0
- package/dist/{ToolResult-BZd-cEWW.d.mts → ToolResult-CQqKkcDa.d.mts} +10 -4
- package/dist/ToolResult-CQqKkcDa.d.mts.map +1 -0
- package/dist/{Toolkit-BKC8b8Xo.d.mts → Toolkit-BYPwwu-j.d.mts} +65 -24
- package/dist/Toolkit-BYPwwu-j.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 +59 -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-DWgRbDN3.mjs → dist-C0m0QnUP.mjs} +71 -30
- package/dist/dist-C0m0QnUP.mjs.map +1 -0
- package/dist/{dist-B04dlFHg.mjs → dist-CcRV3-A5.mjs} +8 -8
- package/dist/{dist-B04dlFHg.mjs.map → dist-CcRV3-A5.mjs.map} +1 -1
- 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/Citation.d.mts +60 -0
- package/dist/domain/Citation.d.mts.map +1 -0
- package/dist/domain/Citation.mjs +7 -0
- package/dist/domain/Citation.mjs.map +1 -0
- package/dist/domain/Image.d.mts +1 -1
- package/dist/domain/Items.d.mts +1 -1
- package/dist/domain/Items.mjs +13 -3
- package/dist/domain/Items.mjs.map +1 -1
- package/dist/domain/Media.d.mts +1 -1
- package/dist/domain/Music.d.mts +1 -1
- package/dist/domain/Research.d.mts +37 -0
- package/dist/domain/Research.d.mts.map +1 -0
- package/dist/domain/Research.mjs +7 -0
- package/dist/domain/Research.mjs.map +1 -0
- package/dist/domain/Turn.d.mts +92 -10
- package/dist/domain/Turn.d.mts.map +1 -1
- package/dist/domain/Turn.mjs +8 -1
- package/dist/domain/Turn.mjs.map +1 -1
- 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.mjs +2 -2
- package/dist/embedding-model/EmbeddingModel.mjs.map +1 -1
- package/dist/embedding-model/EmbeddingModel.test.mjs +1 -1
- package/dist/index.d.mts +22 -13
- package/dist/index.mjs +18 -9
- package/dist/job/Job.d.mts +80 -0
- package/dist/job/Job.d.mts.map +1 -0
- package/dist/job/Job.mjs +49 -0
- package/dist/job/Job.mjs.map +1 -0
- package/dist/language-model/LanguageModel.d.mts +4 -4
- package/dist/language-model/LanguageModel.mjs +2 -2
- package/dist/language-model/LanguageModel.mjs.map +1 -1
- package/dist/language-model/LanguageModel.test.mjs +1 -1
- package/dist/loop/Loop.d.mts +1 -1
- package/dist/loop/Loop.test.mjs +1 -1
- package/dist/music-generator/MusicGenerator.d.mts +2 -2
- package/dist/music-generator/MusicGenerator.mjs +4 -4
- package/dist/music-generator/MusicGenerator.mjs.map +1 -1
- package/dist/music-generator/MusicGenerator.test.mjs +1 -1
- package/dist/observability/Metrics.d.mts +1 -1
- package/dist/observability/Metrics.test.mjs +2 -2
- package/dist/observability/Telemetry.test.mjs +2 -2
- package/dist/research/DeepResearch.d.mts +71 -0
- package/dist/research/DeepResearch.d.mts.map +1 -0
- package/dist/research/DeepResearch.mjs +61 -0
- package/dist/research/DeepResearch.mjs.map +1 -0
- package/dist/retry/Retry.d.mts +1 -1
- package/dist/retry/Retry.test.mjs +1 -1
- package/dist/sandbox/Sandbox.mjs +18 -18
- package/dist/sandbox/Sandbox.mjs.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +2 -2
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs +8 -8
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +1 -1
- package/dist/streaming/JSONL.d.mts +3 -1
- package/dist/streaming/JSONL.d.mts.map +1 -1
- package/dist/streaming/JSONL.mjs +5 -11
- package/dist/streaming/JSONL.mjs.map +1 -1
- package/dist/streaming/JSONL.test.mjs +10 -1
- package/dist/streaming/JSONL.test.mjs.map +1 -1
- package/dist/streaming/SSE.d.mts +5 -3
- package/dist/streaming/SSE.d.mts.map +1 -1
- package/dist/streaming/SSE.mjs +28 -38
- package/dist/streaming/SSE.mjs.map +1 -1
- package/dist/streaming/SSE.test.mjs +54 -4
- package/dist/streaming/SSE.test.mjs.map +1 -1
- package/dist/structured-format/StructuredFormat.d.mts +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 +47 -8
- 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 +112 -28
- package/dist/tool/Tool.mjs.map +1 -1
- package/dist/tool/Tool.test.mjs +127 -3
- package/dist/tool/Tool.test.mjs.map +1 -1
- package/dist/tool/ToolEvent.d.mts +1 -1
- package/dist/tool/ToolEvent.mjs +5 -0
- package/dist/tool/ToolEvent.mjs.map +1 -1
- package/dist/tool/ToolResult.d.mts +2 -2
- package/dist/tool/ToolResult.mjs +15 -6
- package/dist/tool/ToolResult.mjs.map +1 -1
- package/dist/tool/Toolkit.d.mts +2 -2
- package/dist/tool/Toolkit.mjs +62 -22
- package/dist/tool/Toolkit.mjs.map +1 -1
- package/dist/tool/Toolkit.test.mjs +207 -9
- package/dist/tool/Toolkit.test.mjs.map +1 -1
- package/dist/transcriber/Transcriber.d.mts +2 -2
- package/dist/transcriber/Transcriber.mjs +3 -3
- package/dist/transcriber/Transcriber.mjs.map +1 -1
- 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/WebSearch.mjs +1 -1
- package/dist/web-search/WebSearch.mjs.map +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 +39 -3
- 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/domain/Citation.ts +64 -0
- package/src/domain/Items.ts +14 -4
- package/src/domain/Research.ts +42 -0
- package/src/domain/Turn.ts +26 -0
- package/src/embedding-model/EmbeddingModel.ts +2 -2
- package/src/index.ts +9 -0
- package/src/job/Job.ts +125 -0
- package/src/language-model/LanguageModel.ts +2 -2
- package/src/music-generator/MusicGenerator.ts +4 -4
- package/src/research/DeepResearch.ts +137 -0
- package/src/sandbox/Sandbox.ts +18 -18
- package/src/speech-synthesizer/SpeechSynthesizer.ts +8 -8
- package/src/streaming/JSONL.test.ts +19 -0
- package/src/streaming/JSONL.ts +6 -38
- package/src/streaming/SSE.test.ts +86 -3
- package/src/streaming/SSE.ts +36 -80
- package/src/tool/Approval.test.ts +55 -5
- package/src/tool/Approval.ts +72 -27
- package/src/tool/Tool.test.ts +151 -1
- package/src/tool/Tool.ts +263 -66
- package/src/tool/ToolEvent.ts +5 -0
- package/src/tool/ToolResult.ts +14 -8
- package/src/tool/Toolkit.test.ts +231 -2
- package/src/tool/Toolkit.ts +175 -62
- package/src/transcriber/Transcriber.ts +3 -3
- package/src/web-read/WebRead.ts +70 -0
- package/src/web-read/WebReadTool.ts +96 -0
- package/src/web-search/WebSearch.ts +1 -1
- package/src/web-search/WebSearchTool.ts +11 -5
- package/dist/Items-CgKD6Npw.d.mts.map +0 -1
- package/dist/Tool-0XUIT2A2.d.mts.map +0 -1
- package/dist/ToolEvent-DYrAUJGs.d.mts.map +0 -1
- package/dist/ToolResult-BZd-cEWW.d.mts.map +0 -1
- package/dist/Toolkit-BKC8b8Xo.d.mts.map +0 -1
- package/dist/dist-DWgRbDN3.mjs.map +0 -1
package/src/tool/Toolkit.test.ts
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
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
|
|
|
@@ -234,6 +252,66 @@ describe("Toolkit.make / compose / namespace - uniqueness", () => {
|
|
|
234
252
|
})
|
|
235
253
|
})
|
|
236
254
|
|
|
255
|
+
describe("Toolkit.namespace - typed toolkits", () => {
|
|
256
|
+
const Query = Schema.Struct({ q: Schema.String })
|
|
257
|
+
type GeoShape = { readonly lookup: (q: string) => Effect.Effect<string> }
|
|
258
|
+
class Geo extends Context.Service<Geo, GeoShape>()("test/Geo") {}
|
|
259
|
+
|
|
260
|
+
const search = Tool.make({
|
|
261
|
+
name: "search",
|
|
262
|
+
description: "",
|
|
263
|
+
inputSchema: Tool.fromEffectSchema(Query),
|
|
264
|
+
run: ({ q }) =>
|
|
265
|
+
Effect.gen(function* () {
|
|
266
|
+
const geo = yield* Geo
|
|
267
|
+
if (q === "") return yield* Effect.fail("empty_query" as const)
|
|
268
|
+
return yield* geo.lookup(q)
|
|
269
|
+
}),
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
const kit = Toolkit.namespace("geo", Toolkit.make(search))
|
|
273
|
+
|
|
274
|
+
it("type: prefixes the name literal of a service-requiring tool", () => {
|
|
275
|
+
expectTypeOf(kit.geo__search.name).toEqualTypeOf<"geo__search">()
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it("type: preserves E and R through namespace", () => {
|
|
279
|
+
expectTypeOf<Toolkit.ToolkitE<typeof kit>>().toEqualTypeOf<"empty_query">()
|
|
280
|
+
expectTypeOf<Toolkit.ToolkitR<typeof kit>>().toEqualTypeOf<Geo>()
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it("type: preserves E and R through makeNamespaced", () => {
|
|
284
|
+
const namespaced = Toolkit.makeNamespaced("geo", search)
|
|
285
|
+
expectTypeOf(namespaced.geo__search.name).toEqualTypeOf<"geo__search">()
|
|
286
|
+
expectTypeOf<Toolkit.ToolkitE<typeof namespaced>>().toEqualTypeOf<"empty_query">()
|
|
287
|
+
expectTypeOf<Toolkit.ToolkitR<typeof namespaced>>().toEqualTypeOf<Geo>()
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it("type: Toolkit.run surfaces the namespaced tool's R (string E is absorbed)", () => {
|
|
291
|
+
const stream = Toolkit.run(kit, [])
|
|
292
|
+
expectTypeOf(stream).toEqualTypeOf<
|
|
293
|
+
Stream.Stream<import("./ToolEvent.js").ToolEvent, never, Geo>
|
|
294
|
+
>()
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it("runtime: renames the tool and executes it under the prefixed name", async () => {
|
|
298
|
+
const call: ToolCall = {
|
|
299
|
+
type: "function_call",
|
|
300
|
+
call_id: "call_1",
|
|
301
|
+
name: "geo__search",
|
|
302
|
+
arguments: '{"q":"lisbon"}',
|
|
303
|
+
}
|
|
304
|
+
const events = await Effect.runPromise(
|
|
305
|
+
Stream.runCollect(Toolkit.run(kit, [call])).pipe(
|
|
306
|
+
Effect.provide(Layer.succeed(Geo, { lookup: (q) => Effect.succeed(`geo:${q}`) })),
|
|
307
|
+
),
|
|
308
|
+
)
|
|
309
|
+
const outputs = events.filter(isOutput).map((e) => e.result)
|
|
310
|
+
expect(outputs).toHaveLength(1)
|
|
311
|
+
expect(outputs[0]).toMatchObject({ _tag: "Ok", tool: "geo__search", value: "geo:lisbon" })
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
|
|
237
315
|
describe("Toolkit.wrap - middleware", () => {
|
|
238
316
|
const Empty = Schema.Struct({})
|
|
239
317
|
type AuditShape = { readonly record: (name: string) => Effect.Effect<void> }
|
|
@@ -299,3 +377,154 @@ describe("Toolkit.wrap - middleware", () => {
|
|
|
299
377
|
expect(s?._tag === "Failure" && s.kind).toBe("non_local_tool")
|
|
300
378
|
})
|
|
301
379
|
})
|
|
380
|
+
|
|
381
|
+
describe("Toolkit.run - executor outcomes", () => {
|
|
382
|
+
const plain = <N extends string>(name: N, out: unknown) =>
|
|
383
|
+
Tool.make({
|
|
384
|
+
name,
|
|
385
|
+
description: "",
|
|
386
|
+
inputSchema: Tool.fromEffectSchema(Empty),
|
|
387
|
+
run: () => Effect.succeed(out),
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
it("absorbs a bare string failure as a tool_failed result", async () => {
|
|
391
|
+
const t = Tool.make({
|
|
392
|
+
name: "t",
|
|
393
|
+
description: "",
|
|
394
|
+
inputSchema: Tool.fromEffectSchema(Empty),
|
|
395
|
+
run: () => Effect.fail("nope"),
|
|
396
|
+
})
|
|
397
|
+
const events = await Effect.runPromise(runToArray(Toolkit.make(t), [toolCall("t", "1")]))
|
|
398
|
+
const result = events.filter(isOutput)[0]?.result
|
|
399
|
+
expect(result).toMatchObject({ _tag: "Failure", kind: "tool_failed", reason: "nope" })
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it("absorbs Tool.fail with a custom kind", async () => {
|
|
403
|
+
const t = Tool.make({
|
|
404
|
+
name: "t",
|
|
405
|
+
description: "",
|
|
406
|
+
inputSchema: Tool.fromEffectSchema(Empty),
|
|
407
|
+
run: () => Tool.fail("missing", { kind: "not_found" }),
|
|
408
|
+
})
|
|
409
|
+
const events = await Effect.runPromise(runToArray(Toolkit.make(t), [toolCall("t", "1")]))
|
|
410
|
+
const result = events.filter(isOutput)[0]?.result
|
|
411
|
+
expect(result).toMatchObject({ _tag: "Failure", kind: "not_found", reason: "missing" })
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it("propagates a typed non-sentinel failure on the stream", async () => {
|
|
415
|
+
const t = Tool.make({
|
|
416
|
+
name: "t",
|
|
417
|
+
description: "",
|
|
418
|
+
inputSchema: Tool.fromEffectSchema(Empty),
|
|
419
|
+
run: () => Effect.fail(new AiError.RateLimited({ provider: "x", raw: null })),
|
|
420
|
+
})
|
|
421
|
+
const exit = await Effect.runPromiseExit(runToArray(Toolkit.make(t), [toolCall("t", "1")]))
|
|
422
|
+
expect(exit._tag).toBe("Failure")
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it("dies on a defect instead of masking it as a result", async () => {
|
|
426
|
+
const t = Tool.make({
|
|
427
|
+
name: "t",
|
|
428
|
+
description: "",
|
|
429
|
+
inputSchema: Tool.fromEffectSchema(Empty),
|
|
430
|
+
run: () => Effect.die(new Error("boom")),
|
|
431
|
+
})
|
|
432
|
+
const exit = await Effect.runPromiseExit(runToArray(Toolkit.make(t), [toolCall("t", "1")]))
|
|
433
|
+
expect(exit._tag).toBe("Failure")
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
it("streams progress events strictly before the terminal output", async () => {
|
|
437
|
+
const t = Tool.make({
|
|
438
|
+
name: "t",
|
|
439
|
+
description: "",
|
|
440
|
+
inputSchema: Tool.fromEffectSchema(Empty),
|
|
441
|
+
run: (_input, emit) =>
|
|
442
|
+
emit("a").pipe(Effect.andThen(emit("b")), Effect.andThen(Effect.succeed("done"))),
|
|
443
|
+
})
|
|
444
|
+
const events = await Effect.runPromise(runToArray(Toolkit.make(t), [toolCall("t", "1")]))
|
|
445
|
+
expect(events.map((e) => e._tag)).toEqual(["Progress", "Progress", "Output"])
|
|
446
|
+
expect(events.filter(isProgress).map((p) => p.data)).toEqual(["a", "b"])
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
it("reports input validation failures as a result carrying the issue detail", async () => {
|
|
450
|
+
const t = Tool.make({
|
|
451
|
+
name: "t",
|
|
452
|
+
description: "",
|
|
453
|
+
inputSchema: Tool.fromEffectSchema(Schema.Struct({ x: Schema.Number })),
|
|
454
|
+
run: () => Effect.succeed(1),
|
|
455
|
+
})
|
|
456
|
+
const events = await Effect.runPromise(
|
|
457
|
+
runToArray(Toolkit.make(t), [toolCall("t", "1", '{"x":"nope"}')]),
|
|
458
|
+
)
|
|
459
|
+
const result = events.filter(isOutput)[0]?.result
|
|
460
|
+
expect(result).toMatchObject({ _tag: "Failure", kind: "input_validation_error" })
|
|
461
|
+
expect(result !== undefined && isFailure(result) && result.reason).toBeTruthy()
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
it("normalizes empty arguments to {} for a zero-arg tool", async () => {
|
|
465
|
+
const events = await Effect.runPromise(
|
|
466
|
+
runToArray(Toolkit.make(plain("t", "ok")), [toolCall("t", "1", "")]),
|
|
467
|
+
)
|
|
468
|
+
const result = events.filter(isOutput)[0]?.result
|
|
469
|
+
expect(result).toMatchObject({ _tag: "Ok", value: "ok" })
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
it("reports unparseable arguments as input_validation_error", async () => {
|
|
473
|
+
const events = await Effect.runPromise(
|
|
474
|
+
runToArray(Toolkit.make(plain("t", "ok")), [toolCall("t", "1", "not json")]),
|
|
475
|
+
)
|
|
476
|
+
const result = events.filter(isOutput)[0]?.result
|
|
477
|
+
expect(result).toMatchObject({ _tag: "Failure", kind: "input_validation_error" })
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it("a call named constructor is unknown_tool, not non_local_tool", async () => {
|
|
481
|
+
const events = await Effect.runPromise(
|
|
482
|
+
runToArray(Toolkit.make(plain("t", "ok")), [toolCall("constructor", "1")]),
|
|
483
|
+
)
|
|
484
|
+
const result = events.filter(isOutput)[0]?.result
|
|
485
|
+
expect(result).toMatchObject({ _tag: "Failure", kind: "unknown_tool" })
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
it('serializes an undefined output as "null" on the wire', async () => {
|
|
489
|
+
const t = Tool.make({
|
|
490
|
+
name: "t",
|
|
491
|
+
description: "",
|
|
492
|
+
inputSchema: Tool.fromEffectSchema(Empty),
|
|
493
|
+
run: () => Effect.void,
|
|
494
|
+
})
|
|
495
|
+
const events = await Effect.runPromise(runToArray(Toolkit.make(t), [toolCall("t", "1")]))
|
|
496
|
+
const result = events.filter(isOutput)[0]?.result
|
|
497
|
+
expect(result !== undefined && toToolCallOutput(result).output).toBe("null")
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
it("preserves call order with concurrency 1", async () => {
|
|
501
|
+
const kit = Toolkit.make(plain("a", "A"), plain("b", "B"))
|
|
502
|
+
const events = await Effect.runPromise(
|
|
503
|
+
runToArray(kit, [toolCall("a", "a1"), toolCall("b", "b1")], { concurrency: 1 }),
|
|
504
|
+
)
|
|
505
|
+
expect(events.filter(isOutput).map((e) => e.result.call_id)).toEqual(["a1", "b1"])
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
it("type: run of a describeFailures toolkit has no stream error", () => {
|
|
509
|
+
const t = Tool.make({
|
|
510
|
+
name: "t",
|
|
511
|
+
description: "",
|
|
512
|
+
inputSchema: Tool.fromEffectSchema(Empty),
|
|
513
|
+
run: () => Effect.fail(new AiError.RateLimited({ provider: "x", raw: null })),
|
|
514
|
+
})
|
|
515
|
+
const kit = Toolkit.describeFailures(Toolkit.make(t), AiError.describe)
|
|
516
|
+
const stream = Toolkit.run(kit, [])
|
|
517
|
+
expectTypeOf(stream).toEqualTypeOf<Stream.Stream<ToolEvent, never, never>>()
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
it("type: run of a typed toolkit propagates the tool's E", () => {
|
|
521
|
+
const t = Tool.make({
|
|
522
|
+
name: "t",
|
|
523
|
+
description: "",
|
|
524
|
+
inputSchema: Tool.fromEffectSchema(Empty),
|
|
525
|
+
run: () => Effect.fail(new AiError.RateLimited({ provider: "x", raw: null })),
|
|
526
|
+
})
|
|
527
|
+
const stream = Toolkit.run(Toolkit.make(t), [])
|
|
528
|
+
expectTypeOf(stream).toEqualTypeOf<Stream.Stream<ToolEvent, AiError.RateLimited, never>>()
|
|
529
|
+
})
|
|
530
|
+
})
|
package/src/tool/Toolkit.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec"
|
|
1
2
|
import {
|
|
2
3
|
Array as Arr,
|
|
3
4
|
Cause,
|
|
@@ -10,7 +11,6 @@ import {
|
|
|
10
11
|
Queue,
|
|
11
12
|
Record,
|
|
12
13
|
Ref,
|
|
13
|
-
Schema,
|
|
14
14
|
Stream,
|
|
15
15
|
} from "effect"
|
|
16
16
|
import * as Loop from "../loop/Loop.js"
|
|
@@ -19,18 +19,21 @@ import { appendToHistory, type Turn } from "../domain/Turn.js"
|
|
|
19
19
|
import {
|
|
20
20
|
type AnyLocalTool,
|
|
21
21
|
type AnyTool,
|
|
22
|
+
type DecodeResult,
|
|
23
|
+
decodeCallInput,
|
|
22
24
|
type Emit,
|
|
23
25
|
type InteractionTool,
|
|
24
26
|
type ProviderTool,
|
|
25
27
|
type SignalTool,
|
|
26
28
|
type Tool,
|
|
27
29
|
type ToolDescriptor,
|
|
30
|
+
type ToolE,
|
|
31
|
+
ToolFailed,
|
|
28
32
|
type ToolR,
|
|
29
33
|
toDescriptors,
|
|
30
34
|
} from "./Tool.js"
|
|
31
35
|
import {
|
|
32
36
|
ToolResult,
|
|
33
|
-
executionError,
|
|
34
37
|
failed,
|
|
35
38
|
nonLocalTool,
|
|
36
39
|
toToolCallOutput,
|
|
@@ -93,6 +96,15 @@ export const descriptors = (toolkit: Toolkit): ReadonlyArray<ToolDescriptor> =>
|
|
|
93
96
|
*/
|
|
94
97
|
export type ToolkitR<T extends Toolkit> = ToolMap extends T ? never : ToolR<T[keyof T]>
|
|
95
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Union of every tool's `E` error type in a toolkit. `Toolkit.run` reports
|
|
101
|
+
* `Exclude<ToolkitE<T>, string | ToolFailed>` on its stream: the sentinel
|
|
102
|
+
* failures are absorbed into results, everything else propagates typed. The
|
|
103
|
+
* same `ToolMap extends T` guard as `ToolkitR` keeps a wide toolkit at
|
|
104
|
+
* `never` rather than `any`.
|
|
105
|
+
*/
|
|
106
|
+
export type ToolkitE<T extends Toolkit> = ToolMap extends T ? never : ToolE<T[keyof T]>
|
|
107
|
+
|
|
96
108
|
// ---------------------------------------------------------------------------
|
|
97
109
|
// Uniqueness & composition. `make` rejects duplicate literal names at compile
|
|
98
110
|
// time; `compose` rejects cross-source collisions — at compile time when the
|
|
@@ -174,8 +186,8 @@ export class DuplicateToolName extends Data.TaggedError("DuplicateToolName")<{
|
|
|
174
186
|
type PrefixName<Prefix extends string, Name extends string> = `${Prefix}__${Name}`
|
|
175
187
|
|
|
176
188
|
type PrefixTool<Prefix extends string, T extends AnyTool> =
|
|
177
|
-
T extends Tool<infer N, infer I, infer Ev, infer O, infer R>
|
|
178
|
-
? Tool<PrefixName<Prefix, N>, I, Ev, O, R>
|
|
189
|
+
T extends Tool<infer N, infer I, infer Ev, infer O, infer E, infer R>
|
|
190
|
+
? Tool<PrefixName<Prefix, N>, I, Ev, O, E, R>
|
|
179
191
|
: T extends ProviderTool<infer N, infer I, infer P, infer C>
|
|
180
192
|
? ProviderTool<PrefixName<Prefix, N>, I, P, C>
|
|
181
193
|
: T extends SignalTool<infer N, infer I>
|
|
@@ -257,45 +269,99 @@ export const compose = <const Kits extends ReadonlyArray<Toolkit>>(
|
|
|
257
269
|
// widening every tool's type.
|
|
258
270
|
// ---------------------------------------------------------------------------
|
|
259
271
|
|
|
260
|
-
type Handler<Input, Event, Output, R> = (
|
|
272
|
+
type Handler<Input, Event, Output, E, R> = (
|
|
261
273
|
input: Input,
|
|
262
274
|
emit: Emit<Event>,
|
|
263
|
-
) => Effect.Effect<Output,
|
|
275
|
+
) => Effect.Effect<Output, E, R>
|
|
264
276
|
|
|
265
|
-
|
|
266
|
-
|
|
277
|
+
/**
|
|
278
|
+
* A `run` transform, tracking an added requirement `R2` and optional added
|
|
279
|
+
* error `E2`. The tool's own `E` is preserved; `describeFailures` is the
|
|
280
|
+
* combinator that *replaces* it (mapping every error to `string`).
|
|
281
|
+
*/
|
|
282
|
+
export type Middleware<R2 = never, E2 = never> = <Input, Event, Output, E, R>(
|
|
283
|
+
run: Handler<Input, Event, Output, E, R>,
|
|
267
284
|
name: string,
|
|
268
|
-
) => Handler<Input, Event, Output, R | R2>
|
|
285
|
+
) => Handler<Input, Event, Output, E | E2, R | R2>
|
|
269
286
|
|
|
270
|
-
type WrapTool<T extends AnyTool, R2> =
|
|
271
|
-
T extends Tool<infer N, infer I, infer Ev, infer O, infer
|
|
287
|
+
type WrapTool<T extends AnyTool, R2, E2> =
|
|
288
|
+
T extends Tool<infer N, infer I, infer Ev, infer O, infer E, infer R>
|
|
289
|
+
? Tool<N, I, Ev, O, E | E2, R | R2>
|
|
290
|
+
: T
|
|
272
291
|
|
|
273
|
-
type WrapToolkit<T extends ToolMap, R2> = {
|
|
274
|
-
readonly [K in keyof T]: T[K] extends AnyTool ? WrapTool<T[K], R2> : T[K]
|
|
292
|
+
type WrapToolkit<T extends ToolMap, R2, E2> = {
|
|
293
|
+
readonly [K in keyof T]: T[K] extends AnyTool ? WrapTool<T[K], R2, E2> : T[K]
|
|
275
294
|
}
|
|
276
295
|
|
|
277
296
|
/**
|
|
278
297
|
* Wrap every local tool's `run` with a middleware, preserving each tool's
|
|
279
298
|
* `Input`/`Output` and unioning the middleware's requirement `R2` into the
|
|
280
299
|
* toolkit's `ToolkitR`. Non-local kinds pass through unchanged. Stack with
|
|
281
|
-
* `pipe
|
|
300
|
+
* `pipe` - inner-first, so the last `wrap` applied is the outermost and runs
|
|
301
|
+
* first:
|
|
282
302
|
*
|
|
283
303
|
* const observed = pipe(toolkit, Toolkit.wrap(logging), Toolkit.wrap(authz))
|
|
284
|
-
* Toolkit.run(observed, calls) //
|
|
304
|
+
* Toolkit.run(observed, calls) // authz runs, then logging, then the tool
|
|
285
305
|
*
|
|
286
|
-
* A middleware that `
|
|
287
|
-
* executor
|
|
288
|
-
* "
|
|
306
|
+
* A middleware that fails with `string` / `ToolFailed` is absorbed by the
|
|
307
|
+
* executor into a `ToolResult.Failure`; any other error propagates typed. Use
|
|
308
|
+
* `describeFailures` for the total "everything to the model" case, or the
|
|
309
|
+
* `Approval` flow for a distinct "denied" verdict.
|
|
289
310
|
*/
|
|
290
311
|
export const wrap: {
|
|
291
|
-
<R2
|
|
292
|
-
|
|
312
|
+
<R2, E2>(
|
|
313
|
+
middleware: Middleware<R2, E2>,
|
|
314
|
+
): <T extends Toolkit>(toolkit: T) => WrapToolkit<T, R2, E2>
|
|
315
|
+
<T extends Toolkit, R2, E2>(toolkit: T, middleware: Middleware<R2, E2>): WrapToolkit<T, R2, E2>
|
|
293
316
|
} = Function.dual(
|
|
294
317
|
2,
|
|
295
|
-
<T extends Toolkit, R2>(toolkit: T, middleware: Middleware<R2>): WrapToolkit<T, R2> =>
|
|
318
|
+
<T extends Toolkit, R2, E2>(toolkit: T, middleware: Middleware<R2, E2>): WrapToolkit<T, R2, E2> =>
|
|
296
319
|
Record.map(toolkit, (tool) =>
|
|
297
320
|
tool._tag === "LocalTool" ? { ...tool, run: middleware(tool.run, tool.name) } : tool,
|
|
298
|
-
) as WrapToolkit<T, R2>,
|
|
321
|
+
) as WrapToolkit<T, R2, E2>,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
// describeFailures: map every local tool's error to a model-visible string,
|
|
326
|
+
// opting its failures into recovery. Group tools by recovery policy, wrap
|
|
327
|
+
// each group, then compose.
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
|
|
330
|
+
type DescribeTool<T extends AnyTool> =
|
|
331
|
+
T extends Tool<infer N, infer I, infer Ev, infer O, any, infer R>
|
|
332
|
+
? Tool<N, I, Ev, O, string, R>
|
|
333
|
+
: T
|
|
334
|
+
|
|
335
|
+
type DescribeToolkit<T extends ToolMap> = {
|
|
336
|
+
readonly [K in keyof T]: T[K] extends AnyTool ? DescribeTool<T[K]> : T[K]
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Map every local tool's error to a `string`, so `Toolkit.run` absorbs those
|
|
341
|
+
* failures into `ToolResult.Failure` the model reads and adapts to. Pairs
|
|
342
|
+
* with the domain `describe` helpers (`AiError.describe`,
|
|
343
|
+
* `BrowserError.describe`). Non-local kinds pass through.
|
|
344
|
+
*
|
|
345
|
+
* const kit = yield* Toolkit.compose(
|
|
346
|
+
* Toolkit.describeFailures(searchKit, AiError.describe),
|
|
347
|
+
* browserToolkit(session), // failures stay typed
|
|
348
|
+
* )
|
|
349
|
+
*/
|
|
350
|
+
export const describeFailures: {
|
|
351
|
+
<E>(describe: (e: E) => string): <T extends Toolkit>(toolkit: T) => DescribeToolkit<T>
|
|
352
|
+
<T extends Toolkit>(toolkit: T, describe: (e: ToolkitE<T>) => string): DescribeToolkit<T>
|
|
353
|
+
} = Function.dual(
|
|
354
|
+
2,
|
|
355
|
+
<T extends Toolkit>(toolkit: T, describe: (e: any) => string): DescribeToolkit<T> =>
|
|
356
|
+
Record.map(toolkit, (tool) =>
|
|
357
|
+
tool._tag === "LocalTool"
|
|
358
|
+
? {
|
|
359
|
+
...tool,
|
|
360
|
+
run: (input: unknown, emit: Emit<unknown>) =>
|
|
361
|
+
(tool as AnyLocalTool).run(input, emit).pipe(Effect.mapError(describe)),
|
|
362
|
+
}
|
|
363
|
+
: tool,
|
|
364
|
+
) as DescribeToolkit<T>,
|
|
299
365
|
)
|
|
300
366
|
|
|
301
367
|
// ---------------------------------------------------------------------------
|
|
@@ -307,17 +373,26 @@ export type ExecuteOptions = {
|
|
|
307
373
|
readonly concurrency?: number | "unbounded"
|
|
308
374
|
}
|
|
309
375
|
|
|
310
|
-
/**
|
|
376
|
+
/**
|
|
377
|
+
* Execute every provided call. Approval/rejection policy belongs upstream.
|
|
378
|
+
*
|
|
379
|
+
* String and `ToolFailed` failures are absorbed into `ToolResult.Failure`
|
|
380
|
+
* (the model reads and adapts to them); every other tool error propagates
|
|
381
|
+
* typed on the stream, so `Exclude<ToolkitE<T>, string | ToolFailed>` is
|
|
382
|
+
* what a caller catches or lets end the run. If one call in a concurrent
|
|
383
|
+
* batch fails the stream, siblings may go unanswered: a loop that catches
|
|
384
|
+
* and continues must reconcile history first (`HistoryCheck.cancelAllPending`).
|
|
385
|
+
*/
|
|
311
386
|
export const run = <T extends Toolkit>(
|
|
312
387
|
toolkit: T,
|
|
313
388
|
calls: ReadonlyArray<ToolCall>,
|
|
314
389
|
options?: ExecuteOptions,
|
|
315
|
-
): Stream.Stream<ToolEvent,
|
|
390
|
+
): Stream.Stream<ToolEvent, Exclude<ToolkitE<T>, string | ToolFailed>, ToolkitR<T>> =>
|
|
316
391
|
Stream.fromIterable(calls).pipe(
|
|
317
392
|
Stream.flatMap((call) => runOne(toolkit, call), {
|
|
318
393
|
concurrency: options?.concurrency ?? "unbounded",
|
|
319
394
|
}),
|
|
320
|
-
) as Stream.Stream<ToolEvent,
|
|
395
|
+
) as Stream.Stream<ToolEvent, Exclude<ToolkitE<T>, string | ToolFailed>, ToolkitR<T>>
|
|
321
396
|
|
|
322
397
|
const okResult = (call: ToolCall, tool: string, value: unknown): ToolResult =>
|
|
323
398
|
ToolResult.Ok({ call_id: call.call_id, tool, value })
|
|
@@ -325,8 +400,11 @@ const okResult = (call: ToolCall, tool: string, value: unknown): ToolResult =>
|
|
|
325
400
|
const outputEvent = (result: ToolResult): Stream.Stream<ToolEvent, never, any> =>
|
|
326
401
|
Stream.succeed(ToolEvent.Output({ result }))
|
|
327
402
|
|
|
328
|
-
const runOne = (toolkit: ToolMap, call: ToolCall): Stream.Stream<ToolEvent,
|
|
329
|
-
|
|
403
|
+
const runOne = (toolkit: ToolMap, call: ToolCall): Stream.Stream<ToolEvent, unknown, any> => {
|
|
404
|
+
// Own-property lookup: a hallucinated call named `constructor` / `toString`
|
|
405
|
+
// must not resolve to a prototype method.
|
|
406
|
+
const tool = Object.hasOwn(toolkit, call.name) ? toolkit[call.name] : undefined
|
|
407
|
+
return Match.value(tool).pipe(
|
|
330
408
|
// No such tool. Graceful: emit a synthetic Failure so OTHER calls in this
|
|
331
409
|
// turn still execute. LLMs hallucinate tool names; MCP tools come and go.
|
|
332
410
|
Match.when(Match.undefined, () =>
|
|
@@ -338,23 +416,46 @@ const runOne = (toolkit: ToolMap, call: ToolCall): Stream.Stream<ToolEvent, neve
|
|
|
338
416
|
// execution; reaching here means it didn't, so report it rather than crash.
|
|
339
417
|
Match.orElse(() => outputEvent(nonLocalTool(call))),
|
|
340
418
|
)
|
|
419
|
+
}
|
|
341
420
|
|
|
342
|
-
const
|
|
421
|
+
const describeIssues = (issues: ReadonlyArray<StandardSchemaV1.Issue>): string =>
|
|
422
|
+
issues
|
|
423
|
+
.map((issue) => {
|
|
424
|
+
const path = (issue.path ?? [])
|
|
425
|
+
.map((seg) => (typeof seg === "object" && seg !== null ? String(seg.key) : String(seg)))
|
|
426
|
+
.join(".")
|
|
427
|
+
return path === "" ? issue.message : `${path}: ${issue.message}`
|
|
428
|
+
})
|
|
429
|
+
.join("; ")
|
|
430
|
+
.slice(0, 500)
|
|
431
|
+
|
|
432
|
+
// A decode failure is the model's contract violation: report it as a result
|
|
433
|
+
// (never a stream failure) so the model can correct its next call.
|
|
434
|
+
const runTool = (tool: AnyLocalTool, call: ToolCall): Stream.Stream<ToolEvent, unknown, any> =>
|
|
435
|
+
Stream.unwrap(
|
|
436
|
+
decodeCallInput(tool, call).pipe(
|
|
437
|
+
Effect.map(
|
|
438
|
+
Match.type<DecodeResult<unknown>>().pipe(
|
|
439
|
+
Match.tag("parseError", () =>
|
|
440
|
+
outputEvent(validationError(call, "arguments are not valid JSON")),
|
|
441
|
+
),
|
|
442
|
+
Match.tag("invalid", ({ issues }) =>
|
|
443
|
+
outputEvent(validationError(call, describeIssues(issues))),
|
|
444
|
+
),
|
|
445
|
+
Match.tag("ok", ({ input }) => runValidated(tool, call, input)),
|
|
446
|
+
Match.exhaustive,
|
|
447
|
+
),
|
|
448
|
+
),
|
|
449
|
+
),
|
|
450
|
+
)
|
|
343
451
|
|
|
344
|
-
const
|
|
452
|
+
const runValidated = (
|
|
453
|
+
tool: AnyLocalTool,
|
|
454
|
+
call: ToolCall,
|
|
455
|
+
input: unknown,
|
|
456
|
+
): Stream.Stream<ToolEvent, unknown, any> =>
|
|
345
457
|
Stream.unwrap(
|
|
346
458
|
Effect.gen(function* () {
|
|
347
|
-
const parsed = yield* parseJsonUnknown(call.arguments).pipe(
|
|
348
|
-
Effect.mapError(() => "json_parse_error" as const),
|
|
349
|
-
)
|
|
350
|
-
const validated = yield* Effect.tryPromise({
|
|
351
|
-
try: () => Promise.resolve(tool.inputSchema["~standard"].validate(parsed)),
|
|
352
|
-
catch: () => "validation_threw" as const,
|
|
353
|
-
})
|
|
354
|
-
if (validated.issues !== undefined) {
|
|
355
|
-
return Stream.succeed<ToolEvent>(ToolEvent.Output({ result: validationError(call) }))
|
|
356
|
-
}
|
|
357
|
-
|
|
358
459
|
// Per-call queue: `emit` offers events as `run` produces them; the
|
|
359
460
|
// progress stream drains them in real time; `Queue.end` (in the
|
|
360
461
|
// `ensuring`) flushes pending events then signals a clean end, which
|
|
@@ -363,7 +464,7 @@ const runTool = (tool: AnyLocalTool, call: ToolCall): Stream.Stream<ToolEvent, n
|
|
|
363
464
|
const queue = yield* Queue.make<unknown, Cause.Done>({ capacity: tool.emitBufferSize })
|
|
364
465
|
const emit = (event: unknown) => Effect.asVoid(Queue.offer(queue, event))
|
|
365
466
|
const fiber = yield* tool
|
|
366
|
-
.run(
|
|
467
|
+
.run(input, emit)
|
|
367
468
|
.pipe(Effect.ensuring(Queue.end(queue)), Effect.forkScoped)
|
|
368
469
|
|
|
369
470
|
const progress = Stream.fromQueue(queue).pipe(
|
|
@@ -371,24 +472,38 @@ const runTool = (tool: AnyLocalTool, call: ToolCall): Stream.Stream<ToolEvent, n
|
|
|
371
472
|
)
|
|
372
473
|
const output = Stream.fromEffect(
|
|
373
474
|
Fiber.join(fiber).pipe(
|
|
374
|
-
Effect.
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
),
|
|
475
|
+
Effect.matchCauseEffect({
|
|
476
|
+
onSuccess: (value: unknown) =>
|
|
477
|
+
Effect.succeed(ToolEvent.Output({ result: okResult(call, tool.name, value) })),
|
|
478
|
+
onFailure: (cause: Cause.Cause<unknown>) => routeCause(call, cause),
|
|
479
|
+
}),
|
|
380
480
|
),
|
|
381
481
|
)
|
|
382
482
|
return progress.pipe(Stream.concat(output))
|
|
383
483
|
}),
|
|
384
|
-
).pipe(
|
|
385
|
-
// Backstop for the input-parsing failures (`json_parse_error`,
|
|
386
|
-
// `validation_threw`) and any defect before the run fiber is forked.
|
|
387
|
-
Stream.catchCause(() =>
|
|
388
|
-
Stream.succeed(ToolEvent.Output({ result: executionError(call, "Tool execution failed") })),
|
|
389
|
-
),
|
|
390
484
|
)
|
|
391
485
|
|
|
486
|
+
// A clean single string / ToolFailed failure is absorbed as a model-visible
|
|
487
|
+
// result; interruption, defects, and typed non-sentinel errors propagate.
|
|
488
|
+
const routeCause = (
|
|
489
|
+
call: ToolCall,
|
|
490
|
+
cause: Cause.Cause<unknown>,
|
|
491
|
+
): Effect.Effect<ToolEvent, unknown> => {
|
|
492
|
+
const only = cause.reasons.length === 1 ? cause.reasons[0] : undefined
|
|
493
|
+
if (only !== undefined && Cause.isFailReason(only)) {
|
|
494
|
+
const error = only.error
|
|
495
|
+
if (typeof error === "string") {
|
|
496
|
+
return Effect.succeed(ToolEvent.Output({ result: failed(call, "tool_failed", error) }))
|
|
497
|
+
}
|
|
498
|
+
if (error instanceof ToolFailed) {
|
|
499
|
+
return Effect.succeed(
|
|
500
|
+
ToolEvent.Output({ result: failed(call, error.kind ?? "tool_failed", error.message) }),
|
|
501
|
+
)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return Effect.failCause(cause)
|
|
505
|
+
}
|
|
506
|
+
|
|
392
507
|
// ---------------------------------------------------------------------------
|
|
393
508
|
// `continueWithResults` - bridge from a `Stream<ToolEvent>` to the loop's
|
|
394
509
|
// emit shape. Drains the stream to the consumer in real-time, taps every
|
|
@@ -455,19 +570,17 @@ export const collectResults = <E, R>(
|
|
|
455
570
|
export const continueWithResults: {
|
|
456
571
|
<S>(
|
|
457
572
|
build: (results: ReadonlyArray<ToolResult>) => S,
|
|
458
|
-
): <R>(
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
<S, R>(
|
|
462
|
-
stream: Stream.Stream<ToolEvent, never, R>,
|
|
573
|
+
): <E, R>(stream: Stream.Stream<ToolEvent, E, R>) => Stream.Stream<Loop.Step<ToolEvent, S>, E, R>
|
|
574
|
+
<S, E, R>(
|
|
575
|
+
stream: Stream.Stream<ToolEvent, E, R>,
|
|
463
576
|
build: (results: ReadonlyArray<ToolResult>) => S,
|
|
464
|
-
): Stream.Stream<Loop.Step<ToolEvent, S>,
|
|
577
|
+
): Stream.Stream<Loop.Step<ToolEvent, S>, E, R>
|
|
465
578
|
} = Function.dual(
|
|
466
579
|
2,
|
|
467
|
-
<S, R>(
|
|
468
|
-
stream: Stream.Stream<ToolEvent,
|
|
580
|
+
<S, E, R>(
|
|
581
|
+
stream: Stream.Stream<ToolEvent, E, R>,
|
|
469
582
|
build: (results: ReadonlyArray<ToolResult>) => S,
|
|
470
|
-
): Stream.Stream<Loop.Step<ToolEvent, S>,
|
|
583
|
+
): Stream.Stream<Loop.Step<ToolEvent, S>, E, R> =>
|
|
471
584
|
Stream.unwrap(
|
|
472
585
|
Effect.gen(function* () {
|
|
473
586
|
const ref = yield* Ref.make<ReadonlyArray<ToolResult>>([])
|
|
@@ -88,7 +88,7 @@ export class SttStreaming extends Context.Service<SttStreaming, void>()(
|
|
|
88
88
|
export const transcribe = (
|
|
89
89
|
request: CommonTranscribeRequest,
|
|
90
90
|
): Effect.Effect<TranscriptResult, AiError.AiError, Transcriber> =>
|
|
91
|
-
Effect.flatMap(Transcriber
|
|
91
|
+
Effect.flatMap(Transcriber, (t) => t.transcribe(request))
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
94
|
* Live transcription. Dual-arity: pipeable (data-last) and direct
|
|
@@ -122,8 +122,8 @@ export const streamTranscriptionFrom: {
|
|
|
122
122
|
<E, R>(audioIn: Stream.Stream<Uint8Array, E, R>, request: CommonStreamTranscribeRequest) =>
|
|
123
123
|
Stream.unwrap(
|
|
124
124
|
Effect.gen(function* () {
|
|
125
|
-
const t = yield* Transcriber
|
|
126
|
-
yield* SttStreaming
|
|
125
|
+
const t = yield* Transcriber
|
|
126
|
+
yield* SttStreaming
|
|
127
127
|
return t.streamTranscriptionFrom(audioIn, request)
|
|
128
128
|
}),
|
|
129
129
|
),
|