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
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1026 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/errors.ts
|
|
4
|
+
var VocalStackError = class extends Error {
|
|
5
|
+
constructor(message, code, context) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.context = context;
|
|
9
|
+
this.name = "VocalStackError";
|
|
10
|
+
Error.captureStackTrace(this, this.constructor);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var SanitizerError = class extends VocalStackError {
|
|
14
|
+
constructor(message, context) {
|
|
15
|
+
super(message, "SANITIZER_ERROR", context);
|
|
16
|
+
this.name = "SanitizerError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var FlowControlError = class extends VocalStackError {
|
|
20
|
+
constructor(message, context) {
|
|
21
|
+
super(message, "FLOW_CONTROL_ERROR", context);
|
|
22
|
+
this.name = "FlowControlError";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var MonitorError = class extends VocalStackError {
|
|
26
|
+
constructor(message, context) {
|
|
27
|
+
super(message, "MONITOR_ERROR", context);
|
|
28
|
+
this.name = "MonitorError";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// src/sanitizer/rules/code-blocks.ts
|
|
33
|
+
function codeBlocksRule(text, _config) {
|
|
34
|
+
let result = text;
|
|
35
|
+
result = result.replace(/```[\s\S]*?```/g, "");
|
|
36
|
+
result = result.replace(/^(?: {4}|\t).+$/gm, "");
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/sanitizer/rules/markdown.ts
|
|
41
|
+
function markdownRule(text, _config) {
|
|
42
|
+
let result = text;
|
|
43
|
+
result = result.replace(/^#{1,6}\s+/gm, "");
|
|
44
|
+
result = result.replace(/(\*\*|__)(.*?)\1/g, "$2");
|
|
45
|
+
result = result.replace(/(\*|_)(.*?)\1/g, "$2");
|
|
46
|
+
result = result.replace(/`([^`]+)`/g, "$1");
|
|
47
|
+
result = result.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
48
|
+
result = result.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1");
|
|
49
|
+
result = result.replace(/^>\s+/gm, "");
|
|
50
|
+
result = result.replace(/^[\-*_]{3,}$/gm, "");
|
|
51
|
+
result = result.replace(/^[\s]*[-*+]\s+/gm, "");
|
|
52
|
+
result = result.replace(/^[\s]*\d+\.\s+/gm, "");
|
|
53
|
+
result = result.replace(/~~(.*?)~~/g, "$1");
|
|
54
|
+
result = result.replace(/^[\s]*-\s+\[[ xX]\]\s+/gm, "");
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/sanitizer/rules/punctuation.ts
|
|
59
|
+
function punctuationRule(text, _config) {
|
|
60
|
+
let result = text;
|
|
61
|
+
result = result.replace(/!{2,}/g, "!");
|
|
62
|
+
result = result.replace(/\?{2,}/g, "?");
|
|
63
|
+
result = result.replace(/\.{3,}/g, ".");
|
|
64
|
+
result = result.replace(/-{2,}/g, " ");
|
|
65
|
+
result = result.replace(/[—–]/g, " ");
|
|
66
|
+
result = result.replace(/[()[\]{}]/g, "");
|
|
67
|
+
result = result.replace(/,{2,}/g, ",");
|
|
68
|
+
result = result.replace(/[;:]/g, ",");
|
|
69
|
+
result = result.replace(/["'"'`]/g, "");
|
|
70
|
+
result = result.replace(/[*_]/g, "");
|
|
71
|
+
result = result.replace(/[/\\]/g, " ");
|
|
72
|
+
result = result.replace(/[|&]/g, " ");
|
|
73
|
+
result = result.replace(/[@#$%]/g, "");
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/sanitizer/rules/urls.ts
|
|
78
|
+
function urlsRule(text, _config) {
|
|
79
|
+
let result = text;
|
|
80
|
+
const urlWithProtocol = /\b(https?:\/\/|ftp:\/\/|www\.)[^\s<>"{}|\\^`[\]]+/gi;
|
|
81
|
+
const emailPattern = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g;
|
|
82
|
+
result = result.replace(urlWithProtocol, "");
|
|
83
|
+
result = result.replace(emailPattern, "");
|
|
84
|
+
result = result.replace(/\b[a-z0-9-]+\.[a-z]{2,}\b/gi, "");
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/sanitizer/rules/index.ts
|
|
89
|
+
var ruleRegistry = /* @__PURE__ */ new Map([
|
|
90
|
+
["markdown", markdownRule],
|
|
91
|
+
["urls", urlsRule],
|
|
92
|
+
["code-blocks", codeBlocksRule],
|
|
93
|
+
["punctuation", punctuationRule]
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
// src/sanitizer/sanitizer.ts
|
|
97
|
+
var SpeechSanitizer = class {
|
|
98
|
+
config;
|
|
99
|
+
plugins;
|
|
100
|
+
constructor(config = {}) {
|
|
101
|
+
this.config = {
|
|
102
|
+
rules: config.rules ?? ["markdown", "urls", "code-blocks", "punctuation"],
|
|
103
|
+
plugins: config.plugins ?? [],
|
|
104
|
+
preserveLineBreaks: config.preserveLineBreaks ?? false,
|
|
105
|
+
customReplacements: config.customReplacements ?? /* @__PURE__ */ new Map()
|
|
106
|
+
};
|
|
107
|
+
this.plugins = [...this.config.plugins].sort((a, b) => a.priority - b.priority);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Sanitize a string synchronously
|
|
111
|
+
*/
|
|
112
|
+
sanitize(text) {
|
|
113
|
+
if (!text || text.trim().length === 0) {
|
|
114
|
+
return "";
|
|
115
|
+
}
|
|
116
|
+
let result = text;
|
|
117
|
+
try {
|
|
118
|
+
for (const rule of this.config.rules) {
|
|
119
|
+
const ruleFunction = ruleRegistry.get(rule);
|
|
120
|
+
if (ruleFunction) {
|
|
121
|
+
result = ruleFunction(result, this.config);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
for (const plugin of this.plugins) {
|
|
125
|
+
const transformed = plugin.transform(result);
|
|
126
|
+
if (transformed instanceof Promise) {
|
|
127
|
+
throw new SanitizerError(
|
|
128
|
+
`Plugin ${plugin.name} returned a Promise in sync sanitize(). Use sanitizeAsync() instead.`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
result = transformed;
|
|
132
|
+
}
|
|
133
|
+
for (const [pattern, replacement] of this.config.customReplacements) {
|
|
134
|
+
if (typeof pattern === "string") {
|
|
135
|
+
result = result.replaceAll(pattern, replacement);
|
|
136
|
+
} else {
|
|
137
|
+
result = result.replace(pattern, replacement);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (this.config.preserveLineBreaks) {
|
|
141
|
+
result = result.replace(/ +/g, " ").replace(/^\s+|\s+$/gm, "");
|
|
142
|
+
} else {
|
|
143
|
+
result = result.replace(/\s+/g, " ").trim();
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error instanceof SanitizerError) {
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
throw new SanitizerError("Failed to sanitize text", {
|
|
151
|
+
originalText: text.substring(0, 100),
|
|
152
|
+
error
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Sanitize with detailed result metadata
|
|
158
|
+
*/
|
|
159
|
+
sanitizeWithMetadata(text) {
|
|
160
|
+
const original = text;
|
|
161
|
+
const sanitized = this.sanitize(text);
|
|
162
|
+
return {
|
|
163
|
+
original,
|
|
164
|
+
sanitized,
|
|
165
|
+
appliedRules: this.config.rules,
|
|
166
|
+
metadata: {
|
|
167
|
+
removedCount: original.length - sanitized.length,
|
|
168
|
+
transformedCount: this.config.rules.length + this.plugins.length
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Sanitize a stream of text chunks (AsyncIterable)
|
|
174
|
+
*/
|
|
175
|
+
async *sanitizeStream(input) {
|
|
176
|
+
let buffer = "";
|
|
177
|
+
const sentenceBoundary = /[.!?]\s+/;
|
|
178
|
+
for await (const chunk of input) {
|
|
179
|
+
buffer += chunk;
|
|
180
|
+
const sentences = buffer.split(sentenceBoundary);
|
|
181
|
+
buffer = sentences.pop() ?? "";
|
|
182
|
+
for (const sentence of sentences) {
|
|
183
|
+
if (sentence.trim()) {
|
|
184
|
+
yield `${this.sanitize(sentence)} `;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (buffer.trim()) {
|
|
189
|
+
yield this.sanitize(buffer);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
function sanitizeForSpeech(text, config) {
|
|
194
|
+
const sanitizer = new SpeechSanitizer(config);
|
|
195
|
+
return sanitizer.sanitize(text);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/monitor/exporters/csv.ts
|
|
199
|
+
function exportToCsv(metrics) {
|
|
200
|
+
const headers = [
|
|
201
|
+
"id",
|
|
202
|
+
"timestamp",
|
|
203
|
+
"completed",
|
|
204
|
+
"timeToFirstToken",
|
|
205
|
+
"totalDuration",
|
|
206
|
+
"tokenCount",
|
|
207
|
+
"averageTokenLatency",
|
|
208
|
+
"tags"
|
|
209
|
+
];
|
|
210
|
+
const rows = metrics.map((m) => [
|
|
211
|
+
m.id,
|
|
212
|
+
m.timestamp.toString(),
|
|
213
|
+
m.completed.toString(),
|
|
214
|
+
m.metrics.timeToFirstToken?.toString() ?? "",
|
|
215
|
+
m.metrics.totalDuration?.toString() ?? "",
|
|
216
|
+
m.metrics.tokenCount.toString(),
|
|
217
|
+
m.metrics.averageTokenLatency?.toString() ?? "",
|
|
218
|
+
JSON.stringify(m.tags)
|
|
219
|
+
]);
|
|
220
|
+
const csvLines = [
|
|
221
|
+
headers.join(","),
|
|
222
|
+
...rows.map(
|
|
223
|
+
(row) => row.map((cell) => {
|
|
224
|
+
if (cell.includes(",") || cell.includes('"') || cell.includes("\n")) {
|
|
225
|
+
return `"${cell.replace(/"/g, '""')}"`;
|
|
226
|
+
}
|
|
227
|
+
return cell;
|
|
228
|
+
}).join(",")
|
|
229
|
+
)
|
|
230
|
+
];
|
|
231
|
+
return csvLines.join("\n");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/monitor/exporters/json.ts
|
|
235
|
+
function exportToJson(metrics) {
|
|
236
|
+
return JSON.stringify(
|
|
237
|
+
{
|
|
238
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
239
|
+
count: metrics.length,
|
|
240
|
+
metrics: metrics.map((m) => ({
|
|
241
|
+
id: m.id,
|
|
242
|
+
timestamp: m.timestamp,
|
|
243
|
+
completed: m.completed,
|
|
244
|
+
timeToFirstToken: m.metrics.timeToFirstToken,
|
|
245
|
+
totalDuration: m.metrics.totalDuration,
|
|
246
|
+
tokenCount: m.metrics.tokenCount,
|
|
247
|
+
averageTokenLatency: m.metrics.averageTokenLatency,
|
|
248
|
+
tags: m.tags
|
|
249
|
+
}))
|
|
250
|
+
},
|
|
251
|
+
null,
|
|
252
|
+
2
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/monitor/metrics-collector.ts
|
|
257
|
+
var MetricsCollector = class {
|
|
258
|
+
metrics = [];
|
|
259
|
+
addMetric(metric) {
|
|
260
|
+
this.metrics.push(metric);
|
|
261
|
+
}
|
|
262
|
+
getMetrics() {
|
|
263
|
+
return [...this.metrics];
|
|
264
|
+
}
|
|
265
|
+
getSummary() {
|
|
266
|
+
if (this.metrics.length === 0) {
|
|
267
|
+
return {
|
|
268
|
+
count: 0,
|
|
269
|
+
avgTimeToFirstToken: 0,
|
|
270
|
+
avgTotalDuration: 0,
|
|
271
|
+
p50TimeToFirstToken: 0,
|
|
272
|
+
p95TimeToFirstToken: 0,
|
|
273
|
+
p99TimeToFirstToken: 0,
|
|
274
|
+
minTimeToFirstToken: 0,
|
|
275
|
+
maxTimeToFirstToken: 0
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const ttfts = this.metrics.map((m) => m.metrics.timeToFirstToken).filter((t) => t !== null).sort((a, b) => a - b);
|
|
279
|
+
const durations = this.metrics.map((m) => m.metrics.totalDuration).filter((d) => d !== null);
|
|
280
|
+
const percentile = (arr, p) => {
|
|
281
|
+
if (arr.length === 0) return 0;
|
|
282
|
+
const index = Math.ceil(arr.length * p) - 1;
|
|
283
|
+
return arr[index] ?? 0;
|
|
284
|
+
};
|
|
285
|
+
return {
|
|
286
|
+
count: this.metrics.length,
|
|
287
|
+
avgTimeToFirstToken: ttfts.reduce((a, b) => a + b, 0) / ttfts.length || 0,
|
|
288
|
+
avgTotalDuration: durations.reduce((a, b) => a + b, 0) / durations.length || 0,
|
|
289
|
+
p50TimeToFirstToken: percentile(ttfts, 0.5),
|
|
290
|
+
p95TimeToFirstToken: percentile(ttfts, 0.95),
|
|
291
|
+
p99TimeToFirstToken: percentile(ttfts, 0.99),
|
|
292
|
+
minTimeToFirstToken: ttfts.length > 0 ? Math.min(...ttfts) : 0,
|
|
293
|
+
maxTimeToFirstToken: ttfts.length > 0 ? Math.max(...ttfts) : 0
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
clear() {
|
|
297
|
+
this.metrics = [];
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// src/monitor/voice-auditor.ts
|
|
302
|
+
var VoiceAuditor = class {
|
|
303
|
+
config;
|
|
304
|
+
collector;
|
|
305
|
+
activeMetrics = /* @__PURE__ */ new Map();
|
|
306
|
+
constructor(config = {}) {
|
|
307
|
+
this.config = {
|
|
308
|
+
enableRealtime: config.enableRealtime ?? false,
|
|
309
|
+
onMetric: config.onMetric ?? (() => {
|
|
310
|
+
}),
|
|
311
|
+
tags: config.tags ?? {}
|
|
312
|
+
};
|
|
313
|
+
this.collector = new MetricsCollector();
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Start tracking a new voice interaction
|
|
317
|
+
*/
|
|
318
|
+
startTracking(id, tags) {
|
|
319
|
+
if (this.activeMetrics.has(id)) {
|
|
320
|
+
throw new MonitorError(`Metric with id ${id} is already being tracked`);
|
|
321
|
+
}
|
|
322
|
+
const metric = {
|
|
323
|
+
id,
|
|
324
|
+
timestamp: Date.now(),
|
|
325
|
+
startTime: Date.now(),
|
|
326
|
+
firstTokenReceivedTime: null,
|
|
327
|
+
lastTokenReceivedTime: null,
|
|
328
|
+
completed: false,
|
|
329
|
+
metrics: {
|
|
330
|
+
timeToFirstToken: null,
|
|
331
|
+
totalDuration: null,
|
|
332
|
+
tokenCount: 0,
|
|
333
|
+
averageTokenLatency: null
|
|
334
|
+
},
|
|
335
|
+
tags: { ...this.config.tags, ...tags }
|
|
336
|
+
};
|
|
337
|
+
this.activeMetrics.set(id, metric);
|
|
338
|
+
return metric;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Record first token received
|
|
342
|
+
*/
|
|
343
|
+
recordFirstToken(id) {
|
|
344
|
+
const metric = this.activeMetrics.get(id);
|
|
345
|
+
if (!metric) {
|
|
346
|
+
throw new MonitorError(`No active metric found for id ${id}`);
|
|
347
|
+
}
|
|
348
|
+
if (metric.firstTokenReceivedTime === null) {
|
|
349
|
+
const updated = {
|
|
350
|
+
...metric,
|
|
351
|
+
firstTokenReceivedTime: Date.now(),
|
|
352
|
+
metrics: {
|
|
353
|
+
...metric.metrics,
|
|
354
|
+
timeToFirstToken: Date.now() - metric.startTime,
|
|
355
|
+
tokenCount: 1
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
this.activeMetrics.set(id, updated);
|
|
359
|
+
if (this.config.enableRealtime) {
|
|
360
|
+
this.config.onMetric(updated);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Record token received
|
|
366
|
+
*/
|
|
367
|
+
recordToken(id) {
|
|
368
|
+
const metric = this.activeMetrics.get(id);
|
|
369
|
+
if (!metric) {
|
|
370
|
+
throw new MonitorError(`No active metric found for id ${id}`);
|
|
371
|
+
}
|
|
372
|
+
const updated = {
|
|
373
|
+
...metric,
|
|
374
|
+
lastTokenReceivedTime: Date.now(),
|
|
375
|
+
metrics: {
|
|
376
|
+
...metric.metrics,
|
|
377
|
+
tokenCount: metric.metrics.tokenCount + 1
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
this.activeMetrics.set(id, updated);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Complete tracking for a voice interaction
|
|
384
|
+
*/
|
|
385
|
+
completeTracking(id) {
|
|
386
|
+
const metric = this.activeMetrics.get(id);
|
|
387
|
+
if (!metric) {
|
|
388
|
+
throw new MonitorError(`No active metric found for id ${id}`);
|
|
389
|
+
}
|
|
390
|
+
const lastTime = metric.lastTokenReceivedTime ?? Date.now();
|
|
391
|
+
const totalDuration = lastTime - metric.startTime;
|
|
392
|
+
const avgLatency = metric.metrics.tokenCount > 0 ? totalDuration / metric.metrics.tokenCount : null;
|
|
393
|
+
const completed = {
|
|
394
|
+
...metric,
|
|
395
|
+
completed: true,
|
|
396
|
+
metrics: {
|
|
397
|
+
...metric.metrics,
|
|
398
|
+
totalDuration,
|
|
399
|
+
averageTokenLatency: avgLatency
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
this.activeMetrics.delete(id);
|
|
403
|
+
this.collector.addMetric(completed);
|
|
404
|
+
if (this.config.enableRealtime) {
|
|
405
|
+
this.config.onMetric(completed);
|
|
406
|
+
}
|
|
407
|
+
return completed;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Wrap an async iterable with automatic tracking
|
|
411
|
+
*/
|
|
412
|
+
async *track(id, input, tags) {
|
|
413
|
+
this.startTracking(id, tags);
|
|
414
|
+
let firstToken = true;
|
|
415
|
+
try {
|
|
416
|
+
for await (const chunk of input) {
|
|
417
|
+
if (firstToken) {
|
|
418
|
+
this.recordFirstToken(id);
|
|
419
|
+
firstToken = false;
|
|
420
|
+
} else {
|
|
421
|
+
this.recordToken(id);
|
|
422
|
+
}
|
|
423
|
+
yield chunk;
|
|
424
|
+
}
|
|
425
|
+
} finally {
|
|
426
|
+
this.completeTracking(id);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Get all collected metrics
|
|
431
|
+
*/
|
|
432
|
+
getMetrics() {
|
|
433
|
+
return this.collector.getMetrics();
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Get summary statistics
|
|
437
|
+
*/
|
|
438
|
+
getSummary() {
|
|
439
|
+
return this.collector.getSummary();
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Export metrics in specified format
|
|
443
|
+
*/
|
|
444
|
+
export(format) {
|
|
445
|
+
const metrics = this.collector.getMetrics();
|
|
446
|
+
switch (format) {
|
|
447
|
+
case "json":
|
|
448
|
+
return exportToJson(metrics);
|
|
449
|
+
case "csv":
|
|
450
|
+
return exportToCsv(metrics);
|
|
451
|
+
default:
|
|
452
|
+
throw new MonitorError(`Unsupported export format: ${format}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Clear all collected metrics
|
|
457
|
+
*/
|
|
458
|
+
clear() {
|
|
459
|
+
this.activeMetrics.clear();
|
|
460
|
+
this.collector.clear();
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// src/flow/buffer-manager.ts
|
|
465
|
+
var BufferManager = class {
|
|
466
|
+
buffer = [];
|
|
467
|
+
maxSize;
|
|
468
|
+
head = 0;
|
|
469
|
+
size = 0;
|
|
470
|
+
constructor(maxSize = 10) {
|
|
471
|
+
if (maxSize <= 0) {
|
|
472
|
+
throw new Error("Buffer size must be positive");
|
|
473
|
+
}
|
|
474
|
+
this.maxSize = maxSize;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Add chunk to buffer
|
|
478
|
+
*/
|
|
479
|
+
add(chunk) {
|
|
480
|
+
if (this.size < this.maxSize) {
|
|
481
|
+
this.buffer.push(chunk);
|
|
482
|
+
this.size++;
|
|
483
|
+
} else {
|
|
484
|
+
this.buffer[this.head] = chunk;
|
|
485
|
+
this.head = (this.head + 1) % this.maxSize;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Get all buffered chunks in order
|
|
490
|
+
*/
|
|
491
|
+
getAll() {
|
|
492
|
+
if (this.size < this.maxSize) {
|
|
493
|
+
return [...this.buffer];
|
|
494
|
+
}
|
|
495
|
+
return [...this.buffer.slice(this.head), ...this.buffer.slice(0, this.head)];
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Clear all buffered chunks
|
|
499
|
+
*/
|
|
500
|
+
clear() {
|
|
501
|
+
this.buffer = [];
|
|
502
|
+
this.head = 0;
|
|
503
|
+
this.size = 0;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Get current buffer size
|
|
507
|
+
*/
|
|
508
|
+
getSize() {
|
|
509
|
+
return this.size;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Check if buffer is empty
|
|
513
|
+
*/
|
|
514
|
+
isEmpty() {
|
|
515
|
+
return this.size === 0;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Check if buffer is full
|
|
519
|
+
*/
|
|
520
|
+
isFull() {
|
|
521
|
+
return this.size === this.maxSize;
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
// src/flow/constants.ts
|
|
526
|
+
var DEFAULT_STALL_THRESHOLD_MS = 700;
|
|
527
|
+
var DEFAULT_FILLER_PHRASES = ["um", "let me think", "hmm"];
|
|
528
|
+
var DEFAULT_MAX_FILLERS_PER_RESPONSE = 3;
|
|
529
|
+
|
|
530
|
+
// src/flow/filler-injector.ts
|
|
531
|
+
var FillerInjector = class {
|
|
532
|
+
phrases;
|
|
533
|
+
maxFillers;
|
|
534
|
+
fillersUsed = 0;
|
|
535
|
+
lastFillerIndex = -1;
|
|
536
|
+
constructor(phrases, maxFillers) {
|
|
537
|
+
this.phrases = phrases;
|
|
538
|
+
this.maxFillers = maxFillers;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Get next filler phrase (returns null if limit reached)
|
|
542
|
+
*/
|
|
543
|
+
getFiller() {
|
|
544
|
+
if (this.fillersUsed >= this.maxFillers) {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
this.lastFillerIndex = (this.lastFillerIndex + 1) % this.phrases.length;
|
|
548
|
+
this.fillersUsed++;
|
|
549
|
+
return this.phrases[this.lastFillerIndex] ?? null;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Reset filler state
|
|
553
|
+
*/
|
|
554
|
+
reset() {
|
|
555
|
+
this.fillersUsed = 0;
|
|
556
|
+
this.lastFillerIndex = -1;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Check if more fillers can be injected
|
|
560
|
+
*/
|
|
561
|
+
canInjectMore() {
|
|
562
|
+
return this.fillersUsed < this.maxFillers;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Get count of fillers used
|
|
566
|
+
*/
|
|
567
|
+
getUsedCount() {
|
|
568
|
+
return this.fillersUsed;
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
// src/flow/stall-detector.ts
|
|
573
|
+
var StallDetector = class {
|
|
574
|
+
lastChunkTime = null;
|
|
575
|
+
stallTimer = null;
|
|
576
|
+
thresholdMs;
|
|
577
|
+
onStall;
|
|
578
|
+
constructor(thresholdMs, onStall) {
|
|
579
|
+
this.thresholdMs = thresholdMs;
|
|
580
|
+
this.onStall = onStall;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Notify detector that a chunk was received
|
|
584
|
+
*/
|
|
585
|
+
notifyChunk() {
|
|
586
|
+
this.lastChunkTime = Date.now();
|
|
587
|
+
this.clearTimer();
|
|
588
|
+
this.scheduleStallCheck();
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Start monitoring for stalls
|
|
592
|
+
*/
|
|
593
|
+
start() {
|
|
594
|
+
this.lastChunkTime = Date.now();
|
|
595
|
+
this.scheduleStallCheck();
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Stop monitoring
|
|
599
|
+
*/
|
|
600
|
+
stop() {
|
|
601
|
+
this.clearTimer();
|
|
602
|
+
this.lastChunkTime = null;
|
|
603
|
+
}
|
|
604
|
+
scheduleStallCheck() {
|
|
605
|
+
this.clearTimer();
|
|
606
|
+
this.stallTimer = setTimeout(() => {
|
|
607
|
+
if (this.lastChunkTime !== null) {
|
|
608
|
+
const elapsed = Date.now() - this.lastChunkTime;
|
|
609
|
+
if (elapsed >= this.thresholdMs) {
|
|
610
|
+
this.onStall(elapsed);
|
|
611
|
+
this.scheduleStallCheck();
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}, this.thresholdMs);
|
|
615
|
+
}
|
|
616
|
+
clearTimer() {
|
|
617
|
+
if (this.stallTimer) {
|
|
618
|
+
clearTimeout(this.stallTimer);
|
|
619
|
+
this.stallTimer = null;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
// src/flow/types.ts
|
|
625
|
+
var ConversationState = /* @__PURE__ */ ((ConversationState2) => {
|
|
626
|
+
ConversationState2["IDLE"] = "idle";
|
|
627
|
+
ConversationState2["WAITING"] = "waiting";
|
|
628
|
+
ConversationState2["SPEAKING"] = "speaking";
|
|
629
|
+
ConversationState2["INTERRUPTED"] = "interrupted";
|
|
630
|
+
return ConversationState2;
|
|
631
|
+
})(ConversationState || {});
|
|
632
|
+
|
|
633
|
+
// src/flow/state-machine.ts
|
|
634
|
+
var VALID_TRANSITIONS = /* @__PURE__ */ new Map([
|
|
635
|
+
["idle" /* IDLE */, ["speaking" /* SPEAKING */, "waiting" /* WAITING */]],
|
|
636
|
+
[
|
|
637
|
+
"waiting" /* WAITING */,
|
|
638
|
+
["speaking" /* SPEAKING */, "idle" /* IDLE */, "interrupted" /* INTERRUPTED */]
|
|
639
|
+
],
|
|
640
|
+
["speaking" /* SPEAKING */, ["interrupted" /* INTERRUPTED */, "idle" /* IDLE */]],
|
|
641
|
+
["interrupted" /* INTERRUPTED */, ["idle" /* IDLE */, "waiting" /* WAITING */]]
|
|
642
|
+
]);
|
|
643
|
+
var ConversationStateMachine = class {
|
|
644
|
+
currentState = "idle" /* IDLE */;
|
|
645
|
+
listeners = /* @__PURE__ */ new Set();
|
|
646
|
+
/**
|
|
647
|
+
* Get current state
|
|
648
|
+
*/
|
|
649
|
+
getState() {
|
|
650
|
+
return this.currentState;
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Attempt to transition to new state
|
|
654
|
+
*/
|
|
655
|
+
transition(to) {
|
|
656
|
+
const validTransitions = VALID_TRANSITIONS.get(this.currentState);
|
|
657
|
+
if (!validTransitions?.includes(to)) {
|
|
658
|
+
throw new FlowControlError(`Invalid state transition: ${this.currentState} -> ${to}`, {
|
|
659
|
+
from: this.currentState,
|
|
660
|
+
to
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
const from = this.currentState;
|
|
664
|
+
this.currentState = to;
|
|
665
|
+
for (const listener of this.listeners) {
|
|
666
|
+
listener(from, to);
|
|
667
|
+
}
|
|
668
|
+
return true;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Add state change listener
|
|
672
|
+
*/
|
|
673
|
+
onStateChange(listener) {
|
|
674
|
+
this.listeners.add(listener);
|
|
675
|
+
return () => this.listeners.delete(listener);
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Reset to IDLE
|
|
679
|
+
*/
|
|
680
|
+
reset() {
|
|
681
|
+
this.currentState = "idle" /* IDLE */;
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
// src/flow/flow-controller.ts
|
|
686
|
+
var FlowController = class {
|
|
687
|
+
config;
|
|
688
|
+
stateMachine;
|
|
689
|
+
stallDetector;
|
|
690
|
+
fillerInjector;
|
|
691
|
+
bufferManager;
|
|
692
|
+
firstChunkEmitted = false;
|
|
693
|
+
stats = {
|
|
694
|
+
fillersInjected: 0,
|
|
695
|
+
stallsDetected: 0,
|
|
696
|
+
chunksProcessed: 0,
|
|
697
|
+
firstChunkTime: null,
|
|
698
|
+
totalDurationMs: 0
|
|
699
|
+
};
|
|
700
|
+
startTime = null;
|
|
701
|
+
constructor(config = {}) {
|
|
702
|
+
this.config = {
|
|
703
|
+
stallThresholdMs: config.stallThresholdMs ?? DEFAULT_STALL_THRESHOLD_MS,
|
|
704
|
+
fillerPhrases: config.fillerPhrases ?? DEFAULT_FILLER_PHRASES,
|
|
705
|
+
enableFillers: config.enableFillers ?? true,
|
|
706
|
+
maxFillersPerResponse: config.maxFillersPerResponse ?? DEFAULT_MAX_FILLERS_PER_RESPONSE,
|
|
707
|
+
onFillerInjected: config.onFillerInjected ?? (() => {
|
|
708
|
+
}),
|
|
709
|
+
onStallDetected: config.onStallDetected ?? (() => {
|
|
710
|
+
}),
|
|
711
|
+
onFirstChunk: config.onFirstChunk ?? (() => {
|
|
712
|
+
})
|
|
713
|
+
};
|
|
714
|
+
this.stateMachine = new ConversationStateMachine();
|
|
715
|
+
this.fillerInjector = new FillerInjector(
|
|
716
|
+
this.config.fillerPhrases,
|
|
717
|
+
this.config.maxFillersPerResponse
|
|
718
|
+
);
|
|
719
|
+
this.bufferManager = new BufferManager(10);
|
|
720
|
+
this.stallDetector = new StallDetector(
|
|
721
|
+
this.config.stallThresholdMs,
|
|
722
|
+
this.handleStall.bind(this)
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Wrap an async iterable with flow control
|
|
727
|
+
*/
|
|
728
|
+
async *wrap(input) {
|
|
729
|
+
this.reset();
|
|
730
|
+
this.startTime = Date.now();
|
|
731
|
+
this.stateMachine.transition("waiting" /* WAITING */);
|
|
732
|
+
this.stallDetector.start();
|
|
733
|
+
try {
|
|
734
|
+
for await (const chunk of input) {
|
|
735
|
+
if (this.stateMachine.getState() === "interrupted" /* INTERRUPTED */) {
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
this.stallDetector.notifyChunk();
|
|
739
|
+
this.stats.chunksProcessed++;
|
|
740
|
+
this.bufferManager.add(chunk);
|
|
741
|
+
if (!this.firstChunkEmitted) {
|
|
742
|
+
this.firstChunkEmitted = true;
|
|
743
|
+
this.stats.firstChunkTime = Date.now() - (this.startTime ?? Date.now());
|
|
744
|
+
this.stateMachine.transition("speaking" /* SPEAKING */);
|
|
745
|
+
this.config.onFirstChunk();
|
|
746
|
+
}
|
|
747
|
+
yield chunk;
|
|
748
|
+
}
|
|
749
|
+
if (this.stateMachine.getState() !== "interrupted" /* INTERRUPTED */) {
|
|
750
|
+
this.stateMachine.transition("idle" /* IDLE */);
|
|
751
|
+
}
|
|
752
|
+
} catch (error) {
|
|
753
|
+
throw new FlowControlError("Flow control error during stream processing", { error });
|
|
754
|
+
} finally {
|
|
755
|
+
this.stallDetector.stop();
|
|
756
|
+
this.stats.totalDurationMs = Date.now() - (this.startTime ?? Date.now());
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Interrupt the current flow (for barge-in)
|
|
761
|
+
*/
|
|
762
|
+
interrupt() {
|
|
763
|
+
const currentState = this.stateMachine.getState();
|
|
764
|
+
if (currentState === "speaking" /* SPEAKING */ || currentState === "waiting" /* WAITING */) {
|
|
765
|
+
this.stateMachine.transition("interrupted" /* INTERRUPTED */);
|
|
766
|
+
this.stallDetector.stop();
|
|
767
|
+
this.fillerInjector.reset();
|
|
768
|
+
this.bufferManager.clear();
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Get current conversation state
|
|
773
|
+
*/
|
|
774
|
+
getState() {
|
|
775
|
+
return this.stateMachine.getState();
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Get flow statistics
|
|
779
|
+
*/
|
|
780
|
+
getStats() {
|
|
781
|
+
return { ...this.stats };
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Get buffered chunks (for advanced barge-in scenarios)
|
|
785
|
+
*/
|
|
786
|
+
getBufferedChunks() {
|
|
787
|
+
return this.bufferManager.getAll();
|
|
788
|
+
}
|
|
789
|
+
handleStall(durationMs) {
|
|
790
|
+
if (this.config.enableFillers && !this.firstChunkEmitted && this.stateMachine.getState() !== "interrupted" /* INTERRUPTED */) {
|
|
791
|
+
const filler = this.fillerInjector.getFiller();
|
|
792
|
+
if (filler) {
|
|
793
|
+
this.stats.fillersInjected++;
|
|
794
|
+
this.config.onFillerInjected(filler);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
this.stats.stallsDetected++;
|
|
798
|
+
this.config.onStallDetected(durationMs);
|
|
799
|
+
}
|
|
800
|
+
reset() {
|
|
801
|
+
this.firstChunkEmitted = false;
|
|
802
|
+
this.fillerInjector.reset();
|
|
803
|
+
this.bufferManager.clear();
|
|
804
|
+
this.stats = {
|
|
805
|
+
fillersInjected: 0,
|
|
806
|
+
stallsDetected: 0,
|
|
807
|
+
chunksProcessed: 0,
|
|
808
|
+
firstChunkTime: null,
|
|
809
|
+
totalDurationMs: 0
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
function withFlowControl(input, config) {
|
|
814
|
+
const controller = new FlowController(config);
|
|
815
|
+
return controller.wrap(input);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// src/flow/flow-manager.ts
|
|
819
|
+
var FlowManager = class {
|
|
820
|
+
config;
|
|
821
|
+
stateMachine;
|
|
822
|
+
stallDetector;
|
|
823
|
+
fillerInjector;
|
|
824
|
+
bufferManager;
|
|
825
|
+
listeners = /* @__PURE__ */ new Set();
|
|
826
|
+
firstChunkEmitted = false;
|
|
827
|
+
stats = {
|
|
828
|
+
fillersInjected: 0,
|
|
829
|
+
stallsDetected: 0,
|
|
830
|
+
chunksProcessed: 0,
|
|
831
|
+
firstChunkTime: null,
|
|
832
|
+
totalDurationMs: 0
|
|
833
|
+
};
|
|
834
|
+
startTime = null;
|
|
835
|
+
stateChangeUnsubscribe = null;
|
|
836
|
+
constructor(config = {}) {
|
|
837
|
+
this.config = {
|
|
838
|
+
stallThresholdMs: config.stallThresholdMs ?? DEFAULT_STALL_THRESHOLD_MS,
|
|
839
|
+
fillerPhrases: config.fillerPhrases ?? DEFAULT_FILLER_PHRASES,
|
|
840
|
+
enableFillers: config.enableFillers ?? true,
|
|
841
|
+
maxFillersPerResponse: config.maxFillersPerResponse ?? DEFAULT_MAX_FILLERS_PER_RESPONSE,
|
|
842
|
+
bufferSize: config.bufferSize ?? 10
|
|
843
|
+
};
|
|
844
|
+
this.stateMachine = new ConversationStateMachine();
|
|
845
|
+
this.fillerInjector = new FillerInjector(
|
|
846
|
+
this.config.fillerPhrases,
|
|
847
|
+
this.config.maxFillersPerResponse
|
|
848
|
+
);
|
|
849
|
+
this.bufferManager = new BufferManager(this.config.bufferSize);
|
|
850
|
+
this.stallDetector = new StallDetector(
|
|
851
|
+
this.config.stallThresholdMs,
|
|
852
|
+
this.handleStall.bind(this)
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Add event listener
|
|
857
|
+
*/
|
|
858
|
+
on(listener) {
|
|
859
|
+
this.listeners.add(listener);
|
|
860
|
+
return () => this.listeners.delete(listener);
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Start flow tracking
|
|
864
|
+
*/
|
|
865
|
+
start() {
|
|
866
|
+
if (this.startTime !== null) {
|
|
867
|
+
throw new FlowControlError("FlowManager already started");
|
|
868
|
+
}
|
|
869
|
+
this.reset();
|
|
870
|
+
this.startTime = Date.now();
|
|
871
|
+
this.stateMachine.transition("waiting" /* WAITING */);
|
|
872
|
+
this.stallDetector.start();
|
|
873
|
+
this.stateChangeUnsubscribe = this.stateMachine.onStateChange((from, to) => {
|
|
874
|
+
this.emit({
|
|
875
|
+
type: "state-change",
|
|
876
|
+
from,
|
|
877
|
+
to
|
|
878
|
+
});
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Process a chunk from the stream
|
|
883
|
+
*/
|
|
884
|
+
processChunk(chunk) {
|
|
885
|
+
if (this.startTime === null) {
|
|
886
|
+
throw new FlowControlError("FlowManager not started. Call start() first.");
|
|
887
|
+
}
|
|
888
|
+
if (this.stateMachine.getState() === "interrupted" /* INTERRUPTED */) {
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
this.stallDetector.notifyChunk();
|
|
892
|
+
this.stats.chunksProcessed++;
|
|
893
|
+
this.bufferManager.add(chunk);
|
|
894
|
+
if (!this.firstChunkEmitted) {
|
|
895
|
+
this.firstChunkEmitted = true;
|
|
896
|
+
this.stats.firstChunkTime = Date.now() - this.startTime;
|
|
897
|
+
this.stateMachine.transition("speaking" /* SPEAKING */);
|
|
898
|
+
this.emit({
|
|
899
|
+
type: "first-chunk",
|
|
900
|
+
chunk
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
this.emit({
|
|
904
|
+
type: "chunk-processed",
|
|
905
|
+
chunk
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Complete the flow
|
|
910
|
+
*/
|
|
911
|
+
complete() {
|
|
912
|
+
if (this.startTime === null) {
|
|
913
|
+
throw new FlowControlError("FlowManager not started");
|
|
914
|
+
}
|
|
915
|
+
this.stallDetector.stop();
|
|
916
|
+
this.stats.totalDurationMs = Date.now() - this.startTime;
|
|
917
|
+
if (this.stateMachine.getState() !== "interrupted" /* INTERRUPTED */) {
|
|
918
|
+
this.stateMachine.transition("idle" /* IDLE */);
|
|
919
|
+
}
|
|
920
|
+
this.emit({
|
|
921
|
+
type: "completed",
|
|
922
|
+
stats: this.getStats()
|
|
923
|
+
});
|
|
924
|
+
if (this.stateChangeUnsubscribe) {
|
|
925
|
+
this.stateChangeUnsubscribe();
|
|
926
|
+
this.stateChangeUnsubscribe = null;
|
|
927
|
+
}
|
|
928
|
+
this.startTime = null;
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Interrupt the flow (for barge-in)
|
|
932
|
+
*/
|
|
933
|
+
interrupt() {
|
|
934
|
+
const currentState = this.stateMachine.getState();
|
|
935
|
+
if (currentState === "speaking" /* SPEAKING */ || currentState === "waiting" /* WAITING */) {
|
|
936
|
+
this.stateMachine.transition("interrupted" /* INTERRUPTED */);
|
|
937
|
+
this.stallDetector.stop();
|
|
938
|
+
this.fillerInjector.reset();
|
|
939
|
+
this.bufferManager.clear();
|
|
940
|
+
this.emit({
|
|
941
|
+
type: "interrupted"
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Get current conversation state
|
|
947
|
+
*/
|
|
948
|
+
getState() {
|
|
949
|
+
return this.stateMachine.getState();
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Get flow statistics
|
|
953
|
+
*/
|
|
954
|
+
getStats() {
|
|
955
|
+
return { ...this.stats };
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Get buffered chunks
|
|
959
|
+
*/
|
|
960
|
+
getBufferedChunks() {
|
|
961
|
+
return this.bufferManager.getAll();
|
|
962
|
+
}
|
|
963
|
+
handleStall(durationMs) {
|
|
964
|
+
this.stats.stallsDetected++;
|
|
965
|
+
this.emit({
|
|
966
|
+
type: "stall-detected",
|
|
967
|
+
durationMs
|
|
968
|
+
});
|
|
969
|
+
if (this.config.enableFillers && !this.firstChunkEmitted && this.stateMachine.getState() !== "interrupted" /* INTERRUPTED */) {
|
|
970
|
+
const filler = this.fillerInjector.getFiller();
|
|
971
|
+
if (filler) {
|
|
972
|
+
this.stats.fillersInjected++;
|
|
973
|
+
this.emit({
|
|
974
|
+
type: "filler-injected",
|
|
975
|
+
filler
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
emit(event) {
|
|
981
|
+
for (const listener of this.listeners) {
|
|
982
|
+
try {
|
|
983
|
+
listener(event);
|
|
984
|
+
} catch (error) {
|
|
985
|
+
console.error("Error in FlowManager event listener:", error);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
reset() {
|
|
990
|
+
this.firstChunkEmitted = false;
|
|
991
|
+
this.fillerInjector.reset();
|
|
992
|
+
this.bufferManager.clear();
|
|
993
|
+
this.stats = {
|
|
994
|
+
fillersInjected: 0,
|
|
995
|
+
stallsDetected: 0,
|
|
996
|
+
chunksProcessed: 0,
|
|
997
|
+
firstChunkTime: null,
|
|
998
|
+
totalDurationMs: 0
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
exports.BufferManager = BufferManager;
|
|
1004
|
+
exports.ConversationState = ConversationState;
|
|
1005
|
+
exports.ConversationStateMachine = ConversationStateMachine;
|
|
1006
|
+
exports.DEFAULT_FILLER_PHRASES = DEFAULT_FILLER_PHRASES;
|
|
1007
|
+
exports.DEFAULT_MAX_FILLERS_PER_RESPONSE = DEFAULT_MAX_FILLERS_PER_RESPONSE;
|
|
1008
|
+
exports.DEFAULT_STALL_THRESHOLD_MS = DEFAULT_STALL_THRESHOLD_MS;
|
|
1009
|
+
exports.FillerInjector = FillerInjector;
|
|
1010
|
+
exports.FlowControlError = FlowControlError;
|
|
1011
|
+
exports.FlowController = FlowController;
|
|
1012
|
+
exports.FlowManager = FlowManager;
|
|
1013
|
+
exports.MetricsCollector = MetricsCollector;
|
|
1014
|
+
exports.MonitorError = MonitorError;
|
|
1015
|
+
exports.SanitizerError = SanitizerError;
|
|
1016
|
+
exports.SpeechSanitizer = SpeechSanitizer;
|
|
1017
|
+
exports.StallDetector = StallDetector;
|
|
1018
|
+
exports.VocalStackError = VocalStackError;
|
|
1019
|
+
exports.VoiceAuditor = VoiceAuditor;
|
|
1020
|
+
exports.exportToCsv = exportToCsv;
|
|
1021
|
+
exports.exportToJson = exportToJson;
|
|
1022
|
+
exports.ruleRegistry = ruleRegistry;
|
|
1023
|
+
exports.sanitizeForSpeech = sanitizeForSpeech;
|
|
1024
|
+
exports.withFlowControl = withFlowControl;
|
|
1025
|
+
//# sourceMappingURL=index.cjs.map
|
|
1026
|
+
//# sourceMappingURL=index.cjs.map
|