@effect-uai/core 0.10.0 → 0.12.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/{Image-BZmKfIdq.d.mts → Image-C4vuBmu0.d.mts} +11 -11
- package/dist/{Image-BZmKfIdq.d.mts.map → Image-C4vuBmu0.d.mts.map} +1 -1
- package/dist/{Items-BnIduEV2.d.mts → Items-BKQgqJ8A.d.mts} +136 -97
- package/dist/Items-BKQgqJ8A.d.mts.map +1 -0
- package/dist/{Tool-2lC_XMth.d.mts → Tool-u5bag8SO.d.mts} +42 -9
- package/dist/Tool-u5bag8SO.d.mts.map +1 -0
- package/dist/{ToolEvent-cWZ2BnbS.d.mts → ToolEvent-BBfOdhF_.d.mts} +2 -2
- package/dist/{ToolEvent-cWZ2BnbS.d.mts.map → ToolEvent-BBfOdhF_.d.mts.map} +1 -1
- package/dist/{ToolResult-CL4hK_9z.d.mts → ToolResult-CRpmkBXR.d.mts} +2 -2
- package/dist/{ToolResult-CL4hK_9z.d.mts.map → ToolResult-CRpmkBXR.d.mts.map} +1 -1
- package/dist/{Toolkit-Dr9l_hNy.d.mts → Toolkit-D4mQvhjq.d.mts} +6 -6
- package/dist/Toolkit-D4mQvhjq.d.mts.map +1 -0
- package/dist/browser/Browser.mjs +4 -4
- package/dist/browser/Browser.mjs.map +1 -1
- package/dist/browser/BrowserTool.d.mts +34 -33
- package/dist/browser/BrowserTool.d.mts.map +1 -1
- 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/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 +28 -4
- package/dist/domain/Items.mjs.map +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 -9
- 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.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 +12 -8
- package/dist/index.mjs +5 -1
- 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 +2 -2
- 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.test.mjs +1 -1
- 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 +13 -5
- package/dist/observability/Metrics.d.mts.map +1 -1
- package/dist/observability/Metrics.mjs +30 -7
- package/dist/observability/Metrics.mjs.map +1 -1
- package/dist/observability/Metrics.test.mjs +44 -2
- package/dist/observability/Metrics.test.mjs.map +1 -1
- 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 +3 -3
- package/dist/retry/Retry.d.mts.map +1 -1
- package/dist/retry/Retry.mjs +30 -1
- package/dist/retry/Retry.mjs.map +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.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.test.mjs +1 -1
- package/dist/testing/MockProvider.d.mts +1 -1
- package/dist/testing/MockSandbox.d.mts +1 -1
- package/dist/tool/Approval.d.mts +3 -3
- package/dist/tool/Approval.test.mjs +1 -1
- package/dist/tool/HistoryCheck.d.mts +2 -2
- package/dist/tool/Tool.d.mts +2 -2
- package/dist/tool/Tool.mjs +27 -7
- 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/ToolResult.d.mts +1 -1
- package/dist/tool/Toolkit.d.mts +1 -1
- package/dist/tool/Toolkit.mjs.map +1 -1
- package/dist/tool/Toolkit.test.mjs +46 -2
- package/dist/tool/Toolkit.test.mjs.map +1 -1
- 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.mjs +1 -1
- package/dist/web-read/WebRead.mjs.map +1 -1
- package/dist/web-read/WebReadTool.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 +1 -1
- package/package.json +19 -3
- package/src/browser/Browser.ts +4 -4
- package/src/domain/Citation.ts +64 -0
- package/src/domain/Items.ts +32 -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 +4 -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/observability/Metrics.test.ts +65 -0
- package/src/observability/Metrics.ts +53 -11
- package/src/research/DeepResearch.ts +137 -0
- package/src/retry/Retry.ts +44 -3
- 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/Tool.test.ts +151 -1
- package/src/tool/Tool.ts +120 -22
- package/src/tool/Toolkit.test.ts +60 -0
- package/src/tool/Toolkit.ts +2 -2
- package/src/transcriber/Transcriber.ts +3 -3
- package/src/web-read/WebRead.ts +1 -1
- package/src/web-search/WebSearch.ts +1 -1
- package/dist/Items-BnIduEV2.d.mts.map +0 -1
- package/dist/Tool-2lC_XMth.d.mts.map +0 -1
- package/dist/Toolkit-Dr9l_hNy.d.mts.map +0 -1
- package/dist/dist-DWgRbDN3.mjs.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, Stream } from "effect"
|
|
1
|
+
import { Cause, Deferred, Effect, Exit, Fiber, Result, Stream } from "effect"
|
|
2
2
|
import { describe, expect, it } from "vitest"
|
|
3
3
|
import * as SSE from "./SSE.js"
|
|
4
4
|
|
|
@@ -8,6 +8,21 @@ const bytesOf = (...chunks: ReadonlyArray<string>) =>
|
|
|
8
8
|
|
|
9
9
|
const collect = <A, E>(s: Stream.Stream<A, E>) => Effect.runPromise(Stream.runCollect(s))
|
|
10
10
|
|
|
11
|
+
const runUntilEnd = <A, E>(s: Stream.Stream<A, E>) =>
|
|
12
|
+
Effect.runPromise(
|
|
13
|
+
s.pipe(
|
|
14
|
+
Stream.result,
|
|
15
|
+
Stream.runCollect,
|
|
16
|
+
Effect.map((results) => ({
|
|
17
|
+
seen: results.filter(Result.isSuccess).map((r) => r.success),
|
|
18
|
+
failures: results.filter(Result.isFailure).map((r) => r.failure),
|
|
19
|
+
})),
|
|
20
|
+
),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
const failuresOf = (exit: Exit.Exit<unknown, unknown>): ReadonlyArray<unknown> =>
|
|
24
|
+
Exit.isFailure(exit) ? exit.cause.reasons.filter(Cause.isFailReason).map((r) => r.error) : []
|
|
25
|
+
|
|
11
26
|
describe("SSE.fromBytes", () => {
|
|
12
27
|
it("parses a single complete event", async () => {
|
|
13
28
|
const out = await collect(SSE.fromBytes(bytesOf("event: foo\ndata: hello\n\n")))
|
|
@@ -39,9 +54,77 @@ describe("SSE.fromBytes", () => {
|
|
|
39
54
|
expect(out).toEqual([{ id: "42", data: "x" }])
|
|
40
55
|
})
|
|
41
56
|
|
|
42
|
-
it("
|
|
57
|
+
it("parses a data field with no space after the colon", async () => {
|
|
58
|
+
const out = await collect(SSE.fromBytes(bytesOf("data:x\n\n")))
|
|
59
|
+
expect(out).toEqual([{ data: "x" }])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it("ignores an id containing a NUL character", async () => {
|
|
63
|
+
const out = await collect(SSE.fromBytes(bytesOf("id: a\u0000b\ndata: x\n\n")))
|
|
64
|
+
expect(out).toEqual([{ data: "x" }])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it("discards a trailing event without a closing blank line", async () => {
|
|
43
68
|
const out = await collect(SSE.fromBytes(bytesOf("data: tail")))
|
|
44
|
-
expect(out).toEqual([
|
|
69
|
+
expect(out).toEqual([])
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it("discards buffered partial data when the upstream fails mid-stream", async () => {
|
|
73
|
+
const failing = Stream.concat(
|
|
74
|
+
bytesOf("data: good\n\ndata: partial-tr"),
|
|
75
|
+
Stream.fail("boom" as const),
|
|
76
|
+
)
|
|
77
|
+
const { seen, failures } = await runUntilEnd(SSE.fromBytes(failing))
|
|
78
|
+
expect(seen).toEqual([{ data: "good" }])
|
|
79
|
+
expect(failures).toEqual(["boom"])
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it("discards a partial multi-byte char in the text decoder when the upstream fails", async () => {
|
|
83
|
+
const partialSquid = enc.encode("data: 🦑\n\n").slice(0, 8)
|
|
84
|
+
const failing = Stream.concat(
|
|
85
|
+
Stream.fromIterable([enc.encode("data: ok\n\n"), partialSquid]),
|
|
86
|
+
Stream.fail("boom" as const),
|
|
87
|
+
)
|
|
88
|
+
const { seen, failures } = await runUntilEnd(SSE.fromBytes(failing))
|
|
89
|
+
expect(seen).toEqual([{ data: "ok" }])
|
|
90
|
+
expect(failures).toEqual(["boom"])
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it("discards buffered partial data when the stream is interrupted", async () => {
|
|
94
|
+
const { seen, exit } = await Effect.runPromise(
|
|
95
|
+
Effect.gen(function* () {
|
|
96
|
+
const seen: Array<SSE.Event> = []
|
|
97
|
+
const latch = yield* Deferred.make<void>()
|
|
98
|
+
const hanging = Stream.concat(bytesOf("data: good\n\ndata: partial"), Stream.never)
|
|
99
|
+
const fiber = yield* Stream.runForEach(SSE.fromBytes(hanging), (e) =>
|
|
100
|
+
Effect.sync(() => {
|
|
101
|
+
seen.push(e)
|
|
102
|
+
}).pipe(Effect.andThen(Deferred.succeed(latch, void 0))),
|
|
103
|
+
).pipe(Effect.forkChild)
|
|
104
|
+
yield* Deferred.await(latch)
|
|
105
|
+
yield* Fiber.interrupt(fiber)
|
|
106
|
+
const exit = yield* Fiber.await(fiber)
|
|
107
|
+
return { seen, exit }
|
|
108
|
+
}),
|
|
109
|
+
)
|
|
110
|
+
expect(seen).toEqual([{ data: "good" }])
|
|
111
|
+
expect(Exit.isFailure(exit)).toBe(true)
|
|
112
|
+
expect(failuresOf(exit)).toEqual([])
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it("ignores retry directives", async () => {
|
|
116
|
+
const out = await collect(SSE.fromBytes(bytesOf("retry: 3000\n\ndata: a\n\n")))
|
|
117
|
+
expect(out).toEqual([{ data: "a" }])
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it("treats an explicit 'event: message' as the default event", async () => {
|
|
121
|
+
const out = await collect(SSE.fromBytes(bytesOf("event: message\ndata: x\n\n")))
|
|
122
|
+
expect(out).toEqual([{ data: "x" }])
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it("does not dispatch events with no data", async () => {
|
|
126
|
+
const out = await collect(SSE.fromBytes(bytesOf("event: ping\n\ndata: x\n\n")))
|
|
127
|
+
expect(out).toEqual([{ data: "x" }])
|
|
45
128
|
})
|
|
46
129
|
|
|
47
130
|
it("ignores empty blocks between events", async () => {
|
package/src/streaming/SSE.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Stream } from "effect"
|
|
2
|
+
import { Sse } from "effect/unstable/encoding"
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* One Server-Sent Event. Fields per the WHATWG spec:
|
|
5
|
-
* - `event`: optional event name (default "message"
|
|
6
|
+
* - `event`: optional event name (absent for the default "message")
|
|
6
7
|
* - `data`: payload, with multiple `data:` lines joined by `\n`
|
|
7
8
|
* - `id`: optional last-event id
|
|
8
9
|
*/
|
|
@@ -12,98 +13,44 @@ export type Event = {
|
|
|
12
13
|
readonly id?: string
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const toEvent = (wire: Sse.Event): Event => ({
|
|
17
|
+
data: wire.data,
|
|
18
|
+
...(wire.event !== "message" && { event: wire.event }),
|
|
19
|
+
...(wire.id !== undefined && { id: wire.id }),
|
|
20
|
+
})
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Stream.mapAccum(
|
|
24
|
-
(): TextDecoder => new TextDecoder("utf-8"),
|
|
25
|
-
(decoder, chunk: Uint8Array) => [decoder, [decoder.decode(chunk, { stream: true })]] as const,
|
|
26
|
-
{
|
|
27
|
-
onHalt: (decoder: TextDecoder) => {
|
|
28
|
-
const tail = decoder.decode()
|
|
29
|
-
return tail.length > 0 ? [tail] : []
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
),
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
/** Split a text stream on a separator, buffering across chunk boundaries. */
|
|
36
|
-
const splitOn =
|
|
37
|
-
(separator: string) =>
|
|
38
|
-
<E, R>(self: Stream.Stream<string, E, R>): Stream.Stream<string, E, R> =>
|
|
39
|
-
self.pipe(
|
|
40
|
-
Stream.mapAccum(
|
|
41
|
-
(): string => "",
|
|
42
|
-
(buffer, chunk: string) => {
|
|
43
|
-
const parts = (buffer + chunk).split(separator)
|
|
44
|
-
const tail = parts[parts.length - 1] ?? ""
|
|
45
|
-
return [tail, parts.slice(0, -1)] as const
|
|
46
|
-
},
|
|
47
|
-
{ onHalt: (tail: string) => (tail.length > 0 ? [tail] : []) },
|
|
48
|
-
),
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
|
-
// Parser
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
|
|
55
|
-
const parseField = (line: string): readonly [string, string] => {
|
|
56
|
-
const colon = line.indexOf(":")
|
|
57
|
-
if (colon < 0) return [line, ""]
|
|
58
|
-
const value = line.slice(colon + 1)
|
|
59
|
-
return [line.slice(0, colon), value.startsWith(" ") ? value.slice(1) : value]
|
|
22
|
+
type ParserState = {
|
|
23
|
+
readonly parser: Sse.Parser
|
|
24
|
+
readonly out: Array<Event>
|
|
60
25
|
}
|
|
61
26
|
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const id = fields.find(([f]) => f === "id")?.[1]
|
|
70
|
-
|
|
71
|
-
const out: { event?: string; data: string; id?: string } = {
|
|
72
|
-
data: dataLines.join("\n"),
|
|
73
|
-
}
|
|
74
|
-
if (event !== undefined) out.event = event
|
|
75
|
-
if (id !== undefined) out.id = id
|
|
76
|
-
return out as Event
|
|
27
|
+
const makeParserState = (): ParserState => {
|
|
28
|
+
const out: Array<Event> = []
|
|
29
|
+
const parser = Sse.makeParser((event) => {
|
|
30
|
+
// `retry:` reconnection hints have no representation in `Event`
|
|
31
|
+
if (event._tag === "Event") out.push(toEvent(event))
|
|
32
|
+
})
|
|
33
|
+
return { parser, out }
|
|
77
34
|
}
|
|
78
35
|
|
|
79
|
-
// ---------------------------------------------------------------------------
|
|
80
|
-
// Public API
|
|
81
|
-
// ---------------------------------------------------------------------------
|
|
82
|
-
|
|
83
36
|
/**
|
|
84
37
|
* Decode a `Stream<Uint8Array>` (e.g. an HTTP response body) into a
|
|
85
|
-
* `Stream<SSE.Event
|
|
86
|
-
*
|
|
38
|
+
* `Stream<SSE.Event>`, using the spec parser from `effect/unstable/encoding`.
|
|
39
|
+
* An event is dispatched only once its terminating blank line arrives, so a
|
|
40
|
+
* partial event at stream end (clean or failed) is discarded per the spec
|
|
41
|
+
* rather than surfaced as a truncated frame.
|
|
87
42
|
*/
|
|
88
43
|
export const fromBytes = <E, R>(
|
|
89
44
|
self: Stream.Stream<Uint8Array, E, R>,
|
|
90
45
|
): Stream.Stream<Event, E, R> =>
|
|
91
46
|
self.pipe(
|
|
92
|
-
decodeText,
|
|
93
|
-
Stream.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
47
|
+
Stream.decodeText,
|
|
48
|
+
Stream.mapAccum(makeParserState, (state, text: string) => {
|
|
49
|
+
state.parser.feed(text)
|
|
50
|
+
return [state, state.out.splice(0)] as const
|
|
51
|
+
}),
|
|
97
52
|
)
|
|
98
53
|
|
|
99
|
-
const eventToString = (ev: Event): string => {
|
|
100
|
-
const parts: string[] = []
|
|
101
|
-
if (ev.event !== undefined) parts.push(`event: ${ev.event}`)
|
|
102
|
-
if (ev.id !== undefined) parts.push(`id: ${ev.id}`)
|
|
103
|
-
for (const line of ev.data.split("\n")) parts.push(`data: ${line}`)
|
|
104
|
-
return parts.join("\n") + "\n\n"
|
|
105
|
-
}
|
|
106
|
-
|
|
107
54
|
const encoder = new TextEncoder()
|
|
108
55
|
|
|
109
56
|
/**
|
|
@@ -111,4 +58,13 @@ const encoder = new TextEncoder()
|
|
|
111
58
|
* HTTP response with `Content-Type: text/event-stream`.
|
|
112
59
|
*/
|
|
113
60
|
export const toBytes = <E, R>(self: Stream.Stream<Event, E, R>): Stream.Stream<Uint8Array, E, R> =>
|
|
114
|
-
Stream.map(self, (ev) =>
|
|
61
|
+
Stream.map(self, (ev) =>
|
|
62
|
+
encoder.encode(
|
|
63
|
+
Sse.encoder.write({
|
|
64
|
+
_tag: "Event",
|
|
65
|
+
event: ev.event ?? "message",
|
|
66
|
+
id: ev.id,
|
|
67
|
+
data: ev.data,
|
|
68
|
+
}),
|
|
69
|
+
),
|
|
70
|
+
)
|
package/src/tool/Tool.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec"
|
|
2
|
-
import { Effect } from "effect"
|
|
2
|
+
import { Context, Effect, Schema } from "effect"
|
|
3
3
|
import { describe, expect, expectTypeOf, it } from "vitest"
|
|
4
4
|
import type * as Items from "../domain/Items.js"
|
|
5
5
|
import * as Tool from "./Tool.js"
|
|
@@ -156,3 +156,153 @@ describe("Tool.decodeArgs", () => {
|
|
|
156
156
|
expect(exit._tag).toBe("Failure")
|
|
157
157
|
})
|
|
158
158
|
})
|
|
159
|
+
|
|
160
|
+
describe("Effect Schema inputSchema", () => {
|
|
161
|
+
const Recipient = Schema.Struct({ to: Schema.String })
|
|
162
|
+
|
|
163
|
+
const sendEmail = Tool.make({
|
|
164
|
+
name: "send_email",
|
|
165
|
+
description: "Send an email to a single recipient.",
|
|
166
|
+
inputSchema: Recipient,
|
|
167
|
+
run: ({ to }) => Effect.succeed(`queued: ${to}`),
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const callWith = (args: string): Items.ToolCall => ({
|
|
171
|
+
type: "function_call",
|
|
172
|
+
call_id: "call_1",
|
|
173
|
+
name: "send_email",
|
|
174
|
+
arguments: args,
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it("runs with the input inferred from the schema's Type", async () => {
|
|
178
|
+
const result = await Effect.runPromise(sendEmail.run({ to: "x@y.z" }, () => Effect.void))
|
|
179
|
+
expect(result).toBe("queued: x@y.z")
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it("type: Input is the schema's Type", () => {
|
|
183
|
+
type InputOf<T> = T extends Tool.Tool<string, infer I, any, any, any> ? I : never
|
|
184
|
+
expectTypeOf<InputOf<typeof sendEmail>>().toEqualTypeOf<{ readonly to: string }>()
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it("type: E and R are inferred from run through the Effect Schema overload", () => {
|
|
188
|
+
class Mailer extends Context.Service<
|
|
189
|
+
Mailer,
|
|
190
|
+
{ readonly send: (to: string) => Effect.Effect<string> }
|
|
191
|
+
>()("test/Mailer") {}
|
|
192
|
+
|
|
193
|
+
const tool = Tool.make({
|
|
194
|
+
name: "send_email",
|
|
195
|
+
description: "send",
|
|
196
|
+
inputSchema: Recipient,
|
|
197
|
+
run: ({ to }) =>
|
|
198
|
+
Effect.gen(function* () {
|
|
199
|
+
const mailer = yield* Mailer
|
|
200
|
+
if (to === "") return yield* Effect.fail("empty" as const)
|
|
201
|
+
return yield* mailer.send(to)
|
|
202
|
+
}),
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
expectTypeOf<Tool.ToolE<typeof tool>>().toEqualTypeOf<"empty">()
|
|
206
|
+
expectTypeOf<Tool.ToolR<typeof tool>>().toEqualTypeOf<Mailer>()
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it("decodes valid JSON arguments to the typed input", async () => {
|
|
210
|
+
const input = await Effect.runPromise(Tool.decodeArgs(sendEmail, callWith('{"to":"x@y.z"}')))
|
|
211
|
+
expect(input).toEqual({ to: "x@y.z" })
|
|
212
|
+
expectTypeOf(input).toEqualTypeOf<{ readonly to: string }>()
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it("fails with ToolValidationError when the arguments don't satisfy the schema", async () => {
|
|
216
|
+
const exit = await Effect.runPromiseExit(Tool.decodeArgs(sendEmail, callWith('{"to":42}')))
|
|
217
|
+
expect(exit._tag).toBe("Failure")
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it("renders a JSON Schema descriptor from the wrapped schema", () => {
|
|
221
|
+
const [descriptor] = Tool.toDescriptors([sendEmail])
|
|
222
|
+
expect(descriptor?.name).toBe("send_email")
|
|
223
|
+
expect(descriptor?.inputSchema).toMatchObject({
|
|
224
|
+
type: "object",
|
|
225
|
+
properties: { to: { type: "string" } },
|
|
226
|
+
required: ["to"],
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it("accepts an already-wrapped fromEffectSchema value unchanged", async () => {
|
|
231
|
+
const wrapped = Tool.make({
|
|
232
|
+
name: "send_email",
|
|
233
|
+
description: "Send an email to a single recipient.",
|
|
234
|
+
inputSchema: Tool.fromEffectSchema(Recipient),
|
|
235
|
+
run: ({ to }) => Effect.succeed(`queued: ${to}`),
|
|
236
|
+
})
|
|
237
|
+
expect(Tool.toDescriptors([wrapped])).toEqual(Tool.toDescriptors([sendEmail]))
|
|
238
|
+
const input = await Effect.runPromise(Tool.decodeArgs(wrapped, callWith('{"to":"x@y.z"}')))
|
|
239
|
+
expectTypeOf(input).toEqualTypeOf<{ readonly to: string }>()
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it("signal: decode-only kinds accept an Effect Schema directly", async () => {
|
|
243
|
+
const escalate = Tool.signal({
|
|
244
|
+
name: "escalate",
|
|
245
|
+
description: "Escalate to a human.",
|
|
246
|
+
inputSchema: Schema.Struct({ reason: Schema.String }),
|
|
247
|
+
})
|
|
248
|
+
const input = await Effect.runPromise(
|
|
249
|
+
Tool.decodeArgs(escalate, {
|
|
250
|
+
type: "function_call",
|
|
251
|
+
call_id: "call_2",
|
|
252
|
+
name: "escalate",
|
|
253
|
+
arguments: '{"reason":"stuck"}',
|
|
254
|
+
}),
|
|
255
|
+
)
|
|
256
|
+
expect(input).toEqual({ reason: "stuck" })
|
|
257
|
+
expectTypeOf(input).toEqualTypeOf<{ readonly reason: string }>()
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
describe("provider-hosted tools", () => {
|
|
262
|
+
const localTool = Tool.make({
|
|
263
|
+
name: "send_email",
|
|
264
|
+
description: "Send an email.",
|
|
265
|
+
inputSchema: Tool.fromEffectSchema(Schema.Struct({ to: Schema.String })),
|
|
266
|
+
run: ({ to }) => Effect.succeed(`sent: ${to}`),
|
|
267
|
+
})
|
|
268
|
+
const signalTool = Tool.signal({
|
|
269
|
+
name: "escalate",
|
|
270
|
+
description: "Escalate to a human.",
|
|
271
|
+
inputSchema: Schema.Struct({ reason: Schema.String }),
|
|
272
|
+
})
|
|
273
|
+
const hostedTool = Tool.provider({
|
|
274
|
+
name: "web_search",
|
|
275
|
+
description: "Search the web.",
|
|
276
|
+
inputSchema: Tool.noInput,
|
|
277
|
+
provider: "gemini",
|
|
278
|
+
config: { kind: "google_search" },
|
|
279
|
+
})
|
|
280
|
+
const toolkit = { send_email: localTool, escalate: signalTool, web_search: hostedTool }
|
|
281
|
+
|
|
282
|
+
it("descriptorsOf excludes provider-hosted tools", () => {
|
|
283
|
+
const descriptors = Tool.descriptorsOf(toolkit)
|
|
284
|
+
expect(descriptors.map((d) => d.name)).toEqual(["send_email", "escalate"])
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it("toDescriptors drops a ProviderTool from a mixed array", () => {
|
|
288
|
+
expect(Tool.toDescriptors([localTool, hostedTool]).map((d) => d.name)).toEqual(["send_email"])
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it("providerToolsOf returns only the provider-hosted tools", () => {
|
|
292
|
+
const provider = Tool.providerToolsOf(toolkit)
|
|
293
|
+
expect(provider).toHaveLength(1)
|
|
294
|
+
expect(provider[0]?.name).toBe("web_search")
|
|
295
|
+
expect(provider[0]?.provider).toBe("gemini")
|
|
296
|
+
expect(provider[0]?.config).toEqual({ kind: "google_search" })
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it("providerToolsOf on an absent toolkit is empty", () => {
|
|
300
|
+
expect(Tool.providerToolsOf(undefined)).toEqual([])
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it("isProviderTool narrows the tool kind", () => {
|
|
304
|
+
expect(Tool.isProviderTool(hostedTool)).toBe(true)
|
|
305
|
+
expect(Tool.isProviderTool(localTool)).toBe(false)
|
|
306
|
+
expect(Tool.isProviderTool(signalTool)).toBe(false)
|
|
307
|
+
})
|
|
308
|
+
})
|
package/src/tool/Tool.ts
CHANGED
|
@@ -244,37 +244,114 @@ export type ToolDescriptor = {
|
|
|
244
244
|
readonly strict?: boolean
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
const resolveInputSchema = (
|
|
248
|
+
schema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>,
|
|
249
|
+
): ToolInputSchema<any> =>
|
|
250
|
+
Schema.isSchema(schema)
|
|
251
|
+
? fromEffectSchema(schema as Schema.Codec<any, any, never, any>)
|
|
252
|
+
: (schema as ToolInputSchema<any>)
|
|
253
|
+
|
|
247
254
|
/** A model-visible tool with a local `Effect` executor. */
|
|
248
|
-
export const make
|
|
249
|
-
|
|
250
|
-
|
|
255
|
+
export const make: {
|
|
256
|
+
<
|
|
257
|
+
Name extends string,
|
|
258
|
+
S extends Schema.Codec<any, any, never, any>,
|
|
259
|
+
Event,
|
|
260
|
+
Output,
|
|
261
|
+
E = never,
|
|
262
|
+
R = never,
|
|
263
|
+
>(
|
|
264
|
+
spec: Omit<Tool<Name, S["Type"], Event, Output, E, R>, "_tag" | "inputSchema"> & {
|
|
265
|
+
readonly inputSchema: S
|
|
266
|
+
},
|
|
267
|
+
): Tool<Name, S["Type"], Event, Output, E, R>
|
|
268
|
+
<Name extends string, Input, Event, Output, E = never, R = never>(
|
|
269
|
+
spec: Omit<Tool<Name, Input, Event, Output, E, R>, "_tag">,
|
|
270
|
+
): Tool<Name, Input, Event, Output, E, R>
|
|
271
|
+
} = (
|
|
272
|
+
spec: Omit<AnyLocalTool, "_tag" | "inputSchema"> & {
|
|
273
|
+
readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>
|
|
274
|
+
},
|
|
275
|
+
): AnyLocalTool => ({
|
|
276
|
+
_tag: "LocalTool",
|
|
277
|
+
...spec,
|
|
278
|
+
inputSchema: resolveInputSchema(spec.inputSchema),
|
|
279
|
+
})
|
|
251
280
|
|
|
252
281
|
/**
|
|
253
282
|
* A provider-hosted tool (native web search, code execution, RAG grounding).
|
|
254
283
|
* No local executor — `provider`/`config` drive the provider adapter's
|
|
255
284
|
* rendering. The model sees it; `Toolkit.run` reports it as `non_local_tool`.
|
|
256
285
|
*/
|
|
257
|
-
export const provider
|
|
258
|
-
|
|
259
|
-
|
|
286
|
+
export const provider: {
|
|
287
|
+
<
|
|
288
|
+
Name extends string,
|
|
289
|
+
S extends Schema.Codec<any, any, never, any>,
|
|
290
|
+
Provider extends string,
|
|
291
|
+
Config,
|
|
292
|
+
>(
|
|
293
|
+
spec: Omit<ProviderTool<Name, S["Type"], Provider, Config>, "_tag" | "inputSchema"> & {
|
|
294
|
+
readonly inputSchema: S
|
|
295
|
+
},
|
|
296
|
+
): ProviderTool<Name, S["Type"], Provider, Config>
|
|
297
|
+
<Name extends string, Input, Provider extends string, Config>(
|
|
298
|
+
spec: Omit<ProviderTool<Name, Input, Provider, Config>, "_tag">,
|
|
299
|
+
): ProviderTool<Name, Input, Provider, Config>
|
|
300
|
+
} = (
|
|
301
|
+
spec: Omit<ProviderTool<string, any, string, any>, "_tag" | "inputSchema"> & {
|
|
302
|
+
readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>
|
|
303
|
+
},
|
|
304
|
+
): ProviderTool<string, any, string, any> => ({
|
|
305
|
+
_tag: "ProviderTool",
|
|
306
|
+
...spec,
|
|
307
|
+
inputSchema: resolveInputSchema(spec.inputSchema),
|
|
308
|
+
})
|
|
260
309
|
|
|
261
310
|
/**
|
|
262
311
|
* A typed control-flow signal to the agent loop (escalate, pause, schedule,
|
|
263
312
|
* hand off). Decode-only: the loop interprets the call rather than executing a
|
|
264
313
|
* handler, so there is no `run`.
|
|
265
314
|
*/
|
|
266
|
-
export const signal
|
|
267
|
-
|
|
268
|
-
|
|
315
|
+
export const signal: {
|
|
316
|
+
<Name extends string, S extends Schema.Codec<any, any, never, any>>(
|
|
317
|
+
spec: Omit<SignalTool<Name, S["Type"]>, "_tag" | "inputSchema"> & {
|
|
318
|
+
readonly inputSchema: S
|
|
319
|
+
},
|
|
320
|
+
): SignalTool<Name, S["Type"]>
|
|
321
|
+
<Name extends string, Input>(spec: Omit<SignalTool<Name, Input>, "_tag">): SignalTool<Name, Input>
|
|
322
|
+
} = (
|
|
323
|
+
spec: Omit<SignalTool<string, any>, "_tag" | "inputSchema"> & {
|
|
324
|
+
readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>
|
|
325
|
+
},
|
|
326
|
+
): SignalTool<string, any> => ({
|
|
327
|
+
_tag: "SignalTool",
|
|
328
|
+
...spec,
|
|
329
|
+
inputSchema: resolveInputSchema(spec.inputSchema),
|
|
330
|
+
})
|
|
269
331
|
|
|
270
332
|
/**
|
|
271
333
|
* A typed request for an external actor to respond before the loop resumes
|
|
272
334
|
* (frontend, Telegram, CLI prompt). Decode-only; the loop stops/pauses and
|
|
273
335
|
* resumes by appending the actor's `function_call_output`.
|
|
274
336
|
*/
|
|
275
|
-
export const interaction
|
|
276
|
-
|
|
277
|
-
|
|
337
|
+
export const interaction: {
|
|
338
|
+
<Name extends string, S extends Schema.Codec<any, any, never, any>>(
|
|
339
|
+
spec: Omit<InteractionTool<Name, S["Type"]>, "_tag" | "inputSchema"> & {
|
|
340
|
+
readonly inputSchema: S
|
|
341
|
+
},
|
|
342
|
+
): InteractionTool<Name, S["Type"]>
|
|
343
|
+
<Name extends string, Input>(
|
|
344
|
+
spec: Omit<InteractionTool<Name, Input>, "_tag">,
|
|
345
|
+
): InteractionTool<Name, Input>
|
|
346
|
+
} = (
|
|
347
|
+
spec: Omit<InteractionTool<string, any>, "_tag" | "inputSchema"> & {
|
|
348
|
+
readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>
|
|
349
|
+
},
|
|
350
|
+
): InteractionTool<string, any> => ({
|
|
351
|
+
_tag: "InteractionTool",
|
|
352
|
+
...spec,
|
|
353
|
+
inputSchema: resolveInputSchema(spec.inputSchema),
|
|
354
|
+
})
|
|
278
355
|
|
|
279
356
|
/**
|
|
280
357
|
* Return a copy of a tool under a new `name`. Useful for resolving a name
|
|
@@ -312,18 +389,39 @@ export const withRun = <
|
|
|
312
389
|
run: (input: Input, emit: Emit<Event>) => Effect.Effect<Output2, E2, R2>,
|
|
313
390
|
): Tool<Name, Input, Event, Output2, E2, R2> => ({ ...tool, run })
|
|
314
391
|
|
|
315
|
-
/**
|
|
392
|
+
/** True for provider-hosted tools — the kind a provider renders natively. */
|
|
393
|
+
export const isProviderTool = (tool: AnyTool): tool is ProviderTool<string, any, string, any> =>
|
|
394
|
+
tool._tag === "ProviderTool"
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* The `ProviderTool`s in a toolkit, for a provider adapter to render as its
|
|
398
|
+
* native hosted-tool entries (web search, code execution, grounding). The other
|
|
399
|
+
* kinds render as function declarations via `descriptorsOf`.
|
|
400
|
+
*/
|
|
401
|
+
export const providerToolsOf = (
|
|
402
|
+
toolkit?: Readonly<Record<string, AnyTool>>,
|
|
403
|
+
): ReadonlyArray<ProviderTool<string, any, string, any>> =>
|
|
404
|
+
toolkit === undefined ? [] : Object.values(toolkit).filter(isProviderTool)
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Render tools to provider-agnostic descriptors. `ProviderTool`s are excluded:
|
|
408
|
+
* they are provider-hosted and must never leak into the function-declaration
|
|
409
|
+
* list — each adapter renders them natively via `providerToolsOf`. Every other
|
|
410
|
+
* kind is model-visible as a declaration.
|
|
411
|
+
*/
|
|
316
412
|
export const toDescriptors = (tools: ReadonlyArray<AnyTool>): ReadonlyArray<ToolDescriptor> =>
|
|
317
|
-
tools
|
|
318
|
-
|
|
319
|
-
|
|
413
|
+
tools
|
|
414
|
+
.filter((tool) => !isProviderTool(tool))
|
|
415
|
+
.map((tool) => {
|
|
416
|
+
const inputSchema = tool.inputSchema["~standard"].jsonSchema.input({
|
|
417
|
+
target: "draft-2020-12",
|
|
418
|
+
})
|
|
419
|
+
// `strict` only exists on local/provider kinds; signals/interactions omit it.
|
|
420
|
+
const strict = "strict" in tool ? tool.strict : undefined
|
|
421
|
+
return strict !== undefined
|
|
422
|
+
? { name: tool.name, description: tool.description, inputSchema, strict }
|
|
423
|
+
: { name: tool.name, description: tool.description, inputSchema }
|
|
320
424
|
})
|
|
321
|
-
// `strict` only exists on local/provider kinds; signals/interactions omit it.
|
|
322
|
-
const strict = "strict" in tool ? tool.strict : undefined
|
|
323
|
-
return strict !== undefined
|
|
324
|
-
? { name: tool.name, description: tool.description, inputSchema, strict }
|
|
325
|
-
: { name: tool.name, description: tool.description, inputSchema }
|
|
326
|
-
})
|
|
327
425
|
|
|
328
426
|
/**
|
|
329
427
|
* Render a `Toolkit` (a name-indexed record of tools) to descriptors, treating
|
package/src/tool/Toolkit.test.ts
CHANGED
|
@@ -252,6 +252,66 @@ describe("Toolkit.make / compose / namespace - uniqueness", () => {
|
|
|
252
252
|
})
|
|
253
253
|
})
|
|
254
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
|
+
|
|
255
315
|
describe("Toolkit.wrap - middleware", () => {
|
|
256
316
|
const Empty = Schema.Struct({})
|
|
257
317
|
type AuditShape = { readonly record: (name: string) => Effect.Effect<void> }
|
package/src/tool/Toolkit.ts
CHANGED
|
@@ -186,8 +186,8 @@ export class DuplicateToolName extends Data.TaggedError("DuplicateToolName")<{
|
|
|
186
186
|
type PrefixName<Prefix extends string, Name extends string> = `${Prefix}__${Name}`
|
|
187
187
|
|
|
188
188
|
type PrefixTool<Prefix extends string, T extends AnyTool> =
|
|
189
|
-
T extends Tool<infer N, infer I, infer Ev, infer O, infer R>
|
|
190
|
-
? 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>
|
|
191
191
|
: T extends ProviderTool<infer N, infer I, infer P, infer C>
|
|
192
192
|
? ProviderTool<PrefixName<Prefix, N>, I, P, C>
|
|
193
193
|
: T extends SignalTool<infer N, infer I>
|