@effect-uai/core 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +30 -16
  2. package/dist/{AiError-eLV9t1lU.d.mts → AiError-DvhLIbmd.d.mts} +15 -4
  3. package/dist/{AiError-eLV9t1lU.d.mts.map → AiError-DvhLIbmd.d.mts.map} +1 -1
  4. package/dist/{Loop-B54AB1ME.d.mts → Loop-ChpaO-ya.d.mts} +2 -2
  5. package/dist/{Loop-B54AB1ME.d.mts.map → Loop-ChpaO-ya.d.mts.map} +1 -1
  6. package/dist/MessengerError-CeK6VAcu.d.mts +64 -0
  7. package/dist/MessengerError-CeK6VAcu.d.mts.map +1 -0
  8. package/dist/{Toolkit-DAjJ9Tvl.d.mts → Toolkit-tlL0F2YR.d.mts} +2 -2
  9. package/dist/{Toolkit-DAjJ9Tvl.d.mts.map → Toolkit-tlL0F2YR.d.mts.map} +1 -1
  10. package/dist/chunker/Chunker.d.mts +1 -1
  11. package/dist/domain/AiError.d.mts +2 -2
  12. package/dist/domain/AiError.mjs +9 -2
  13. package/dist/domain/AiError.mjs.map +1 -1
  14. package/dist/domain/Realtime.d.mts +616 -0
  15. package/dist/domain/Realtime.d.mts.map +1 -0
  16. package/dist/domain/Realtime.mjs +21 -0
  17. package/dist/domain/Realtime.mjs.map +1 -0
  18. package/dist/domain/Transcript.d.mts +17 -3
  19. package/dist/domain/Transcript.d.mts.map +1 -1
  20. package/dist/domain/Transcript.mjs +30 -1
  21. package/dist/domain/Transcript.mjs.map +1 -1
  22. package/dist/domain/Transcript.test.d.mts +1 -0
  23. package/dist/domain/Transcript.test.mjs +110 -0
  24. package/dist/domain/Transcript.test.mjs.map +1 -0
  25. package/dist/embedding-model/EmbeddingModel.d.mts +1 -1
  26. package/dist/image-generator/ImageGenerator.d.mts +1 -1
  27. package/dist/index.d.mts +11 -4
  28. package/dist/index.mjs +9 -2
  29. package/dist/job/Job.d.mts +1 -1
  30. package/dist/language-model/LanguageModel.d.mts +2 -2
  31. package/dist/loop/Loop.d.mts +1 -1
  32. package/dist/messenger/Messenger.d.mts +472 -0
  33. package/dist/messenger/Messenger.d.mts.map +1 -0
  34. package/dist/messenger/Messenger.mjs +66 -0
  35. package/dist/messenger/Messenger.mjs.map +1 -0
  36. package/dist/messenger/MessengerAdapter.d.mts +35 -0
  37. package/dist/messenger/MessengerAdapter.d.mts.map +1 -0
  38. package/dist/messenger/MessengerAdapter.mjs +102 -0
  39. package/dist/messenger/MessengerAdapter.mjs.map +1 -0
  40. package/dist/messenger/MessengerAdapter.test.d.mts +1 -0
  41. package/dist/messenger/MessengerAdapter.test.mjs +138 -0
  42. package/dist/messenger/MessengerAdapter.test.mjs.map +1 -0
  43. package/dist/messenger/MessengerError.d.mts +2 -0
  44. package/dist/messenger/MessengerError.mjs +39 -0
  45. package/dist/messenger/MessengerError.mjs.map +1 -0
  46. package/dist/music-generator/MusicGenerator.d.mts +1 -1
  47. package/dist/realtime/RealtimeSession.d.mts +50 -0
  48. package/dist/realtime/RealtimeSession.d.mts.map +1 -0
  49. package/dist/realtime/RealtimeSession.mjs +45 -0
  50. package/dist/realtime/RealtimeSession.mjs.map +1 -0
  51. package/dist/realtime/RealtimeSession.test.d.mts +1 -0
  52. package/dist/realtime/RealtimeSession.test.mjs +95 -0
  53. package/dist/realtime/RealtimeSession.test.mjs.map +1 -0
  54. package/dist/reranker/Reranker.d.mts +1 -1
  55. package/dist/research/DeepResearch.d.mts +1 -1
  56. package/dist/retry/Retry.d.mts +1 -1
  57. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +1 -1
  58. package/dist/streaming/Settle.d.mts +45 -0
  59. package/dist/streaming/Settle.d.mts.map +1 -0
  60. package/dist/streaming/Settle.mjs +95 -0
  61. package/dist/streaming/Settle.mjs.map +1 -0
  62. package/dist/streaming/Settle.test.d.mts +1 -0
  63. package/dist/streaming/Settle.test.mjs +252 -0
  64. package/dist/streaming/Settle.test.mjs.map +1 -0
  65. package/dist/streaming/WebSocketSession.d.mts +36 -0
  66. package/dist/streaming/WebSocketSession.d.mts.map +1 -0
  67. package/dist/streaming/WebSocketSession.mjs +89 -0
  68. package/dist/streaming/WebSocketSession.mjs.map +1 -0
  69. package/dist/streaming/WebSocketSession.test.d.mts +1 -0
  70. package/dist/streaming/WebSocketSession.test.mjs +51 -0
  71. package/dist/streaming/WebSocketSession.test.mjs.map +1 -0
  72. package/dist/testing/FakeWebSocket.d.mts +25 -0
  73. package/dist/testing/FakeWebSocket.d.mts.map +1 -0
  74. package/dist/testing/FakeWebSocket.mjs +79 -0
  75. package/dist/testing/FakeWebSocket.mjs.map +1 -0
  76. package/dist/testing/MockMessenger.d.mts +192 -0
  77. package/dist/testing/MockMessenger.d.mts.map +1 -0
  78. package/dist/testing/MockMessenger.mjs +76 -0
  79. package/dist/testing/MockMessenger.mjs.map +1 -0
  80. package/dist/testing/MockRealtimeSession.d.mts +30 -0
  81. package/dist/testing/MockRealtimeSession.d.mts.map +1 -0
  82. package/dist/testing/MockRealtimeSession.mjs +52 -0
  83. package/dist/testing/MockRealtimeSession.mjs.map +1 -0
  84. package/dist/tool/Toolkit.d.mts +1 -1
  85. package/dist/transcriber/Transcriber.d.mts +1 -1
  86. package/dist/web-read/WebRead.d.mts +1 -1
  87. package/dist/web-read/WebReadTool.d.mts +1 -1
  88. package/dist/web-search/WebSearch.d.mts +1 -1
  89. package/dist/web-search/WebSearchTool.d.mts +1 -1
  90. package/package.json +41 -1
  91. package/src/domain/AiError.ts +11 -0
  92. package/src/domain/Realtime.ts +101 -0
  93. package/src/domain/Transcript.test.ts +98 -0
  94. package/src/domain/Transcript.ts +53 -1
  95. package/src/index.ts +7 -0
  96. package/src/messenger/Messenger.ts +293 -0
  97. package/src/messenger/MessengerAdapter.test.ts +179 -0
  98. package/src/messenger/MessengerAdapter.ts +189 -0
  99. package/src/messenger/MessengerError.ts +67 -0
  100. package/src/realtime/RealtimeSession.test.ts +100 -0
  101. package/src/realtime/RealtimeSession.ts +77 -0
  102. package/src/streaming/Settle.test.ts +344 -0
  103. package/src/streaming/Settle.ts +196 -0
  104. package/src/streaming/WebSocketSession.test.ts +52 -0
  105. package/src/streaming/WebSocketSession.ts +132 -0
  106. package/src/testing/FakeWebSocket.ts +120 -0
  107. package/src/testing/MockMessenger.ts +147 -0
  108. package/src/testing/MockRealtimeSession.ts +92 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-uai/core",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "Low-level primitives (loop, conversation, items, tools, streaming codecs) for building AI agents with Effect.",
