@juspay/neurolink 9.61.1 → 9.62.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 (141) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +23 -17
  3. package/dist/adapters/tts/googleTTSHandler.js +1 -1
  4. package/dist/browser/neurolink.min.js +382 -364
  5. package/dist/cli/commands/serve.js +9 -0
  6. package/dist/cli/commands/voiceServer.d.ts +7 -0
  7. package/dist/cli/commands/voiceServer.js +9 -1
  8. package/dist/cli/factories/commandFactory.js +136 -11
  9. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  10. package/dist/cli/utils/audioFileUtils.d.ts +3 -3
  11. package/dist/cli/utils/audioFileUtils.js +5 -1
  12. package/dist/core/baseProvider.js +29 -6
  13. package/dist/factories/providerRegistry.d.ts +14 -0
  14. package/dist/factories/providerRegistry.js +141 -2
  15. package/dist/lib/adapters/tts/googleTTSHandler.js +1 -1
  16. package/dist/lib/core/baseProvider.js +29 -6
  17. package/dist/lib/factories/providerRegistry.d.ts +14 -0
  18. package/dist/lib/factories/providerRegistry.js +141 -2
  19. package/dist/lib/mcp/toolRegistry.js +7 -1
  20. package/dist/lib/neurolink.d.ts +19 -0
  21. package/dist/lib/neurolink.js +252 -14
  22. package/dist/lib/observability/exporters/laminarExporter.js +1 -0
  23. package/dist/lib/observability/exporters/posthogExporter.js +1 -0
  24. package/dist/lib/observability/utils/spanSerializer.js +1 -0
  25. package/dist/lib/server/voice/tokenCompare.d.ts +14 -0
  26. package/dist/lib/server/voice/tokenCompare.js +23 -0
  27. package/dist/lib/server/voice/voiceServerApp.js +62 -3
  28. package/dist/lib/server/voice/voiceWebSocketHandler.d.ts +20 -3
  29. package/dist/lib/server/voice/voiceWebSocketHandler.js +555 -435
  30. package/dist/lib/types/generate.d.ts +47 -0
  31. package/dist/lib/types/hitl.d.ts +3 -0
  32. package/dist/lib/types/index.d.ts +1 -1
  33. package/dist/lib/types/index.js +1 -1
  34. package/dist/lib/types/realtime.d.ts +243 -0
  35. package/dist/lib/types/realtime.js +70 -0
  36. package/dist/lib/types/server.d.ts +68 -0
  37. package/dist/lib/types/span.d.ts +2 -0
  38. package/dist/lib/types/span.js +2 -0
  39. package/dist/lib/types/stream.d.ts +36 -14
  40. package/dist/lib/types/stt.d.ts +585 -0
  41. package/dist/lib/types/stt.js +90 -0
  42. package/dist/lib/types/tools.d.ts +2 -0
  43. package/dist/lib/types/tts.d.ts +23 -11
  44. package/dist/lib/types/tts.js +7 -0
  45. package/dist/lib/types/voice.d.ts +272 -0
  46. package/dist/lib/types/voice.js +137 -0
  47. package/dist/lib/utils/audioFormatDetector.d.ts +15 -0
  48. package/dist/lib/utils/audioFormatDetector.js +34 -0
  49. package/dist/lib/utils/errorHandling.js +4 -0
  50. package/dist/lib/utils/sttProcessor.d.ts +115 -0
  51. package/dist/lib/utils/sttProcessor.js +295 -0
  52. package/dist/lib/voice/RealtimeVoiceAPI.d.ts +183 -0
  53. package/dist/lib/voice/RealtimeVoiceAPI.js +439 -0
  54. package/dist/lib/voice/audio-utils.d.ts +135 -0
  55. package/dist/lib/voice/audio-utils.js +435 -0
  56. package/dist/lib/voice/errors.d.ts +123 -0
  57. package/dist/lib/voice/errors.js +386 -0
  58. package/dist/lib/voice/index.d.ts +26 -0
  59. package/dist/lib/voice/index.js +55 -0
  60. package/dist/lib/voice/providers/AzureSTT.d.ts +47 -0
  61. package/dist/lib/voice/providers/AzureSTT.js +345 -0
  62. package/dist/lib/voice/providers/AzureTTS.d.ts +59 -0
  63. package/dist/lib/voice/providers/AzureTTS.js +349 -0
  64. package/dist/lib/voice/providers/DeepgramSTT.d.ts +40 -0
  65. package/dist/lib/voice/providers/DeepgramSTT.js +550 -0
  66. package/dist/lib/voice/providers/ElevenLabsTTS.d.ts +53 -0
  67. package/dist/lib/voice/providers/ElevenLabsTTS.js +311 -0
  68. package/dist/lib/voice/providers/GeminiLive.d.ts +52 -0
  69. package/dist/lib/voice/providers/GeminiLive.js +372 -0
  70. package/dist/lib/voice/providers/GoogleSTT.d.ts +60 -0
  71. package/dist/lib/voice/providers/GoogleSTT.js +454 -0
  72. package/dist/lib/voice/providers/OpenAIRealtime.d.ts +47 -0
  73. package/dist/lib/voice/providers/OpenAIRealtime.js +412 -0
  74. package/dist/lib/voice/providers/OpenAISTT.d.ts +41 -0
  75. package/dist/lib/voice/providers/OpenAISTT.js +286 -0
  76. package/dist/lib/voice/providers/OpenAITTS.d.ts +49 -0
  77. package/dist/lib/voice/providers/OpenAITTS.js +271 -0
  78. package/dist/lib/voice/stream-handler.d.ts +166 -0
  79. package/dist/lib/voice/stream-handler.js +514 -0
  80. package/dist/mcp/toolRegistry.js +7 -1
  81. package/dist/neurolink.d.ts +19 -0
  82. package/dist/neurolink.js +252 -14
  83. package/dist/observability/exporters/laminarExporter.js +1 -0
  84. package/dist/observability/exporters/posthogExporter.js +1 -0
  85. package/dist/observability/utils/spanSerializer.js +1 -0
  86. package/dist/server/voice/tokenCompare.d.ts +14 -0
  87. package/dist/server/voice/tokenCompare.js +22 -0
  88. package/dist/server/voice/voiceServerApp.js +62 -3
  89. package/dist/server/voice/voiceWebSocketHandler.d.ts +20 -3
  90. package/dist/server/voice/voiceWebSocketHandler.js +555 -435
  91. package/dist/types/generate.d.ts +47 -0
  92. package/dist/types/hitl.d.ts +3 -0
  93. package/dist/types/index.d.ts +1 -1
  94. package/dist/types/index.js +1 -1
  95. package/dist/types/realtime.d.ts +243 -0
  96. package/dist/types/realtime.js +69 -0
  97. package/dist/types/server.d.ts +68 -0
  98. package/dist/types/span.d.ts +2 -0
  99. package/dist/types/span.js +2 -0
  100. package/dist/types/stream.d.ts +36 -14
  101. package/dist/types/stt.d.ts +585 -0
  102. package/dist/types/stt.js +89 -0
  103. package/dist/types/tools.d.ts +2 -0
  104. package/dist/types/tts.d.ts +23 -11
  105. package/dist/types/tts.js +7 -0
  106. package/dist/types/voice.d.ts +272 -0
  107. package/dist/types/voice.js +136 -0
  108. package/dist/utils/audioFormatDetector.d.ts +15 -0
  109. package/dist/utils/audioFormatDetector.js +33 -0
  110. package/dist/utils/errorHandling.js +4 -0
  111. package/dist/utils/sttProcessor.d.ts +115 -0
  112. package/dist/utils/sttProcessor.js +294 -0
  113. package/dist/voice/RealtimeVoiceAPI.d.ts +183 -0
  114. package/dist/voice/RealtimeVoiceAPI.js +438 -0
  115. package/dist/voice/audio-utils.d.ts +135 -0
  116. package/dist/voice/audio-utils.js +434 -0
  117. package/dist/voice/errors.d.ts +123 -0
  118. package/dist/voice/errors.js +385 -0
  119. package/dist/voice/index.d.ts +26 -0
  120. package/dist/voice/index.js +54 -0
  121. package/dist/voice/providers/AzureSTT.d.ts +47 -0
  122. package/dist/voice/providers/AzureSTT.js +344 -0
  123. package/dist/voice/providers/AzureTTS.d.ts +59 -0
  124. package/dist/voice/providers/AzureTTS.js +348 -0
  125. package/dist/voice/providers/DeepgramSTT.d.ts +40 -0
  126. package/dist/voice/providers/DeepgramSTT.js +549 -0
  127. package/dist/voice/providers/ElevenLabsTTS.d.ts +53 -0
  128. package/dist/voice/providers/ElevenLabsTTS.js +310 -0
  129. package/dist/voice/providers/GeminiLive.d.ts +52 -0
  130. package/dist/voice/providers/GeminiLive.js +371 -0
  131. package/dist/voice/providers/GoogleSTT.d.ts +60 -0
  132. package/dist/voice/providers/GoogleSTT.js +453 -0
  133. package/dist/voice/providers/OpenAIRealtime.d.ts +47 -0
  134. package/dist/voice/providers/OpenAIRealtime.js +411 -0
  135. package/dist/voice/providers/OpenAISTT.d.ts +41 -0
  136. package/dist/voice/providers/OpenAISTT.js +285 -0
  137. package/dist/voice/providers/OpenAITTS.d.ts +49 -0
  138. package/dist/voice/providers/OpenAITTS.js +270 -0
  139. package/dist/voice/stream-handler.d.ts +166 -0
  140. package/dist/voice/stream-handler.js +513 -0
  141. package/package.json +5 -2
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Stream Handler for Voice Module
3
+ *
4
+ * Provides audio stream chunking, backpressure handling, and stream coordination.
5
+ *
6
+ * @module voice/stream-handler
7
+ */
8
+ import { EventEmitter } from "events";
9
+ import type { AudioStreamChunk, StreamHandlerConfig } from "../types/index.js";
10
+ /**
11
+ * Chunked Audio Stream Handler
12
+ *
13
+ * Handles audio stream chunking with backpressure management.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * const handler = new ChunkedAudioStream({
18
+ * chunkDurationMs: 100,
19
+ * sampleRate: 16000,
20
+ * });
21
+ *
22
+ * handler.on('chunk', (chunk) => {
23
+ * // Process audio chunk
24
+ * });
25
+ *
26
+ * handler.write(audioData);
27
+ * handler.end();
28
+ * ```
29
+ */
30
+ export declare class ChunkedAudioStream extends EventEmitter {
31
+ private readonly config;
32
+ private readonly chunkSize;
33
+ private buffer;
34
+ private chunkIndex;
35
+ private timestampMs;
36
+ private isPaused;
37
+ private isEnded;
38
+ private pendingData;
39
+ private bufferTimeout;
40
+ constructor(config?: StreamHandlerConfig);
41
+ /**
42
+ * Write audio data to the stream
43
+ *
44
+ * @param data - Audio data buffer
45
+ * @returns True if more data can be written, false if backpressure
46
+ */
47
+ write(data: Buffer): boolean;
48
+ /**
49
+ * Process incoming data
50
+ */
51
+ private processData;
52
+ /**
53
+ * End the stream
54
+ */
55
+ end(): void;
56
+ /**
57
+ * Reset buffer timeout
58
+ */
59
+ private resetBufferTimeout;
60
+ /**
61
+ * Clear buffer timeout
62
+ */
63
+ private clearBufferTimeout;
64
+ /**
65
+ * Cleanup resources
66
+ */
67
+ private cleanup;
68
+ /**
69
+ * Get stream statistics
70
+ */
71
+ getStats(): {
72
+ chunksEmitted: number;
73
+ bufferedBytes: number;
74
+ pendingChunks: number;
75
+ totalDurationMs: number;
76
+ isPaused: boolean;
77
+ isEnded: boolean;
78
+ };
79
+ }
80
+ /**
81
+ * Stream merger for combining multiple audio streams
82
+ */
83
+ export declare class StreamMerger extends EventEmitter {
84
+ private readonly streams;
85
+ private readonly config;
86
+ constructor(config?: StreamHandlerConfig);
87
+ /**
88
+ * Add a stream to merge
89
+ *
90
+ * @param id - Stream identifier
91
+ * @returns The created stream
92
+ */
93
+ addStream(id: string): ChunkedAudioStream;
94
+ /**
95
+ * Remove a stream
96
+ *
97
+ * @param id - Stream identifier
98
+ */
99
+ removeStream(id: string): void;
100
+ /**
101
+ * Write to a specific stream
102
+ *
103
+ * @param id - Stream identifier
104
+ * @param data - Audio data
105
+ */
106
+ write(id: string, data: Buffer): boolean;
107
+ /**
108
+ * End all streams
109
+ */
110
+ endAll(): void;
111
+ /**
112
+ * Get number of active streams
113
+ */
114
+ get activeStreams(): number;
115
+ }
116
+ /**
117
+ * Stream splitter for distributing audio to multiple consumers
118
+ */
119
+ export declare class StreamSplitter extends EventEmitter {
120
+ private readonly consumers;
121
+ private readonly input;
122
+ constructor(config?: StreamHandlerConfig);
123
+ /**
124
+ * Write audio data
125
+ *
126
+ * @param data - Audio data buffer
127
+ */
128
+ write(data: Buffer): boolean;
129
+ /**
130
+ * End the stream
131
+ */
132
+ end(): void;
133
+ /**
134
+ * Add a consumer
135
+ *
136
+ * @param id - Consumer identifier
137
+ * @param handler - Chunk handler function
138
+ */
139
+ addConsumer(id: string, handler: (chunk: AudioStreamChunk) => void): void;
140
+ /**
141
+ * Remove a consumer
142
+ *
143
+ * @param id - Consumer identifier
144
+ */
145
+ removeConsumer(id: string): void;
146
+ /**
147
+ * Get number of consumers
148
+ */
149
+ get consumerCount(): number;
150
+ }
151
+ /**
152
+ * Create an async iterable from a chunked audio stream
153
+ *
154
+ * @param stream - Chunked audio stream
155
+ * @returns Async iterable of audio chunks
156
+ */
157
+ export declare function streamToAsyncIterable(stream: ChunkedAudioStream): AsyncIterable<AudioStreamChunk>;
158
+ /**
159
+ * Create a chunked audio stream from an async iterable
160
+ *
161
+ * @param iterable - Async iterable of audio buffers
162
+ * @param config - Stream configuration
163
+ * @returns Chunked audio stream
164
+ */
165
+ export declare function asyncIterableToStream(iterable: AsyncIterable<Buffer>, config?: StreamHandlerConfig): Promise<ChunkedAudioStream>;
166
+ export { ChunkedAudioStream as StreamHandler };
@@ -0,0 +1,514 @@
1
+ /**
2
+ * Stream Handler for Voice Module
3
+ *
4
+ * Provides audio stream chunking, backpressure handling, and stream coordination.
5
+ *
6
+ * @module voice/stream-handler
7
+ */
8
+ import { EventEmitter } from "events";
9
+ import { logger } from "../utils/logger.js";
10
+ /**
11
+ * Default configuration
12
+ */
13
+ const DEFAULT_CONFIG = {
14
+ chunkDurationMs: 100, // 100ms chunks
15
+ sampleRate: 16000,
16
+ bytesPerSample: 2, // 16-bit mono
17
+ format: "wav",
18
+ highWaterMark: 64 * 1024, // 64KB
19
+ bufferTimeoutMs: 5000, // 5 seconds
20
+ };
21
+ /**
22
+ * Chunked Audio Stream Handler
23
+ *
24
+ * Handles audio stream chunking with backpressure management.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * const handler = new ChunkedAudioStream({
29
+ * chunkDurationMs: 100,
30
+ * sampleRate: 16000,
31
+ * });
32
+ *
33
+ * handler.on('chunk', (chunk) => {
34
+ * // Process audio chunk
35
+ * });
36
+ *
37
+ * handler.write(audioData);
38
+ * handler.end();
39
+ * ```
40
+ */
41
+ export class ChunkedAudioStream extends EventEmitter {
42
+ config;
43
+ chunkSize;
44
+ buffer;
45
+ chunkIndex;
46
+ timestampMs;
47
+ isPaused;
48
+ isEnded;
49
+ pendingData;
50
+ bufferTimeout;
51
+ constructor(config = {}) {
52
+ super();
53
+ this.config = { ...DEFAULT_CONFIG, ...config };
54
+ if (this.config.sampleRate <= 0) {
55
+ throw new Error("Invalid stream configuration: sampleRate must be positive");
56
+ }
57
+ if (this.config.bytesPerSample <= 0) {
58
+ throw new Error("Invalid stream configuration: bytesPerSample must be positive");
59
+ }
60
+ // Calculate chunk size based on duration
61
+ const bytesPerMs = (this.config.sampleRate * this.config.bytesPerSample) / 1000;
62
+ this.chunkSize = Math.round(this.config.chunkDurationMs * bytesPerMs);
63
+ if (this.chunkSize <= 0) {
64
+ throw new Error("Invalid stream configuration: chunkSize must be positive (check chunkDurationMs, sampleRate, bytesPerSample)");
65
+ }
66
+ this.buffer = Buffer.alloc(0);
67
+ this.chunkIndex = 0;
68
+ this.timestampMs = 0;
69
+ this.isPaused = false;
70
+ this.isEnded = false;
71
+ this.pendingData = [];
72
+ this.bufferTimeout = null;
73
+ }
74
+ /**
75
+ * Write audio data to the stream
76
+ *
77
+ * @param data - Audio data buffer
78
+ * @returns True if more data can be written, false if backpressure
79
+ */
80
+ write(data) {
81
+ if (this.isEnded) {
82
+ throw new Error("Cannot write to ended stream");
83
+ }
84
+ // Decide backpressure state BEFORE processing so the producer can rely on
85
+ // the returned flag without depending on a follow-up `write()` to flip it.
86
+ // Also queue the chunk into pendingData when paused — `processData()` is
87
+ // what eventually triggers `drain` via the `resume`/`drain` path, so it
88
+ // must run after the resume condition is met (CodeRabbit review:
89
+ // previously, processing first could emit `drain` synchronously inside
90
+ // `processData()` if buffer drained below high-water mid-call, and a
91
+ // producer that waited for `drain` after seeing `false` could deadlock).
92
+ const willOverflow = this.buffer.length + data.length > this.config.highWaterMark;
93
+ if (willOverflow) {
94
+ if (!this.isPaused) {
95
+ this.isPaused = true;
96
+ this.emit("pause");
97
+ }
98
+ this.pendingData.push(data);
99
+ return false;
100
+ }
101
+ this.processData(data);
102
+ return true;
103
+ }
104
+ /**
105
+ * Process incoming data
106
+ */
107
+ processData(data) {
108
+ // Append to buffer
109
+ this.buffer = Buffer.concat([this.buffer, data]);
110
+ // Reset buffer timeout
111
+ this.resetBufferTimeout();
112
+ // Emit chunks while we have enough data
113
+ while (this.buffer.length >= this.chunkSize) {
114
+ const chunkData = this.buffer.subarray(0, this.chunkSize);
115
+ this.buffer = this.buffer.subarray(this.chunkSize);
116
+ const chunk = {
117
+ data: chunkData,
118
+ index: this.chunkIndex++,
119
+ isFinal: false,
120
+ format: this.config.format,
121
+ sampleRate: this.config.sampleRate,
122
+ timestampMs: this.timestampMs,
123
+ durationMs: this.config.chunkDurationMs,
124
+ };
125
+ this.timestampMs += this.config.chunkDurationMs;
126
+ this.emit("chunk", chunk);
127
+ }
128
+ // Process pending data if backpressure released
129
+ if (this.isPaused && this.buffer.length < this.config.highWaterMark / 2) {
130
+ this.isPaused = false;
131
+ this.emit("resume");
132
+ this.emit("drain");
133
+ // Process pending data. `shift()` returns `undefined` only when the
134
+ // array is empty, which the loop guard already rules out — but check
135
+ // explicitly so we don't carry a non-null assertion (Issue 9).
136
+ while (this.pendingData.length > 0 && !this.isPaused) {
137
+ const pending = this.pendingData.shift();
138
+ if (pending === undefined) {
139
+ break;
140
+ }
141
+ if (!this.write(pending)) {
142
+ break;
143
+ }
144
+ }
145
+ }
146
+ }
147
+ /**
148
+ * End the stream
149
+ */
150
+ end() {
151
+ if (this.isEnded) {
152
+ return;
153
+ }
154
+ this.isEnded = true;
155
+ this.clearBufferTimeout();
156
+ // Drain any pending data that was buffered during backpressure
157
+ for (const pending of this.pendingData) {
158
+ this.buffer = Buffer.concat([this.buffer, pending]);
159
+ }
160
+ this.pendingData = [];
161
+ // Emit final chunk with remaining data
162
+ if (this.buffer.length > 0) {
163
+ const durationMs = (this.buffer.length /
164
+ this.config.bytesPerSample /
165
+ this.config.sampleRate) *
166
+ 1000;
167
+ const chunk = {
168
+ data: this.buffer,
169
+ index: this.chunkIndex++,
170
+ isFinal: true,
171
+ format: this.config.format,
172
+ sampleRate: this.config.sampleRate,
173
+ timestampMs: this.timestampMs,
174
+ durationMs,
175
+ };
176
+ this.emit("chunk", chunk);
177
+ }
178
+ else {
179
+ // Emit empty final chunk to signal end
180
+ const chunk = {
181
+ data: Buffer.alloc(0),
182
+ index: this.chunkIndex,
183
+ isFinal: true,
184
+ format: this.config.format,
185
+ sampleRate: this.config.sampleRate,
186
+ timestampMs: this.timestampMs,
187
+ durationMs: 0,
188
+ };
189
+ this.emit("chunk", chunk);
190
+ }
191
+ this.emit("end");
192
+ this.cleanup();
193
+ }
194
+ /**
195
+ * Reset buffer timeout
196
+ */
197
+ resetBufferTimeout() {
198
+ this.clearBufferTimeout();
199
+ this.bufferTimeout = setTimeout(() => {
200
+ if (this.buffer.length > 0 && !this.isEnded) {
201
+ logger.warn(`[ChunkedAudioStream] Buffer timeout, forcing flush of ${this.buffer.length} bytes`);
202
+ this.end();
203
+ }
204
+ }, this.config.bufferTimeoutMs);
205
+ }
206
+ /**
207
+ * Clear buffer timeout
208
+ */
209
+ clearBufferTimeout() {
210
+ if (this.bufferTimeout) {
211
+ clearTimeout(this.bufferTimeout);
212
+ this.bufferTimeout = null;
213
+ }
214
+ }
215
+ /**
216
+ * Cleanup resources
217
+ */
218
+ cleanup() {
219
+ this.clearBufferTimeout();
220
+ this.buffer = Buffer.alloc(0);
221
+ this.pendingData = [];
222
+ }
223
+ /**
224
+ * Get stream statistics
225
+ */
226
+ getStats() {
227
+ return {
228
+ chunksEmitted: this.chunkIndex,
229
+ bufferedBytes: this.buffer.length,
230
+ pendingChunks: this.pendingData.length,
231
+ totalDurationMs: this.timestampMs,
232
+ isPaused: this.isPaused,
233
+ isEnded: this.isEnded,
234
+ };
235
+ }
236
+ }
237
+ /**
238
+ * Stream merger for combining multiple audio streams
239
+ */
240
+ export class StreamMerger extends EventEmitter {
241
+ streams;
242
+ config;
243
+ constructor(config = {}) {
244
+ super();
245
+ this.streams = new Map();
246
+ this.config = { ...DEFAULT_CONFIG, ...config };
247
+ }
248
+ /**
249
+ * Add a stream to merge
250
+ *
251
+ * @param id - Stream identifier
252
+ * @returns The created stream
253
+ */
254
+ addStream(id) {
255
+ if (this.streams.has(id)) {
256
+ throw new Error(`Stream ${id} already exists`);
257
+ }
258
+ const stream = new ChunkedAudioStream(this.config);
259
+ stream.on("chunk", (chunk) => {
260
+ this.emit("chunk", { id, chunk });
261
+ });
262
+ stream.on("end", () => {
263
+ this.emit("streamEnd", id);
264
+ this.streams.delete(id);
265
+ if (this.streams.size === 0) {
266
+ this.emit("end");
267
+ }
268
+ });
269
+ stream.on("error", (error) => {
270
+ this.emit("error", { id, error });
271
+ });
272
+ this.streams.set(id, stream);
273
+ return stream;
274
+ }
275
+ /**
276
+ * Remove a stream
277
+ *
278
+ * @param id - Stream identifier
279
+ */
280
+ removeStream(id) {
281
+ const stream = this.streams.get(id);
282
+ if (stream) {
283
+ stream.end();
284
+ this.streams.delete(id);
285
+ }
286
+ }
287
+ /**
288
+ * Write to a specific stream
289
+ *
290
+ * @param id - Stream identifier
291
+ * @param data - Audio data
292
+ */
293
+ write(id, data) {
294
+ const stream = this.streams.get(id);
295
+ if (!stream) {
296
+ throw new Error(`Stream ${id} not found`);
297
+ }
298
+ return stream.write(data);
299
+ }
300
+ /**
301
+ * End all streams
302
+ */
303
+ endAll() {
304
+ for (const stream of this.streams.values()) {
305
+ stream.end();
306
+ }
307
+ }
308
+ /**
309
+ * Get number of active streams
310
+ */
311
+ get activeStreams() {
312
+ return this.streams.size;
313
+ }
314
+ }
315
+ /**
316
+ * Stream splitter for distributing audio to multiple consumers
317
+ */
318
+ export class StreamSplitter extends EventEmitter {
319
+ consumers;
320
+ input;
321
+ constructor(config = {}) {
322
+ super();
323
+ this.consumers = new Map();
324
+ this.input = new ChunkedAudioStream(config);
325
+ this.input.on("chunk", (chunk) => {
326
+ for (const [id, consumer] of this.consumers) {
327
+ try {
328
+ consumer(chunk);
329
+ }
330
+ catch (err) {
331
+ this.emit("error", {
332
+ consumerId: id,
333
+ error: err instanceof Error ? err : new Error(String(err)),
334
+ });
335
+ }
336
+ }
337
+ });
338
+ this.input.on("end", () => {
339
+ this.emit("end");
340
+ });
341
+ this.input.on("error", (error) => {
342
+ this.emit("error", { error });
343
+ });
344
+ }
345
+ /**
346
+ * Write audio data
347
+ *
348
+ * @param data - Audio data buffer
349
+ */
350
+ write(data) {
351
+ return this.input.write(data);
352
+ }
353
+ /**
354
+ * End the stream
355
+ */
356
+ end() {
357
+ this.input.end();
358
+ }
359
+ /**
360
+ * Add a consumer
361
+ *
362
+ * @param id - Consumer identifier
363
+ * @param handler - Chunk handler function
364
+ */
365
+ addConsumer(id, handler) {
366
+ if (this.consumers.has(id)) {
367
+ throw new Error(`Consumer ${id} already exists`);
368
+ }
369
+ this.consumers.set(id, handler);
370
+ }
371
+ /**
372
+ * Remove a consumer
373
+ *
374
+ * @param id - Consumer identifier
375
+ */
376
+ removeConsumer(id) {
377
+ this.consumers.delete(id);
378
+ }
379
+ /**
380
+ * Get number of consumers
381
+ */
382
+ get consumerCount() {
383
+ return this.consumers.size;
384
+ }
385
+ }
386
+ /**
387
+ * Create an async iterable from a chunked audio stream
388
+ *
389
+ * @param stream - Chunked audio stream
390
+ * @returns Async iterable of audio chunks
391
+ */
392
+ export function streamToAsyncIterable(stream) {
393
+ return {
394
+ [Symbol.asyncIterator]() {
395
+ const queue = [];
396
+ let resolveNext = null;
397
+ let rejectNext = null;
398
+ let done = false;
399
+ let error = null;
400
+ const onChunk = (chunk) => {
401
+ if (resolveNext) {
402
+ resolveNext({ value: chunk, done: false });
403
+ resolveNext = null;
404
+ rejectNext = null;
405
+ }
406
+ else {
407
+ queue.push(chunk);
408
+ }
409
+ };
410
+ const onEnd = () => {
411
+ done = true;
412
+ if (resolveNext) {
413
+ resolveNext({
414
+ value: undefined,
415
+ done: true,
416
+ });
417
+ resolveNext = null;
418
+ rejectNext = null;
419
+ }
420
+ };
421
+ const onError = (err) => {
422
+ // NEW5: surface stream errors to the consumer instead of resolving
423
+ // with `done: true` (which silently terminated the for-await loop
424
+ // and lost the error entirely). When a `next()` is pending, reject
425
+ // it. When no `next()` is pending, store the error so the next call
426
+ // throws it.
427
+ error = err;
428
+ if (rejectNext) {
429
+ rejectNext(err);
430
+ resolveNext = null;
431
+ rejectNext = null;
432
+ }
433
+ };
434
+ stream.on("chunk", onChunk);
435
+ stream.on("end", onEnd);
436
+ stream.on("error", onError);
437
+ // M8: track listener removal so the iterator's `return()` can detach
438
+ // them when the consumer breaks early; otherwise the three listeners
439
+ // hang on the EventEmitter for the stream's lifetime, leaking closures.
440
+ const cleanup = () => {
441
+ stream.off("chunk", onChunk);
442
+ stream.off("end", onEnd);
443
+ stream.off("error", onError);
444
+ };
445
+ return {
446
+ async next() {
447
+ if (error) {
448
+ throw error;
449
+ }
450
+ if (queue.length > 0) {
451
+ // The length-guard above proves `shift()` returns a value, but
452
+ // narrow explicitly to avoid the non-null assertion (Issue 9).
453
+ const next = queue.shift();
454
+ if (next !== undefined) {
455
+ return { value: next, done: false };
456
+ }
457
+ }
458
+ if (done) {
459
+ return {
460
+ value: undefined,
461
+ done: true,
462
+ };
463
+ }
464
+ return new Promise((resolve, reject) => {
465
+ resolveNext = resolve;
466
+ rejectNext = reject;
467
+ });
468
+ },
469
+ async return() {
470
+ // M8: called when the consumer breaks out of `for await` early —
471
+ // detach listeners to prevent leak.
472
+ cleanup();
473
+ done = true;
474
+ return {
475
+ value: undefined,
476
+ done: true,
477
+ };
478
+ },
479
+ };
480
+ },
481
+ };
482
+ }
483
+ /**
484
+ * Create a chunked audio stream from an async iterable
485
+ *
486
+ * @param iterable - Async iterable of audio buffers
487
+ * @param config - Stream configuration
488
+ * @returns Chunked audio stream
489
+ */
490
+ export async function asyncIterableToStream(iterable, config = {}) {
491
+ const stream = new ChunkedAudioStream(config);
492
+ // Process iterable in background.
493
+ // Honour backpressure — `write()` returns false on overflow and pushes the
494
+ // chunk into pendingData. Without awaiting 'drain' here, a slow consumer
495
+ // would let pendingData grow unbounded for a long/live iterable.
496
+ (async () => {
497
+ try {
498
+ for await (const data of iterable) {
499
+ const ok = stream.write(data);
500
+ if (!ok) {
501
+ await new Promise((resolve) => stream.once("drain", resolve));
502
+ }
503
+ }
504
+ stream.end();
505
+ }
506
+ catch (err) {
507
+ stream.emit("error", err instanceof Error ? err : new Error(String(err)));
508
+ }
509
+ })();
510
+ return stream;
511
+ }
512
+ // Export main class with alias
513
+ export { ChunkedAudioStream as StreamHandler };
514
+ //# sourceMappingURL=stream-handler.js.map
@@ -301,12 +301,18 @@ export class MCPToolRegistry extends MCPRegistry {
301
301
  span.setAttribute("tool.arguments_size", argsStr.length);
302
302
  // HITL Safety Check: Request confirmation if required
303
303
  let finalArgs = args;
304
- if (this.hitlManager && this.hitlManager.isEnabled()) {
304
+ const HITLState = context?.hitlState;
305
+ if (!HITLState?.triggered &&
306
+ this.hitlManager &&
307
+ this.hitlManager.isEnabled()) {
305
308
  const requiresConfirmation = this.hitlManager.requiresConfirmation(toolName, args);
306
309
  if (requiresConfirmation) {
307
310
  registryLogger.info(`Tool '${toolName}' requires HITL confirmation`);
308
311
  span.addEvent("tool.hitl_requested");
309
312
  try {
313
+ if (HITLState) {
314
+ HITLState.triggered = true;
315
+ }
310
316
  const confirmationResult = await this.hitlManager.requestConfirmation(toolName, args, {
311
317
  serverId: tool.serverId,
312
318
  sessionId: execContext.sessionId,