UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

266 lines (265 loc) 10.9 kB
import { c as isRecord } from "./record-coerce-DItp3I4t.js"; import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { t as canonicalizeBase64 } from "./base64-Vw7DZYSc.js"; import "./string-coerce-runtime-GQa0ehRA.js"; import { j as normalizeRealtimeVoiceResponseOutcome } from "./agent-run-control-shared-zVaKVJuy.js"; import "./realtime-voice-provider-CoGArOTN.js"; import { v as readXaiRealtimeErrorDetail } from "./realtime-voice-config-BzEI0RLc.js"; import { n as XaiRealtimeVoiceProtocol } from "./realtime-voice-protocol-DoGkphR0.js"; //#region extensions/xai/realtime-voice-events.ts var XaiRealtimeMalformedAudioError = class extends Error {}; var XaiRealtimeVoiceEvents = class extends XaiRealtimeVoiceProtocol { constructor(..._args) { super(..._args); this.assistantTranscriptBuffer = ""; this.assistantTranscriptFinalized = false; this.finalizedToolCallItems = /* @__PURE__ */ new Set(); this.inputTranscriptReplacements = /* @__PURE__ */ new Map(); } handleEvent(event, connection) { if (event.type === "response.created" && this.acceptsEvent(connection)) { this.outputAudioGeneration += 1; this.responseActive = true; this.responseCreateInFlight = false; this.markQueue = []; this.assistantAudioItem = null; this.resetAssistantTranscript(); } const audioGeneration = this.outputAudioGeneration; const bridgeEvent = { direction: "server", type: event.type, detail: this.describeServerEvent(event), ...event.item_id ? { itemId: event.item_id } : {}, ...event.response_id ?? event.response?.id ? { responseId: event.response_id ?? event.response?.id } : {} }; const emitBridgeEvent = () => this.config.onEvent?.(bridgeEvent); if (event.type !== "response.done" || !this.acceptsEvent(connection)) emitBridgeEvent(); if (!this.acceptsEvent(connection)) return; switch (event.type) { case "session.created": return; case "conversation.created": { const conversationId = normalizeOptionalString(event.conversation?.id); if (conversationId) this.conversationId = conversationId; return; } case "conversation.item.created": case "conversation.item.added": { const item = event.item; const callId = normalizeOptionalString(item?.call_id); if (item?.type === "function_call_output" && callId) { this.pendingToolResultAcks.delete(callId); return; } if (event.type === "conversation.item.created") this.emitCompletedToolCall(item, event); return; } case "session.updated": this.onSessionUpdated(connection); return; case "response.output_audio.delta": { const audioDelta = event.delta ?? event.data; if (!audioDelta || this.responseCancelInFlight || audioGeneration !== this.outputAudioGeneration) return; const canonicalAudio = canonicalizeBase64(audioDelta); if (!canonicalAudio) throw new XaiRealtimeMalformedAudioError("xAI realtime voice stream returned malformed base64 audio data"); const audio = Buffer.from(canonicalAudio, "base64"); if (event.item_id && event.item_id !== this.assistantAudioItem?.itemId) this.assistantAudioItem = { itemId: event.item_id, bytes: audio.byteLength, startTimestamp: this.latestMediaTimestamp }; else if (this.assistantAudioItem) this.assistantAudioItem.bytes += audio.byteLength; this.responseActive = true; const markName = this.createPlaybackMark(); this.config.onAudio(audio, event.item_id ? { itemId: event.item_id } : void 0); if (audioGeneration === this.outputAudioGeneration && this.acceptsEvent(connection)) this.config.onMark?.(markName, () => { if (this.acceptsEvent(connection)) this.acknowledgeMark(markName); }); return; } case "input_audio_buffer.speech_started": this.handleServerVadBargeIn(); return; case "response.text.delta": case "response.output_text.delta": case "response.output_audio_transcript.delta": if (event.delta) this.appendAssistantTranscriptDelta(event.delta); return; case "response.text.done": case "response.output_text.done": case "response.output_audio_transcript.done": this.flushAssistantTranscript(event.transcript ?? event.text); return; case "conversation.item.input_audio_transcription.delta": if (event.delta) this.config.onTranscript?.("user", event.delta, false); return; case "conversation.item.input_audio_transcription.updated": if (event.transcript) this.inputTranscriptReplacements.set(this.inputTranscriptKey(event), event.transcript); return; case "conversation.item.input_audio_transcription.completed": { const key = this.inputTranscriptKey(event); const transcript = event.transcript ?? this.inputTranscriptReplacements.get(key); this.inputTranscriptReplacements.delete(key); if (transcript) this.config.onTranscript?.("user", transcript, true); return; } case "conversation.item.input_audio_transcription.failed": this.inputTranscriptReplacements.delete(this.inputTranscriptKey(event)); this.config.onError?.(new Error(readXaiRealtimeErrorDetail(event.error))); return; case "response.done": { const output = Array.isArray(event.response?.output) ? event.response.output.filter(isRecord) : []; const outcome = normalizeRealtimeVoiceResponseOutcome({ providerLabel: "xAI realtime voice", response: event.response, responseId: event.response_id }); if (outcome.status !== "completed") { this.markQueue = []; this.assistantAudioItem = null; } let callbackError; const invoke = (callback) => { try { callback(); } catch (error) { callbackError ??= error; } }; try { invoke(() => { if (outcome.status === "completed") { for (const [itemId, toolCall] of this.toolCallBuffers) { if (!this.acceptsEvent(connection)) return; this.emitToolCallOnce({ itemId, callId: toolCall.callId, name: toolCall.name, rawArgs: toolCall.args }); } for (const item of output) { if (!this.acceptsEvent(connection)) return; this.emitCompletedToolCall(item, event); } } if (!this.acceptsEvent(connection)) return; const terminalTranscript = output.filter((item) => item.type === "message" && item.role === "assistant").flatMap((item) => Array.isArray(item.content) ? item.content.filter(isRecord) : []).map((content) => typeof content.transcript === "string" ? content.transcript : typeof content.text === "string" ? content.text : "").join(""); this.flushAssistantTranscript(terminalTranscript); }); invoke(() => this.config.onResponseDone?.(outcome)); invoke(emitBridgeEvent); } finally { this.responseActive = false; this.responseCreateInFlight = false; this.responseCancelInFlight = false; this.toolCallBuffers.clear(); this.finalizedToolCallItems.clear(); this.flushPendingResponseCreate(); } if (callbackError) throw callbackError instanceof Error ? callbackError : new Error("xAI realtime response callback failed", { cause: callbackError }); return; } case "response.function_call_arguments.delta": { const key = event.item_id ?? "unknown"; const existing = this.toolCallBuffers.get(key); if (existing && event.delta) existing.args += event.delta; else if (event.item_id) this.toolCallBuffers.set(event.item_id, { name: event.name ?? "", callId: event.call_id ?? "", args: event.delta ?? "" }); return; } case "response.function_call_arguments.done": { const key = event.item_id ?? "unknown"; if (this.finalizedToolCallItems.has(key)) return; const buffered = this.toolCallBuffers.get(key); if (event.item_id) { this.finalizedToolCallItems.add(event.item_id); this.toolCallBuffers.set(event.item_id, { name: buffered?.name || event.name || "", callId: buffered?.callId || event.call_id || "", args: event.arguments ?? buffered?.args ?? "" }); } return; } case "response.output_item.done": this.bufferCompletedToolCall(event.item, event); return; case "error": this.handleErrorEvent(event.error); } } resetInputTranscripts() { this.inputTranscriptReplacements.clear(); this.finalizedToolCallItems.clear(); } emitCompletedToolCall(item, event) { if (item?.type === "function_call" && (!item.status || item.status === "completed")) this.emitToolCallOnce({ itemId: item.id ?? event.item_id, callId: item.call_id, name: item.name, rawArgs: item.arguments }); } bufferCompletedToolCall(item, event) { if (item?.type !== "function_call" || item.status && item.status !== "completed") return; const itemId = item.id ?? event.item_id; if (!itemId) return; this.toolCallBuffers.set(itemId, { name: item.name ?? "", callId: item.call_id ?? "", args: item.arguments ?? "" }); } appendAssistantTranscriptDelta(delta) { if (this.assistantTranscriptFinalized) { this.assistantTranscriptBuffer = ""; this.assistantTranscriptFinalized = false; } this.assistantTranscriptBuffer += delta; this.config.onTranscript?.("assistant", delta, false); } flushAssistantTranscript(finalTranscript) { if (this.assistantTranscriptFinalized) return; const transcript = finalTranscript || this.assistantTranscriptBuffer; if (transcript) { this.config.onTranscript?.("assistant", transcript, true); this.assistantTranscriptFinalized = true; } this.assistantTranscriptBuffer = ""; } resetAssistantTranscript() { this.assistantTranscriptBuffer = ""; this.assistantTranscriptFinalized = false; } inputTranscriptKey(event) { return event.item_id ?? event.response_id ?? "default"; } handleErrorEvent(error) { const detail = readXaiRealtimeErrorDetail(error); if (detail.startsWith("Conversation already has an active response in progress:")) { this.responseActive = true; this.responseCreateInFlight = false; this.responseCreatePending = true; return; } if (detail === "Cancellation failed: no active response found") { this.responseActive = false; this.responseCancelInFlight = false; this.flushPendingResponseCreate(); return; } this.config.onError?.(new Error(detail)); } describeServerEvent(event) { if (event.type === "error" || event.type === "conversation.item.input_audio_transcription.failed") return readXaiRealtimeErrorDetail(event.error); if (event.type !== "response.done") return; const status = event.response?.status; const details = event.response?.status_details === void 0 ? void 0 : JSON.stringify(event.response.status_details); return [status ? `status=${status}` : void 0, details].filter(Boolean).join(" ") || void 0; } }; //#endregion export { XaiRealtimeVoiceEvents as n, XaiRealtimeMalformedAudioError as t };