5
5
  "keywords": [
6
6
  "agents",
@@ -90,6 +90,14 @@
90
90
  "types": "./dist/domain/Music.d.mts",
91
91
  "import": "./dist/domain/Music.mjs"
92
92
  },
93
+ "./Realtime": {
94
+ "types": "./dist/domain/Realtime.d.mts",
95
+ "import": "./dist/domain/Realtime.mjs"
96
+ },
97
+ "./RealtimeSession": {
98
+ "types": "./dist/realtime/RealtimeSession.d.mts",
99
+ "import": "./dist/realtime/RealtimeSession.mjs"
100
+ },
93
101
  "./Transcript": {
94
102
  "types": "./dist/domain/Transcript.d.mts",
95
103
  "import": "./dist/domain/Transcript.mjs"
@@ -114,6 +122,18 @@
114
122
  "types": "./dist/language-model/LanguageModel.d.mts",
115
123
  "import": "./dist/language-model/LanguageModel.mjs"
116
124
  },
125
+ "./Messenger": {
126
+ "types": "./dist/messenger/Messenger.d.mts",
127
+ "import": "./dist/messenger/Messenger.mjs"
128
+ },
129
+ "./MessengerAdapter": {
130
+ "types": "./dist/messenger/MessengerAdapter.d.mts",
131
+ "import": "./dist/messenger/MessengerAdapter.mjs"
132
+ },
133
+ "./MessengerError": {
134
+ "types": "./dist/messenger/MessengerError.d.mts",
135
+ "import": "./dist/messenger/MessengerError.mjs"
136
+ },
117
137
  "./MusicGenerator": {
118
138
  "types": "./dist/music-generator/MusicGenerator.d.mts",
119
139
  "import": "./dist/music-generator/MusicGenerator.mjs"
@@ -166,6 +186,14 @@
166
186
  "types": "./dist/web-search/WebSearchTool.d.mts",
167
187
  "import": "./dist/web-search/WebSearchTool.mjs"
168
188
  },
