@kuralle-syrinx/core 4.1.0 → 4.3.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/README.md +4 -0
- package/package.json +22 -3
- package/src/audio/alaw.ts +56 -0
- package/src/audio/g722.ts +329 -0
- package/src/audio/index.ts +12 -0
- package/src/audio/loudness.ts +87 -0
- package/src/confidence-to-wait.ts +30 -0
- package/src/idle-timeout.ts +1 -0
- package/src/incremental-unit.ts +19 -0
- package/src/index.ts +101 -1
- package/src/interaction-coordinator.ts +240 -0
- package/src/interaction-policy.ts +116 -0
- package/src/iu-ledger.ts +110 -0
- package/src/observability-observer.ts +8 -4
- package/src/observability.ts +30 -1
- package/src/packet-factories.ts +137 -2
- package/src/packets.ts +140 -4
- package/src/plugin-contract.ts +41 -0
- package/src/policies/defer.ts +15 -0
- package/src/policies/rule-based.ts +155 -0
- package/src/pricing.ts +137 -0
- package/src/primary-speaker-gate.ts +23 -3
- package/src/reasoner-hedge.ts +216 -0
- package/src/reasoner-route.ts +113 -0
- package/src/reasoner.ts +28 -1
- package/src/spend-cap.ts +52 -0
- package/src/turn-arbiter.ts +45 -4
- package/src/voice-agent-session.ts +682 -53
- package/src/voice-text.ts +69 -0
- package/src/audio/audio.test.ts +0 -285
- package/src/audio-envelope.test.ts +0 -167
- package/src/error-handler.test.ts +0 -56
- package/src/latency-filler.test.ts +0 -62
- package/src/observability-observer.test.ts +0 -245
- package/src/observability.test.ts +0 -85
- package/src/packet-factories.test.ts +0 -34
- package/src/pipeline-bus.g10.test.ts +0 -145
- package/src/pipeline-bus.test.ts +0 -211
- package/src/primary-speaker-gate.test.ts +0 -150
- package/src/provider-fallback.test.ts +0 -87
- package/src/reasoner.test.ts +0 -69
- package/src/retry.test.ts +0 -83
- package/src/tts-playout-clock.test.ts +0 -125
- package/src/turn-arbiter.characterization.test.ts +0 -477
- package/src/turn-arbiter.test.ts +0 -567
- package/src/voice-agent-session.test.ts +0 -2879
- package/src/voice-text.test.ts +0 -94
- package/tsconfig.json +0 -21
package/src/observability.ts
CHANGED
|
@@ -7,6 +7,21 @@ export function monotonicNowMs(): number {
|
|
|
7
7
|
return performance.timeOrigin + performance.now();
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export type ObservabilityLayer = "infrastructure" | "conversation";
|
|
11
|
+
|
|
12
|
+
export type TurnLocalizationVerdict = ObservabilityLayer | "none";
|
|
13
|
+
|
|
14
|
+
export interface TurnLocalizationSignals {
|
|
15
|
+
readonly infrastructureBreached: boolean;
|
|
16
|
+
readonly conversationFlagged: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function localizeTurn(signals: TurnLocalizationSignals): TurnLocalizationVerdict {
|
|
20
|
+
if (signals.infrastructureBreached) return "infrastructure";
|
|
21
|
+
if (signals.conversationFlagged) return "conversation";
|
|
22
|
+
return "none";
|
|
23
|
+
}
|
|
24
|
+
|
|
10
25
|
/** One step of a reconstructed turn timeline, with elapsed ms since the prior boundary. */
|
|
11
26
|
export interface TurnTimelineStep {
|
|
12
27
|
readonly boundary: TurnBoundaryEventPacket["boundary"];
|
|
@@ -46,7 +61,8 @@ export function reconstructTurnTimeline(
|
|
|
46
61
|
}
|
|
47
62
|
|
|
48
63
|
export interface MetricTags {
|
|
49
|
-
readonly [key: string]: string;
|
|
64
|
+
readonly [key: string]: string | undefined;
|
|
65
|
+
readonly layer?: ObservabilityLayer;
|
|
50
66
|
}
|
|
51
67
|
|
|
52
68
|
export interface SpanHandle {
|
|
@@ -56,12 +72,20 @@ export interface SpanHandle {
|
|
|
56
72
|
/** Export seam — implementations (Prometheus/OTel) live in optional packages, NOT here. */
|
|
57
73
|
export interface MetricsExporter {
|
|
58
74
|
observeHistogram(name: string, valueMs: number, tags: MetricTags): void;
|
|
75
|
+
/**
|
|
76
|
+
* A monotonic count (tokens, characters, audio-seconds). Counters, not histograms:
|
|
77
|
+
* usage sums to a bill, latency distributes to a percentile — different instruments.
|
|
78
|
+
* Optional so an existing exporter that predates it still satisfies the interface;
|
|
79
|
+
* producers call it as `exporter.observeCounter?.(...)`.
|
|
80
|
+
*/
|
|
81
|
+
observeCounter?(name: string, value: number, tags: MetricTags): void;
|
|
59
82
|
startSpan(name: string, tags: MetricTags): SpanHandle;
|
|
60
83
|
}
|
|
61
84
|
|
|
62
85
|
/** Default no-op exporter (core never depends on a backend). */
|
|
63
86
|
export const noopMetricsExporter: MetricsExporter = {
|
|
64
87
|
observeHistogram() {},
|
|
88
|
+
observeCounter() {},
|
|
65
89
|
startSpan() {
|
|
66
90
|
return { end() {} };
|
|
67
91
|
},
|
|
@@ -70,12 +94,17 @@ export const noopMetricsExporter: MetricsExporter = {
|
|
|
70
94
|
/** In-memory exporter for tests + incident reconstruction. */
|
|
71
95
|
export class InMemoryMetricsExporter implements MetricsExporter {
|
|
72
96
|
readonly histograms: Array<{ name: string; valueMs: number; tags: MetricTags }> = [];
|
|
97
|
+
readonly counters: Array<{ name: string; value: number; tags: MetricTags }> = [];
|
|
73
98
|
readonly spans: Array<{ name: string; tags: MetricTags; durationMs?: number }> = [];
|
|
74
99
|
|
|
75
100
|
observeHistogram(name: string, valueMs: number, tags: MetricTags): void {
|
|
76
101
|
this.histograms.push({ name, valueMs, tags });
|
|
77
102
|
}
|
|
78
103
|
|
|
104
|
+
observeCounter(name: string, value: number, tags: MetricTags): void {
|
|
105
|
+
this.counters.push({ name, value, tags });
|
|
106
|
+
}
|
|
107
|
+
|
|
79
108
|
startSpan(name: string, tags: MetricTags): SpanHandle {
|
|
80
109
|
const startMs = monotonicNowMs();
|
|
81
110
|
const spanIndex = this.spans.length;
|
package/src/packet-factories.ts
CHANGED
|
@@ -7,13 +7,21 @@
|
|
|
7
7
|
// definition and call sites need no `as` assertion — illegal packet shapes are
|
|
8
8
|
// unrepresentable at construction instead of asserted-valid by a cast (CR-05).
|
|
9
9
|
|
|
10
|
+
import type { WordTiming } from "./interaction-policy.js";
|
|
10
11
|
import { ErrorCategory } from "./packets.js";
|
|
11
12
|
import type {
|
|
13
|
+
UsageRecordedPacket,
|
|
12
14
|
ConversationMetricPacket,
|
|
15
|
+
AcousticSignalPacket,
|
|
16
|
+
AcousticSignal,
|
|
17
|
+
TurnLocalizationPacket,
|
|
13
18
|
TurnBoundaryKind,
|
|
14
19
|
TurnBoundaryEventPacket,
|
|
15
20
|
DtmfDigit,
|
|
16
21
|
DtmfReceivedPacket,
|
|
22
|
+
DtmfSendPacket,
|
|
23
|
+
CallTransferPacket,
|
|
24
|
+
CallTransferMode,
|
|
17
25
|
RecordUserAudioPacket,
|
|
18
26
|
RecordAssistantAudioDataPacket,
|
|
19
27
|
RecordAssistantAudioTruncatePacket,
|
|
@@ -21,7 +29,9 @@ import type {
|
|
|
21
29
|
SpeechToTextAudioPacket,
|
|
22
30
|
EndOfSpeechAudioPacket,
|
|
23
31
|
EndOfSpeechPacket,
|
|
32
|
+
SttPartialPacket,
|
|
24
33
|
SttResultPacket,
|
|
34
|
+
FinalizeSttPacket,
|
|
25
35
|
UserInputPacket,
|
|
26
36
|
TextToSpeechTextPacket,
|
|
27
37
|
TextToSpeechDonePacket,
|
|
@@ -32,6 +42,7 @@ import type {
|
|
|
32
42
|
InterruptLlmPacket,
|
|
33
43
|
InterruptSttPacket,
|
|
34
44
|
InjectMessagePacket,
|
|
45
|
+
SttReconfigurePacket,
|
|
35
46
|
LlmDeltaPacket,
|
|
36
47
|
LlmResponseDonePacket,
|
|
37
48
|
ReasoningSuspendedPacket,
|
|
@@ -39,7 +50,11 @@ import type {
|
|
|
39
50
|
StartIdleTimeoutPacket,
|
|
40
51
|
StopIdleTimeoutPacket,
|
|
41
52
|
ModeSwitchRequestedPacket,
|
|
53
|
+
InteractionBackchannelPacket,
|
|
54
|
+
InteractionDuckPacket,
|
|
55
|
+
InteractionResumePacket,
|
|
42
56
|
} from "./packets.js";
|
|
57
|
+
import type { SttReconfigurePartial } from "./plugin-contract.js";
|
|
43
58
|
|
|
44
59
|
const DTMF_DIGITS = new Set<DtmfDigit>(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "#"]);
|
|
45
60
|
|
|
@@ -59,6 +74,36 @@ export function dtmfReceived(
|
|
|
59
74
|
return { kind: "dtmf.received", contextId, timestampMs, digit, provider, rawDigit };
|
|
60
75
|
}
|
|
61
76
|
|
|
77
|
+
const DTMF_SEND_CHARS = /^[0-9*#wW]+$/;
|
|
78
|
+
|
|
79
|
+
/** Validate + build a `dtmf.send` packet. Throws on empty/invalid digit strings. */
|
|
80
|
+
export function dtmfSend(contextId: string, timestampMs: number, digits: string): DtmfSendPacket {
|
|
81
|
+
if (!digits || !DTMF_SEND_CHARS.test(digits)) {
|
|
82
|
+
throw new Error(`Invalid dtmf.send digits: ${JSON.stringify(digits)} (expected [0-9*#wW]+)`);
|
|
83
|
+
}
|
|
84
|
+
return { kind: "dtmf.send", contextId, timestampMs, digits };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Build a `call.transfer` packet. */
|
|
88
|
+
export function callTransfer(
|
|
89
|
+
contextId: string,
|
|
90
|
+
timestampMs: number,
|
|
91
|
+
mode: CallTransferMode,
|
|
92
|
+
target: string,
|
|
93
|
+
summary?: string,
|
|
94
|
+
): CallTransferPacket {
|
|
95
|
+
const trimmed = target.trim();
|
|
96
|
+
if (!trimmed) throw new Error("call.transfer target must be a non-empty E.164 / SIP URI");
|
|
97
|
+
return {
|
|
98
|
+
kind: "call.transfer",
|
|
99
|
+
contextId,
|
|
100
|
+
timestampMs,
|
|
101
|
+
mode,
|
|
102
|
+
target: trimmed,
|
|
103
|
+
...(summary !== undefined ? { summary } : {}),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
62
107
|
export function metric(
|
|
63
108
|
contextId: string,
|
|
64
109
|
name: string,
|
|
@@ -68,6 +113,46 @@ export function metric(
|
|
|
68
113
|
return { kind: "metric.conversation", contextId, timestampMs, name, value };
|
|
69
114
|
}
|
|
70
115
|
|
|
116
|
+
export function acousticSignal(
|
|
117
|
+
contextId: string,
|
|
118
|
+
timestampMs: number,
|
|
119
|
+
signal: AcousticSignal,
|
|
120
|
+
payload?: Readonly<Record<string, unknown>>,
|
|
121
|
+
): AcousticSignalPacket {
|
|
122
|
+
return {
|
|
123
|
+
kind: "acoustic.signal",
|
|
124
|
+
contextId,
|
|
125
|
+
timestampMs,
|
|
126
|
+
signal,
|
|
127
|
+
...(payload ? { payload } : {}),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function turnLocalization(
|
|
132
|
+
contextId: string,
|
|
133
|
+
timestampMs: number,
|
|
134
|
+
value: TurnLocalizationPacket["value"],
|
|
135
|
+
infrastructureBreached: boolean,
|
|
136
|
+
conversationFlagged: boolean,
|
|
137
|
+
): TurnLocalizationPacket {
|
|
138
|
+
return {
|
|
139
|
+
kind: "turn.localization",
|
|
140
|
+
contextId,
|
|
141
|
+
timestampMs,
|
|
142
|
+
value,
|
|
143
|
+
infrastructureBreached,
|
|
144
|
+
conversationFlagged,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function usageRecorded(
|
|
149
|
+
contextId: string,
|
|
150
|
+
fields: Omit<UsageRecordedPacket, "kind" | "contextId" | "timestampMs">,
|
|
151
|
+
timestampMs: number = Date.now(),
|
|
152
|
+
): UsageRecordedPacket {
|
|
153
|
+
return { kind: "usage.recorded", contextId, timestampMs, ...fields };
|
|
154
|
+
}
|
|
155
|
+
|
|
71
156
|
export function turnBoundary(
|
|
72
157
|
contextId: string,
|
|
73
158
|
timestampMs: number,
|
|
@@ -115,6 +200,17 @@ export function sttAudio(contextId: string, timestampMs: number, audio: Uint8Arr
|
|
|
115
200
|
return { kind: "stt.audio", contextId, timestampMs, audio };
|
|
116
201
|
}
|
|
117
202
|
|
|
203
|
+
export function sttPartial(
|
|
204
|
+
contextId: string,
|
|
205
|
+
timestampMs: number,
|
|
206
|
+
text: string,
|
|
207
|
+
wordTimings?: readonly WordTiming[],
|
|
208
|
+
): SttPartialPacket {
|
|
209
|
+
return wordTimings
|
|
210
|
+
? { kind: "stt.partial", contextId, timestampMs, text, wordTimings }
|
|
211
|
+
: { kind: "stt.partial", contextId, timestampMs, text };
|
|
212
|
+
}
|
|
213
|
+
|
|
118
214
|
export function eosAudio(contextId: string, timestampMs: number, audio: Uint8Array): EndOfSpeechAudioPacket {
|
|
119
215
|
return { kind: "eos.audio", contextId, timestampMs, audio };
|
|
120
216
|
}
|
|
@@ -128,6 +224,10 @@ export function eosTurnComplete(
|
|
|
128
224
|
return { kind: "eos.turn_complete", contextId, timestampMs, text, transcripts };
|
|
129
225
|
}
|
|
130
226
|
|
|
227
|
+
export function finalizeStt(contextId: string, timestampMs: number): FinalizeSttPacket {
|
|
228
|
+
return { kind: "stt.finalize", contextId, timestampMs };
|
|
229
|
+
}
|
|
230
|
+
|
|
131
231
|
export function userInput(
|
|
132
232
|
contextId: string,
|
|
133
233
|
timestampMs: number,
|
|
@@ -191,8 +291,21 @@ export function ttsError(
|
|
|
191
291
|
return { kind: "tts.error", contextId, timestampMs, component: "tts", category, cause, isRecoverable };
|
|
192
292
|
}
|
|
193
293
|
|
|
194
|
-
export function injectMessage(
|
|
195
|
-
|
|
294
|
+
export function injectMessage(
|
|
295
|
+
contextId: string,
|
|
296
|
+
timestampMs: number,
|
|
297
|
+
text: string,
|
|
298
|
+
mode?: InjectMessagePacket["mode"],
|
|
299
|
+
): InjectMessagePacket {
|
|
300
|
+
return { kind: "inject.message", contextId, timestampMs, text, ...(mode ? { mode } : {}) };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function sttReconfigure(
|
|
304
|
+
contextId: string,
|
|
305
|
+
timestampMs: number,
|
|
306
|
+
partial: SttReconfigurePartial,
|
|
307
|
+
): SttReconfigurePacket {
|
|
308
|
+
return { kind: "stt.reconfigure", contextId, timestampMs, partial };
|
|
196
309
|
}
|
|
197
310
|
|
|
198
311
|
export function llmDelta(contextId: string, timestampMs: number, text: string): LlmDeltaPacket {
|
|
@@ -241,3 +354,25 @@ export function modeSwitchRequested(
|
|
|
241
354
|
): ModeSwitchRequestedPacket {
|
|
242
355
|
return { kind: "mode.switch_requested", contextId, timestampMs, mode };
|
|
243
356
|
}
|
|
357
|
+
|
|
358
|
+
export function interactionBackchannel(
|
|
359
|
+
contextId: string,
|
|
360
|
+
timestampMs: number,
|
|
361
|
+
cue: string,
|
|
362
|
+
): InteractionBackchannelPacket {
|
|
363
|
+
return { kind: "interaction.backchannel", contextId, timestampMs, cue };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function interactionDuck(
|
|
367
|
+
contextId: string,
|
|
368
|
+
timestampMs: number,
|
|
369
|
+
): InteractionDuckPacket {
|
|
370
|
+
return { kind: "interaction.duck", contextId, timestampMs };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function interactionResume(
|
|
374
|
+
contextId: string,
|
|
375
|
+
timestampMs: number,
|
|
376
|
+
): InteractionResumePacket {
|
|
377
|
+
return { kind: "interaction.resume", contextId, timestampMs };
|
|
378
|
+
}
|
package/src/packets.ts
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
// Errors: SttError, TtsError, LlmError
|
|
10
10
|
// Lifecycle: InitStepCompleted, InitFailed, InitCompleted
|
|
11
11
|
|
|
12
|
+
import type { WordTiming } from "./interaction-policy.js";
|
|
13
|
+
import type { SttReconfigurePartial } from "./plugin-contract.js";
|
|
14
|
+
|
|
12
15
|
// =============================================================================
|
|
13
16
|
// Base Types
|
|
14
17
|
// =============================================================================
|
|
@@ -44,7 +47,7 @@ export enum ErrorCategory {
|
|
|
44
47
|
|
|
45
48
|
export interface VoiceErrorPacket extends VoicePacket {
|
|
46
49
|
/** Which component emitted the error. */
|
|
47
|
-
readonly component: "stt" | "tts" | "vad" | "eos" | "denoiser" | "llm" | "bridge" | "pipeline";
|
|
50
|
+
readonly component: "stt" | "tts" | "vad" | "eos" | "denoiser" | "llm" | "bridge" | "pipeline" | "iu_ledger";
|
|
48
51
|
/** Machine-readable error category. */
|
|
49
52
|
readonly category: ErrorCategory;
|
|
50
53
|
/** Original error. May contain provider-specific details. */
|
|
@@ -120,8 +123,10 @@ export interface AudioFormat {
|
|
|
120
123
|
|
|
121
124
|
export interface UserAudioReceivedPacket extends VoicePacket {
|
|
122
125
|
readonly kind: "user.audio_received";
|
|
123
|
-
/** Raw PCM audio (16-bit, mono
|
|
126
|
+
/** Raw PCM audio (16-bit, mono). Rate given by `sampleRateHz` (defaults to 16kHz). */
|
|
124
127
|
readonly audio: Uint8Array;
|
|
128
|
+
/** Sample rate of `audio` in Hz. Omitted means 16000 (the legacy default). */
|
|
129
|
+
readonly sampleRateHz?: number;
|
|
125
130
|
}
|
|
126
131
|
|
|
127
132
|
export interface UserTextReceivedPacket extends VoicePacket {
|
|
@@ -176,6 +181,12 @@ export interface SttInterimPacket extends VoicePacket {
|
|
|
176
181
|
readonly text: string;
|
|
177
182
|
}
|
|
178
183
|
|
|
184
|
+
export interface SttPartialPacket extends VoicePacket {
|
|
185
|
+
readonly kind: "stt.partial";
|
|
186
|
+
readonly text: string;
|
|
187
|
+
readonly wordTimings?: readonly WordTiming[];
|
|
188
|
+
}
|
|
189
|
+
|
|
179
190
|
export interface SttResultPacket extends VoicePacket {
|
|
180
191
|
readonly kind: "stt.result";
|
|
181
192
|
readonly text: string;
|
|
@@ -189,6 +200,16 @@ export interface FinalizeSttPacket extends VoicePacket {
|
|
|
189
200
|
readonly kind: "stt.finalize";
|
|
190
201
|
}
|
|
191
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Per-turn STT reconfigure actuation. Session routes to the stt plugin's
|
|
205
|
+
* `sttReconfigure` seam when present (warn-and-no-op otherwise).
|
|
206
|
+
* Call at a turn boundary so reconnect-based STTs do not drop mid-utterance audio.
|
|
207
|
+
*/
|
|
208
|
+
export interface SttReconfigurePacket extends VoicePacket {
|
|
209
|
+
readonly kind: "stt.reconfigure";
|
|
210
|
+
readonly partial: SttReconfigurePartial;
|
|
211
|
+
}
|
|
212
|
+
|
|
192
213
|
export interface SttErrorPacket extends VoicePacket, VoiceErrorPacket {
|
|
193
214
|
readonly kind: "stt.error";
|
|
194
215
|
readonly component: "stt";
|
|
@@ -270,6 +291,32 @@ export interface DtmfReceivedPacket extends VoicePacket {
|
|
|
270
291
|
readonly rawDigit: string;
|
|
271
292
|
}
|
|
272
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Outbound DTMF request (IVR navigation). Digits may include pause syntax
|
|
296
|
+
* `w` (0.5s) / `W` (1s). Mechanism unit-tested; live carrier decode unverified.
|
|
297
|
+
*/
|
|
298
|
+
export interface DtmfSendPacket extends VoicePacket {
|
|
299
|
+
readonly kind: "dtmf.send";
|
|
300
|
+
/** Digits in `[0-9*#wW]+`. */
|
|
301
|
+
readonly digits: string;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export type CallTransferMode = "warm" | "cold" | "sip_refer";
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Outbound call transfer. Prefer Call-Control transfer over SIP REFER where
|
|
308
|
+
* answer-rate matters (REFER drops STIR/SHAKEN attestation). Mechanism
|
|
309
|
+
* unit-tested; live transfer bridge unverified against a carrier.
|
|
310
|
+
*/
|
|
311
|
+
export interface CallTransferPacket extends VoicePacket {
|
|
312
|
+
readonly kind: "call.transfer";
|
|
313
|
+
readonly mode: CallTransferMode;
|
|
314
|
+
/** E.164 number or SIP URI. */
|
|
315
|
+
readonly target: string;
|
|
316
|
+
/** Warm-handoff context for the receiving agent/human (mode `"warm"`). */
|
|
317
|
+
readonly summary?: string;
|
|
318
|
+
}
|
|
319
|
+
|
|
273
320
|
// =============================================================================
|
|
274
321
|
// LLM Pipeline Packets
|
|
275
322
|
// =============================================================================
|
|
@@ -286,7 +333,7 @@ export interface LlmResponseDonePacket extends VoicePacket {
|
|
|
286
333
|
|
|
287
334
|
export interface LlmErrorPacket extends VoicePacket, VoiceErrorPacket {
|
|
288
335
|
readonly kind: "llm.error";
|
|
289
|
-
readonly component: "llm" | "bridge";
|
|
336
|
+
readonly component: "llm" | "bridge" | "iu_ledger";
|
|
290
337
|
}
|
|
291
338
|
|
|
292
339
|
export interface LlmToolCallPacket extends VoicePacket {
|
|
@@ -344,6 +391,14 @@ export interface DelegateResultPacket extends VoicePacket {
|
|
|
344
391
|
readonly grounded: boolean;
|
|
345
392
|
readonly toolId?: string;
|
|
346
393
|
readonly toolName?: string;
|
|
394
|
+
readonly control?: {
|
|
395
|
+
readonly name: string;
|
|
396
|
+
readonly payload: unknown;
|
|
397
|
+
};
|
|
398
|
+
readonly blocked?: {
|
|
399
|
+
readonly userFacingMessage: string;
|
|
400
|
+
readonly payload?: unknown;
|
|
401
|
+
};
|
|
347
402
|
}
|
|
348
403
|
|
|
349
404
|
export interface ReasoningResumePacket extends VoicePacket {
|
|
@@ -460,6 +515,26 @@ export interface RecordAssistantAudioTruncatePacket extends VoicePacket {
|
|
|
460
515
|
|
|
461
516
|
export type RecordAssistantAudioPacket = RecordAssistantAudioDataPacket | RecordAssistantAudioTruncatePacket;
|
|
462
517
|
|
|
518
|
+
// =============================================================================
|
|
519
|
+
// Interaction Policy Packets
|
|
520
|
+
// =============================================================================
|
|
521
|
+
|
|
522
|
+
/** Pre-cached backchannel cue id rendered by the outbound mixer (IP-C3). */
|
|
523
|
+
export interface InteractionBackchannelPacket extends VoicePacket {
|
|
524
|
+
readonly kind: "interaction.backchannel";
|
|
525
|
+
readonly cue: string;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Signal to attenuate active TTS while a barge-in is being evaluated (pending window). */
|
|
529
|
+
export interface InteractionDuckPacket extends VoicePacket {
|
|
530
|
+
readonly kind: "interaction.duck";
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** Signal to restore attenuated TTS when the pending window resolved without an interrupt. */
|
|
534
|
+
export interface InteractionResumePacket extends VoicePacket {
|
|
535
|
+
readonly kind: "interaction.resume";
|
|
536
|
+
}
|
|
537
|
+
|
|
463
538
|
// =============================================================================
|
|
464
539
|
// Behavior Packets
|
|
465
540
|
// =============================================================================
|
|
@@ -476,6 +551,8 @@ export interface StopIdleTimeoutPacket extends VoicePacket {
|
|
|
476
551
|
export interface InjectMessagePacket extends VoicePacket {
|
|
477
552
|
readonly kind: "inject.message";
|
|
478
553
|
readonly text: string;
|
|
554
|
+
/** Defaults to speak for compatibility with existing inject.message producers. */
|
|
555
|
+
readonly mode?: "speak" | "context";
|
|
479
556
|
}
|
|
480
557
|
|
|
481
558
|
export interface DisconnectRequestedPacket extends VoicePacket {
|
|
@@ -517,6 +594,58 @@ export interface ConversationMetricPacket extends VoicePacket {
|
|
|
517
594
|
readonly value: string;
|
|
518
595
|
}
|
|
519
596
|
|
|
597
|
+
export type AcousticSignal =
|
|
598
|
+
| "prosody"
|
|
599
|
+
| "backchannel"
|
|
600
|
+
| "interruption"
|
|
601
|
+
| "primary_speaker"
|
|
602
|
+
| "echo_rejected"
|
|
603
|
+
| "cadence";
|
|
604
|
+
|
|
605
|
+
export interface AcousticSignalPacket extends VoicePacket {
|
|
606
|
+
readonly kind: "acoustic.signal";
|
|
607
|
+
readonly signal: AcousticSignal;
|
|
608
|
+
readonly payload?: Readonly<Record<string, unknown>>;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export type TurnLocalizationVerdict = "infrastructure" | "conversation" | "none";
|
|
612
|
+
|
|
613
|
+
export interface TurnLocalizationPacket extends VoicePacket {
|
|
614
|
+
readonly kind: "turn.localization";
|
|
615
|
+
readonly value: TurnLocalizationVerdict;
|
|
616
|
+
readonly infrastructureBreached: boolean;
|
|
617
|
+
readonly conversationFlagged: boolean;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/** The pipeline stage that consumed resources. Billing planes group cost by this. */
|
|
621
|
+
export type UsageStage = "llm" | "stt" | "tts";
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* One unit of billable resource consumption, recorded where it happens.
|
|
625
|
+
*
|
|
626
|
+
* The full shape is defined up front — LLM tokens, STT audio-seconds, TTS characters —
|
|
627
|
+
* so a producer added later needs no schema change; only the LLM producer is wired today
|
|
628
|
+
* (the AI SDK finish part carries usage the bridge had been dropping). `VoiceAgentSession`
|
|
629
|
+
* accumulates these into an end-of-session `session.usage` manifest and exports them as
|
|
630
|
+
* counters — the load-bearing seam for metering, spend caps, and eventual per-tenant billing.
|
|
631
|
+
* Fields not applicable to a stage are simply absent (tokens on STT/TTS, seconds on LLM).
|
|
632
|
+
*/
|
|
633
|
+
export interface UsageRecordedPacket extends VoicePacket {
|
|
634
|
+
readonly kind: "usage.recorded";
|
|
635
|
+
readonly stage: UsageStage;
|
|
636
|
+
readonly provider?: string;
|
|
637
|
+
readonly model?: string;
|
|
638
|
+
// LLM
|
|
639
|
+
readonly inputTokens?: number;
|
|
640
|
+
readonly outputTokens?: number;
|
|
641
|
+
readonly totalTokens?: number;
|
|
642
|
+
readonly cachedInputTokens?: number;
|
|
643
|
+
readonly reasoningTokens?: number;
|
|
644
|
+
// STT / TTS
|
|
645
|
+
readonly audioSeconds?: number;
|
|
646
|
+
readonly characters?: number;
|
|
647
|
+
}
|
|
648
|
+
|
|
520
649
|
export type TurnBoundaryKind =
|
|
521
650
|
| "user_started_speaking"
|
|
522
651
|
| "user_stopped_speaking"
|
|
@@ -560,8 +689,10 @@ export type InputPacket =
|
|
|
560
689
|
| VadSpeechActivityPacket
|
|
561
690
|
| SpeechToTextAudioPacket
|
|
562
691
|
| SttInterimPacket
|
|
692
|
+
| SttPartialPacket
|
|
563
693
|
| SttResultPacket
|
|
564
694
|
| FinalizeSttPacket
|
|
695
|
+
| SttReconfigurePacket
|
|
565
696
|
| SttErrorPacket
|
|
566
697
|
| EndOfSpeechAudioPacket
|
|
567
698
|
| EndOfSpeechPacket
|
|
@@ -607,7 +738,12 @@ export type AnyErrorPacket =
|
|
|
607
738
|
| InitializationFailedPacket;
|
|
608
739
|
|
|
609
740
|
/** Observability packets (Background route). */
|
|
610
|
-
export type ObservabilityPacket =
|
|
741
|
+
export type ObservabilityPacket =
|
|
742
|
+
| ConversationMetricPacket
|
|
743
|
+
| TurnBoundaryEventPacket
|
|
744
|
+
| UsageRecordedPacket
|
|
745
|
+
| AcousticSignalPacket
|
|
746
|
+
| TurnLocalizationPacket;
|
|
611
747
|
|
|
612
748
|
/** Delegate (Responder-Thinker) lifecycle packets (Background route). */
|
|
613
749
|
export type DelegatePacket = DelegateQueryPacket | DelegateResultPacket;
|
package/src/plugin-contract.ts
CHANGED
|
@@ -22,9 +22,43 @@ export interface EndpointingCapability {
|
|
|
22
22
|
readonly disableConfig?: PluginConfig;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Vendor-agnostic mid-stream STT reconfiguration. Per-turn reconfigure is a COMMODITY STT capability
|
|
27
|
+
* (Deepgram Flux `Configure`, AssemblyAI `UpdateConfiguration`, Speechmatics `SetRecognitionConfig`) —
|
|
28
|
+
* the value of THIS seam is normalizing those differing wire shapes behind one interface so an
|
|
29
|
+
* InteractionPolicy can actuate any STT without vendor-specific plumbing. All fields optional; a plugin
|
|
30
|
+
* applies what it supports and ignores the rest (best-effort — e.g. Deepgram ignores `contextText`).
|
|
31
|
+
*/
|
|
32
|
+
export interface SttReconfigurePartial {
|
|
33
|
+
readonly keyterms?: readonly string[];
|
|
34
|
+
readonly eotThreshold?: number;
|
|
35
|
+
readonly eagerEotThreshold?: number;
|
|
36
|
+
readonly eotTimeoutMs?: number;
|
|
37
|
+
/** Silence-based endpointing (ms). Nova-style; Flux may ignore (it uses eotTimeoutMs). */
|
|
38
|
+
readonly endpointingMs?: number;
|
|
39
|
+
readonly vadThreshold?: number;
|
|
40
|
+
readonly languageHints?: readonly string[];
|
|
41
|
+
/**
|
|
42
|
+
* Hard recognition-language switch (e.g. "en-US" → "es-ES", or Nova-3 "multi" for code-switch).
|
|
43
|
+
* Distinct from `languageHints` (soft bias): this changes the recognizer's language. Providers
|
|
44
|
+
* that carry language in the connection URL (Nova) reconnect to apply it; model-fixed providers
|
|
45
|
+
* (Flux, `flux-general-en`) ignore it and rely on `languageHints`.
|
|
46
|
+
*/
|
|
47
|
+
readonly language?: string;
|
|
48
|
+
/** AssemblyAI-style agent-context biasing (the agent's own prior reply). Ignored where unsupported. */
|
|
49
|
+
readonly contextText?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface SttReconfigure {
|
|
53
|
+
reconfigure(partial: SttReconfigurePartial): void;
|
|
54
|
+
}
|
|
55
|
+
|
|
25
56
|
export interface VoicePlugin {
|
|
26
57
|
readonly endpointingCapability?: EndpointingCapability;
|
|
27
58
|
|
|
59
|
+
/** Present when the STT supports mid-stream reconfiguration (see {@link SttReconfigure}). */
|
|
60
|
+
readonly sttReconfigure?: SttReconfigure;
|
|
61
|
+
|
|
28
62
|
/**
|
|
29
63
|
* Initialize the plugin. Called during the init chain.
|
|
30
64
|
* Connect to provider, start streams, register bus handlers if needed.
|
|
@@ -34,6 +68,13 @@ export interface VoicePlugin {
|
|
|
34
68
|
*/
|
|
35
69
|
initialize(bus: PipelineBus, config: PluginConfig): Promise<void>;
|
|
36
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Best-effort warm of remote/expensive resources (open connections, wake a scaled-to-zero
|
|
73
|
+
* endpoint). Called AFTER initialize, before the first turn. Must not throw fatally — swallow
|
|
74
|
+
* errors internally. Optional; plugins with nothing to warm omit it.
|
|
75
|
+
*/
|
|
76
|
+
prewarm?(): Promise<void>;
|
|
77
|
+
|
|
37
78
|
/**
|
|
38
79
|
* Tear down the plugin. Called during the finalize chain (reverse order).
|
|
39
80
|
* Close connections, flush buffers, release resources.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
import type { InteractionDecision, InteractionObservation, InteractionPolicy } from "../interaction-policy.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The front owns full-duplex interaction (turn-taking, barge-in, backchannels). This policy takes no
|
|
7
|
+
* action — it observes only, so the coordinator drives nothing and the front's own decisions (surfaced
|
|
8
|
+
* by its adapter/bridge as interrupt.detected / eos.turn_complete) stand. RFC InteractionPolicy REQ-4.
|
|
9
|
+
*/
|
|
10
|
+
export class DeferInteractionPolicy implements InteractionPolicy {
|
|
11
|
+
observe(_obs: InteractionObservation): readonly InteractionDecision[] {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
reset(_contextId: string): void {}
|
|
15
|
+
}
|