@effect-uai/core 0.3.0 → 0.5.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-CBuPHVKA.d.mts → AiError-CAX_48RU.d.mts} +27 -5
- package/dist/{AiError-CBuPHVKA.d.mts.map → AiError-CAX_48RU.d.mts.map} +1 -1
- package/dist/Audio-BfCTGnH3.d.mts +61 -0
- package/dist/Audio-BfCTGnH3.d.mts.map +1 -0
- package/dist/{Image-BZmKfIdq.d.mts → Image-HNmMpMTh.d.mts} +1 -1
- package/dist/{Image-BZmKfIdq.d.mts.map → Image-HNmMpMTh.d.mts.map} +1 -1
- package/dist/{Items-CB8Bo3FI.d.mts → Items-DqbaJoz7.d.mts} +5 -5
- package/dist/{Items-CB8Bo3FI.d.mts.map → Items-DqbaJoz7.d.mts.map} +1 -1
- package/dist/{StructuredFormat-BWq5Hd1O.d.mts → StructuredFormat-BbN4dosH.d.mts} +11 -4
- package/dist/StructuredFormat-BbN4dosH.d.mts.map +1 -0
- package/dist/{Tool-DjVufH7i.d.mts → Tool-Y0__Py1H.d.mts} +20 -4
- package/dist/Tool-Y0__Py1H.d.mts.map +1 -0
- package/dist/Turn-ChbL2foc.d.mts +388 -0
- package/dist/Turn-ChbL2foc.d.mts.map +1 -0
- package/dist/domain/AiError.d.mts +2 -2
- package/dist/domain/AiError.mjs +19 -3
- package/dist/domain/AiError.mjs.map +1 -1
- package/dist/domain/Audio.d.mts +2 -0
- package/dist/domain/Audio.mjs +14 -0
- package/dist/domain/Audio.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 +1 -1
- package/dist/domain/Items.mjs.map +1 -1
- package/dist/domain/Music.d.mts +116 -0
- package/dist/domain/Music.d.mts.map +1 -0
- package/dist/domain/Music.mjs +29 -0
- package/dist/domain/Music.mjs.map +1 -0
- package/dist/domain/Transcript.d.mts +95 -0
- package/dist/domain/Transcript.d.mts.map +1 -0
- package/dist/domain/Transcript.mjs +22 -0
- package/dist/domain/Transcript.mjs.map +1 -0
- package/dist/domain/Turn.d.mts +2 -2
- package/dist/domain/Turn.mjs +22 -4
- package/dist/domain/Turn.mjs.map +1 -1
- package/dist/domain/Turn.test.d.mts +1 -0
- package/dist/domain/Turn.test.mjs +136 -0
- package/dist/domain/Turn.test.mjs.map +1 -0
- package/dist/embedding-model/Embedding.d.mts +15 -3
- package/dist/embedding-model/Embedding.d.mts.map +1 -1
- package/dist/embedding-model/Embedding.mjs.map +1 -1
- package/dist/embedding-model/EmbeddingModel.d.mts +33 -17
- package/dist/embedding-model/EmbeddingModel.d.mts.map +1 -1
- package/dist/embedding-model/EmbeddingModel.mjs.map +1 -1
- package/dist/embedding-model/EmbeddingModel.test.d.mts +1 -0
- package/dist/embedding-model/EmbeddingModel.test.mjs +59 -0
- package/dist/embedding-model/EmbeddingModel.test.mjs.map +1 -0
- package/dist/index.d.mts +13 -7
- package/dist/index.mjs +7 -1
- package/dist/language-model/LanguageModel.d.mts +30 -8
- package/dist/language-model/LanguageModel.d.mts.map +1 -1
- package/dist/language-model/LanguageModel.mjs +33 -3
- package/dist/language-model/LanguageModel.mjs.map +1 -1
- package/dist/language-model/LanguageModel.test.d.mts +1 -0
- package/dist/language-model/LanguageModel.test.mjs +143 -0
- package/dist/language-model/LanguageModel.test.mjs.map +1 -0
- package/dist/loop/Loop.d.mts +94 -11
- package/dist/loop/Loop.d.mts.map +1 -1
- package/dist/loop/Loop.mjs +92 -26
- package/dist/loop/Loop.mjs.map +1 -1
- package/dist/loop/Loop.test.mjs +171 -3
- package/dist/loop/Loop.test.mjs.map +1 -1
- package/dist/music-generator/MusicGenerator.d.mts +77 -0
- package/dist/music-generator/MusicGenerator.d.mts.map +1 -0
- package/dist/music-generator/MusicGenerator.mjs +51 -0
- package/dist/music-generator/MusicGenerator.mjs.map +1 -0
- package/dist/music-generator/MusicGenerator.test.d.mts +1 -0
- package/dist/music-generator/MusicGenerator.test.mjs +154 -0
- package/dist/music-generator/MusicGenerator.test.mjs.map +1 -0
- package/dist/observability/Metrics.d.mts +1 -1
- package/dist/observability/Metrics.mjs +1 -1
- package/dist/observability/Metrics.mjs.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +96 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts.map +1 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs +48 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.test.d.mts +1 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +112 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs.map +1 -0
- package/dist/streaming/JSONL.d.mts +10 -3
- package/dist/streaming/JSONL.d.mts.map +1 -1
- package/dist/streaming/JSONL.mjs +15 -9
- package/dist/streaming/JSONL.mjs.map +1 -1
- package/dist/structured-format/StructuredFormat.d.mts +2 -2
- package/dist/structured-format/StructuredFormat.mjs +9 -1
- package/dist/structured-format/StructuredFormat.mjs.map +1 -1
- package/dist/structured-format/StructuredFormat.test.d.mts +1 -0
- package/dist/structured-format/StructuredFormat.test.mjs +70 -0
- package/dist/structured-format/StructuredFormat.test.mjs.map +1 -0
- package/dist/testing/MockMusicGenerator.d.mts +39 -0
- package/dist/testing/MockMusicGenerator.d.mts.map +1 -0
- package/dist/testing/MockMusicGenerator.mjs +96 -0
- package/dist/testing/MockMusicGenerator.mjs.map +1 -0
- package/dist/testing/MockProvider.d.mts +23 -18
- package/dist/testing/MockProvider.d.mts.map +1 -1
- package/dist/testing/MockProvider.mjs +56 -72
- package/dist/testing/MockProvider.mjs.map +1 -1
- package/dist/testing/MockSpeechSynthesizer.d.mts +37 -0
- package/dist/testing/MockSpeechSynthesizer.d.mts.map +1 -0
- package/dist/testing/MockSpeechSynthesizer.mjs +95 -0
- package/dist/testing/MockSpeechSynthesizer.mjs.map +1 -0
- package/dist/testing/MockTranscriber.d.mts +37 -0
- package/dist/testing/MockTranscriber.d.mts.map +1 -0
- package/dist/testing/MockTranscriber.mjs +77 -0
- package/dist/testing/MockTranscriber.mjs.map +1 -0
- package/dist/tool/HistoryCheck.d.mts +1 -1
- package/dist/tool/Outcome.d.mts +1 -1
- package/dist/tool/Resolvers.d.mts +65 -8
- package/dist/tool/Resolvers.d.mts.map +1 -1
- package/dist/tool/Resolvers.mjs +8 -12
- package/dist/tool/Resolvers.mjs.map +1 -1
- package/dist/tool/Resolvers.test.mjs +6 -5
- package/dist/tool/Resolvers.test.mjs.map +1 -1
- package/dist/tool/Tool.d.mts +2 -2
- package/dist/tool/Tool.mjs +18 -1
- package/dist/tool/Tool.mjs.map +1 -1
- package/dist/tool/Tool.test.d.mts +1 -0
- package/dist/tool/Tool.test.mjs +66 -0
- package/dist/tool/Tool.test.mjs.map +1 -0
- package/dist/tool/Toolkit.d.mts +4 -6
- package/dist/tool/Toolkit.d.mts.map +1 -1
- package/dist/tool/Toolkit.mjs +14 -43
- package/dist/tool/Toolkit.mjs.map +1 -1
- package/dist/transcriber/Transcriber.d.mts +101 -0
- package/dist/transcriber/Transcriber.d.mts.map +1 -0
- package/dist/transcriber/Transcriber.mjs +49 -0
- package/dist/transcriber/Transcriber.mjs.map +1 -0
- package/dist/transcriber/Transcriber.test.d.mts +1 -0
- package/dist/transcriber/Transcriber.test.mjs +130 -0
- package/dist/transcriber/Transcriber.test.mjs.map +1 -0
- package/package.json +37 -1
- package/src/domain/AiError.ts +22 -1
- package/src/domain/Audio.ts +88 -0
- package/src/domain/Items.ts +1 -1
- package/src/domain/Music.ts +121 -0
- package/src/domain/Transcript.ts +83 -0
- package/src/domain/Turn.test.ts +141 -0
- package/src/domain/Turn.ts +50 -43
- package/src/embedding-model/Embedding.ts +23 -0
- package/src/embedding-model/EmbeddingModel.test.ts +92 -0
- package/src/embedding-model/EmbeddingModel.ts +30 -20
- package/src/index.ts +6 -0
- package/src/language-model/LanguageModel.test.ts +170 -0
- package/src/language-model/LanguageModel.ts +64 -1
- package/src/loop/Loop.test.ts +256 -3
- package/src/loop/Loop.ts +225 -49
- package/src/music-generator/MusicGenerator.test.ts +170 -0
- package/src/music-generator/MusicGenerator.ts +123 -0
- package/src/observability/Metrics.ts +1 -1
- package/src/speech-synthesizer/SpeechSynthesizer.test.ts +141 -0
- package/src/speech-synthesizer/SpeechSynthesizer.ts +131 -0
- package/src/streaming/JSONL.ts +16 -13
- package/src/structured-format/StructuredFormat.test.ts +105 -0
- package/src/structured-format/StructuredFormat.ts +14 -1
- package/src/testing/MockMusicGenerator.ts +168 -0
- package/src/testing/MockProvider.ts +126 -105
- package/src/testing/MockSpeechSynthesizer.ts +163 -0
- package/src/testing/MockTranscriber.ts +137 -0
- package/src/tool/Resolvers.test.ts +8 -5
- package/src/tool/Resolvers.ts +17 -19
- package/src/tool/Tool.test.ts +105 -0
- package/src/tool/Tool.ts +20 -0
- package/src/tool/Toolkit.ts +49 -50
- package/src/transcriber/Transcriber.test.ts +125 -0
- package/src/transcriber/Transcriber.ts +127 -0
- package/dist/StructuredFormat-BWq5Hd1O.d.mts.map +0 -1
- package/dist/Tool-DjVufH7i.d.mts.map +0 -1
- package/dist/Turn-OPaILVIB.d.mts +0 -194
- package/dist/Turn-OPaILVIB.d.mts.map +0 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Effect, Layer, Ref, Stream } from "effect"
|
|
2
|
+
import * as AiError from "../domain/AiError.js"
|
|
3
|
+
import type { TranscriptEvent, TranscriptResult } from "../domain/Transcript.js"
|
|
4
|
+
import {
|
|
5
|
+
SttStreaming,
|
|
6
|
+
Transcriber,
|
|
7
|
+
type CommonStreamTranscribeRequest,
|
|
8
|
+
type CommonTranscribeRequest,
|
|
9
|
+
type TranscriberService,
|
|
10
|
+
} from "../transcriber/Transcriber.js"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Recorder of every call made to the mock.
|
|
14
|
+
*/
|
|
15
|
+
export type MockTranscriberRecorder = {
|
|
16
|
+
readonly transcribeCalls: ReadonlyArray<CommonTranscribeRequest>
|
|
17
|
+
readonly streamCalls: ReadonlyArray<CommonStreamTranscribeRequest>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type MockTranscriberScript = {
|
|
21
|
+
/** One result per `transcribe` call, consumed in order. */
|
|
22
|
+
readonly transcripts?: ReadonlyArray<TranscriptResult>
|
|
23
|
+
/** One event-list per `streamTranscriptionFrom` call, consumed in order. */
|
|
24
|
+
readonly streams?: ReadonlyArray<ReadonlyArray<TranscriptEvent>>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const makeService = (
|
|
28
|
+
script: MockTranscriberScript,
|
|
29
|
+
record: {
|
|
30
|
+
readonly transcribe: (req: CommonTranscribeRequest) => Effect.Effect<void>
|
|
31
|
+
readonly stream: (req: CommonStreamTranscribeRequest) => Effect.Effect<void>
|
|
32
|
+
},
|
|
33
|
+
) =>
|
|
34
|
+
Effect.gen(function* () {
|
|
35
|
+
const tCursor = yield* Ref.make(0)
|
|
36
|
+
const sCursor = yield* Ref.make(0)
|
|
37
|
+
const service: TranscriberService = {
|
|
38
|
+
transcribe: (request) =>
|
|
39
|
+
Effect.gen(function* () {
|
|
40
|
+
yield* record.transcribe(request)
|
|
41
|
+
const i = yield* Ref.getAndUpdate(tCursor, (n) => n + 1)
|
|
42
|
+
const scripted = script.transcripts ?? []
|
|
43
|
+
if (i >= scripted.length) {
|
|
44
|
+
return yield* new AiError.InvalidRequest({
|
|
45
|
+
provider: "mock",
|
|
46
|
+
raw: `MockTranscriber exhausted: ${scripted.length} transcripts scripted, but call ${i + 1} was made`,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
return scripted[i]!
|
|
50
|
+
}),
|
|
51
|
+
streamTranscriptionFrom: <E, R>(
|
|
52
|
+
audioIn: Stream.Stream<Uint8Array, E, R>,
|
|
53
|
+
request: CommonStreamTranscribeRequest,
|
|
54
|
+
): Stream.Stream<TranscriptEvent, AiError.AiError | E, R> =>
|
|
55
|
+
Stream.unwrap(
|
|
56
|
+
Effect.gen(function* () {
|
|
57
|
+
yield* record.stream(request)
|
|
58
|
+
const i = yield* Ref.getAndUpdate(sCursor, (n) => n + 1)
|
|
59
|
+
const scripted = script.streams ?? []
|
|
60
|
+
if (i >= scripted.length) {
|
|
61
|
+
const exhausted: Stream.Stream<TranscriptEvent, AiError.AiError | E, R> = Stream.fail(
|
|
62
|
+
new AiError.InvalidRequest({
|
|
63
|
+
provider: "mock",
|
|
64
|
+
raw: `MockTranscriber exhausted: ${scripted.length} streams scripted, but call ${i + 1} was made`,
|
|
65
|
+
}),
|
|
66
|
+
)
|
|
67
|
+
return exhausted
|
|
68
|
+
}
|
|
69
|
+
// Drain the input audio fully before emitting the scripted events,
|
|
70
|
+
// so consumers can assert on what bytes were pushed.
|
|
71
|
+
return Stream.drain(audioIn).pipe(Stream.concat(Stream.fromIterable(scripted[i]!)))
|
|
72
|
+
}),
|
|
73
|
+
),
|
|
74
|
+
}
|
|
75
|
+
return service
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Returns a Layer that provides both the `Transcriber` service and the
|
|
80
|
+
* `SttStreaming` capability marker. Use when the code under test calls
|
|
81
|
+
* `streamTranscriptionFrom`.
|
|
82
|
+
*/
|
|
83
|
+
export const layer = (
|
|
84
|
+
script: MockTranscriberScript,
|
|
85
|
+
): {
|
|
86
|
+
readonly layer: Layer.Layer<Transcriber | SttStreaming>
|
|
87
|
+
readonly recorder: Effect.Effect<MockTranscriberRecorder>
|
|
88
|
+
} => {
|
|
89
|
+
const tCalls = Ref.makeUnsafe<ReadonlyArray<CommonTranscribeRequest>>([])
|
|
90
|
+
const sCalls = Ref.makeUnsafe<ReadonlyArray<CommonStreamTranscribeRequest>>([])
|
|
91
|
+
const transcriberLayer = Layer.effect(
|
|
92
|
+
Transcriber,
|
|
93
|
+
makeService(script, {
|
|
94
|
+
transcribe: (req) => Ref.update(tCalls, (xs) => [...xs, req]),
|
|
95
|
+
stream: (req) => Ref.update(sCalls, (xs) => [...xs, req]),
|
|
96
|
+
}),
|
|
97
|
+
)
|
|
98
|
+
const live = Layer.merge(transcriberLayer, Layer.succeed(SttStreaming, undefined))
|
|
99
|
+
return {
|
|
100
|
+
layer: live,
|
|
101
|
+
recorder: Effect.gen(function* () {
|
|
102
|
+
const transcribeCalls = yield* Ref.get(tCalls)
|
|
103
|
+
const streamCalls = yield* Ref.get(sCalls)
|
|
104
|
+
return { transcribeCalls, streamCalls }
|
|
105
|
+
}),
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Variant that omits the `SttStreaming` marker — use to test that
|
|
111
|
+
* consumers calling `streamTranscriptionFrom` fail to compile against
|
|
112
|
+
* a non-streaming provider.
|
|
113
|
+
*/
|
|
114
|
+
export const layerSyncOnly = (
|
|
115
|
+
script: MockTranscriberScript,
|
|
116
|
+
): {
|
|
117
|
+
readonly layer: Layer.Layer<Transcriber>
|
|
118
|
+
readonly recorder: Effect.Effect<MockTranscriberRecorder>
|
|
119
|
+
} => {
|
|
120
|
+
const tCalls = Ref.makeUnsafe<ReadonlyArray<CommonTranscribeRequest>>([])
|
|
121
|
+
const sCalls = Ref.makeUnsafe<ReadonlyArray<CommonStreamTranscribeRequest>>([])
|
|
122
|
+
const live = Layer.effect(
|
|
123
|
+
Transcriber,
|
|
124
|
+
makeService(script, {
|
|
125
|
+
transcribe: (req) => Ref.update(tCalls, (xs) => [...xs, req]),
|
|
126
|
+
stream: (req) => Ref.update(sCalls, (xs) => [...xs, req]),
|
|
127
|
+
}),
|
|
128
|
+
)
|
|
129
|
+
return {
|
|
130
|
+
layer: live,
|
|
131
|
+
recorder: Effect.gen(function* () {
|
|
132
|
+
const transcribeCalls = yield* Ref.get(tCalls)
|
|
133
|
+
const streamCalls = yield* Ref.get(sCalls)
|
|
134
|
+
return { transcribeCalls, streamCalls }
|
|
135
|
+
}),
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -22,8 +22,8 @@ import {
|
|
|
22
22
|
fromVerdictQueue,
|
|
23
23
|
} from "./Resolvers.js"
|
|
24
24
|
import { fromEffectSchema, make as makeTool, streaming } from "./Tool.js"
|
|
25
|
-
import { executeAll
|
|
26
|
-
import {
|
|
25
|
+
import { executeAll } from "./Toolkit.js"
|
|
26
|
+
import { ToolEvent, isApprovalRequested, isIntermediate, isOutput } from "./ToolEvent.js"
|
|
27
27
|
|
|
28
28
|
// ---------------------------------------------------------------------------
|
|
29
29
|
// Three demo tools covering the matrix:
|
|
@@ -91,15 +91,18 @@ const resultsFrom = (collected: ReadonlyArray<ToolEvent>): ReadonlyArray<ToolRes
|
|
|
91
91
|
|
|
92
92
|
const byCallId = (results: ReadonlyArray<ToolResult>) => new Map(results.map((r) => [r.call_id, r]))
|
|
93
93
|
|
|
94
|
+
const rejectedStream = (rejected: ReadonlyArray<ToolResult>) =>
|
|
95
|
+
Stream.fromIterable(rejected.map((result) => ToolEvent.Output({ result })))
|
|
96
|
+
|
|
94
97
|
const eventsFromApprovalMap = (approvals: ReadonlyMap<string, ApprovalMapEntry>) => {
|
|
95
98
|
const plan = fromApprovalMap(isSensitive, approvals)(calls)
|
|
96
|
-
return Stream.merge(executeAll(allTools, plan.approved),
|
|
99
|
+
return Stream.merge(executeAll(allTools, plan.approved), rejectedStream(plan.rejected))
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
const eventsFromDecision = (decision: ToolCallDecision): Stream.Stream<ToolEvent> =>
|
|
100
103
|
decision._tag === "Approved"
|
|
101
104
|
? executeAll(allTools, [decision.call])
|
|
102
|
-
: Stream.succeed(
|
|
105
|
+
: Stream.succeed(ToolEvent.Output({ result: decision.result }))
|
|
103
106
|
|
|
104
107
|
// ---------------------------------------------------------------------------
|
|
105
108
|
// fromApprovalMap: HTTP-style scenarios
|
|
@@ -189,7 +192,7 @@ describe("executeAll: graceful degradation", () => {
|
|
|
189
192
|
isSensitive,
|
|
190
193
|
new Map([["c3", { decision: "approve" }]]),
|
|
191
194
|
)(callsWithBogus)
|
|
192
|
-
return Stream.merge(executeAll(allTools, plan.approved),
|
|
195
|
+
return Stream.merge(executeAll(allTools, plan.approved), rejectedStream(plan.rejected))
|
|
193
196
|
})(),
|
|
194
197
|
),
|
|
195
198
|
)
|
package/src/tool/Resolvers.ts
CHANGED
|
@@ -5,29 +5,27 @@
|
|
|
5
5
|
* results must be returned to the model. Tool execution stays explicit at
|
|
6
6
|
* the recipe boundary via `Toolkit.executeAll`.
|
|
7
7
|
*/
|
|
8
|
-
import { Deferred, Effect, Queue, Scope, Stream } from "effect"
|
|
8
|
+
import { Data, Deferred, Effect, Queue, Scope, Stream } from "effect"
|
|
9
9
|
import type { FunctionCall } from "../domain/Items.js"
|
|
10
10
|
import { type ToolResult, cancelled, denied } from "./Outcome.js"
|
|
11
|
-
import
|
|
11
|
+
import { ToolEvent } from "./ToolEvent.js"
|
|
12
12
|
|
|
13
13
|
export type ToolCallPlan = {
|
|
14
14
|
readonly approved: ReadonlyArray<FunctionCall>
|
|
15
15
|
readonly rejected: ReadonlyArray<ToolResult>
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export type ToolCallDecision =
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
export type ToolCallDecision = Data.TaggedEnum<{
|
|
19
|
+
Approved: { readonly call: FunctionCall }
|
|
20
|
+
Rejected: { readonly result: ToolResult }
|
|
21
|
+
}>
|
|
21
22
|
|
|
22
|
-
export const
|
|
23
|
-
_tag: "Approved",
|
|
24
|
-
call,
|
|
25
|
-
})
|
|
23
|
+
export const ToolCallDecision = Data.taggedEnum<ToolCallDecision>()
|
|
26
24
|
|
|
27
|
-
export const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
})
|
|
25
|
+
export const approve = (call: FunctionCall): ToolCallDecision => ToolCallDecision.Approved({ call })
|
|
26
|
+
|
|
27
|
+
export const reject = (result: ToolResult): ToolCallDecision =>
|
|
28
|
+
ToolCallDecision.Rejected({ result })
|
|
31
29
|
|
|
32
30
|
export const splitToolCallDecisions = (decisions: ReadonlyArray<ToolCallDecision>): ToolCallPlan =>
|
|
33
31
|
decisions.reduce<ToolCallPlan>(
|
|
@@ -38,12 +36,12 @@ export const splitToolCallDecisions = (decisions: ReadonlyArray<ToolCallDecision
|
|
|
38
36
|
{ approved: [], rejected: [] },
|
|
39
37
|
)
|
|
40
38
|
|
|
41
|
-
export const approvalRequested = (call: FunctionCall): ToolEvent =>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
})
|
|
39
|
+
export const approvalRequested = (call: FunctionCall): ToolEvent =>
|
|
40
|
+
ToolEvent.ApprovalRequested({
|
|
41
|
+
call_id: call.call_id,
|
|
42
|
+
tool: call.name,
|
|
43
|
+
arguments: call.arguments,
|
|
44
|
+
})
|
|
47
45
|
|
|
48
46
|
// ---------------------------------------------------------------------------
|
|
49
47
|
// Verdict queue (WebSocket-style transport).
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import { describe, expect, expectTypeOf, it } from "vitest"
|
|
4
|
+
import * as Tool from "./Tool.js"
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Minimal dual-standard schema for testing — no need to pull Zod / Valibot /
|
|
8
|
+
// ArkType as devDeps. This is the smallest possible object satisfying both
|
|
9
|
+
// `StandardSchemaV1` and `StandardJSONSchemaV1` per their published specs.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
type EmailRecipient = { readonly to: string }
|
|
13
|
+
|
|
14
|
+
const emailRecipientSchema: StandardSchemaV1<unknown, EmailRecipient> &
|
|
15
|
+
StandardJSONSchemaV1<unknown, EmailRecipient> = {
|
|
16
|
+
"~standard": {
|
|
17
|
+
version: 1,
|
|
18
|
+
vendor: "test-fixture",
|
|
19
|
+
validate: (value) => {
|
|
20
|
+
if (
|
|
21
|
+
typeof value === "object" &&
|
|
22
|
+
value !== null &&
|
|
23
|
+
"to" in value &&
|
|
24
|
+
typeof (value as { to: unknown }).to === "string"
|
|
25
|
+
) {
|
|
26
|
+
return { value: value as EmailRecipient }
|
|
27
|
+
}
|
|
28
|
+
return { issues: [{ message: "expected { to: string }" }] }
|
|
29
|
+
},
|
|
30
|
+
jsonSchema: {
|
|
31
|
+
input: () => ({
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: { to: { type: "string" } },
|
|
34
|
+
required: ["to"],
|
|
35
|
+
}),
|
|
36
|
+
output: () => ({
|
|
37
|
+
type: "object",
|
|
38
|
+
properties: { to: { type: "string" } },
|
|
39
|
+
required: ["to"],
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// A schema that satisfies StandardSchemaV1 only (no JSON Schema). Used to
|
|
46
|
+
// verify the helper's compile-time guard.
|
|
47
|
+
const standardOnly: StandardSchemaV1<unknown, EmailRecipient> = {
|
|
48
|
+
"~standard": {
|
|
49
|
+
version: 1,
|
|
50
|
+
vendor: "test-fixture",
|
|
51
|
+
validate: () => ({ value: { to: "" } }),
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("Tool.fromStandardSchema", () => {
|
|
56
|
+
it("returns the schema (structurally) typed as ToolInputSchema<Output>", () => {
|
|
57
|
+
const adapted = Tool.fromStandardSchema(emailRecipientSchema)
|
|
58
|
+
|
|
59
|
+
// Same object — helper is a type-narrowing identity at runtime.
|
|
60
|
+
expect(adapted).toBe(emailRecipientSchema)
|
|
61
|
+
|
|
62
|
+
// Both interfaces accessible through the same `~standard` key.
|
|
63
|
+
const valid = adapted["~standard"].validate({ to: "hi@example.com" })
|
|
64
|
+
expect(valid).toEqual({ value: { to: "hi@example.com" } })
|
|
65
|
+
|
|
66
|
+
const json = adapted["~standard"].jsonSchema.input({ target: "draft-2020-12" })
|
|
67
|
+
expect(json).toEqual({
|
|
68
|
+
type: "object",
|
|
69
|
+
properties: { to: { type: "string" } },
|
|
70
|
+
required: ["to"],
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("composes with Tool.make so Input is inferred from the schema's Output", async () => {
|
|
75
|
+
const sendEmail = Tool.make({
|
|
76
|
+
name: "send_email",
|
|
77
|
+
description: "Send an email to a single recipient.",
|
|
78
|
+
inputSchema: Tool.fromStandardSchema(emailRecipientSchema),
|
|
79
|
+
run: ({ to }) => Effect.succeed(`queued: ${to}`),
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// `run`'s parameter is typed as { to: string } via the schema's Output —
|
|
83
|
+
// this property access compiles without annotation.
|
|
84
|
+
const result = await Effect.runPromise(sendEmail.run({ to: "x@y.z" }))
|
|
85
|
+
expect(result).toBe("queued: x@y.z")
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it("type: rejects schemas missing the Standard JSON Schema half at compile time", () => {
|
|
89
|
+
// @ts-expect-error — `standardOnly` lacks `jsonSchema`; helper's
|
|
90
|
+
// intersection constraint refuses it.
|
|
91
|
+
Tool.fromStandardSchema(standardOnly)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it("type: Output type flows through fromStandardSchema into Tool.make", () => {
|
|
95
|
+
const tool = Tool.make({
|
|
96
|
+
name: "send_email",
|
|
97
|
+
description: "send",
|
|
98
|
+
inputSchema: Tool.fromStandardSchema(emailRecipientSchema),
|
|
99
|
+
run: (input) => Effect.succeed(input),
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
type InputOf<T> = T extends Tool.Tool<string, infer I, unknown, never> ? I : never
|
|
103
|
+
expectTypeOf<InputOf<typeof tool>>().toEqualTypeOf<EmailRecipient>()
|
|
104
|
+
})
|
|
105
|
+
})
|
package/src/tool/Tool.ts
CHANGED
|
@@ -36,6 +36,26 @@ export const fromEffectSchema = <S extends Schema.Codec<any, any, never, any>>(
|
|
|
36
36
|
Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(schema)) as unknown as S &
|
|
37
37
|
ToolInputSchema<S["Type"]>
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Use any schema library that implements both Standard Schema (validation)
|
|
41
|
+
* and Standard JSON Schema (JSON Schema generation) as a `Tool.inputSchema`.
|
|
42
|
+
* Covers Zod 4.2+, Valibot 1.2+, and ArkType 2.1.28+ in one helper.
|
|
43
|
+
*
|
|
44
|
+
* Effect Schema doesn't implement Standard JSON Schema natively — use
|
|
45
|
+
* `fromEffectSchema` for those.
|
|
46
|
+
*
|
|
47
|
+
* The intersection constraint catches missing interfaces at compile time:
|
|
48
|
+
* a Zod v3 schema (no Standard JSON Schema) produces a precise type error
|
|
49
|
+
* pointing at the missing interface rather than a runtime surprise. The
|
|
50
|
+
* helper itself is a thin type-narrowing identity — schemas that satisfy
|
|
51
|
+
* both standards already structurally satisfy `ToolInputSchema`; the
|
|
52
|
+
* helper makes the input type inference explicit at the call site.
|
|
53
|
+
*/
|
|
54
|
+
export const fromStandardSchema = <S extends StandardSchemaV1 & StandardJSONSchemaV1>(
|
|
55
|
+
schema: S,
|
|
56
|
+
): S & ToolInputSchema<StandardSchemaV1.InferOutput<S>> =>
|
|
57
|
+
schema as S & ToolInputSchema<StandardSchemaV1.InferOutput<S>>
|
|
58
|
+
|
|
39
59
|
export type Tool<Name extends string, Input, Output, R = never> = {
|
|
40
60
|
readonly name: Name
|
|
41
61
|
readonly description: string
|
package/src/tool/Toolkit.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Array as Arr, Effect, Function, Ref, Stream } from "effect"
|
|
1
|
+
import { Array as Arr, Effect, Function, Ref, Schema, Stream } from "effect"
|
|
2
2
|
import * as Loop from "../loop/Loop.js"
|
|
3
3
|
import type { FunctionCall } from "../domain/Items.js"
|
|
4
4
|
import {
|
|
@@ -10,8 +10,8 @@ import {
|
|
|
10
10
|
type Tool,
|
|
11
11
|
type ToolDescriptor,
|
|
12
12
|
} from "./Tool.js"
|
|
13
|
-
import {
|
|
14
|
-
import
|
|
13
|
+
import { ToolResult, executionError, rejected } from "./Outcome.js"
|
|
14
|
+
import { ToolEvent } from "./ToolEvent.js"
|
|
15
15
|
import { isOutput } from "./ToolEvent.js"
|
|
16
16
|
|
|
17
17
|
export type AnyTool = Tool<string, any, any, any>
|
|
@@ -52,7 +52,12 @@ export const toDescriptors = <Tools extends ReadonlyArray<AnyTool>>(
|
|
|
52
52
|
target: "draft-2020-12",
|
|
53
53
|
})
|
|
54
54
|
return tool.strict !== undefined
|
|
55
|
-
? {
|
|
55
|
+
? {
|
|
56
|
+
name: tool.name,
|
|
57
|
+
description: tool.description,
|
|
58
|
+
inputSchema,
|
|
59
|
+
strict: tool.strict,
|
|
60
|
+
}
|
|
56
61
|
: { name: tool.name, description: tool.description, inputSchema }
|
|
57
62
|
})
|
|
58
63
|
|
|
@@ -78,17 +83,12 @@ export const executeAll = <Tools extends ReadonlyArray<AnyKindTool<any>>>(
|
|
|
78
83
|
}),
|
|
79
84
|
)
|
|
80
85
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
_tag: "Value",
|
|
88
|
-
call_id: call.call_id,
|
|
89
|
-
tool,
|
|
90
|
-
value,
|
|
91
|
-
})
|
|
86
|
+
const valueResult = (call: FunctionCall, tool: string, value: unknown): ToolResult =>
|
|
87
|
+
ToolResult.Value({
|
|
88
|
+
call_id: call.call_id,
|
|
89
|
+
tool,
|
|
90
|
+
value,
|
|
91
|
+
})
|
|
92
92
|
|
|
93
93
|
const runOne = <R>(
|
|
94
94
|
tools: ReadonlyArray<AnyKindTool<R>>,
|
|
@@ -98,25 +98,27 @@ const runOne = <R>(
|
|
|
98
98
|
if (tool === undefined) {
|
|
99
99
|
// Graceful: emit a synthetic Failure so OTHER calls in this turn
|
|
100
100
|
// still execute. LLMs hallucinate tool names; MCP tools come and go.
|
|
101
|
-
return Stream.succeed
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
return Stream.succeed(
|
|
102
|
+
ToolEvent.Output({
|
|
103
|
+
result: rejected(call, "unknown_tool", `No tool registered with name "${call.name}"`),
|
|
104
|
+
}),
|
|
105
|
+
)
|
|
105
106
|
}
|
|
106
107
|
if (isStreamingTool(tool)) return runStreaming(tool, call)
|
|
107
108
|
return runPlain(tool, call)
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
const parseJsonUnknown = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown))
|
|
112
|
+
|
|
110
113
|
const runPlain = <R>(
|
|
111
114
|
tool: AnyPlainTool<R>,
|
|
112
115
|
call: FunctionCall,
|
|
113
116
|
): Stream.Stream<ToolEvent, never, R> =>
|
|
114
117
|
Stream.fromEffect(
|
|
115
118
|
Effect.gen(function* () {
|
|
116
|
-
const parsed = yield*
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
})
|
|
119
|
+
const parsed = yield* parseJsonUnknown(call.arguments).pipe(
|
|
120
|
+
Effect.mapError(() => "json_parse_error" as const),
|
|
121
|
+
)
|
|
120
122
|
const validated = yield* Effect.tryPromise({
|
|
121
123
|
try: () => Promise.resolve(tool.inputSchema["~standard"].validate(parsed)),
|
|
122
124
|
catch: () => "validation_threw" as const,
|
|
@@ -128,7 +130,7 @@ const runPlain = <R>(
|
|
|
128
130
|
return valueResult(call, tool.name, output)
|
|
129
131
|
}).pipe(
|
|
130
132
|
Effect.catchCause(() => Effect.succeed(executionError(call, "Tool execution failed"))),
|
|
131
|
-
Effect.map((result) => ({
|
|
133
|
+
Effect.map((result) => ToolEvent.Output({ result })),
|
|
132
134
|
),
|
|
133
135
|
)
|
|
134
136
|
|
|
@@ -138,19 +140,19 @@ const runStreaming = <R>(
|
|
|
138
140
|
): Stream.Stream<ToolEvent, never, R> =>
|
|
139
141
|
Stream.unwrap(
|
|
140
142
|
Effect.gen(function* () {
|
|
141
|
-
const parsed = yield*
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
})
|
|
143
|
+
const parsed = yield* parseJsonUnknown(call.arguments).pipe(
|
|
144
|
+
Effect.mapError(() => "json_parse_error" as const),
|
|
145
|
+
)
|
|
145
146
|
const validated = yield* Effect.tryPromise({
|
|
146
147
|
try: () => Promise.resolve(tool.inputSchema["~standard"].validate(parsed)),
|
|
147
148
|
catch: () => "validation_threw" as const,
|
|
148
149
|
})
|
|
149
150
|
if (validated.issues !== undefined) {
|
|
150
|
-
return Stream.succeed<ToolEvent>(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
return Stream.succeed<ToolEvent>(
|
|
152
|
+
ToolEvent.Output({
|
|
153
|
+
result: executionError(call, "Tool input failed schema validation"),
|
|
154
|
+
}),
|
|
155
|
+
)
|
|
154
156
|
}
|
|
155
157
|
|
|
156
158
|
// Real-time: tap each event into a Ref as it flows; emit one
|
|
@@ -159,24 +161,20 @@ const runStreaming = <R>(
|
|
|
159
161
|
const ref = yield* Ref.make<Array<unknown>>([])
|
|
160
162
|
const intermediates = tool.run(validated.value).pipe(
|
|
161
163
|
Stream.tap((event) => Ref.update(ref, Arr.append(event))),
|
|
162
|
-
Stream.map(
|
|
163
|
-
(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
data,
|
|
169
|
-
}) satisfies ToolEvent,
|
|
164
|
+
Stream.map((data) =>
|
|
165
|
+
ToolEvent.Intermediate({
|
|
166
|
+
call_id: call.call_id,
|
|
167
|
+
tool: tool.name,
|
|
168
|
+
data,
|
|
169
|
+
}),
|
|
170
170
|
),
|
|
171
171
|
)
|
|
172
172
|
const output = Stream.fromEffect(
|
|
173
173
|
Ref.get(ref).pipe(
|
|
174
|
-
Effect.map(
|
|
175
|
-
(
|
|
176
|
-
(
|
|
177
|
-
|
|
178
|
-
result: valueResult(call, tool.name, tool.finalize(events)),
|
|
179
|
-
}) satisfies ToolEvent,
|
|
174
|
+
Effect.map((events) =>
|
|
175
|
+
ToolEvent.Output({
|
|
176
|
+
result: valueResult(call, tool.name, tool.finalize(events)),
|
|
177
|
+
}),
|
|
180
178
|
),
|
|
181
179
|
),
|
|
182
180
|
)
|
|
@@ -184,10 +182,11 @@ const runStreaming = <R>(
|
|
|
184
182
|
}),
|
|
185
183
|
).pipe(
|
|
186
184
|
Stream.catchCause(() =>
|
|
187
|
-
Stream.succeed
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
185
|
+
Stream.succeed(
|
|
186
|
+
ToolEvent.Output({
|
|
187
|
+
result: executionError(call, "Tool execution failed"),
|
|
188
|
+
}),
|
|
189
|
+
),
|
|
191
190
|
),
|
|
192
191
|
)
|
|
193
192
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Effect, Stream } from "effect"
|
|
2
|
+
import { describe, expect, expectTypeOf, it } from "vitest"
|
|
3
|
+
import type * as AiError from "../domain/AiError.js"
|
|
4
|
+
import type { TranscriptEvent, TranscriptResult } from "../domain/Transcript.js"
|
|
5
|
+
import * as MockTranscriber from "../testing/MockTranscriber.js"
|
|
6
|
+
import * as Transcriber from "./Transcriber.js"
|
|
7
|
+
|
|
8
|
+
describe("Transcriber.transcribe", () => {
|
|
9
|
+
it("returns the scripted TranscriptResult", async () => {
|
|
10
|
+
const mock = MockTranscriber.layer({
|
|
11
|
+
transcripts: [{ text: "hello world", durationSeconds: 1.23 }],
|
|
12
|
+
})
|
|
13
|
+
const program = Transcriber.transcribe({
|
|
14
|
+
audio: { _tag: "bytes", bytes: new Uint8Array([0]), mimeType: "audio/wav" },
|
|
15
|
+
model: "mock-stt",
|
|
16
|
+
})
|
|
17
|
+
const result = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)))
|
|
18
|
+
expect(result.text).toBe("hello world")
|
|
19
|
+
expect(result.durationSeconds).toBe(1.23)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it("records each transcribe call", async () => {
|
|
23
|
+
const mock = MockTranscriber.layer({
|
|
24
|
+
transcripts: [{ text: "a" }, { text: "b" }],
|
|
25
|
+
})
|
|
26
|
+
const program = Effect.gen(function* () {
|
|
27
|
+
yield* Transcriber.transcribe({
|
|
28
|
+
audio: { _tag: "bytes", bytes: new Uint8Array([1]), mimeType: "audio/wav" },
|
|
29
|
+
model: "m1",
|
|
30
|
+
})
|
|
31
|
+
yield* Transcriber.transcribe({
|
|
32
|
+
audio: { _tag: "bytes", bytes: new Uint8Array([2]), mimeType: "audio/wav" },
|
|
33
|
+
model: "m2",
|
|
34
|
+
})
|
|
35
|
+
return yield* mock.recorder
|
|
36
|
+
})
|
|
37
|
+
const rec = await Effect.runPromise(program.pipe(Effect.provide(mock.layer)))
|
|
38
|
+
expect(rec.transcribeCalls.map((c) => c.model)).toEqual(["m1", "m2"])
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe("Transcriber capability marker (compile-time)", () => {
|
|
43
|
+
const sttReq: Transcriber.CommonStreamTranscribeRequest = {
|
|
44
|
+
model: "mock-stt",
|
|
45
|
+
inputFormat: { container: "raw", encoding: "pcm_s16le", sampleRate: 16000 },
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
it("requires `SttStreaming` on the R channel of streamTranscriptionFrom", () => {
|
|
49
|
+
const audio: Stream.Stream<Uint8Array> = Stream.fromIterable([new Uint8Array([0])])
|
|
50
|
+
const events = audio.pipe(Transcriber.streamTranscriptionFrom(sttReq))
|
|
51
|
+
expectTypeOf(events).toEqualTypeOf<
|
|
52
|
+
Stream.Stream<
|
|
53
|
+
TranscriptEvent,
|
|
54
|
+
AiError.AiError,
|
|
55
|
+
Transcriber.Transcriber | Transcriber.SttStreaming
|
|
56
|
+
>
|
|
57
|
+
>()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it("does NOT require `SttStreaming` for sync `transcribe`", () => {
|
|
61
|
+
const eff = Transcriber.transcribe({
|
|
62
|
+
audio: { _tag: "bytes", bytes: new Uint8Array([0]), mimeType: "audio/wav" },
|
|
63
|
+
model: "m",
|
|
64
|
+
})
|
|
65
|
+
expectTypeOf(eff).toEqualTypeOf<
|
|
66
|
+
Effect.Effect<TranscriptResult, AiError.AiError, Transcriber.Transcriber>
|
|
67
|
+
>()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it("a sync-only layer leaves `SttStreaming` unsatisfied in R", () => {
|
|
71
|
+
const syncOnly = MockTranscriber.layerSyncOnly({})
|
|
72
|
+
const audio: Stream.Stream<Uint8Array> = Stream.fromIterable([new Uint8Array([0])])
|
|
73
|
+
const events = audio.pipe(Transcriber.streamTranscriptionFrom(sttReq))
|
|
74
|
+
const program = Stream.runDrain(events).pipe(Effect.provide(syncOnly.layer))
|
|
75
|
+
// `Transcriber` is provided by syncOnly.layer; `SttStreaming` is not.
|
|
76
|
+
expectTypeOf(program).toEqualTypeOf<
|
|
77
|
+
Effect.Effect<void, AiError.AiError, Transcriber.SttStreaming>
|
|
78
|
+
>()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("a full layer (with marker) clears R to never", () => {
|
|
82
|
+
const fullMock = MockTranscriber.layer({ streams: [[]] })
|
|
83
|
+
const audio: Stream.Stream<Uint8Array> = Stream.fromIterable([new Uint8Array([0])])
|
|
84
|
+
const events = audio.pipe(Transcriber.streamTranscriptionFrom(sttReq))
|
|
85
|
+
const program = Stream.runDrain(events).pipe(Effect.provide(fullMock.layer))
|
|
86
|
+
expectTypeOf(program).toEqualTypeOf<Effect.Effect<void, AiError.AiError, never>>()
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe("Transcriber.streamTranscriptionFrom", () => {
|
|
91
|
+
const sttReq: Transcriber.CommonStreamTranscribeRequest = {
|
|
92
|
+
model: "mock-stt",
|
|
93
|
+
inputFormat: { container: "raw", encoding: "pcm_s16le", sampleRate: 16000 },
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
it("emits scripted events after draining the input audio stream", async () => {
|
|
97
|
+
const mock = MockTranscriber.layer({
|
|
98
|
+
streams: [
|
|
99
|
+
[
|
|
100
|
+
{ _tag: "partial", text: "hello" },
|
|
101
|
+
{ _tag: "final", text: "hello world" },
|
|
102
|
+
],
|
|
103
|
+
],
|
|
104
|
+
})
|
|
105
|
+
const audio = Stream.fromIterable([new Uint8Array([0, 1, 2]), new Uint8Array([3, 4, 5])])
|
|
106
|
+
const events = audio.pipe(Transcriber.streamTranscriptionFrom(sttReq))
|
|
107
|
+
const collected = await Effect.runPromise(
|
|
108
|
+
Stream.runCollect(events).pipe(Effect.provide(mock.layer)),
|
|
109
|
+
)
|
|
110
|
+
expect(collected).toEqual([
|
|
111
|
+
{ _tag: "partial", text: "hello" },
|
|
112
|
+
{ _tag: "final", text: "hello world" },
|
|
113
|
+
])
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it("works data-first (direct call) as well as pipeable (data-last)", async () => {
|
|
117
|
+
const mock = MockTranscriber.layer({
|
|
118
|
+
streams: [[{ _tag: "final", text: "x" }]],
|
|
119
|
+
})
|
|
120
|
+
const audio = Stream.fromIterable([new Uint8Array([0])])
|
|
121
|
+
const events = Transcriber.streamTranscriptionFrom(audio, sttReq)
|
|
122
|
+
const out = await Effect.runPromise(Stream.runCollect(events).pipe(Effect.provide(mock.layer)))
|
|
123
|
+
expect(out).toEqual([{ _tag: "final", text: "x" }])
|
|
124
|
+
})
|
|
125
|
+
})
|