189
+ "./WebSocketSession": {
190
+ "types": "./dist/streaming/WebSocketSession.d.mts",
191
+ "import": "./dist/streaming/WebSocketSession.mjs"
192
+ },
193
+ "./Settle": {
194
+ "types": "./dist/streaming/Settle.d.mts",
195
+ "import": "./dist/streaming/Settle.mjs"
196
+ },
169
197
  "./Loop": {
170
198
  "types": "./dist/loop/Loop.d.mts",
171
199
  "import": "./dist/loop/Loop.mjs"
@@ -238,6 +266,10 @@
238
266
  "types": "./dist/testing/MockProvider.d.mts",
239
267
  "import": "./dist/testing/MockProvider.mjs"
240
268
  },
269
+ "./testing/MockMessenger": {
270
+ "types": "./dist/testing/MockMessenger.d.mts",
271
+ "import": "./dist/testing/MockMessenger.mjs"
272
+ },
241
273
  "./testing/MockMusicGenerator": {
242
274
  "types": "./dist/testing/MockMusicGenerator.d.mts",
243
275
  "import": "./dist/testing/MockMusicGenerator.mjs"
@@ -250,6 +282,14 @@
250
282
  "types": "./dist/testing/MockSandbox.d.mts",
251
283
  "import": "./dist/testing/MockSandbox.mjs"
252
284
  },
