@kuralle-syrinx/core 2.1.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 +41 -0
- package/package.json +22 -0
- package/src/audio/audio.test.ts +285 -0
- package/src/audio/index.ts +5 -0
- package/src/audio/mulaw.ts +42 -0
- package/src/audio/pcm.ts +43 -0
- package/src/audio/resample.ts +177 -0
- package/src/audio-envelope.test.ts +167 -0
- package/src/audio-envelope.ts +143 -0
- package/src/conversation-event.ts +62 -0
- package/src/error-handler.test.ts +56 -0
- package/src/error-handler.ts +149 -0
- package/src/idle-timeout.ts +210 -0
- package/src/index.ts +215 -0
- package/src/init-chain.ts +137 -0
- package/src/init-stage-order.ts +79 -0
- package/src/latency-filler-fixtures.ts +16 -0
- package/src/latency-filler.test.ts +62 -0
- package/src/latency-filler.ts +125 -0
- package/src/mode-switcher.ts +110 -0
- package/src/observability-observer.test.ts +245 -0
- package/src/observability-observer.ts +195 -0
- package/src/observability.test.ts +85 -0
- package/src/observability.ts +93 -0
- package/src/packet-factories.test.ts +34 -0
- package/src/packet-factories.ts +243 -0
- package/src/packets.ts +553 -0
- package/src/pipeline-bus.g10.test.ts +145 -0
- package/src/pipeline-bus.test.ts +197 -0
- package/src/pipeline-bus.ts +369 -0
- package/src/plugin-contract.ts +79 -0
- package/src/primary-speaker-fixtures.ts +45 -0
- package/src/primary-speaker-gate.test.ts +150 -0
- package/src/primary-speaker-gate.ts +186 -0
- package/src/provider-fallback.test.ts +87 -0
- package/src/provider-fallback.ts +88 -0
- package/src/reasoner.test.ts +69 -0
- package/src/reasoner.ts +54 -0
- package/src/retry.test.ts +83 -0
- package/src/retry.ts +106 -0
- package/src/scheduler.ts +28 -0
- package/src/tts-playout-clock.test.ts +125 -0
- package/src/tts-playout-clock.ts +116 -0
- package/src/turn-arbiter.characterization.test.ts +477 -0
- package/src/turn-arbiter.test.ts +567 -0
- package/src/turn-arbiter.ts +283 -0
- package/src/voice-agent-session-util.ts +240 -0
- package/src/voice-agent-session.test.ts +2487 -0
- package/src/voice-agent-session.ts +1175 -0
- package/src/voice-text.test.ts +81 -0
- package/src/voice-text.ts +102 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
//
|
|
3
|
+
// Syrinx Kernel v2 — Idle Timeout Manager
|
|
4
|
+
//
|
|
5
|
+
// Tracks user silence and escalates: inject message → inject warning → disconnect.
|
|
6
|
+
// Configurable duration, escalation messages, and consecutive backoff.
|
|
7
|
+
//
|
|
8
|
+
// Design decision (per RFC Q4 resolution): idle messages are injected as
|
|
9
|
+
// synthetic LlmDeltaPacket + LlmDonePacket through the normal TTS path.
|
|
10
|
+
// This keeps voice style, logging, interruption, and abort semantics consistent.
|
|
11
|
+
|
|
12
|
+
import type { PipelineBus } from "./pipeline-bus.js";
|
|
13
|
+
import { Route } from "./pipeline-bus.js";
|
|
14
|
+
import type {
|
|
15
|
+
StartIdleTimeoutPacket,
|
|
16
|
+
StopIdleTimeoutPacket,
|
|
17
|
+
InjectMessagePacket,
|
|
18
|
+
DisconnectRequestedPacket,
|
|
19
|
+
} from "./packets.js";
|
|
20
|
+
import { TimerScheduler, type Scheduler } from "./scheduler.js";
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// Configuration
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
export interface IdleTimeoutConfig {
|
|
27
|
+
/** Base duration before triggering idle behavior (ms). Default: 15000 */
|
|
28
|
+
durationMs: number;
|
|
29
|
+
/**
|
|
30
|
+
* Number of consecutive timeouts before disconnecting.
|
|
31
|
+
* 0 = never disconnect (just inject messages repeatedly).
|
|
32
|
+
* Default: 3
|
|
33
|
+
*/
|
|
34
|
+
maxConsecutive: number;
|
|
35
|
+
/**
|
|
36
|
+
* Messages to inject at each consecutive idle.
|
|
37
|
+
* Index 0 = first timeout, index 1 = second timeout, etc.
|
|
38
|
+
* If more timeouts occur than messages, the last message repeats.
|
|
39
|
+
* Default: ["Are you still there?", "I'll end this call soon."]
|
|
40
|
+
*/
|
|
41
|
+
escalationMessages: string[];
|
|
42
|
+
/**
|
|
43
|
+
* Whether to disconnect the session after maxConsecutive is reached.
|
|
44
|
+
* If false, the maxConsecutive counter stops incrementing and the
|
|
45
|
+
* last escalation message repeats indefinitely.
|
|
46
|
+
* Default: true
|
|
47
|
+
*/
|
|
48
|
+
disconnectAfterMax: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const DEFAULT_IDLE_TIMEOUT_CONFIG: IdleTimeoutConfig = {
|
|
52
|
+
durationMs: 15_000,
|
|
53
|
+
maxConsecutive: 3,
|
|
54
|
+
escalationMessages: ["Are you still there?", "I'll end this call soon."],
|
|
55
|
+
disconnectAfterMax: true,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// =============================================================================
|
|
59
|
+
// Manager
|
|
60
|
+
// =============================================================================
|
|
61
|
+
|
|
62
|
+
export class IdleTimeoutManager {
|
|
63
|
+
private timerScheduled = false;
|
|
64
|
+
private count = 0;
|
|
65
|
+
private readonly config: IdleTimeoutConfig;
|
|
66
|
+
private readonly bus: PipelineBus;
|
|
67
|
+
private currentContextId: string;
|
|
68
|
+
private readonly scheduler: Scheduler;
|
|
69
|
+
|
|
70
|
+
constructor(
|
|
71
|
+
bus: PipelineBus,
|
|
72
|
+
config?: Partial<IdleTimeoutConfig>,
|
|
73
|
+
scheduler?: Scheduler,
|
|
74
|
+
) {
|
|
75
|
+
this.bus = bus;
|
|
76
|
+
this.config = { ...DEFAULT_IDLE_TIMEOUT_CONFIG, ...config };
|
|
77
|
+
this.currentContextId = "";
|
|
78
|
+
this.scheduler = scheduler ?? new TimerScheduler();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// -------------------------------------------------------------------------
|
|
82
|
+
// Public API
|
|
83
|
+
// -------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
/** Set the current turn context ID (used for injected message context). */
|
|
86
|
+
setContextId(id: string): void {
|
|
87
|
+
this.currentContextId = id;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Start (or restart) the idle timeout timer. */
|
|
91
|
+
start(): void {
|
|
92
|
+
this.clearTimer();
|
|
93
|
+
if (this.config.durationMs <= 0) return;
|
|
94
|
+
|
|
95
|
+
this.timerScheduled = true;
|
|
96
|
+
this.scheduler.schedule("voice.idle_timeout", this.config.durationMs, () => {
|
|
97
|
+
this.timerScheduled = false;
|
|
98
|
+
this.onTimeout();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Stop the idle timeout timer.
|
|
104
|
+
* @param resetCount — If true, resets the consecutive idle counter.
|
|
105
|
+
* Use true when the user actively engages (speaks or types).
|
|
106
|
+
* Use false for system-driven stops (e.g., TTS still playing).
|
|
107
|
+
*/
|
|
108
|
+
stop(resetCount: boolean): void {
|
|
109
|
+
this.clearTimer();
|
|
110
|
+
if (resetCount) {
|
|
111
|
+
this.count = 0;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Extend the current timer by a duration (e.g., TTS audio playback time).
|
|
117
|
+
* Stops the current timer and restarts with the remaining time + extension.
|
|
118
|
+
*/
|
|
119
|
+
extend(ms: number): void {
|
|
120
|
+
this.clearTimer();
|
|
121
|
+
this.timerScheduled = true;
|
|
122
|
+
this.scheduler.schedule("voice.idle_timeout", this.config.durationMs + ms, () => {
|
|
123
|
+
this.timerScheduled = false;
|
|
124
|
+
this.onTimeout();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Clean up — stop timer without side effects. */
|
|
129
|
+
dispose(): void {
|
|
130
|
+
this.clearTimer();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// -------------------------------------------------------------------------
|
|
134
|
+
// Private
|
|
135
|
+
// -------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
private clearTimer(): void {
|
|
138
|
+
if (!this.timerScheduled) return;
|
|
139
|
+
this.scheduler.cancel("voice.idle_timeout");
|
|
140
|
+
this.timerScheduled = false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Resolve a non-empty context id for injected idle turns. If no turn context has been seen yet
|
|
145
|
+
* (e.g. the idle timer fired before the first user turn completed), synthesize a stable one —
|
|
146
|
+
* an empty contextId propagates downstream and is rejected by providers that validate it
|
|
147
|
+
* (e.g. Cartesia TTS: "context_id must be alphanumeric/_/-").
|
|
148
|
+
*/
|
|
149
|
+
private ensureContextId(): string {
|
|
150
|
+
if (this.currentContextId === "") {
|
|
151
|
+
this.currentContextId = `idle-${String(Date.now())}`;
|
|
152
|
+
}
|
|
153
|
+
return this.currentContextId;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private onTimeout(): void {
|
|
157
|
+
this.count++;
|
|
158
|
+
const contextId = this.ensureContextId();
|
|
159
|
+
|
|
160
|
+
// Check if we should disconnect
|
|
161
|
+
if (
|
|
162
|
+
this.config.maxConsecutive > 0 &&
|
|
163
|
+
this.count >= this.config.maxConsecutive &&
|
|
164
|
+
this.config.disconnectAfterMax
|
|
165
|
+
) {
|
|
166
|
+
const disconnect: DisconnectRequestedPacket = {
|
|
167
|
+
kind: "session.disconnect",
|
|
168
|
+
contextId,
|
|
169
|
+
timestampMs: Date.now(),
|
|
170
|
+
reason: `idle_timeout: ${this.count} consecutive`,
|
|
171
|
+
};
|
|
172
|
+
this.bus.push(Route.Critical, disconnect);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Inject escalation message
|
|
177
|
+
const msgIdx = Math.min(
|
|
178
|
+
this.count - 1,
|
|
179
|
+
this.config.escalationMessages.length - 1,
|
|
180
|
+
);
|
|
181
|
+
const message = this.config.escalationMessages[msgIdx];
|
|
182
|
+
if (message) {
|
|
183
|
+
const inject: InjectMessagePacket = {
|
|
184
|
+
kind: "inject.message",
|
|
185
|
+
contextId,
|
|
186
|
+
timestampMs: Date.now(),
|
|
187
|
+
text: message,
|
|
188
|
+
};
|
|
189
|
+
this.bus.push(Route.Main, inject);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Restart timer for next escalation
|
|
193
|
+
this.start();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// -------------------------------------------------------------------------
|
|
197
|
+
// Bus packet handlers (for integration with VoiceAgentSession)
|
|
198
|
+
// -------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
/** Handle a StartIdleTimeoutPacket from the bus. */
|
|
201
|
+
handleStart(pkt: StartIdleTimeoutPacket): void {
|
|
202
|
+
if (pkt.contextId) this.setContextId(pkt.contextId);
|
|
203
|
+
this.start();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Handle a StopIdleTimeoutPacket from the bus. */
|
|
207
|
+
handleStop(pkt: StopIdleTimeoutPacket): void {
|
|
208
|
+
this.stop(pkt.resetCount);
|
|
209
|
+
}
|
|
210
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
//
|
|
3
|
+
// Syrinx Kernel v2 — Public API
|
|
4
|
+
//
|
|
5
|
+
// Everything a consumer needs to build voice agents with the new kernel.
|
|
6
|
+
|
|
7
|
+
// Core types
|
|
8
|
+
export {
|
|
9
|
+
type AudioFormat,
|
|
10
|
+
type VoicePacket,
|
|
11
|
+
type AsyncPacket,
|
|
12
|
+
type VoiceErrorPacket,
|
|
13
|
+
ErrorCategory,
|
|
14
|
+
SessionState,
|
|
15
|
+
InitStage,
|
|
16
|
+
type InitStepCompletedPacket,
|
|
17
|
+
type InitializationFailedPacket,
|
|
18
|
+
type InitializationCompletedPacket,
|
|
19
|
+
} from "./packets.js";
|
|
20
|
+
|
|
21
|
+
// Pipeline packets — input
|
|
22
|
+
export {
|
|
23
|
+
type UserAudioReceivedPacket,
|
|
24
|
+
type UserTextReceivedPacket,
|
|
25
|
+
type DenoiseAudioPacket,
|
|
26
|
+
type DenoisedAudioPacket,
|
|
27
|
+
type VadAudioPacket,
|
|
28
|
+
type VadSpeechStartedPacket,
|
|
29
|
+
type VadSpeechEndedPacket,
|
|
30
|
+
type VadSpeechActivityPacket,
|
|
31
|
+
type SpeechToTextAudioPacket,
|
|
32
|
+
type SttInterimPacket,
|
|
33
|
+
type SttResultPacket,
|
|
34
|
+
type FinalizeSttPacket,
|
|
35
|
+
type SttErrorPacket,
|
|
36
|
+
type EndOfSpeechAudioPacket,
|
|
37
|
+
type EndOfSpeechPacket,
|
|
38
|
+
type InterimEndOfSpeechPacket,
|
|
39
|
+
type UserInputPacket,
|
|
40
|
+
} from "./packets.js";
|
|
41
|
+
|
|
42
|
+
// Pipeline packets — interruption
|
|
43
|
+
export {
|
|
44
|
+
type InterruptionDetectedPacket,
|
|
45
|
+
type InterruptTtsPacket,
|
|
46
|
+
type InterruptLlmPacket,
|
|
47
|
+
type InterruptSttPacket,
|
|
48
|
+
type TurnChangePacket,
|
|
49
|
+
} from "./packets.js";
|
|
50
|
+
|
|
51
|
+
// Pipeline packets — LLM
|
|
52
|
+
export {
|
|
53
|
+
type LlmDeltaPacket,
|
|
54
|
+
type LlmResponseDonePacket,
|
|
55
|
+
type LlmErrorPacket,
|
|
56
|
+
type LlmToolCallPacket,
|
|
57
|
+
type LlmToolResultPacket,
|
|
58
|
+
} from "./packets.js";
|
|
59
|
+
|
|
60
|
+
// Pipeline packets — reasoning (suspend/resume)
|
|
61
|
+
export {
|
|
62
|
+
type ReasoningSuspendedPacket,
|
|
63
|
+
type ReasoningResumePacket,
|
|
64
|
+
} from "./packets.js";
|
|
65
|
+
|
|
66
|
+
// Pipeline packets — TTS
|
|
67
|
+
export {
|
|
68
|
+
type TextToSpeechTextPacket,
|
|
69
|
+
type TextToSpeechDonePacket,
|
|
70
|
+
type TextToSpeechAudioPacket,
|
|
71
|
+
type TextToSpeechEndPacket,
|
|
72
|
+
type TextToSpeechPlayoutStartedPacket,
|
|
73
|
+
type TextToSpeechPlayoutProgressPacket,
|
|
74
|
+
type TextToSpeechWordTimestampsPacket,
|
|
75
|
+
type TtsWordTimestamp,
|
|
76
|
+
type TtsErrorPacket,
|
|
77
|
+
} from "./packets.js";
|
|
78
|
+
|
|
79
|
+
// Pipeline packets — behavior
|
|
80
|
+
export {
|
|
81
|
+
type RecordAssistantAudioDataPacket,
|
|
82
|
+
type RecordAssistantAudioPacket,
|
|
83
|
+
type RecordAssistantAudioTruncatePacket,
|
|
84
|
+
type RecordUserAudioPacket,
|
|
85
|
+
type StartIdleTimeoutPacket,
|
|
86
|
+
type StopIdleTimeoutPacket,
|
|
87
|
+
type InjectMessagePacket,
|
|
88
|
+
type DisconnectRequestedPacket,
|
|
89
|
+
} from "./packets.js";
|
|
90
|
+
|
|
91
|
+
// Pipeline packets — mode
|
|
92
|
+
export {
|
|
93
|
+
type ModeSwitchRequestedPacket,
|
|
94
|
+
type ModeSwitchCompletedPacket,
|
|
95
|
+
} from "./packets.js";
|
|
96
|
+
|
|
97
|
+
// Pipeline packets — persistence
|
|
98
|
+
export {
|
|
99
|
+
type MessageCreatePacket,
|
|
100
|
+
type ConversationMetricPacket,
|
|
101
|
+
type TurnBoundaryKind,
|
|
102
|
+
type TurnBoundaryEventPacket,
|
|
103
|
+
type ObservabilityPacket,
|
|
104
|
+
type PipelineErrorPacket,
|
|
105
|
+
} from "./packets.js";
|
|
106
|
+
|
|
107
|
+
// Observability backbone (VE-07)
|
|
108
|
+
export {
|
|
109
|
+
monotonicNowMs,
|
|
110
|
+
type MetricTags,
|
|
111
|
+
type SpanHandle,
|
|
112
|
+
type MetricsExporter,
|
|
113
|
+
noopMetricsExporter,
|
|
114
|
+
InMemoryMetricsExporter,
|
|
115
|
+
reconstructTurnTimeline,
|
|
116
|
+
type TurnTimelineStep,
|
|
117
|
+
} from "./observability.js";
|
|
118
|
+
|
|
119
|
+
export {
|
|
120
|
+
ObservabilityObserver,
|
|
121
|
+
type ObservabilityObserverDeps,
|
|
122
|
+
type ObservabilityDims,
|
|
123
|
+
} from "./observability-observer.js";
|
|
124
|
+
|
|
125
|
+
// PipelineBus
|
|
126
|
+
export { PipelineBusImpl, Route, type PipelineBus, type PipelineBusConfig, type PacketHandler } from "./pipeline-bus.js";
|
|
127
|
+
|
|
128
|
+
// Init chain
|
|
129
|
+
export { runInitChain, runFinalizeChain, type InitStep, InitializationError } from "./init-chain.js";
|
|
130
|
+
|
|
131
|
+
// Plugin contract
|
|
132
|
+
export {
|
|
133
|
+
type VoicePlugin,
|
|
134
|
+
type PluginConfig,
|
|
135
|
+
type EndpointingOwner,
|
|
136
|
+
type EndpointingCapability,
|
|
137
|
+
requireStringConfig,
|
|
138
|
+
optionalStringConfig,
|
|
139
|
+
} from "./plugin-contract.js";
|
|
140
|
+
|
|
141
|
+
// Error handler
|
|
142
|
+
export { categorizeSttError, categorizeTtsError, categorizeLlmError, isRecoverable, isFatalError } from "./error-handler.js";
|
|
143
|
+
|
|
144
|
+
// Retry helpers
|
|
145
|
+
export { DEFAULT_RETRY_CONFIG, VOICE_PROVIDER_RETRY_CONFIG, VOICE_PROVIDER_OUTAGE_RETRY_CONFIG, readRetryConfig, readProviderRetryConfig, retryDelayMs, waitForRetryDelay, type RetryConfig } from "./retry.js";
|
|
146
|
+
|
|
147
|
+
// Provider fallback/degradation
|
|
148
|
+
export { ProviderFallback, type FallbackProvider, type ProviderFallbackOptions } from "./provider-fallback.js";
|
|
149
|
+
|
|
150
|
+
// Runtime scheduler seam
|
|
151
|
+
export { TimerScheduler, type Scheduler, type ScheduledCallback } from "./scheduler.js";
|
|
152
|
+
|
|
153
|
+
// Idle timeout
|
|
154
|
+
export { IdleTimeoutManager, type IdleTimeoutConfig, DEFAULT_IDLE_TIMEOUT_CONFIG } from "./idle-timeout.js";
|
|
155
|
+
|
|
156
|
+
// Mode switcher
|
|
157
|
+
export { ModeSwitcher, type ModeSwitchHandlers } from "./mode-switcher.js";
|
|
158
|
+
|
|
159
|
+
// Conversation events
|
|
160
|
+
export { type ConversationEvent, createConversationEventStream } from "./conversation-event.js";
|
|
161
|
+
|
|
162
|
+
// Websocket audio envelope
|
|
163
|
+
export {
|
|
164
|
+
SYRINX_AUDIO_ENVELOPE_NAME,
|
|
165
|
+
SYRINX_AUDIO_ENVELOPE_MAGIC,
|
|
166
|
+
assertAudioFormat,
|
|
167
|
+
assertAudioPayload,
|
|
168
|
+
encodeSyrinxAudioEnvelope,
|
|
169
|
+
decodeSyrinxAudioEnvelope,
|
|
170
|
+
hasSyrinxAudioEnvelope,
|
|
171
|
+
type SyrinxAudioEnvelope,
|
|
172
|
+
type SyrinxAudioEnvelopeHeader,
|
|
173
|
+
} from "./audio-envelope.js";
|
|
174
|
+
|
|
175
|
+
// VoiceAgentSession
|
|
176
|
+
export { VoiceAgentSession, type VoiceAgentSessionConfig, type VoiceAgentSessionEvents } from "./voice-agent-session.js";
|
|
177
|
+
|
|
178
|
+
// Primary-speaker barge-in gate (VE-02)
|
|
179
|
+
export {
|
|
180
|
+
PrimarySpeakerGate,
|
|
181
|
+
extractSpeakerFingerprint,
|
|
182
|
+
fingerprintSimilarity,
|
|
183
|
+
type SpeakerFingerprint,
|
|
184
|
+
type PrimarySpeakerGateConfig,
|
|
185
|
+
} from "./primary-speaker-gate.js";
|
|
186
|
+
export {
|
|
187
|
+
synthesizeTonePcm16,
|
|
188
|
+
mixPcm16,
|
|
189
|
+
PRIMARY_SPEAKER_TONE_HZ,
|
|
190
|
+
BYSTANDER_SPEAKER_TONE_HZ,
|
|
191
|
+
ASSISTANT_ECHO_TONE_HZ,
|
|
192
|
+
} from "./primary-speaker-fixtures.js";
|
|
193
|
+
|
|
194
|
+
// Latency-hiding filler track (VE-03)
|
|
195
|
+
export {
|
|
196
|
+
LatencyFillerController,
|
|
197
|
+
selectLatencyFillerConnective,
|
|
198
|
+
stripRedundantFillerPrefix,
|
|
199
|
+
LATENCY_FILLER_CONNECTIVES,
|
|
200
|
+
type LatencyFillerConfig,
|
|
201
|
+
type LatencyFillerState,
|
|
202
|
+
type LatencyFillerConnective,
|
|
203
|
+
} from "./latency-filler.js";
|
|
204
|
+
export {
|
|
205
|
+
LATENCY_FILLER_FIXTURES,
|
|
206
|
+
type LatencyFillerFixture,
|
|
207
|
+
} from "./latency-filler-fixtures.js";
|
|
208
|
+
|
|
209
|
+
// Reasoner seam (RFC §4.2)
|
|
210
|
+
export {
|
|
211
|
+
type Reasoner,
|
|
212
|
+
type ReasonerTurn,
|
|
213
|
+
type ReasonerMessage,
|
|
214
|
+
type ReasoningPart,
|
|
215
|
+
} from "./reasoner.js";
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
//
|
|
3
|
+
// Syrinx Kernel v2 — Init Chain
|
|
4
|
+
//
|
|
5
|
+
// Serial initialization of pipeline components with ordered teardown on failure.
|
|
6
|
+
// Each step runs synchronously in order. On failure, already-initialized steps
|
|
7
|
+
// are torn down in reverse order.
|
|
8
|
+
|
|
9
|
+
import type { PipelineBus } from "./pipeline-bus.js";
|
|
10
|
+
import { Route } from "./pipeline-bus.js";
|
|
11
|
+
import type {
|
|
12
|
+
InitStage,
|
|
13
|
+
InitStepCompletedPacket,
|
|
14
|
+
InitializationFailedPacket,
|
|
15
|
+
InitializationCompletedPacket,
|
|
16
|
+
} from "./packets.js";
|
|
17
|
+
import { ErrorCategory } from "./packets.js";
|
|
18
|
+
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Types
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
export interface InitStep {
|
|
24
|
+
/** Human-readable name for logging and error messages. */
|
|
25
|
+
readonly name: string;
|
|
26
|
+
/** Which initialization stage this step represents. */
|
|
27
|
+
readonly stage: InitStage;
|
|
28
|
+
/** Run the initialization. Throws on failure. */
|
|
29
|
+
run(): Promise<void>;
|
|
30
|
+
/** Clean up resources. Called during teardown (on failure) or finalize chain. */
|
|
31
|
+
cleanup?(): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// =============================================================================
|
|
35
|
+
// Error
|
|
36
|
+
// =============================================================================
|
|
37
|
+
|
|
38
|
+
export class InitializationError extends Error {
|
|
39
|
+
constructor(
|
|
40
|
+
public readonly stage: InitStage,
|
|
41
|
+
public readonly component: string,
|
|
42
|
+
cause: Error,
|
|
43
|
+
) {
|
|
44
|
+
super(`Initialization failed at ${stage}/${component}: ${cause.message}`);
|
|
45
|
+
this.name = "InitializationError";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// =============================================================================
|
|
50
|
+
// Core
|
|
51
|
+
// =============================================================================
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Run a serial init chain. Each step runs in order. On failure:
|
|
55
|
+
* 1. Emits InitializationFailedPacket through the bus.
|
|
56
|
+
* 2. Tears down already-initialized steps in reverse order.
|
|
57
|
+
* 3. Throws InitializationError.
|
|
58
|
+
*
|
|
59
|
+
* On success:
|
|
60
|
+
* 1. Emits InitStepCompletedPacket for each step (with initMs).
|
|
61
|
+
* 2. Emits InitializationCompletedPacket.
|
|
62
|
+
*/
|
|
63
|
+
export async function runInitChain(
|
|
64
|
+
bus: PipelineBus,
|
|
65
|
+
steps: readonly InitStep[],
|
|
66
|
+
): Promise<void> {
|
|
67
|
+
const initialized: InitStep[] = [];
|
|
68
|
+
|
|
69
|
+
for (const step of steps) {
|
|
70
|
+
const startMs = performance.now();
|
|
71
|
+
try {
|
|
72
|
+
await step.run();
|
|
73
|
+
const initMs = performance.now() - startMs;
|
|
74
|
+
|
|
75
|
+
const completed: InitStepCompletedPacket = {
|
|
76
|
+
kind: "init.step_completed",
|
|
77
|
+
contextId: "",
|
|
78
|
+
timestampMs: Date.now(),
|
|
79
|
+
stage: step.stage,
|
|
80
|
+
component: step.name,
|
|
81
|
+
initMs,
|
|
82
|
+
};
|
|
83
|
+
bus.push(Route.Main, completed);
|
|
84
|
+
initialized.push(step);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
87
|
+
|
|
88
|
+
const failed: InitializationFailedPacket = {
|
|
89
|
+
kind: "init.failed",
|
|
90
|
+
contextId: "",
|
|
91
|
+
timestampMs: Date.now(),
|
|
92
|
+
stage: step.stage,
|
|
93
|
+
component: step.name,
|
|
94
|
+
category: ErrorCategory.InternalFault,
|
|
95
|
+
cause: error,
|
|
96
|
+
isRecoverable: false,
|
|
97
|
+
};
|
|
98
|
+
bus.push(Route.Main, failed);
|
|
99
|
+
|
|
100
|
+
// Reverse teardown of already-initialized steps
|
|
101
|
+
for (const done of [...initialized].reverse()) {
|
|
102
|
+
try {
|
|
103
|
+
await done.cleanup?.();
|
|
104
|
+
} catch (cleanupErr) {
|
|
105
|
+
// Log but don't throw — cleanup failures shouldn't mask the init error
|
|
106
|
+
// In production, this goes to the bus as a warning
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
throw new InitializationError(step.stage, step.name, error);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const completed: InitializationCompletedPacket = {
|
|
115
|
+
kind: "init.completed",
|
|
116
|
+
contextId: "",
|
|
117
|
+
timestampMs: Date.now(),
|
|
118
|
+
};
|
|
119
|
+
bus.push(Route.Main, completed);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Run a reverse finalize chain. Steps are torn down in reverse order.
|
|
124
|
+
* Errors during teardown are logged but do not stop the chain.
|
|
125
|
+
* All steps get their cleanup called regardless of earlier failures.
|
|
126
|
+
*/
|
|
127
|
+
export async function runFinalizeChain(
|
|
128
|
+
steps: readonly InitStep[],
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
for (const step of [...steps].reverse()) {
|
|
131
|
+
try {
|
|
132
|
+
await step.cleanup?.();
|
|
133
|
+
} catch {
|
|
134
|
+
// Log but continue — finalize must complete
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
//
|
|
3
|
+
// Syrinx Kernel v2 — Init Stage Ordering
|
|
4
|
+
//
|
|
5
|
+
// Pure mapping from plugin name → init stage, and the deterministic order in
|
|
6
|
+
// which stages initialize (and, reversed, finalize). Extracted from
|
|
7
|
+
// VoiceAgentSession so the orchestrator owns chain assembly, not the ordering
|
|
8
|
+
// policy.
|
|
9
|
+
|
|
10
|
+
import { InitStage } from "./packets.js";
|
|
11
|
+
|
|
12
|
+
export function pluginStage(name: string): InitStage {
|
|
13
|
+
switch (name) {
|
|
14
|
+
case "stt":
|
|
15
|
+
case "deepgram":
|
|
16
|
+
return InitStage.STT;
|
|
17
|
+
case "tts":
|
|
18
|
+
case "cartesia":
|
|
19
|
+
case "elevenlabs":
|
|
20
|
+
return InitStage.TTS;
|
|
21
|
+
case "vad":
|
|
22
|
+
case "silero":
|
|
23
|
+
return InitStage.VAD;
|
|
24
|
+
case "eos":
|
|
25
|
+
case "pipecat":
|
|
26
|
+
return InitStage.EOS;
|
|
27
|
+
case "denoiser":
|
|
28
|
+
case "rnnoise":
|
|
29
|
+
return InitStage.Denoiser;
|
|
30
|
+
case "bridge":
|
|
31
|
+
case "aisdk":
|
|
32
|
+
return InitStage.Assistant;
|
|
33
|
+
case "recorder":
|
|
34
|
+
return InitStage.Recorder;
|
|
35
|
+
case "auth":
|
|
36
|
+
return InitStage.Auth;
|
|
37
|
+
default:
|
|
38
|
+
return InitStage.Assistant;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function stageOrder(stage: InitStage): number {
|
|
43
|
+
switch (stage) {
|
|
44
|
+
case InitStage.Assistant:
|
|
45
|
+
return 10;
|
|
46
|
+
case InitStage.Conversation:
|
|
47
|
+
return 20;
|
|
48
|
+
case InitStage.Recorder:
|
|
49
|
+
return 30;
|
|
50
|
+
case InitStage.Normalizer:
|
|
51
|
+
return 40;
|
|
52
|
+
case InitStage.Auth:
|
|
53
|
+
return 50;
|
|
54
|
+
case InitStage.STT:
|
|
55
|
+
return 60;
|
|
56
|
+
case InitStage.TTS:
|
|
57
|
+
return 70;
|
|
58
|
+
case InitStage.VAD:
|
|
59
|
+
return 80;
|
|
60
|
+
case InitStage.EOS:
|
|
61
|
+
return 90;
|
|
62
|
+
case InitStage.Denoiser:
|
|
63
|
+
return 100;
|
|
64
|
+
case InitStage.Behavior:
|
|
65
|
+
return 110;
|
|
66
|
+
case InitStage.Telemetry:
|
|
67
|
+
return 120;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function isAudioStage(stage: InitStage): boolean {
|
|
72
|
+
return (
|
|
73
|
+
stage === InitStage.STT ||
|
|
74
|
+
stage === InitStage.TTS ||
|
|
75
|
+
stage === InitStage.VAD ||
|
|
76
|
+
stage === InitStage.EOS ||
|
|
77
|
+
stage === InitStage.Denoiser
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
//
|
|
3
|
+
// Labeled utterances for latency-filler connective selection tests.
|
|
4
|
+
|
|
5
|
+
export interface LatencyFillerFixture {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly userText: string;
|
|
8
|
+
readonly expectedConnective: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const LATENCY_FILLER_FIXTURES: readonly LatencyFillerFixture[] = [
|
|
12
|
+
{ id: "question", userText: "Can I still add Biology 101?", expectedConnective: "Well," },
|
|
13
|
+
{ id: "thanks", userText: "Thanks for checking that.", expectedConnective: "Right," },
|
|
14
|
+
{ id: "statement-0", userText: "I need the late add form.", expectedConnective: "So," },
|
|
15
|
+
{ id: "statement-1", userText: "My hold is blocking registration.", expectedConnective: "Well," },
|
|
16
|
+
] as const;
|