// SPDX-License-Identifier: MIT // // Syrinx Kernel v2 — Packet Factories // // Typed constructors for the packets the session orchestrator pushes onto the // bus. Each returns a fully-typed packet, so the shape is checked at the factory // definition and call sites need no `as` assertion — illegal packet shapes are // unrepresentable at construction instead of asserted-valid by a cast (CR-05). import type { WordTiming } from "./interaction-policy.js"; import { ErrorCategory } from "./packets.js"; import type { UsageRecordedPacket, ConversationMetricPacket, AcousticSignalPacket, AcousticSignal, TurnLocalizationPacket, TurnBoundaryKind, TurnBoundaryEventPacket, DtmfDigit, DtmfReceivedPacket, DtmfSendPacket, CallTransferPacket, CallTransferMode, RecordUserAudioPacket, RecordAssistantAudioDataPacket, RecordAssistantAudioTruncatePacket, VadAudioPacket, SpeechToTextAudioPacket, EndOfSpeechAudioPacket, EndOfSpeechPacket, TurnEndOwner, TurnEndReason, SttPartialPacket, SttResultPacket, FinalizeSttPacket, UserInputPacket, TextToSpeechTextPacket, TextToSpeechDonePacket, TtsErrorPacket, InterruptionDetectedPacket, InterruptionSource, InterruptTtsPacket, InterruptLlmPacket, InterruptSttPacket, InjectMessagePacket, SttReconfigurePacket, LlmDeltaPacket, LlmResponseDonePacket, ReasoningSuspendedPacket, ReasoningResumePacket, StartIdleTimeoutPacket, StopIdleTimeoutPacket, ModeSwitchRequestedPacket, InteractionBackchannelPacket, InteractionDuckPacket, InteractionResumePacket, } from "./packets.js"; import type { SttReconfigurePartial } from "./plugin-contract.js"; const DTMF_DIGITS = new Set(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "#"]); export function parseDtmfDigit(raw: string): DtmfDigit | null { const trimmed = raw.trim(); if (trimmed.length !== 1 || !DTMF_DIGITS.has(trimmed as DtmfDigit)) return null; return trimmed as DtmfDigit; } export function dtmfReceived( contextId: string, timestampMs: number, digit: DtmfDigit, provider: DtmfReceivedPacket["provider"], rawDigit: string, ): DtmfReceivedPacket { return { kind: "dtmf.received", contextId, timestampMs, digit, provider, rawDigit }; } const DTMF_SEND_CHARS = /^[0-9*#wW]+$/; /** Validate + build a `dtmf.send` packet. Throws on empty/invalid digit strings. */ export function dtmfSend(contextId: string, timestampMs: number, digits: string): DtmfSendPacket { if (!digits || !DTMF_SEND_CHARS.test(digits)) { throw new Error(`Invalid dtmf.send digits: ${JSON.stringify(digits)} (expected [0-9*#wW]+)`); } return { kind: "dtmf.send", contextId, timestampMs, digits }; } /** Build a `call.transfer` packet. */ export function callTransfer( contextId: string, timestampMs: number, mode: CallTransferMode, target: string, summary?: string, ): CallTransferPacket { const trimmed = target.trim(); if (!trimmed) throw new Error("call.transfer target must be a non-empty E.164 / SIP URI"); return { kind: "call.transfer", contextId, timestampMs, mode, target: trimmed, ...(summary !== undefined ? { summary } : {}), }; } export function metric( contextId: string, name: string, value: string, timestampMs: number = Date.now(), ): ConversationMetricPacket { return { kind: "metric.conversation", contextId, timestampMs, name, value }; } export function acousticSignal( contextId: string, timestampMs: number, signal: AcousticSignal, payload?: Readonly>, ): AcousticSignalPacket { return { kind: "acoustic.signal", contextId, timestampMs, signal, ...(payload ? { payload } : {}), }; } export function turnLocalization( contextId: string, timestampMs: number, value: TurnLocalizationPacket["value"], infrastructureBreached: boolean, conversationFlagged: boolean, ): TurnLocalizationPacket { return { kind: "turn.localization", contextId, timestampMs, value, infrastructureBreached, conversationFlagged, }; } export function usageRecorded( contextId: string, fields: Omit, timestampMs: number = Date.now(), ): UsageRecordedPacket { return { kind: "usage.recorded", contextId, timestampMs, ...fields }; } export function turnBoundary( contextId: string, timestampMs: number, fields: { boundary: TurnBoundaryKind; sessionId: string; speechId: string; monotonicMs: number; provider?: string; model?: string; region?: string; cancelled?: boolean; requestId?: string; }, ): TurnBoundaryEventPacket { return { kind: "obs.turn_boundary", contextId, timestampMs, boundary: fields.boundary, sessionId: fields.sessionId, speechId: fields.speechId, monotonicMs: fields.monotonicMs, provider: fields.provider, model: fields.model, region: fields.region, cancelled: fields.cancelled, requestId: fields.requestId, }; } export function recordUserAudio( contextId: string, timestampMs: number, audio: Uint8Array, ): RecordUserAudioPacket { return { kind: "record.user_audio", contextId, timestampMs, audio }; } export function vadAudio(contextId: string, timestampMs: number, audio: Uint8Array): VadAudioPacket { return { kind: "vad.audio", contextId, timestampMs, audio }; } export function sttAudio(contextId: string, timestampMs: number, audio: Uint8Array): SpeechToTextAudioPacket { return { kind: "stt.audio", contextId, timestampMs, audio }; } export function sttPartial( contextId: string, timestampMs: number, text: string, wordTimings?: readonly WordTiming[], ): SttPartialPacket { return wordTimings ? { kind: "stt.partial", contextId, timestampMs, text, wordTimings } : { kind: "stt.partial", contextId, timestampMs, text }; } export function eosAudio(contextId: string, timestampMs: number, audio: Uint8Array): EndOfSpeechAudioPacket { return { kind: "eos.audio", contextId, timestampMs, audio }; } export function eosTurnComplete( contextId: string, timestampMs: number, text: string, transcripts: readonly SttResultPacket[], endpointing?: { readonly owner: TurnEndOwner; readonly reason: TurnEndReason }, ): EndOfSpeechPacket { return endpointing === undefined ? { kind: "eos.turn_complete", contextId, timestampMs, text, transcripts } : { kind: "eos.turn_complete", contextId, timestampMs, text, transcripts, endpointingOwner: endpointing.owner, endpointingReason: endpointing.reason, }; } export function finalizeStt(contextId: string, timestampMs: number): FinalizeSttPacket { return { kind: "stt.finalize", contextId, timestampMs }; } export function userInput( contextId: string, timestampMs: number, text: string, language: string, ): UserInputPacket { return { kind: "user.input", contextId, timestampMs, text, language }; } export function ttsText(contextId: string, timestampMs: number, text: string): TextToSpeechTextPacket { return { kind: "tts.text", contextId, timestampMs, text }; } export function ttsDone(contextId: string, timestampMs: number, text: string): TextToSpeechDonePacket { return { kind: "tts.done", contextId, timestampMs, text }; } export function recordAssistantAudio( contextId: string, timestampMs: number, audio: Uint8Array, sampleRateHz: number, ): RecordAssistantAudioDataPacket { return { kind: "record.assistant_audio", contextId, timestampMs, audio, sampleRateHz, truncate: false }; } export function recordAssistantTruncate( contextId: string, timestampMs: number, ): RecordAssistantAudioTruncatePacket { return { kind: "record.assistant_audio", contextId, timestampMs, audio: new Uint8Array(0), truncate: true }; } export function interruptDetected( contextId: string, timestampMs: number, source: InterruptionSource, ): InterruptionDetectedPacket { return { kind: "interrupt.detected", contextId, timestampMs, source }; } export function interruptTts(contextId: string, timestampMs: number): InterruptTtsPacket { return { kind: "interrupt.tts", contextId, timestampMs }; } export function interruptLlm(contextId: string, timestampMs: number): InterruptLlmPacket { return { kind: "interrupt.llm", contextId, timestampMs }; } export function interruptStt(contextId: string, timestampMs: number): InterruptSttPacket { return { kind: "interrupt.stt", contextId, timestampMs }; } export function ttsError( contextId: string, timestampMs: number, cause: Error, category: ErrorCategory, isRecoverable: boolean, ): TtsErrorPacket { return { kind: "tts.error", contextId, timestampMs, component: "tts", category, cause, isRecoverable }; } export function injectMessage( contextId: string, timestampMs: number, text: string, mode?: InjectMessagePacket["mode"], ): InjectMessagePacket { return { kind: "inject.message", contextId, timestampMs, text, ...(mode ? { mode } : {}) }; } export function sttReconfigure( contextId: string, timestampMs: number, partial: SttReconfigurePartial, ): SttReconfigurePacket { return { kind: "stt.reconfigure", contextId, timestampMs, partial }; } export function llmDelta(contextId: string, timestampMs: number, text: string): LlmDeltaPacket { return { kind: "llm.delta", contextId, timestampMs, text }; } export function llmDone(contextId: string, timestampMs: number, text: string): LlmResponseDonePacket { return { kind: "llm.done", contextId, timestampMs, text }; } export function reasoningSuspended( contextId: string, timestampMs: number, runId: string, payload: unknown, prompt?: string, ): ReasoningSuspendedPacket { return { kind: "reasoning.suspended", contextId, timestampMs, runId, prompt, payload }; } export function reasoningResume( contextId: string, timestampMs: number, runId: string, data: unknown, ): ReasoningResumePacket { return { kind: "reasoning.resume", contextId, timestampMs, runId, data }; } export function startIdleTimeout(contextId: string, timestampMs: number): StartIdleTimeoutPacket { return { kind: "behavior.idle_timeout_start", contextId, timestampMs }; } export function stopIdleTimeout( contextId: string, timestampMs: number, resetCount: boolean, ): StopIdleTimeoutPacket { return { kind: "behavior.idle_timeout_stop", contextId, timestampMs, resetCount }; } export function modeSwitchRequested( contextId: string, timestampMs: number, mode: "text" | "audio", ): ModeSwitchRequestedPacket { return { kind: "mode.switch_requested", contextId, timestampMs, mode }; } export function interactionBackchannel( contextId: string, timestampMs: number, cue: string, ): InteractionBackchannelPacket { return { kind: "interaction.backchannel", contextId, timestampMs, cue }; } export function interactionDuck( contextId: string, timestampMs: number, ): InteractionDuckPacket { return { kind: "interaction.duck", contextId, timestampMs }; } export function interactionResume( contextId: string, timestampMs: number, ): InteractionResumePacket { return { kind: "interaction.resume", contextId, timestampMs }; }