285
+ "./testing/FakeWebSocket": {
286
+ "types": "./dist/testing/FakeWebSocket.d.mts",
287
+ "import": "./dist/testing/FakeWebSocket.mjs"
288
+ },
289
+ "./testing/MockRealtimeSession": {
290
+ "types": "./dist/testing/MockRealtimeSession.d.mts",
291
+ "import": "./dist/testing/MockRealtimeSession.mjs"
292
+ },
253
293
  "./testing/MockTranscriber": {
254
294
  "types": "./dist/testing/MockTranscriber.d.mts",
255
295
  "import": "./dist/testing/MockTranscriber.mjs"
@@ -100,6 +100,15 @@ export class Unsupported extends Data.TaggedError("Unsupported")<{
100
100
  reason?: string
101
101
  }> {}
102
102
 
103
+ /**
104
+ * A long-lived session hit the provider's lifetime cap. Distinct from
105
+ * `Unavailable`: reconnecting works, retrying the same session does not.
106
+ */
107
+ export class SessionExpired extends Data.TaggedError("SessionExpired")<{
108
+ provider: string
109
+ raw?: unknown
110
+ }> {}
111
+
103
112
  export type AiError =
104
113
  | RateLimited
105
114
  | Unavailable
@@ -112,6 +121,7 @@ export type AiError =
112
121
  | IncompleteTurn
113
122
  | GenerationFailed
114
123
  | Unsupported
124
+ | SessionExpired
115
125
 
116
126
  const withReason = (base: string, reason: string | undefined): string =>
117
127
  reason === undefined ? base : `${base}: ${reason}`
@@ -136,5 +146,6 @@ export const describe: (e: AiError) => string = Match.type<AiError>().pipe(
136
146
  IncompleteTurn: () => "the provider stream ended without completing",
137
147
  GenerationFailed: (e) => e.message ?? `${e.provider} failed mid-generation`,
138
148
  Unsupported: (e) => withReason(`${e.provider} does not support ${e.capability}`, e.reason),
149
+ SessionExpired: (e) => `the ${e.provider} session reached its lifetime limit`,
139
150
  }),
