vocal-stack 1.0.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/LICENSE +21 -0
- package/README.md +269 -0
- package/dist/flow/index.cjs +571 -0
- package/dist/flow/index.cjs.map +1 -0
- package/dist/flow/index.d.cts +337 -0
- package/dist/flow/index.d.ts +337 -0
- package/dist/flow/index.js +559 -0
- package/dist/flow/index.js.map +1 -0
- package/dist/index.cjs +1026 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +32 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +1003 -0
- package/dist/index.js.map +1 -0
- package/dist/monitor/index.cjs +291 -0
- package/dist/monitor/index.cjs.map +1 -0
- package/dist/monitor/index.d.cts +122 -0
- package/dist/monitor/index.d.ts +122 -0
- package/dist/monitor/index.js +286 -0
- package/dist/monitor/index.js.map +1 -0
- package/dist/sanitizer/index.cjs +190 -0
- package/dist/sanitizer/index.cjs.map +1 -0
- package/dist/sanitizer/index.d.cts +83 -0
- package/dist/sanitizer/index.d.ts +83 -0
- package/dist/sanitizer/index.js +186 -0
- package/dist/sanitizer/index.js.map +1 -0
- package/package.json +90 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var VocalStackError = class extends Error {
|
|
3
|
+
constructor(message, code, context) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.context = context;
|
|
7
|
+
this.name = "VocalStackError";
|
|
8
|
+
Error.captureStackTrace(this, this.constructor);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var FlowControlError = class extends VocalStackError {
|
|
12
|
+
constructor(message, context) {
|
|
13
|
+
super(message, "FLOW_CONTROL_ERROR", context);
|
|
14
|
+
this.name = "FlowControlError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/flow/buffer-manager.ts
|
|
19
|
+
var BufferManager = class {
|
|
20
|
+
buffer = [];
|
|
21
|
+
maxSize;
|
|
22
|
+
head = 0;
|
|
23
|
+
size = 0;
|
|
24
|
+
constructor(maxSize = 10) {
|
|
25
|
+
if (maxSize <= 0) {
|
|
26
|
+
throw new Error("Buffer size must be positive");
|
|
27
|
+
}
|
|
28
|
+
this.maxSize = maxSize;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Add chunk to buffer
|
|
32
|
+
*/
|
|
33
|
+
add(chunk) {
|
|
34
|
+
if (this.size < this.maxSize) {
|
|
35
|
+
this.buffer.push(chunk);
|
|
36
|
+
this.size++;
|
|
37
|
+
} else {
|
|
38
|
+
this.buffer[this.head] = chunk;
|
|
39
|
+
this.head = (this.head + 1) % this.maxSize;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get all buffered chunks in order
|
|
44
|
+
*/
|
|
45
|
+
getAll() {
|
|
46
|
+
if (this.size < this.maxSize) {
|
|
47
|
+
return [...this.buffer];
|
|
48
|
+
}
|
|
49
|
+
return [...this.buffer.slice(this.head), ...this.buffer.slice(0, this.head)];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Clear all buffered chunks
|
|
53
|
+
*/
|
|
54
|
+
clear() {
|
|
55
|
+
this.buffer = [];
|
|
56
|
+
this.head = 0;
|
|
57
|
+
this.size = 0;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get current buffer size
|
|
61
|
+
*/
|
|
62
|
+
getSize() {
|
|
63
|
+
return this.size;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Check if buffer is empty
|
|
67
|
+
*/
|
|
68
|
+
isEmpty() {
|
|
69
|
+
return this.size === 0;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Check if buffer is full
|
|
73
|
+
*/
|
|
74
|
+
isFull() {
|
|
75
|
+
return this.size === this.maxSize;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// src/flow/constants.ts
|
|
80
|
+
var DEFAULT_STALL_THRESHOLD_MS = 700;
|
|
81
|
+
var DEFAULT_FILLER_PHRASES = ["um", "let me think", "hmm"];
|
|
82
|
+
var DEFAULT_MAX_FILLERS_PER_RESPONSE = 3;
|
|
83
|
+
|
|
84
|
+
// src/flow/filler-injector.ts
|
|
85
|
+
var FillerInjector = class {
|
|
86
|
+
phrases;
|
|
87
|
+
maxFillers;
|
|
88
|
+
fillersUsed = 0;
|
|
89
|
+
lastFillerIndex = -1;
|
|
90
|
+
constructor(phrases, maxFillers) {
|
|
91
|
+
this.phrases = phrases;
|
|
92
|
+
this.maxFillers = maxFillers;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get next filler phrase (returns null if limit reached)
|
|
96
|
+
*/
|
|
97
|
+
getFiller() {
|
|
98
|
+
if (this.fillersUsed >= this.maxFillers) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
this.lastFillerIndex = (this.lastFillerIndex + 1) % this.phrases.length;
|
|
102
|
+
this.fillersUsed++;
|
|
103
|
+
return this.phrases[this.lastFillerIndex] ?? null;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Reset filler state
|
|
107
|
+
*/
|
|
108
|
+
reset() {
|
|
109
|
+
this.fillersUsed = 0;
|
|
110
|
+
this.lastFillerIndex = -1;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Check if more fillers can be injected
|
|
114
|
+
*/
|
|
115
|
+
canInjectMore() {
|
|
116
|
+
return this.fillersUsed < this.maxFillers;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get count of fillers used
|
|
120
|
+
*/
|
|
121
|
+
getUsedCount() {
|
|
122
|
+
return this.fillersUsed;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// src/flow/stall-detector.ts
|
|
127
|
+
var StallDetector = class {
|
|
128
|
+
lastChunkTime = null;
|
|
129
|
+
stallTimer = null;
|
|
130
|
+
thresholdMs;
|
|
131
|
+
onStall;
|
|
132
|
+
constructor(thresholdMs, onStall) {
|
|
133
|
+
this.thresholdMs = thresholdMs;
|
|
134
|
+
this.onStall = onStall;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Notify detector that a chunk was received
|
|
138
|
+
*/
|
|
139
|
+
notifyChunk() {
|
|
140
|
+
this.lastChunkTime = Date.now();
|
|
141
|
+
this.clearTimer();
|
|
142
|
+
this.scheduleStallCheck();
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Start monitoring for stalls
|
|
146
|
+
*/
|
|
147
|
+
start() {
|
|
148
|
+
this.lastChunkTime = Date.now();
|
|
149
|
+
this.scheduleStallCheck();
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Stop monitoring
|
|
153
|
+
*/
|
|
154
|
+
stop() {
|
|
155
|
+
this.clearTimer();
|
|
156
|
+
this.lastChunkTime = null;
|
|
157
|
+
}
|
|
158
|
+
scheduleStallCheck() {
|
|
159
|
+
this.clearTimer();
|
|
160
|
+
this.stallTimer = setTimeout(() => {
|
|
161
|
+
if (this.lastChunkTime !== null) {
|
|
162
|
+
const elapsed = Date.now() - this.lastChunkTime;
|
|
163
|
+
if (elapsed >= this.thresholdMs) {
|
|
164
|
+
this.onStall(elapsed);
|
|
165
|
+
this.scheduleStallCheck();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}, this.thresholdMs);
|
|
169
|
+
}
|
|
170
|
+
clearTimer() {
|
|
171
|
+
if (this.stallTimer) {
|
|
172
|
+
clearTimeout(this.stallTimer);
|
|
173
|
+
this.stallTimer = null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// src/flow/types.ts
|
|
179
|
+
var ConversationState = /* @__PURE__ */ ((ConversationState2) => {
|
|
180
|
+
ConversationState2["IDLE"] = "idle";
|
|
181
|
+
ConversationState2["WAITING"] = "waiting";
|
|
182
|
+
ConversationState2["SPEAKING"] = "speaking";
|
|
183
|
+
ConversationState2["INTERRUPTED"] = "interrupted";
|
|
184
|
+
return ConversationState2;
|
|
185
|
+
})(ConversationState || {});
|
|
186
|
+
|
|
187
|
+
// src/flow/state-machine.ts
|
|
188
|
+
var VALID_TRANSITIONS = /* @__PURE__ */ new Map([
|
|
189
|
+
["idle" /* IDLE */, ["speaking" /* SPEAKING */, "waiting" /* WAITING */]],
|
|
190
|
+
[
|
|
191
|
+
"waiting" /* WAITING */,
|
|
192
|
+
["speaking" /* SPEAKING */, "idle" /* IDLE */, "interrupted" /* INTERRUPTED */]
|
|
193
|
+
],
|
|
194
|
+
["speaking" /* SPEAKING */, ["interrupted" /* INTERRUPTED */, "idle" /* IDLE */]],
|
|
195
|
+
["interrupted" /* INTERRUPTED */, ["idle" /* IDLE */, "waiting" /* WAITING */]]
|
|
196
|
+
]);
|
|
197
|
+
var ConversationStateMachine = class {
|
|
198
|
+
currentState = "idle" /* IDLE */;
|
|
199
|
+
listeners = /* @__PURE__ */ new Set();
|
|
200
|
+
/**
|
|
201
|
+
* Get current state
|
|
202
|
+
*/
|
|
203
|
+
getState() {
|
|
204
|
+
return this.currentState;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Attempt to transition to new state
|
|
208
|
+
*/
|
|
209
|
+
transition(to) {
|
|
210
|
+
const validTransitions = VALID_TRANSITIONS.get(this.currentState);
|
|
211
|
+
if (!validTransitions?.includes(to)) {
|
|
212
|
+
throw new FlowControlError(`Invalid state transition: ${this.currentState} -> ${to}`, {
|
|
213
|
+
from: this.currentState,
|
|
214
|
+
to
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
const from = this.currentState;
|
|
218
|
+
this.currentState = to;
|
|
219
|
+
for (const listener of this.listeners) {
|
|
220
|
+
listener(from, to);
|
|
221
|
+
}
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Add state change listener
|
|
226
|
+
*/
|
|
227
|
+
onStateChange(listener) {
|
|
228
|
+
this.listeners.add(listener);
|
|
229
|
+
return () => this.listeners.delete(listener);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Reset to IDLE
|
|
233
|
+
*/
|
|
234
|
+
reset() {
|
|
235
|
+
this.currentState = "idle" /* IDLE */;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// src/flow/flow-controller.ts
|
|
240
|
+
var FlowController = class {
|
|
241
|
+
config;
|
|
242
|
+
stateMachine;
|
|
243
|
+
stallDetector;
|
|
244
|
+
fillerInjector;
|
|
245
|
+
bufferManager;
|
|
246
|
+
firstChunkEmitted = false;
|
|
247
|
+
stats = {
|
|
248
|
+
fillersInjected: 0,
|
|
249
|
+
stallsDetected: 0,
|
|
250
|
+
chunksProcessed: 0,
|
|
251
|
+
firstChunkTime: null,
|
|
252
|
+
totalDurationMs: 0
|
|
253
|
+
};
|
|
254
|
+
startTime = null;
|
|
255
|
+
constructor(config = {}) {
|
|
256
|
+
this.config = {
|
|
257
|
+
stallThresholdMs: config.stallThresholdMs ?? DEFAULT_STALL_THRESHOLD_MS,
|
|
258
|
+
fillerPhrases: config.fillerPhrases ?? DEFAULT_FILLER_PHRASES,
|
|
259
|
+
enableFillers: config.enableFillers ?? true,
|
|
260
|
+
maxFillersPerResponse: config.maxFillersPerResponse ?? DEFAULT_MAX_FILLERS_PER_RESPONSE,
|
|
261
|
+
onFillerInjected: config.onFillerInjected ?? (() => {
|
|
262
|
+
}),
|
|
263
|
+
onStallDetected: config.onStallDetected ?? (() => {
|
|
264
|
+
}),
|
|
265
|
+
onFirstChunk: config.onFirstChunk ?? (() => {
|
|
266
|
+
})
|
|
267
|
+
};
|
|
268
|
+
this.stateMachine = new ConversationStateMachine();
|
|
269
|
+
this.fillerInjector = new FillerInjector(
|
|
270
|
+
this.config.fillerPhrases,
|
|
271
|
+
this.config.maxFillersPerResponse
|
|
272
|
+
);
|
|
273
|
+
this.bufferManager = new BufferManager(10);
|
|
274
|
+
this.stallDetector = new StallDetector(
|
|
275
|
+
this.config.stallThresholdMs,
|
|
276
|
+
this.handleStall.bind(this)
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Wrap an async iterable with flow control
|
|
281
|
+
*/
|
|
282
|
+
async *wrap(input) {
|
|
283
|
+
this.reset();
|
|
284
|
+
this.startTime = Date.now();
|
|
285
|
+
this.stateMachine.transition("waiting" /* WAITING */);
|
|
286
|
+
this.stallDetector.start();
|
|
287
|
+
try {
|
|
288
|
+
for await (const chunk of input) {
|
|
289
|
+
if (this.stateMachine.getState() === "interrupted" /* INTERRUPTED */) {
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
this.stallDetector.notifyChunk();
|
|
293
|
+
this.stats.chunksProcessed++;
|
|
294
|
+
this.bufferManager.add(chunk);
|
|
295
|
+
if (!this.firstChunkEmitted) {
|
|
296
|
+
this.firstChunkEmitted = true;
|
|
297
|
+
this.stats.firstChunkTime = Date.now() - (this.startTime ?? Date.now());
|
|
298
|
+
this.stateMachine.transition("speaking" /* SPEAKING */);
|
|
299
|
+
this.config.onFirstChunk();
|
|
300
|
+
}
|
|
301
|
+
yield chunk;
|
|
302
|
+
}
|
|
303
|
+
if (this.stateMachine.getState() !== "interrupted" /* INTERRUPTED */) {
|
|
304
|
+
this.stateMachine.transition("idle" /* IDLE */);
|
|
305
|
+
}
|
|
306
|
+
} catch (error) {
|
|
307
|
+
throw new FlowControlError("Flow control error during stream processing", { error });
|
|
308
|
+
} finally {
|
|
309
|
+
this.stallDetector.stop();
|
|
310
|
+
this.stats.totalDurationMs = Date.now() - (this.startTime ?? Date.now());
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Interrupt the current flow (for barge-in)
|
|
315
|
+
*/
|
|
316
|
+
interrupt() {
|
|
317
|
+
const currentState = this.stateMachine.getState();
|
|
318
|
+
if (currentState === "speaking" /* SPEAKING */ || currentState === "waiting" /* WAITING */) {
|
|
319
|
+
this.stateMachine.transition("interrupted" /* INTERRUPTED */);
|
|
320
|
+
this.stallDetector.stop();
|
|
321
|
+
this.fillerInjector.reset();
|
|
322
|
+
this.bufferManager.clear();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Get current conversation state
|
|
327
|
+
*/
|
|
328
|
+
getState() {
|
|
329
|
+
return this.stateMachine.getState();
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Get flow statistics
|
|
333
|
+
*/
|
|
334
|
+
getStats() {
|
|
335
|
+
return { ...this.stats };
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Get buffered chunks (for advanced barge-in scenarios)
|
|
339
|
+
*/
|
|
340
|
+
getBufferedChunks() {
|
|
341
|
+
return this.bufferManager.getAll();
|
|
342
|
+
}
|
|
343
|
+
handleStall(durationMs) {
|
|
344
|
+
if (this.config.enableFillers && !this.firstChunkEmitted && this.stateMachine.getState() !== "interrupted" /* INTERRUPTED */) {
|
|
345
|
+
const filler = this.fillerInjector.getFiller();
|
|
346
|
+
if (filler) {
|
|
347
|
+
this.stats.fillersInjected++;
|
|
348
|
+
this.config.onFillerInjected(filler);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
this.stats.stallsDetected++;
|
|
352
|
+
this.config.onStallDetected(durationMs);
|
|
353
|
+
}
|
|
354
|
+
reset() {
|
|
355
|
+
this.firstChunkEmitted = false;
|
|
356
|
+
this.fillerInjector.reset();
|
|
357
|
+
this.bufferManager.clear();
|
|
358
|
+
this.stats = {
|
|
359
|
+
fillersInjected: 0,
|
|
360
|
+
stallsDetected: 0,
|
|
361
|
+
chunksProcessed: 0,
|
|
362
|
+
firstChunkTime: null,
|
|
363
|
+
totalDurationMs: 0
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
function withFlowControl(input, config) {
|
|
368
|
+
const controller = new FlowController(config);
|
|
369
|
+
return controller.wrap(input);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/flow/flow-manager.ts
|
|
373
|
+
var FlowManager = class {
|
|
374
|
+
config;
|
|
375
|
+
stateMachine;
|
|
376
|
+
stallDetector;
|
|
377
|
+
fillerInjector;
|
|
378
|
+
bufferManager;
|
|
379
|
+
listeners = /* @__PURE__ */ new Set();
|
|
380
|
+
firstChunkEmitted = false;
|
|
381
|
+
stats = {
|
|
382
|
+
fillersInjected: 0,
|
|
383
|
+
stallsDetected: 0,
|
|
384
|
+
chunksProcessed: 0,
|
|
385
|
+
firstChunkTime: null,
|
|
386
|
+
totalDurationMs: 0
|
|
387
|
+
};
|
|
388
|
+
startTime = null;
|
|
389
|
+
stateChangeUnsubscribe = null;
|
|
390
|
+
constructor(config = {}) {
|
|
391
|
+
this.config = {
|
|
392
|
+
stallThresholdMs: config.stallThresholdMs ?? DEFAULT_STALL_THRESHOLD_MS,
|
|
393
|
+
fillerPhrases: config.fillerPhrases ?? DEFAULT_FILLER_PHRASES,
|
|
394
|
+
enableFillers: config.enableFillers ?? true,
|
|
395
|
+
maxFillersPerResponse: config.maxFillersPerResponse ?? DEFAULT_MAX_FILLERS_PER_RESPONSE,
|
|
396
|
+
bufferSize: config.bufferSize ?? 10
|
|
397
|
+
};
|
|
398
|
+
this.stateMachine = new ConversationStateMachine();
|
|
399
|
+
this.fillerInjector = new FillerInjector(
|
|
400
|
+
this.config.fillerPhrases,
|
|
401
|
+
this.config.maxFillersPerResponse
|
|
402
|
+
);
|
|
403
|
+
this.bufferManager = new BufferManager(this.config.bufferSize);
|
|
404
|
+
this.stallDetector = new StallDetector(
|
|
405
|
+
this.config.stallThresholdMs,
|
|
406
|
+
this.handleStall.bind(this)
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Add event listener
|
|
411
|
+
*/
|
|
412
|
+
on(listener) {
|
|
413
|
+
this.listeners.add(listener);
|
|
414
|
+
return () => this.listeners.delete(listener);
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Start flow tracking
|
|
418
|
+
*/
|
|
419
|
+
start() {
|
|
420
|
+
if (this.startTime !== null) {
|
|
421
|
+
throw new FlowControlError("FlowManager already started");
|
|
422
|
+
}
|
|
423
|
+
this.reset();
|
|
424
|
+
this.startTime = Date.now();
|
|
425
|
+
this.stateMachine.transition("waiting" /* WAITING */);
|
|
426
|
+
this.stallDetector.start();
|
|
427
|
+
this.stateChangeUnsubscribe = this.stateMachine.onStateChange((from, to) => {
|
|
428
|
+
this.emit({
|
|
429
|
+
type: "state-change",
|
|
430
|
+
from,
|
|
431
|
+
to
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Process a chunk from the stream
|
|
437
|
+
*/
|
|
438
|
+
processChunk(chunk) {
|
|
439
|
+
if (this.startTime === null) {
|
|
440
|
+
throw new FlowControlError("FlowManager not started. Call start() first.");
|
|
441
|
+
}
|
|
442
|
+
if (this.stateMachine.getState() === "interrupted" /* INTERRUPTED */) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
this.stallDetector.notifyChunk();
|
|
446
|
+
this.stats.chunksProcessed++;
|
|
447
|
+
this.bufferManager.add(chunk);
|
|
448
|
+
if (!this.firstChunkEmitted) {
|
|
449
|
+
this.firstChunkEmitted = true;
|
|
450
|
+
this.stats.firstChunkTime = Date.now() - this.startTime;
|
|
451
|
+
this.stateMachine.transition("speaking" /* SPEAKING */);
|
|
452
|
+
this.emit({
|
|
453
|
+
type: "first-chunk",
|
|
454
|
+
chunk
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
this.emit({
|
|
458
|
+
type: "chunk-processed",
|
|
459
|
+
chunk
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Complete the flow
|
|
464
|
+
*/
|
|
465
|
+
complete() {
|
|
466
|
+
if (this.startTime === null) {
|
|
467
|
+
throw new FlowControlError("FlowManager not started");
|
|
468
|
+
}
|
|
469
|
+
this.stallDetector.stop();
|
|
470
|
+
this.stats.totalDurationMs = Date.now() - this.startTime;
|
|
471
|
+
if (this.stateMachine.getState() !== "interrupted" /* INTERRUPTED */) {
|
|
472
|
+
this.stateMachine.transition("idle" /* IDLE */);
|
|
473
|
+
}
|
|
474
|
+
this.emit({
|
|
475
|
+
type: "completed",
|
|
476
|
+
stats: this.getStats()
|
|
477
|
+
});
|
|
478
|
+
if (this.stateChangeUnsubscribe) {
|
|
479
|
+
this.stateChangeUnsubscribe();
|
|
480
|
+
this.stateChangeUnsubscribe = null;
|
|
481
|
+
}
|
|
482
|
+
this.startTime = null;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Interrupt the flow (for barge-in)
|
|
486
|
+
*/
|
|
487
|
+
interrupt() {
|
|
488
|
+
const currentState = this.stateMachine.getState();
|
|
489
|
+
if (currentState === "speaking" /* SPEAKING */ || currentState === "waiting" /* WAITING */) {
|
|
490
|
+
this.stateMachine.transition("interrupted" /* INTERRUPTED */);
|
|
491
|
+
this.stallDetector.stop();
|
|
492
|
+
this.fillerInjector.reset();
|
|
493
|
+
this.bufferManager.clear();
|
|
494
|
+
this.emit({
|
|
495
|
+
type: "interrupted"
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Get current conversation state
|
|
501
|
+
*/
|
|
502
|
+
getState() {
|
|
503
|
+
return this.stateMachine.getState();
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Get flow statistics
|
|
507
|
+
*/
|
|
508
|
+
getStats() {
|
|
509
|
+
return { ...this.stats };
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Get buffered chunks
|
|
513
|
+
*/
|
|
514
|
+
getBufferedChunks() {
|
|
515
|
+
return this.bufferManager.getAll();
|
|
516
|
+
}
|
|
517
|
+
handleStall(durationMs) {
|
|
518
|
+
this.stats.stallsDetected++;
|
|
519
|
+
this.emit({
|
|
520
|
+
type: "stall-detected",
|
|
521
|
+
durationMs
|
|
522
|
+
});
|
|
523
|
+
if (this.config.enableFillers && !this.firstChunkEmitted && this.stateMachine.getState() !== "interrupted" /* INTERRUPTED */) {
|
|
524
|
+
const filler = this.fillerInjector.getFiller();
|
|
525
|
+
if (filler) {
|
|
526
|
+
this.stats.fillersInjected++;
|
|
527
|
+
this.emit({
|
|
528
|
+
type: "filler-injected",
|
|
529
|
+
filler
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
emit(event) {
|
|
535
|
+
for (const listener of this.listeners) {
|
|
536
|
+
try {
|
|
537
|
+
listener(event);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
console.error("Error in FlowManager event listener:", error);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
reset() {
|
|
544
|
+
this.firstChunkEmitted = false;
|
|
545
|
+
this.fillerInjector.reset();
|
|
546
|
+
this.bufferManager.clear();
|
|
547
|
+
this.stats = {
|
|
548
|
+
fillersInjected: 0,
|
|
549
|
+
stallsDetected: 0,
|
|
550
|
+
chunksProcessed: 0,
|
|
551
|
+
firstChunkTime: null,
|
|
552
|
+
totalDurationMs: 0
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
export { BufferManager, ConversationState, ConversationStateMachine, DEFAULT_FILLER_PHRASES, DEFAULT_MAX_FILLERS_PER_RESPONSE, DEFAULT_STALL_THRESHOLD_MS, FillerInjector, FlowController, FlowManager, StallDetector, withFlowControl };
|
|
558
|
+
//# sourceMappingURL=index.js.map
|
|
559
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/errors.ts","../../src/flow/buffer-manager.ts","../../src/flow/constants.ts","../../src/flow/filler-injector.ts","../../src/flow/stall-detector.ts","../../src/flow/types.ts","../../src/flow/state-machine.ts","../../src/flow/flow-controller.ts","../../src/flow/flow-manager.ts"],"names":["ConversationState"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Base error class for all vocal-stack errors\n */\nexport class VocalStackError extends Error {\n constructor(\n message: string,\n public readonly code: string,\n public readonly context?: Record<string, unknown>\n ) {\n super(message);\n this.name = 'VocalStackError';\n Error.captureStackTrace(this, this.constructor);\n }\n}\n\n/**\n * Error thrown during text sanitization\n */\nexport class SanitizerError extends VocalStackError {\n constructor(message: string, context?: Record<string, unknown>) {\n super(message, 'SANITIZER_ERROR', context);\n this.name = 'SanitizerError';\n }\n}\n\n/**\n * Error thrown during flow control operations\n */\nexport class FlowControlError extends VocalStackError {\n constructor(message: string, context?: Record<string, unknown>) {\n super(message, 'FLOW_CONTROL_ERROR', context);\n this.name = 'FlowControlError';\n }\n}\n\n/**\n * Error thrown during monitoring operations\n */\nexport class MonitorError extends VocalStackError {\n constructor(message: string, context?: Record<string, unknown>) {\n super(message, 'MONITOR_ERROR', context);\n this.name = 'MonitorError';\n }\n}\n","/**\n * Buffer manager for barge-in scenarios\n */\nexport class BufferManager {\n private buffer: string[] = [];\n private readonly maxSize: number;\n private head = 0;\n private size = 0;\n\n constructor(maxSize = 10) {\n if (maxSize <= 0) {\n throw new Error('Buffer size must be positive');\n }\n this.maxSize = maxSize;\n }\n\n /**\n * Add chunk to buffer\n */\n add(chunk: string): void {\n if (this.size < this.maxSize) {\n this.buffer.push(chunk);\n this.size++;\n } else {\n // Circular buffer - overwrite oldest\n this.buffer[this.head] = chunk;\n this.head = (this.head + 1) % this.maxSize;\n }\n }\n\n /**\n * Get all buffered chunks in order\n */\n getAll(): readonly string[] {\n if (this.size < this.maxSize) {\n return [...this.buffer];\n }\n\n // Return in order: from head to end, then from start to head\n return [...this.buffer.slice(this.head), ...this.buffer.slice(0, this.head)];\n }\n\n /**\n * Clear all buffered chunks\n */\n clear(): void {\n this.buffer = [];\n this.head = 0;\n this.size = 0;\n }\n\n /**\n * Get current buffer size\n */\n getSize(): number {\n return this.size;\n }\n\n /**\n * Check if buffer is empty\n */\n isEmpty(): boolean {\n return this.size === 0;\n }\n\n /**\n * Check if buffer is full\n */\n isFull(): boolean {\n return this.size === this.maxSize;\n }\n}\n","/**\n * Default stall threshold in milliseconds\n * Based on human perception of silence: 500-1000ms feels like a pause\n * Most LLM APIs stream chunks every 50-200ms when active\n */\nexport const DEFAULT_STALL_THRESHOLD_MS = 700;\n\n/**\n * Default filler phrases to inject during stalls\n */\nexport const DEFAULT_FILLER_PHRASES = ['um', 'let me think', 'hmm'];\n\n/**\n * Default maximum fillers per response\n * Prevents over-use of filler words\n */\nexport const DEFAULT_MAX_FILLERS_PER_RESPONSE = 3;\n","/**\n * Manages filler phrase injection\n */\nexport class FillerInjector {\n private readonly phrases: readonly string[];\n private readonly maxFillers: number;\n private fillersUsed = 0;\n private lastFillerIndex = -1;\n\n constructor(phrases: readonly string[], maxFillers: number) {\n this.phrases = phrases;\n this.maxFillers = maxFillers;\n }\n\n /**\n * Get next filler phrase (returns null if limit reached)\n */\n getFiller(): string | null {\n if (this.fillersUsed >= this.maxFillers) {\n return null;\n }\n\n // Rotate through phrases to avoid repetition\n this.lastFillerIndex = (this.lastFillerIndex + 1) % this.phrases.length;\n this.fillersUsed++;\n\n return this.phrases[this.lastFillerIndex] ?? null;\n }\n\n /**\n * Reset filler state\n */\n reset(): void {\n this.fillersUsed = 0;\n this.lastFillerIndex = -1;\n }\n\n /**\n * Check if more fillers can be injected\n */\n canInjectMore(): boolean {\n return this.fillersUsed < this.maxFillers;\n }\n\n /**\n * Get count of fillers used\n */\n getUsedCount(): number {\n return this.fillersUsed;\n }\n}\n","/**\n * Detects stream stalls based on timing\n */\nexport class StallDetector {\n private lastChunkTime: number | null = null;\n private stallTimer: NodeJS.Timeout | null = null;\n private readonly thresholdMs: number;\n private readonly onStall: (durationMs: number) => void;\n\n constructor(thresholdMs: number, onStall: (durationMs: number) => void) {\n this.thresholdMs = thresholdMs;\n this.onStall = onStall;\n }\n\n /**\n * Notify detector that a chunk was received\n */\n notifyChunk(): void {\n this.lastChunkTime = Date.now();\n this.clearTimer();\n this.scheduleStallCheck();\n }\n\n /**\n * Start monitoring for stalls\n */\n start(): void {\n this.lastChunkTime = Date.now();\n this.scheduleStallCheck();\n }\n\n /**\n * Stop monitoring\n */\n stop(): void {\n this.clearTimer();\n this.lastChunkTime = null;\n }\n\n private scheduleStallCheck(): void {\n this.clearTimer();\n this.stallTimer = setTimeout(() => {\n if (this.lastChunkTime !== null) {\n const elapsed = Date.now() - this.lastChunkTime;\n if (elapsed >= this.thresholdMs) {\n this.onStall(elapsed);\n // Continue checking for additional stalls\n this.scheduleStallCheck();\n }\n }\n }, this.thresholdMs);\n }\n\n private clearTimer(): void {\n if (this.stallTimer) {\n clearTimeout(this.stallTimer);\n this.stallTimer = null;\n }\n }\n}\n","/**\n * Configuration for flow control\n */\nexport interface FlowConfig {\n /**\n * Stall threshold in milliseconds\n * @default 700\n */\n readonly stallThresholdMs?: number;\n\n /**\n * Filler phrases to inject\n * @default ['um', 'let me think', 'hmm']\n */\n readonly fillerPhrases?: readonly string[];\n\n /**\n * Whether to enable filler injection\n * @default true\n */\n readonly enableFillers?: boolean;\n\n /**\n * Maximum number of fillers to inject per response\n * @default 3\n */\n readonly maxFillersPerResponse?: number;\n\n /**\n * Callback when filler is injected\n */\n readonly onFillerInjected?: (filler: string) => void;\n\n /**\n * Callback when stall is detected\n */\n readonly onStallDetected?: (durationMs: number) => void;\n\n /**\n * Callback when first chunk is emitted\n */\n readonly onFirstChunk?: () => void;\n}\n\n/**\n * Conversation states\n */\nexport enum ConversationState {\n IDLE = 'idle',\n WAITING = 'waiting',\n SPEAKING = 'speaking',\n INTERRUPTED = 'interrupted',\n}\n\n/**\n * Statistics tracked by flow controller\n */\nexport interface FlowStats {\n readonly fillersInjected: number;\n readonly stallsDetected: number;\n readonly chunksProcessed: number;\n readonly firstChunkTime: number | null;\n readonly totalDurationMs: number;\n}\n\n/**\n * Flow events for low-level API\n */\nexport type FlowEvent =\n | {\n type: 'stall-detected';\n durationMs: number;\n }\n | {\n type: 'filler-injected';\n filler: string;\n }\n | {\n type: 'first-chunk';\n chunk: string;\n }\n | {\n type: 'state-change';\n from: ConversationState;\n to: ConversationState;\n }\n | {\n type: 'interrupted';\n }\n | {\n type: 'chunk-processed';\n chunk: string;\n }\n | {\n type: 'completed';\n stats: FlowStats;\n };\n\n/**\n * Event listener for flow events\n */\nexport type FlowEventListener = (event: FlowEvent) => void;\n\n/**\n * Configuration for low-level FlowManager\n */\nexport interface FlowManagerConfig {\n /**\n * Stall threshold in milliseconds\n * @default 700\n */\n readonly stallThresholdMs?: number;\n\n /**\n * Filler phrases to inject\n * @default ['um', 'let me think', 'hmm']\n */\n readonly fillerPhrases?: readonly string[];\n\n /**\n * Whether to enable filler injection\n * @default true\n */\n readonly enableFillers?: boolean;\n\n /**\n * Maximum number of fillers to inject per response\n * @default 3\n */\n readonly maxFillersPerResponse?: number;\n\n /**\n * Buffer size for barge-in scenarios\n * @default 10\n */\n readonly bufferSize?: number;\n}\n","import { FlowControlError } from '../errors';\nimport { ConversationState } from './types';\n\n/**\n * Valid state transitions\n * IDLE → SPEAKING/WAITING\n * WAITING → SPEAKING/IDLE/INTERRUPTED\n * SPEAKING → INTERRUPTED/IDLE\n * INTERRUPTED → IDLE/WAITING\n */\nconst VALID_TRANSITIONS: ReadonlyMap<ConversationState, readonly ConversationState[]> = new Map([\n [ConversationState.IDLE, [ConversationState.SPEAKING, ConversationState.WAITING]],\n [\n ConversationState.WAITING,\n [ConversationState.SPEAKING, ConversationState.IDLE, ConversationState.INTERRUPTED],\n ],\n [ConversationState.SPEAKING, [ConversationState.INTERRUPTED, ConversationState.IDLE]],\n [ConversationState.INTERRUPTED, [ConversationState.IDLE, ConversationState.WAITING]],\n]);\n\n/**\n * Conversation state machine\n */\nexport class ConversationStateMachine {\n private currentState: ConversationState = ConversationState.IDLE;\n private readonly listeners: Set<(from: ConversationState, to: ConversationState) => void> =\n new Set();\n\n /**\n * Get current state\n */\n getState(): ConversationState {\n return this.currentState;\n }\n\n /**\n * Attempt to transition to new state\n */\n transition(to: ConversationState): boolean {\n const validTransitions = VALID_TRANSITIONS.get(this.currentState);\n\n if (!validTransitions?.includes(to)) {\n throw new FlowControlError(`Invalid state transition: ${this.currentState} -> ${to}`, {\n from: this.currentState,\n to,\n });\n }\n\n const from = this.currentState;\n this.currentState = to;\n\n // Notify listeners\n for (const listener of this.listeners) {\n listener(from, to);\n }\n\n return true;\n }\n\n /**\n * Add state change listener\n */\n onStateChange(listener: (from: ConversationState, to: ConversationState) => void): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n /**\n * Reset to IDLE\n */\n reset(): void {\n this.currentState = ConversationState.IDLE;\n }\n}\n","import { FlowControlError } from '../errors';\nimport { BufferManager } from './buffer-manager';\nimport {\n DEFAULT_FILLER_PHRASES,\n DEFAULT_MAX_FILLERS_PER_RESPONSE,\n DEFAULT_STALL_THRESHOLD_MS,\n} from './constants';\nimport { FillerInjector } from './filler-injector';\nimport { StallDetector } from './stall-detector';\nimport { ConversationStateMachine } from './state-machine';\nimport { ConversationState, type FlowConfig, type FlowStats } from './types';\n\n/**\n * High-level stream wrapper for flow control\n */\nexport class FlowController {\n private readonly config: Required<FlowConfig>;\n private readonly stateMachine: ConversationStateMachine;\n private readonly stallDetector: StallDetector;\n private readonly fillerInjector: FillerInjector;\n private readonly bufferManager: BufferManager;\n private firstChunkEmitted = false;\n private stats: {\n fillersInjected: number;\n stallsDetected: number;\n chunksProcessed: number;\n firstChunkTime: number | null;\n totalDurationMs: number;\n } = {\n fillersInjected: 0,\n stallsDetected: 0,\n chunksProcessed: 0,\n firstChunkTime: null,\n totalDurationMs: 0,\n };\n private startTime: number | null = null;\n\n constructor(config: FlowConfig = {}) {\n this.config = {\n stallThresholdMs: config.stallThresholdMs ?? DEFAULT_STALL_THRESHOLD_MS,\n fillerPhrases: config.fillerPhrases ?? DEFAULT_FILLER_PHRASES,\n enableFillers: config.enableFillers ?? true,\n maxFillersPerResponse: config.maxFillersPerResponse ?? DEFAULT_MAX_FILLERS_PER_RESPONSE,\n onFillerInjected: config.onFillerInjected ?? (() => {}),\n onStallDetected: config.onStallDetected ?? (() => {}),\n onFirstChunk: config.onFirstChunk ?? (() => {}),\n };\n\n this.stateMachine = new ConversationStateMachine();\n this.fillerInjector = new FillerInjector(\n this.config.fillerPhrases,\n this.config.maxFillersPerResponse\n );\n this.bufferManager = new BufferManager(10); // Default buffer size of 10\n\n this.stallDetector = new StallDetector(\n this.config.stallThresholdMs,\n this.handleStall.bind(this)\n );\n }\n\n /**\n * Wrap an async iterable with flow control\n */\n async *wrap(input: AsyncIterable<string>): AsyncIterable<string> {\n this.reset();\n this.startTime = Date.now();\n this.stateMachine.transition(ConversationState.WAITING);\n this.stallDetector.start();\n\n try {\n for await (const chunk of input) {\n // Check if interrupted\n if (this.stateMachine.getState() === ConversationState.INTERRUPTED) {\n break;\n }\n\n this.stallDetector.notifyChunk();\n this.stats.chunksProcessed++;\n\n // Add to buffer before yielding\n this.bufferManager.add(chunk);\n\n // Handle first chunk\n if (!this.firstChunkEmitted) {\n this.firstChunkEmitted = true;\n this.stats.firstChunkTime = Date.now() - (this.startTime ?? Date.now());\n this.stateMachine.transition(ConversationState.SPEAKING);\n this.config.onFirstChunk();\n }\n\n yield chunk;\n }\n\n // Only transition to IDLE if not interrupted\n if (this.stateMachine.getState() !== ConversationState.INTERRUPTED) {\n this.stateMachine.transition(ConversationState.IDLE);\n }\n } catch (error) {\n throw new FlowControlError('Flow control error during stream processing', { error });\n } finally {\n this.stallDetector.stop();\n this.stats.totalDurationMs = Date.now() - (this.startTime ?? Date.now());\n }\n }\n\n /**\n * Interrupt the current flow (for barge-in)\n */\n interrupt(): void {\n const currentState = this.stateMachine.getState();\n if (currentState === ConversationState.SPEAKING || currentState === ConversationState.WAITING) {\n this.stateMachine.transition(ConversationState.INTERRUPTED);\n this.stallDetector.stop();\n this.fillerInjector.reset(); // Cancel any pending fillers\n this.bufferManager.clear(); // Clear buffered chunks\n }\n }\n\n /**\n * Get current conversation state\n */\n getState(): ConversationState {\n return this.stateMachine.getState();\n }\n\n /**\n * Get flow statistics\n */\n getStats(): FlowStats {\n return { ...this.stats };\n }\n\n /**\n * Get buffered chunks (for advanced barge-in scenarios)\n */\n getBufferedChunks(): readonly string[] {\n return this.bufferManager.getAll();\n }\n\n private handleStall(durationMs: number): void {\n // Only inject fillers if:\n // 1. Fillers are enabled\n // 2. First chunk hasn't been emitted yet\n // 3. Not interrupted\n if (\n this.config.enableFillers &&\n !this.firstChunkEmitted &&\n this.stateMachine.getState() !== ConversationState.INTERRUPTED\n ) {\n const filler = this.fillerInjector.getFiller();\n if (filler) {\n this.stats.fillersInjected++;\n this.config.onFillerInjected(filler);\n }\n }\n\n this.stats.stallsDetected++;\n this.config.onStallDetected(durationMs);\n }\n\n private reset(): void {\n this.firstChunkEmitted = false;\n this.fillerInjector.reset();\n this.bufferManager.clear();\n this.stats = {\n fillersInjected: 0,\n stallsDetected: 0,\n chunksProcessed: 0,\n firstChunkTime: null,\n totalDurationMs: 0,\n };\n }\n}\n\n/**\n * Convenience function to create and use flow controller\n */\nexport function withFlowControl(\n input: AsyncIterable<string>,\n config?: FlowConfig\n): AsyncIterable<string> {\n const controller = new FlowController(config);\n return controller.wrap(input);\n}\n","import { FlowControlError } from '../errors';\nimport { BufferManager } from './buffer-manager';\nimport {\n DEFAULT_FILLER_PHRASES,\n DEFAULT_MAX_FILLERS_PER_RESPONSE,\n DEFAULT_STALL_THRESHOLD_MS,\n} from './constants';\nimport { FillerInjector } from './filler-injector';\nimport { StallDetector } from './stall-detector';\nimport { ConversationStateMachine } from './state-machine';\nimport {\n ConversationState,\n type FlowEvent,\n type FlowEventListener,\n type FlowManagerConfig,\n type FlowStats,\n} from './types';\n\n/**\n * Low-level event-based flow manager\n */\nexport class FlowManager {\n private readonly config: Required<FlowManagerConfig>;\n private readonly stateMachine: ConversationStateMachine;\n private readonly stallDetector: StallDetector;\n private readonly fillerInjector: FillerInjector;\n private readonly bufferManager: BufferManager;\n private readonly listeners: Set<FlowEventListener> = new Set();\n private firstChunkEmitted = false;\n private stats: {\n fillersInjected: number;\n stallsDetected: number;\n chunksProcessed: number;\n firstChunkTime: number | null;\n totalDurationMs: number;\n } = {\n fillersInjected: 0,\n stallsDetected: 0,\n chunksProcessed: 0,\n firstChunkTime: null,\n totalDurationMs: 0,\n };\n private startTime: number | null = null;\n private stateChangeUnsubscribe: (() => void) | null = null;\n\n constructor(config: FlowManagerConfig = {}) {\n this.config = {\n stallThresholdMs: config.stallThresholdMs ?? DEFAULT_STALL_THRESHOLD_MS,\n fillerPhrases: config.fillerPhrases ?? DEFAULT_FILLER_PHRASES,\n enableFillers: config.enableFillers ?? true,\n maxFillersPerResponse: config.maxFillersPerResponse ?? DEFAULT_MAX_FILLERS_PER_RESPONSE,\n bufferSize: config.bufferSize ?? 10,\n };\n\n this.stateMachine = new ConversationStateMachine();\n this.fillerInjector = new FillerInjector(\n this.config.fillerPhrases,\n this.config.maxFillersPerResponse\n );\n this.bufferManager = new BufferManager(this.config.bufferSize);\n this.stallDetector = new StallDetector(\n this.config.stallThresholdMs,\n this.handleStall.bind(this)\n );\n }\n\n /**\n * Add event listener\n */\n on(listener: FlowEventListener): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n /**\n * Start flow tracking\n */\n start(): void {\n if (this.startTime !== null) {\n throw new FlowControlError('FlowManager already started');\n }\n\n this.reset();\n this.startTime = Date.now();\n this.stateMachine.transition(ConversationState.WAITING);\n this.stallDetector.start();\n\n // Listen to state changes\n this.stateChangeUnsubscribe = this.stateMachine.onStateChange((from, to) => {\n this.emit({\n type: 'state-change',\n from,\n to,\n });\n });\n }\n\n /**\n * Process a chunk from the stream\n */\n processChunk(chunk: string): void {\n if (this.startTime === null) {\n throw new FlowControlError('FlowManager not started. Call start() first.');\n }\n\n // Check if interrupted\n if (this.stateMachine.getState() === ConversationState.INTERRUPTED) {\n return;\n }\n\n this.stallDetector.notifyChunk();\n this.stats.chunksProcessed++;\n\n // Add to buffer\n this.bufferManager.add(chunk);\n\n // Handle first chunk\n if (!this.firstChunkEmitted) {\n this.firstChunkEmitted = true;\n this.stats.firstChunkTime = Date.now() - this.startTime;\n this.stateMachine.transition(ConversationState.SPEAKING);\n this.emit({\n type: 'first-chunk',\n chunk,\n });\n }\n\n this.emit({\n type: 'chunk-processed',\n chunk,\n });\n }\n\n /**\n * Complete the flow\n */\n complete(): void {\n if (this.startTime === null) {\n throw new FlowControlError('FlowManager not started');\n }\n\n this.stallDetector.stop();\n this.stats.totalDurationMs = Date.now() - this.startTime;\n\n // Only transition to IDLE if not interrupted\n if (this.stateMachine.getState() !== ConversationState.INTERRUPTED) {\n this.stateMachine.transition(ConversationState.IDLE);\n }\n\n this.emit({\n type: 'completed',\n stats: this.getStats(),\n });\n\n // Cleanup\n if (this.stateChangeUnsubscribe) {\n this.stateChangeUnsubscribe();\n this.stateChangeUnsubscribe = null;\n }\n\n this.startTime = null;\n }\n\n /**\n * Interrupt the flow (for barge-in)\n */\n interrupt(): void {\n const currentState = this.stateMachine.getState();\n if (currentState === ConversationState.SPEAKING || currentState === ConversationState.WAITING) {\n this.stateMachine.transition(ConversationState.INTERRUPTED);\n this.stallDetector.stop();\n this.fillerInjector.reset();\n this.bufferManager.clear();\n\n this.emit({\n type: 'interrupted',\n });\n }\n }\n\n /**\n * Get current conversation state\n */\n getState(): ConversationState {\n return this.stateMachine.getState();\n }\n\n /**\n * Get flow statistics\n */\n getStats(): FlowStats {\n return { ...this.stats };\n }\n\n /**\n * Get buffered chunks\n */\n getBufferedChunks(): readonly string[] {\n return this.bufferManager.getAll();\n }\n\n private handleStall(durationMs: number): void {\n this.stats.stallsDetected++;\n\n this.emit({\n type: 'stall-detected',\n durationMs,\n });\n\n // Only inject fillers if:\n // 1. Fillers are enabled\n // 2. First chunk hasn't been emitted yet\n // 3. Not interrupted\n if (\n this.config.enableFillers &&\n !this.firstChunkEmitted &&\n this.stateMachine.getState() !== ConversationState.INTERRUPTED\n ) {\n const filler = this.fillerInjector.getFiller();\n if (filler) {\n this.stats.fillersInjected++;\n this.emit({\n type: 'filler-injected',\n filler,\n });\n }\n }\n }\n\n private emit(event: FlowEvent): void {\n for (const listener of this.listeners) {\n try {\n listener(event);\n } catch (error) {\n // Don't let listener errors break the flow\n console.error('Error in FlowManager event listener:', error);\n }\n }\n }\n\n private reset(): void {\n this.firstChunkEmitted = false;\n this.fillerInjector.reset();\n this.bufferManager.clear();\n this.stats = {\n fillersInjected: 0,\n stallsDetected: 0,\n chunksProcessed: 0,\n firstChunkTime: null,\n totalDurationMs: 0,\n };\n }\n}\n"]}
|