140
151
  )
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Types for a duplex speech-to-speech session: one long-lived connection the
3
+ * server drives, rather than a caller-driven turn.
4
+ *
5
+ * Only what both OpenAI Realtime and Gemini Live do natively lives here. VAD
6
+ * knobs, truncation, resumption, thinking, grounding and noise reduction are
7
+ * provider-typed on each adapter's request.
8
+ */
9
+ import { Data, type Duration } from "effect"
10
+ import type { AudioFormat } from "./Audio.js"
11
+ import type { ImageSource } from "./Image.js"
12
+ import type { HistoryItem, ToolCall, ToolCallOutput, Usage } from "./Items.js"
13
+ import type { ToolDescriptor } from "../tool/Tool.js"
14
+
15
+ /**
16
+ * What the caller pushes into a live session.
17
+ *
18
+ * - `Audio`: encoded per `request.inputFormat`. The adapter neither paces nor
19
+ * buffers; send at real time.
20
+ * - `Text`: role `system` maps to a system item where the provider has one and
21
+ * fails `Unsupported` where it does not.
22
+ * - `Interrupt`: cancel the response in flight.
23
+ * - `VideoFrame`: gated by the `RealtimeVideoInput` marker.
24
+ * - `ActivityStart` / `ActivityEnd`: manual turn detection only; `ActivityEnd`
25
+ * also asks for a response. Dropped with a warning on a `"server"` session.
26
+ * - `PlaybackPosition`: how much of a response the user actually heard, so the
27
+ * adapter can trim the rest from context. A no-op where the wire has no
28
+ * truncate.
29
+ */
30
+ export type RealtimeInput = Data.TaggedEnum<{
31
+ Audio: { readonly bytes: Uint8Array }
32
+ Text: { readonly text: string; readonly role?: "user" | "system" }
33
+ ToolResult: { readonly output: ToolCallOutput }
34
+ Interrupt: {}
35
+ VideoFrame: { readonly frame: ImageSource }
36
+ ActivityStart: {}
37
+ ActivityEnd: {}
38
+ PlaybackPosition: { readonly responseId: string; readonly playedMs: number }
39
+ }>
40
+
41
+ export const RealtimeInput = Data.taggedEnum<RealtimeInput>()
42
+
43
+ /**
44
+ * What the session emits. Every `AudioDelta` and `OutputTranscriptDelta` is
45
+ * bracketed by `ResponseStarted` and `ResponseDone` carrying the same
46
+ * `responseId`.
47
+ *
48
+ * - `InputTranscript`: transcription of the user's own speech.
49
+ * - `SpeechStarted` / `SpeechStopped`: sparse. OpenAI emits them, Gemini
50
+ * does not.
51
+ * - `Interrupted`: stop and flush playback now. Always precedes the
52
+ * `ResponseDone { reason: "interrupted" }` for that response.
53
+ * - `ToolCallCancelled`: calls of an interrupted response that will never be
54
+ * answered.
55
+ * - `ResumptionHandle`: Gemini only; pass it back as `request.resume`.
56
+ * - `SessionEnding`: the connection is about to close on the server's terms.
57
+ * - `Error`: non-fatal. A fatal error fails the stream instead.
58
+ */
59
+ export type RealtimeEvent = Data.TaggedEnum<{
60
+ ResponseStarted: { readonly responseId: string }
61
+ AudioDelta: { readonly responseId: string; readonly bytes: Uint8Array }
62
+ OutputTranscriptDelta: { readonly responseId: string; readonly text: string }
63
+ InputTranscript: { readonly text: string; readonly final: boolean }
64
+ SpeechStarted: {}
65
+ SpeechStopped: {}
66
+ ToolCall: { readonly responseId: string; readonly call: ToolCall }
67
+ ToolCallCancelled: { readonly callIds: ReadonlyArray<string> }
68
+ Interrupted: { readonly responseId: string }
69
+ ResponseDone: {
70
+ readonly responseId: string
71
+ readonly reason: "complete" | "interrupted" | "cancelled" | "error"
72
+ readonly usage?: Usage
73
+ }
74
+ ResumptionHandle: { readonly handle: string }
75
+ SessionEnding: { readonly timeLeft?: Duration.Duration }
76
+ Error: { readonly code?: string; readonly message: string }
77
+ }>
78
+
79
+ export const RealtimeEvent = Data.taggedEnum<RealtimeEvent>()
80
+
81
+ /**
82
+ * Cross-provider session setup. Each adapter extends this, narrowing `model`
83
+ * and adding its own knobs.
84
+ */
85
+ export type CommonSessionRequest = {
86
+ readonly model: string
87
+ readonly instructions?: string
88
+ readonly voiceId?: string
89
+ /** `Toolkit.descriptors(toolkit)`. Immutable for the connection on Gemini. */
90
+ readonly tools?: ReadonlyArray<ToolDescriptor>
91
+ readonly inputFormat: AudioFormat
92
+ readonly outputFormat: AudioFormat
93
+ /** Who decides a turn ended. Defaults to `"server"`. */
94
+ readonly turnDetection?: "server" | "manual"
95
+ /** Transcribe the user's own audio. Defaults to `true`. */
96
+ readonly transcribeInput?: boolean
97
+ /** Text-only conversation seed. */
98
+ readonly history?: ReadonlyArray<HistoryItem>
99
+ /** A `ResumptionHandle` from a previous session. `Unsupported` off Gemini. */
100
+ readonly resume?: string
101
+ }
@@ -0,0 +1,98 @@
1
+ import { describe, it } from "@effect/vitest"
2
+ import { Cause, Effect, Queue, Ref, Stream } from "effect"
3
+ import { TestClock } from "effect/testing"
4
+ import { expect } from "vitest"
5
+ import { accumulatePartials, type TranscriptEvent } from "./Transcript.js"
6
+
7
+ const collecting = (stream: Stream.Stream<TranscriptEvent, never>) =>
8
+ Effect.gen(function* () {
9
+ const seen = yield* Ref.make<ReadonlyArray<TranscriptEvent>>([])
10
+ yield* Effect.forkChild(Stream.runForEach(stream, (e) => Ref.update(seen, (xs) => [...xs, e])))
11
+ return seen
12
+ })
13
+
14
+ /** Deltas arriving faster than any settle window. */
15
+ const speak = (
16
+ queue: Queue.Queue<TranscriptEvent, Cause.Done>,
17
+ tokens: ReadonlyArray<string>,
18
+ ): Effect.Effect<void> =>
19
+ Effect.forEach(
20
+ tokens,
21
+ (text) =>
22
+ Effect.andThen(Queue.offer(queue, { _tag: "partial", text }), TestClock.adjust("100 millis")),
23
+ { discard: true },
24
+ )
25
+
26
+ const opened = Effect.gen(function* () {
27
+ const queue = yield* Queue.unbounded<TranscriptEvent, Cause.Done>()
28
+ const seen = yield* collecting(
29
+ Stream.fromQueue(queue).pipe(accumulatePartials({ silence: "1 second" })),
30
+ )
31
+ return { queue, seen }
32
+ })
33
+
34
+ describe("accumulatePartials", () => {
35
+ it.effect("joins deltas into the running hypothesis, forwarding each as it arrives", () =>
36
+ Effect.gen(function* () {
37
+ const { queue, seen } = yield* opened
38
+
39
+ yield* speak(queue, [" Hi", ",", " how"])
40
+
41
+ // Three in, three out, each the whole sentence so far, none delayed.
42
+ expect(yield* Ref.get(seen)).toEqual([
43
+ { _tag: "partial", text: "Hi" },
44
+ { _tag: "partial", text: "Hi," },
45
+ { _tag: "partial", text: "Hi, how" },
46
+ ])
47
+ }),
48
+ )
49
+
50
+ it.effect("commits on silence and starts the next utterance from empty", () =>
51
+ Effect.gen(function* () {
52
+ const { queue, seen } = yield* opened
53
+
54
+ yield* speak(queue, [" Hi", " there"])
55
+ yield* TestClock.adjust("3 seconds")
56
+ yield* speak(queue, [" Bye"])
57
+ yield* TestClock.adjust("3 seconds")
58
+
59
+ expect(yield* Ref.get(seen)).toEqual([
60
+ { _tag: "partial", text: "Hi" },
61
+ { _tag: "partial", text: "Hi there" },
62
+ { _tag: "final", text: "Hi there" },
63
+ { _tag: "partial", text: "Bye" },
64
+ { _tag: "final", text: "Bye" },
65
+ ])
66
+ }),
67
+ )
68
+
69
+ it.effect("a provider's own final resets the accumulator, so silence adds no duplicate", () =>
70
+ Effect.gen(function* () {
71
+ const { queue, seen } = yield* opened
72
+
73
+ yield* speak(queue, [" Hi"])
74
+ yield* Queue.offer(queue, { _tag: "final", text: "Hi." })
75
+ yield* TestClock.adjust("3 seconds")
76
+
77
+ expect(yield* Ref.get(seen)).toEqual([
78
+ { _tag: "partial", text: "Hi" },
79
+ { _tag: "final", text: "Hi." },
80
+ ])
81
+ }),
82
+ )
83
+
84
+ it.effect("commits the pending text when the stream ends mid-utterance", () =>
85
+ Effect.gen(function* () {
86
+ const { queue, seen } = yield* opened
87
+
88
+ yield* speak(queue, [" Half a sen"])
89
+ yield* Queue.end(queue)
90
+ yield* TestClock.adjust("10 millis")
91
+
92
+ expect(yield* Ref.get(seen)).toEqual([
93
+ { _tag: "partial", text: "Half a sen" },
94
+ { _tag: "final", text: "Half a sen" },
95
+ ])
96
+ }),
97
+ )
98
+ })
@@ -1,4 +1,5 @@
1
- import type { Duration } from "effect"
1
+ import { type Duration, Effect, Match, Option, Ref, Stream } from "effect"
2
+ import * as Settle from "../streaming/Settle.js"
2
3
 
3
4
  /**
4
5
  * Per-word timing + metadata. `confidence` and `speakerId` are optional
@@ -87,3 +88,54 @@ export const isMetadata = (
87
88
  ): e is Extract<TranscriptEvent, { _tag: "metadata" }> => e._tag === "metadata"
88
89
  export const isError = (e: TranscriptEvent): e is Extract<TranscriptEvent, { _tag: "error" }> =>
89
90
  e._tag === "error"
91
+
92
+ export type AccumulatePartialsOptions = {
93
+ /** Silence after the last `partial` before the text so far is committed. */
94
+ readonly silence?: Duration.Input
95
+ }
96
+
97
+ const DEFAULT_SILENCE = "700 millis"
98
+
99
+ const finalOf = (text: string): Option.Option<TranscriptEvent> =>
100
+ text.trim().length === 0 ? Option.none() : Option.some({ _tag: "final", text: text.trim() })
101
+
102
+ /**
103
+ * Join fragment `partial`s into the running hypothesis and commit it as a
104
+ * `final` after `silence`. For providers that stream token-sized deltas
105
+ * (OpenAI Realtime) or never segment; piping one that already accumulates
106
+ * repeats its text.
107
+ *
108
+ * Partials are forwarded as they arrive; only the synthetic `final` waits.
109
+ * A real `final` resets the accumulator, and the stream end flushes it.
110
+ */
111
+ export const accumulatePartials =
112
+ (options?: AccumulatePartialsOptions) =>
113
+ <E, R>(self: Stream.Stream<TranscriptEvent, E, R>): Stream.Stream<TranscriptEvent, E, R> =>
114
+ Stream.unwrap(
115
+ Effect.gen(function* () {
116
+ const acc = yield* Ref.make("")
117
+ const flush = Effect.map(Ref.getAndSet(acc, ""), finalOf)
118
+
119
+ const rewrite = (event: TranscriptEvent): Effect.Effect<TranscriptEvent> =>
120
+ Match.value(event).pipe(
121
+ Match.when({ _tag: "partial" }, (e) =>
122
+ Effect.map(
123
+ Ref.updateAndGet(acc, (text) => text + e.text),
124
+ (text): TranscriptEvent => ({ ...e, text: text.trim() }),
125
+ ),
126
+ ),
127
+ Match.when({ _tag: "final" }, (e) => Effect.as(Ref.set(acc, ""), e as TranscriptEvent)),
128
+ Match.orElse((e) => Effect.succeed(e as TranscriptEvent)),
129
+ )
130
+
131
+ return self.pipe(
132
+ Stream.mapEffect(rewrite),
133
+ Settle.onQuiet(options?.silence ?? DEFAULT_SILENCE, flush),
134
+ Stream.concat(
135
+ Stream.unwrap(
136
+ Effect.map(flush, Option.match({ onNone: () => Stream.empty, onSome: Stream.make })),
137
+ ),
138
+ ),
139
+ )
140
+ }),
141
+ )
package/src/index.ts CHANGED
@@ -13,12 +13,17 @@ export * as Job from "./job/Job.js"
13
13
  export * as Media from "./domain/Media.js"
14
14
  export * as Music from "./domain/Music.js"
15
15
  export * as Research from "./domain/Research.js"
16
+ export * as Realtime from "./domain/Realtime.js"
17
+ export * as RealtimeSession from "./realtime/RealtimeSession.js"
16
18
  export * as Transcript from "./domain/Transcript.js"
17
19
  export * as Turn from "./domain/Turn.js"
18
20
  export * as Embedding from "./embedding-model/Embedding.js"
19
21
  export * as EmbeddingModel from "./embedding-model/EmbeddingModel.js"
20
22
  export * as ImageGenerator from "./image-generator/ImageGenerator.js"
21
23
  export * as LanguageModel from "./language-model/LanguageModel.js"
24
+ export * as Messenger from "./messenger/Messenger.js"
25
+ export * as MessengerAdapter from "./messenger/MessengerAdapter.js"
26
+ export * as MessengerError from "./messenger/MessengerError.js"
22
27
  export * as MusicGenerator from "./music-generator/MusicGenerator.js"
23
28
  export * as Reranker from "./reranker/Reranker.js"
24
29
  export * as Memory from "./sandbox/Memory.js"
@@ -34,6 +39,8 @@ export * as WebSearch from "./web-search/WebSearch.js"
34
39
  export * as WebSearchTool from "./web-search/WebSearchTool.js"
35
40
  export * as Vector from "./math/Vector.js"
36
41
  export * as Tokenizer from "./tokenizer/Tokenizer.js"
42
+ export * as Settle from "./streaming/Settle.js"
43
+ export * as WebSocketSession from "./streaming/WebSocketSession.js"
37
44
  export * as Loop from "./loop/Loop.js"
38
45
  export * as Retry from "./retry/Retry.js"
39
46
  export * as Tool from "./tool/Tool.js"