agents
Version:
A home for your AI agents
1,812 lines • 76.4 kB
JavaScript
import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, r as _assertClassBrand, t as _classPrivateFieldGet2 } from "../classPrivateFieldGet2-DZBYAB34.js";
import { t as _classPrivateMethodInitSpec } from "../classPrivateMethodInitSpec-qMjJ6sHQ.js";
import { VOICE_PROTOCOL_VERSION } from "./types.js";
import { logVoiceError, toVoiceError, voiceErrorMessage } from "./errors.js";
import { n as ServerDiagnostics } from "../diagnostics-C4jcz3VK.js";
import { n as iterateTextEvents, r as SentenceChunker, t as iterateText } from "../text-stream-CpdiKrJB.js";
import { addSFUTracks, createSFUSession, createSFUWebSocketAdapter, decodeVarint, downsample48kStereoTo16kMono, encodePayloadToProtobuf, encodeVarint, extractPayloadFromProtobuf, renegotiateSFUSession, sfuFetch, upsample16kMonoTo48kStereo } from "./sfu.js";
import { WorkersAIFluxSTT, WorkersAINova3STT, WorkersAITTS } from "./workers-ai.js";
/**
* True when an error is the platform's signal that a connection (or its
* Durable Object) was torn down while an operation was in flight. A client
* can drop at any moment — including mid-`start_call` while a `keepAlive()`
* alarm write is still pending — and the runtime surfaces that as a
* retryable "Network connection lost." rejection (or a Durable Object reset).
* These are expected races during shutdown, not bugs.
*/
function isConnectionTeardownError(err) {
if (typeof err !== "object" || err === null) return false;
const e = err;
const message = typeof e.message === "string" ? e.message : "";
return e.retryable === true || message.includes("Network connection lost") || message.includes("Durable Object reset") || message.includes("Durable Object is overloaded") || message.includes("cannot access storage");
}
/**
* Run a fire-and-forget task triggered by a WebSocket message so that it can
* never surface as an unhandled rejection. Voice lifecycle handlers (start
* call, end call, interrupt, transcript emission) do async work — storage
* writes for `keepAlive()`, user-defined hooks — but are dispatched without
* being awaited from the synchronous `onMessage` handler. If the connection
* is torn down before that work settles, the rejection would otherwise be
* unhandled. Expected teardown races are swallowed; anything else is logged.
*/
function runBackground(label, fn) {
Promise.resolve().then(fn).catch((error) => {
if (isConnectionTeardownError(error)) return;
logVoiceError({
component: "voice",
stage: "background_task",
message: `${label} failed`,
error: toVoiceError(error, `${label} failed`)
});
});
}
function sendVoiceJSON(connection, data, _logPrefix, _skipLog = false) {
const json = JSON.stringify(data);
connection.send(json);
}
var _audioBuffers = /* @__PURE__ */ new WeakMap();
var _transcriberSessions = /* @__PURE__ */ new WeakMap();
var _activePipeline = /* @__PURE__ */ new WeakMap();
/**
* Manages per-connection audio pipeline state for voice mixins.
* Owns the Maps for audio buffers, transcriber sessions, and abort controllers.
* Does not own pipeline orchestration — that stays in each mixin.
*/
var AudioConnectionManager = class {
constructor(_logPrefix) {
_classPrivateFieldInitSpec(this, _audioBuffers, /* @__PURE__ */ new Map());
_classPrivateFieldInitSpec(this, _transcriberSessions, /* @__PURE__ */ new Map());
_classPrivateFieldInitSpec(this, _activePipeline, /* @__PURE__ */ new Map());
}
initConnection(connectionId) {
if (!_classPrivateFieldGet2(_audioBuffers, this).has(connectionId)) _classPrivateFieldGet2(_audioBuffers, this).set(connectionId, []);
}
isInCall(connectionId) {
return _classPrivateFieldGet2(_audioBuffers, this).has(connectionId);
}
cleanup(connectionId) {
this.abortPipeline(connectionId);
_classPrivateFieldGet2(_audioBuffers, this).delete(connectionId);
this.closeTranscriberSession(connectionId);
}
bufferAudio(connectionId, chunk) {
const buffer = _classPrivateFieldGet2(_audioBuffers, this).get(connectionId);
if (!buffer) return;
buffer.push(chunk);
let totalBytes = 0;
for (const buf of buffer) totalBytes += buf.byteLength;
while (totalBytes > 96e4 && buffer.length > 1) totalBytes -= buffer.shift().byteLength;
const session = _classPrivateFieldGet2(_transcriberSessions, this).get(connectionId);
if (session) session.feed(chunk);
}
clearAudioBuffer(connectionId) {
if (_classPrivateFieldGet2(_audioBuffers, this).has(connectionId)) _classPrivateFieldGet2(_audioBuffers, this).set(connectionId, []);
}
hasTranscriberSession(connectionId) {
return _classPrivateFieldGet2(_transcriberSessions, this).has(connectionId);
}
startTranscriberSession(connectionId, transcriber, options) {
const hadSession = this.closeTranscriberSession(connectionId);
const session = transcriber.createSession(options);
_classPrivateFieldGet2(_transcriberSessions, this).set(connectionId, session);
const buffer = _classPrivateFieldGet2(_audioBuffers, this).get(connectionId);
if (!hadSession && buffer) for (const chunk of buffer) session.feed(chunk);
return session;
}
closeTranscriberSession(connectionId) {
const session = _classPrivateFieldGet2(_transcriberSessions, this).get(connectionId);
if (!session) return false;
session.close();
_classPrivateFieldGet2(_transcriberSessions, this).delete(connectionId);
return true;
}
/**
* Forward the agent's most recent spoken reply to the active transcriber
* session for conversational context carryover. No-op when there is no
* session or the provider does not implement `updateAgentContext`.
*/
updateAgentContext(connectionId, text) {
_classPrivateFieldGet2(_transcriberSessions, this).get(connectionId)?.updateAgentContext?.(text);
}
/**
* Abort any in-flight pipeline and create a new AbortController.
* Returns the new AbortSignal.
*/
createPipelineAbort(connectionId) {
this.abortPipeline(connectionId);
const controller = new AbortController();
_classPrivateFieldGet2(_activePipeline, this).set(connectionId, controller);
return controller.signal;
}
abortPipeline(connectionId) {
const controller = _classPrivateFieldGet2(_activePipeline, this).get(connectionId);
if (!controller) return false;
controller.abort();
_classPrivateFieldGet2(_activePipeline, this).delete(connectionId);
return true;
}
/**
* Clear a pipeline abort controller only if it still matches the
* given signal. Prevents a finished pipeline from deleting a
* successor pipeline's controller in a concurrent scenario.
*/
clearPipelineAbort(connectionId, signal) {
if (signal) {
const controller = _classPrivateFieldGet2(_activePipeline, this).get(connectionId);
if (controller && controller.signal === signal) _classPrivateFieldGet2(_activePipeline, this).delete(connectionId);
} else _classPrivateFieldGet2(_activePipeline, this).delete(connectionId);
}
};
//#endregion
//#region src/voice/voice-input.ts
/**
* Voice-to-text input mixin. Adds STT-only voice input to an Agent class.
*
* Subclasses must set a `transcriber` property (or override `createTranscriber`).
* No TTS provider is needed. Override `onTranscript` to handle each
* transcribed utterance.
*
* @param Base - The Agent class to extend (e.g. `Agent`).
* @param voiceInputOptions - Optional pipeline configuration.
*
* @example
* ```typescript
* import { Agent } from "../index";
* import { withVoiceInput, WorkersAINova3STT } from "agents/voice";
*
* const InputAgent = withVoiceInput(Agent);
*
* class MyAgent extends InputAgent<Env> {
* transcriber = new WorkersAINova3STT(this.env.AI);
*
* onTranscript(text, connection) {
* console.log("User said:", text);
* }
* }
* ```
*/
function withVoiceInput(Base, voiceInputOptions) {
const diagnostics = new ServerDiagnostics(voiceInputOptions?.diagnostics?.browserConsole === true);
var _cm = /* @__PURE__ */ new WeakMap();
var _keepAliveDispose = /* @__PURE__ */ new WeakMap();
var _startupTokens = /* @__PURE__ */ new WeakMap();
var _callTokens = /* @__PURE__ */ new WeakMap();
var _turnSequence = /* @__PURE__ */ new WeakMap();
var _inputTurns = /* @__PURE__ */ new WeakMap();
var _activeTurns = /* @__PURE__ */ new WeakMap();
var _VoiceInputMixin_brand = /* @__PURE__ */ new WeakSet();
class VoiceInputMixin extends Base {
constructor(...args) {
super(...args);
_classPrivateMethodInitSpec(this, _VoiceInputMixin_brand);
_classPrivateFieldInitSpec(this, _cm, new AudioConnectionManager("VoiceInput"));
_classPrivateFieldInitSpec(this, _keepAliveDispose, /* @__PURE__ */ new Map());
_classPrivateFieldInitSpec(this, _startupTokens, /* @__PURE__ */ new Map());
_classPrivateFieldInitSpec(this, _callTokens, /* @__PURE__ */ new Map());
_classPrivateFieldInitSpec(this, _turnSequence, 0);
_classPrivateFieldInitSpec(this, _inputTurns, /* @__PURE__ */ new Map());
_classPrivateFieldInitSpec(this, _activeTurns, /* @__PURE__ */ new Map());
const _onConnect = this.onConnect?.bind(this);
const _onClose = this.onClose?.bind(this);
const _onMessage = this.onMessage?.bind(this);
this.onConnect = (connection, ...rest) => {
sendVoiceJSON(connection, {
type: "welcome",
protocol_version: 1,
...diagnostics.browserConsole ? { diagnostics: { browser_console: true } } : {}
}, "VoiceInput");
_assertClassBrand(_VoiceInputMixin_brand, this, _diagnose).call(this, connection, "connection.opened");
sendVoiceJSON(connection, {
type: "status",
status: "idle"
}, "VoiceInput");
return _onConnect?.(connection, ...rest);
};
this.onClose = (connection, ...rest) => {
_assertClassBrand(_VoiceInputMixin_brand, this, _diagnose).call(this, connection, "connection.closed", { in_call: _classPrivateFieldGet2(_cm, this).isInCall(connection.id) });
_assertClassBrand(_VoiceInputMixin_brand, this, _abortInputTurn).call(this, connection, "connection_closed");
_assertClassBrand(_VoiceInputMixin_brand, this, _requestActiveTurnAbort).call(this, connection, "connection_closed");
_classPrivateFieldGet2(_startupTokens, this).delete(connection.id);
_classPrivateFieldGet2(_callTokens, this).delete(connection.id);
_assertClassBrand(_VoiceInputMixin_brand, this, _releaseKeepAlive).call(this, connection.id);
_classPrivateFieldGet2(_cm, this).cleanup(connection.id);
return _onClose?.(connection, ...rest);
};
this.onMessage = (connection, message) => {
if (message instanceof ArrayBuffer) {
_classPrivateFieldGet2(_cm, this).bufferAudio(connection.id, message);
return;
}
if (typeof message !== "string") return _onMessage?.(connection, message);
let parsed;
try {
parsed = JSON.parse(message);
} catch {
return _onMessage?.(connection, message);
}
if (_VOICE_MESSAGES._.has(parsed.type)) {
switch (parsed.type) {
case "hello": break;
case "start_call":
runBackground("start_call", () => _assertClassBrand(_VoiceInputMixin_brand, this, _handleStartCall).call(this, connection));
break;
case "end_call":
runBackground("end_call", () => _assertClassBrand(_VoiceInputMixin_brand, this, _handleEndCall).call(this, connection));
break;
case "start_of_speech":
case "end_of_speech": break;
case "interrupt":
runBackground("interrupt", () => _assertClassBrand(_VoiceInputMixin_brand, this, _handleInterrupt).call(this, connection));
break;
}
return;
}
return _onMessage?.(connection, message);
};
}
onTranscript(_text, _connection) {}
/**
* Override to create a transcriber dynamically per connection.
* Return null to fall back to the `transcriber` property.
*/
createTranscriber(_connection) {
return null;
}
beforeCallStart(_connection) {
return true;
}
onCallStart(_connection) {}
onCallEnd(_connection) {}
onInterrupt(_connection) {}
afterTranscribe(transcript, _connection) {
return transcript;
}
}
function _diagnose(connection, event, data) {
diagnostics.emit(connection, event, data);
}
async function _handleStartCall(connection) {
if (_classPrivateFieldGet2(_cm, this).isInCall(connection.id)) {
_assertClassBrand(_VoiceInputMixin_brand, this, _diagnose).call(this, connection, "call.start_ignored", { reason: "already_active" });
return;
}
_assertClassBrand(_VoiceInputMixin_brand, this, _diagnose).call(this, connection, "call.starting");
_assertClassBrand(_VoiceInputMixin_brand, this, _abortInputTurn).call(this, connection, "call_restarted");
const startupToken = Symbol(connection.id);
_classPrivateFieldGet2(_startupTokens, this).set(connection.id, startupToken);
_classPrivateFieldGet2(_callTokens, this).set(connection.id, startupToken);
_classPrivateFieldGet2(_cm, this).initConnection(connection.id);
let startingTranscriber = false;
try {
const allowed = await this.beforeCallStart(connection);
if (!_assertClassBrand(_VoiceInputMixin_brand, this, _isCurrentStartup).call(this, connection.id, startupToken)) return;
if (!allowed) {
await _assertClassBrand(_VoiceInputMixin_brand, this, _handleStartupFailure).call(this, connection, startupToken, void 0, "Voice call was rejected", null);
return;
}
const provider = this.createTranscriber(connection) ?? this.transcriber;
if (!provider) {
const message = "No transcriber configured. Set 'transcriber' on your VoiceInput subclass or override createTranscriber().";
logVoiceError({
component: "VoiceInput",
stage: "configuration",
message,
connectionId: connection.id,
error: /* @__PURE__ */ new Error(message)
});
await _assertClassBrand(_VoiceInputMixin_brand, this, _handleStartupFailure).call(this, connection, startupToken, void 0, message, null);
return;
}
const dispose = await this.keepAlive();
if (!_assertClassBrand(_VoiceInputMixin_brand, this, _isCurrentStartup).call(this, connection.id, startupToken)) {
dispose();
return;
}
_classPrivateFieldGet2(_keepAliveDispose, this).set(connection.id, dispose);
startingTranscriber = true;
_assertClassBrand(_VoiceInputMixin_brand, this, _diagnose).call(this, connection, "stt.starting");
await _classPrivateFieldGet2(_cm, this).startTranscriberSession(connection.id, provider, {
onInterim: (text) => {
if (_classPrivateFieldGet2(_callTokens, this).get(connection.id) !== startupToken) return;
_assertClassBrand(_VoiceInputMixin_brand, this, _getOrCreateInputTurn).call(this, connection).firstInterim(text.length);
sendVoiceJSON(connection, {
type: "transcript_interim",
text
}, "VoiceInput");
},
onSpeechStart: () => {
if (_classPrivateFieldGet2(_callTokens, this).get(connection.id) !== startupToken) return;
_assertClassBrand(_VoiceInputMixin_brand, this, _replaceInputTurn).call(this, connection).speechStarted();
},
onUtterance: (transcript) => {
if (_classPrivateFieldGet2(_callTokens, this).get(connection.id) !== startupToken) return;
const turn = _assertClassBrand(_VoiceInputMixin_brand, this, _takeInputTurn).call(this, connection);
turn.finalInput(transcript.length);
runBackground("emitTranscript", () => _assertClassBrand(_VoiceInputMixin_brand, this, _emitTranscript).call(this, connection, transcript, turn));
},
onFatalError: (error) => {
runBackground("transcriber_fatal", () => _assertClassBrand(_VoiceInputMixin_brand, this, _handleTranscriberFatal).call(this, connection, startupToken, error));
}
}).waitUntilReady?.();
startingTranscriber = false;
} catch (error) {
const clientMessage = startingTranscriber ? "Speech recognition failed to start" : "Voice input failed to start";
await _assertClassBrand(_VoiceInputMixin_brand, this, _handleStartupFailure).call(this, connection, startupToken, toVoiceError(error, clientMessage), clientMessage, startingTranscriber ? "transcriber_startup" : "call_startup", startingTranscriber ? {
code: "stt_startup_failed",
stage: "stt",
retryable: false
} : void 0);
return;
}
if (!_assertClassBrand(_VoiceInputMixin_brand, this, _isCurrentStartup).call(this, connection.id, startupToken)) return;
_classPrivateFieldGet2(_startupTokens, this).delete(connection.id);
_assertClassBrand(_VoiceInputMixin_brand, this, _diagnose).call(this, connection, "stt.ready");
sendVoiceJSON(connection, {
type: "status",
status: "listening"
}, "VoiceInput");
_assertClassBrand(_VoiceInputMixin_brand, this, _diagnose).call(this, connection, "call.ready");
await this.onCallStart(connection);
}
function _isCurrentStartup(connectionId, startupToken) {
return _classPrivateFieldGet2(_startupTokens, this).get(connectionId) === startupToken && _classPrivateFieldGet2(_cm, this).isInCall(connectionId);
}
async function _handleStartupFailure(connection, startupToken, error, clientMessage, logStage = "call_startup", structuredError) {
if (!_assertClassBrand(_VoiceInputMixin_brand, this, _isCurrentStartup).call(this, connection.id, startupToken)) return;
if (logStage && error !== void 0) logVoiceError({
component: "VoiceInput",
stage: logStage,
message: clientMessage,
connectionId: connection.id,
error
});
_classPrivateFieldGet2(_startupTokens, this).delete(connection.id);
if (_classPrivateFieldGet2(_callTokens, this).get(connection.id) === startupToken) _classPrivateFieldGet2(_callTokens, this).delete(connection.id);
_assertClassBrand(_VoiceInputMixin_brand, this, _diagnose).call(this, connection, "call.start_failed", {
stage: logStage ?? "authorization",
retryable: structuredError?.retryable ?? false,
...error === void 0 ? {} : { error }
});
sendVoiceJSON(connection, {
type: "error",
message: clientMessage,
...structuredError
}, "VoiceInput");
_classPrivateFieldGet2(_cm, this).cleanup(connection.id);
_assertClassBrand(_VoiceInputMixin_brand, this, _releaseKeepAlive).call(this, connection.id);
_assertClassBrand(_VoiceInputMixin_brand, this, _diagnose).call(this, connection, "cleanup.completed");
_assertClassBrand(_VoiceInputMixin_brand, this, _diagnose).call(this, connection, "call.ended", { reason: "startup_failed" });
sendVoiceJSON(connection, {
type: "status",
status: "idle"
}, "VoiceInput");
await this.onCallEnd(connection);
}
async function _handleTranscriberFatal(connection, callToken, error) {
if (_classPrivateFieldGet2(_callTokens, this).get(connection.id) !== callToken || !_classPrivateFieldGet2(_cm, this).isInCall(connection.id)) return;
const isStarting = _classPrivateFieldGet2(_startupTokens, this).get(connection.id) === callToken;
const message = isStarting ? "Speech recognition failed to start" : "Speech recognition connection was lost";
logVoiceError({
component: "VoiceInput",
stage: isStarting ? "transcriber_startup" : "transcriber_runtime",
message,
connectionId: connection.id,
error
});
_classPrivateFieldGet2(_startupTokens, this).delete(connection.id);
_classPrivateFieldGet2(_callTokens, this).delete(connection.id);
_assertClassBrand(_VoiceInputMixin_brand, this, _abortInputTurn).call(this, connection, "stt_fatal");
_assertClassBrand(_VoiceInputMixin_brand, this, _requestActiveTurnAbort).call(this, connection, "stt_fatal");
_assertClassBrand(_VoiceInputMixin_brand, this, _diagnose).call(this, connection, "stt.fatal", {
stage: isStarting ? "startup" : "runtime",
retryable: !isStarting,
error
});
sendVoiceJSON(connection, {
type: "error",
message,
code: isStarting ? "stt_startup_failed" : "stt_connection_lost",
stage: "stt",
retryable: !isStarting
}, "VoiceInput");
_classPrivateFieldGet2(_cm, this).cleanup(connection.id);
_assertClassBrand(_VoiceInputMixin_brand, this, _releaseKeepAlive).call(this, connection.id);
_assertClassBrand(_VoiceInputMixin_brand, this, _diagnose).call(this, connection, "cleanup.completed");
_assertClassBrand(_VoiceInputMixin_brand, this, _diagnose).call(this, connection, "call.ended", { reason: "stt_fatal" });
sendVoiceJSON(connection, {
type: "status",
status: "idle"
}, "VoiceInput");
await this.onCallEnd(connection);
}
function _createTurn(connection) {
var _this$turnSequence;
const turn = diagnostics.turn(connection, `turn_${_classPrivateFieldSet2(_turnSequence, this, (_this$turnSequence = _classPrivateFieldGet2(_turnSequence, this), ++_this$turnSequence)).toString(36)}`, "speech");
turn.emit("turn.started", { source: "speech" });
return turn;
}
function _replaceInputTurn(connection) {
_assertClassBrand(_VoiceInputMixin_brand, this, _abortInputTurn).call(this, connection, "replaced");
const turn = _assertClassBrand(_VoiceInputMixin_brand, this, _createTurn).call(this, connection);
_classPrivateFieldGet2(_inputTurns, this).set(connection.id, turn);
return turn;
}
function _getOrCreateInputTurn(connection) {
const current = _classPrivateFieldGet2(_inputTurns, this).get(connection.id);
if (current) return current;
const turn = _assertClassBrand(_VoiceInputMixin_brand, this, _createTurn).call(this, connection);
_classPrivateFieldGet2(_inputTurns, this).set(connection.id, turn);
return turn;
}
function _takeInputTurn(connection) {
const turn = _assertClassBrand(_VoiceInputMixin_brand, this, _getOrCreateInputTurn).call(this, connection);
_classPrivateFieldGet2(_inputTurns, this).delete(connection.id);
return turn;
}
function _abortInputTurn(connection, reason) {
const turn = _classPrivateFieldGet2(_inputTurns, this).get(connection.id);
if (!turn) return;
_classPrivateFieldGet2(_inputTurns, this).delete(connection.id);
turn.emit("turn.aborted", { reason });
turn.finish("aborted");
}
function _activateTurn(connection, turn) {
_assertClassBrand(_VoiceInputMixin_brand, this, _requestActiveTurnAbort).call(this, connection, "replaced");
const active = {
signal: _classPrivateFieldGet2(_cm, this).createPipelineAbort(connection.id),
turn
};
_classPrivateFieldGet2(_activeTurns, this).set(connection.id, active);
return active;
}
function _requestActiveTurnAbort(connection, reason) {
const active = _classPrivateFieldGet2(_activeTurns, this).get(connection.id);
if (!active) return;
active.turn.emit("turn.abort_requested", { reason });
}
function _clearActiveTurn(connectionId, active) {
if (_classPrivateFieldGet2(_activeTurns, this).get(connectionId) === active) _classPrivateFieldGet2(_activeTurns, this).delete(connectionId);
}
function _releaseKeepAlive(connectionId) {
const dispose = _classPrivateFieldGet2(_keepAliveDispose, this).get(connectionId);
if (dispose) {
dispose();
_classPrivateFieldGet2(_keepAliveDispose, this).delete(connectionId);
}
}
function _handleEndCall(connection) {
_assertClassBrand(_VoiceInputMixin_brand, this, _diagnose).call(this, connection, "call.ended", { reason: "requested" });
_classPrivateFieldGet2(_startupTokens, this).delete(connection.id);
_classPrivateFieldGet2(_callTokens, this).delete(connection.id);
_assertClassBrand(_VoiceInputMixin_brand, this, _abortInputTurn).call(this, connection, "call_ended");
_assertClassBrand(_VoiceInputMixin_brand, this, _requestActiveTurnAbort).call(this, connection, "call_ended");
_classPrivateFieldGet2(_cm, this).cleanup(connection.id);
_assertClassBrand(_VoiceInputMixin_brand, this, _releaseKeepAlive).call(this, connection.id);
_assertClassBrand(_VoiceInputMixin_brand, this, _diagnose).call(this, connection, "cleanup.completed");
sendVoiceJSON(connection, {
type: "status",
status: "idle"
}, "VoiceInput");
return this.onCallEnd(connection);
}
function _handleInterrupt(connection) {
_assertClassBrand(_VoiceInputMixin_brand, this, _abortInputTurn).call(this, connection, "client_interrupt");
_assertClassBrand(_VoiceInputMixin_brand, this, _requestActiveTurnAbort).call(this, connection, "client_interrupt");
const interrupted = _classPrivateFieldGet2(_cm, this).abortPipeline(connection.id);
_assertClassBrand(_VoiceInputMixin_brand, this, _diagnose).call(this, connection, "turn.interrupt_requested", { active_turn: interrupted });
_classPrivateFieldGet2(_cm, this).clearAudioBuffer(connection.id);
sendVoiceJSON(connection, {
type: "status",
status: "listening"
}, "VoiceInput");
return this.onInterrupt(connection);
}
async function _emitTranscript(connection, transcript, turn) {
const active = _assertClassBrand(_VoiceInputMixin_brand, this, _activateTurn).call(this, connection, turn);
const { signal } = active;
let outcome = "completed";
let restoreListening = true;
try {
const afterTranscribeStart = Date.now();
const userText = await this.afterTranscribe(transcript, connection);
turn.recordAfterTranscribe(Date.now() - afterTranscribeStart, userText ? "accepted" : "skipped", userText?.length ?? 0);
if (signal.aborted) return;
if (!userText) {
outcome = "skipped";
restoreListening = false;
return;
}
sendVoiceJSON(connection, {
type: "transcript_interim",
text: ""
}, "VoiceInput");
sendVoiceJSON(connection, {
type: "transcript",
role: "user",
text: userText
}, "VoiceInput");
await this.onTranscript(userText, connection);
} catch (error) {
if (signal.aborted) return;
const voiceError = toVoiceError(error, "Transcript processing failed");
outcome = "error";
turn.emit("turn.error", {
stage: "transcript",
error: voiceError
});
logVoiceError({
component: "VoiceInput",
stage: "transcript",
message: "Transcript processing failed",
connectionId: connection.id,
error: voiceError
});
sendVoiceJSON(connection, {
type: "error",
message: voiceErrorMessage(voiceError, "Transcript processing failed")
}, "VoiceInput");
} finally {
if (signal.aborted) {
outcome = "aborted";
turn.emit("turn.aborted");
}
turn.finish(outcome);
_classPrivateFieldGet2(_cm, this).clearPipelineAbort(connection.id, signal);
_assertClassBrand(_VoiceInputMixin_brand, this, _clearActiveTurn).call(this, connection.id, active);
if (restoreListening && _classPrivateFieldGet2(_cm, this).isInCall(connection.id)) sendVoiceJSON(connection, {
type: "status",
status: "listening"
}, "VoiceInput");
}
}
var _VOICE_MESSAGES = { _: /* @__PURE__ */ new Set([
"hello",
"start_call",
"end_call",
"start_of_speech",
"end_of_speech",
"interrupt"
]) };
return VoiceInputMixin;
}
//#endregion
//#region src/voice/index.ts
const DEFAULT_HISTORY_LIMIT = 20;
const DEFAULT_MAX_MESSAGE_COUNT = 1e3;
const DEFAULT_SAMPLE_RATE = 16e3;
var ModelStreamError = class extends Error {
constructor(streamError, partialOutput) {
super(streamError.message, { cause: streamError });
this.name = "ModelStreamError";
this.streamError = streamError;
this.partialOutput = partialOutput;
}
};
function completionOutcomeCode(finishReason, hasOutput) {
if (finishReason === "length") return "output_limit";
if (finishReason === "content-filter") return "content_filtered";
if (finishReason === "error") return "model_error";
return hasOutput ? null : "no_output";
}
function stableTurnOutcome(finishReason, hasOutput) {
return completionOutcomeCode(finishReason, hasOutput) ?? "completed";
}
function createCompletionOutcome(finishReason, partialOutput) {
const code = completionOutcomeCode(finishReason, partialOutput);
return code === null ? null : {
code,
stage: "llm",
...finishReason === void 0 ? {} : { finishReason },
partialOutput
};
}
/**
* Voice pipeline mixin. Adds the full voice pipeline to an Agent class.
*
* Subclasses must set a `transcriber` property (or override `createTranscriber`)
* and a `tts` provider property. The transcriber session is per-call — created
* at start_call and closed at end_call. The model handles turn detection.
*
* @param Base - The Agent class to extend (e.g. `Agent`).
* @param voiceOptions - Optional pipeline configuration.
*
* @example
* ```typescript
* import { Agent } from "../index";
* import { withVoice, WorkersAIFluxSTT, WorkersAITTS } from "agents/voice";
*
* const VoiceAgent = withVoice(Agent);
*
* class MyAgent extends VoiceAgent<Env> {
* transcriber = new WorkersAIFluxSTT(this.env.AI);
* tts = new WorkersAITTS(this.env.AI);
*
* async onTurn(transcript, context) {
* return "Hello! I heard you say: " + transcript;
* }
* }
* ```
*/
function withVoice(Base, voiceOptions) {
const opts = voiceOptions ?? {};
const diagnostics = new ServerDiagnostics(opts.diagnostics?.browserConsole === true);
function opt(key, fallback) {
return opts[key] ?? fallback;
}
var _cm = /* @__PURE__ */ new WeakMap();
var _keepAliveDispose = /* @__PURE__ */ new WeakMap();
var _startupTokens = /* @__PURE__ */ new WeakMap();
var _callTokens = /* @__PURE__ */ new WeakMap();
var _turnSequence = /* @__PURE__ */ new WeakMap();
var _inputTurns = /* @__PURE__ */ new WeakMap();
var _activeTurnDiagnostics = /* @__PURE__ */ new WeakMap();
var _schemaReady = /* @__PURE__ */ new WeakMap();
var _VoiceAgentMixin_brand = /* @__PURE__ */ new WeakSet();
class VoiceAgentMixin extends Base {
constructor(...args) {
super(...args);
_classPrivateMethodInitSpec(this, _VoiceAgentMixin_brand);
_classPrivateFieldInitSpec(this, _cm, new AudioConnectionManager("VoiceAgent"));
_classPrivateFieldInitSpec(this, _keepAliveDispose, /* @__PURE__ */ new Map());
_classPrivateFieldInitSpec(this, _startupTokens, /* @__PURE__ */ new Map());
_classPrivateFieldInitSpec(this, _callTokens, /* @__PURE__ */ new Map());
_classPrivateFieldInitSpec(this, _turnSequence, 0);
_classPrivateFieldInitSpec(this, _inputTurns, /* @__PURE__ */ new Map());
_classPrivateFieldInitSpec(this, _activeTurnDiagnostics, /* @__PURE__ */ new Map());
_classPrivateFieldInitSpec(this, _schemaReady, false);
const _onConnect = this.onConnect?.bind(this);
const _onClose = this.onClose?.bind(this);
const _onMessage = this.onMessage?.bind(this);
this.onConnect = (connection, ...rest) => {
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "welcome",
protocol_version: 1,
...diagnostics.browserConsole ? { diagnostics: { browser_console: true } } : {}
});
_assertClassBrand(_VoiceAgentMixin_brand, this, _diagnose).call(this, connection, "connection.opened");
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "status",
status: "idle"
});
return _onConnect?.(connection, ...rest);
};
this.onClose = (connection, ...rest) => {
_assertClassBrand(_VoiceAgentMixin_brand, this, _diagnose).call(this, connection, "connection.closed", { in_call: _classPrivateFieldGet2(_cm, this).isInCall(connection.id) });
_assertClassBrand(_VoiceAgentMixin_brand, this, _requestActiveTurnAbort).call(this, connection, "turn.abort_requested", "connection_closed");
_assertClassBrand(_VoiceAgentMixin_brand, this, _abortInputTurn).call(this, connection, "connection_closed");
_classPrivateFieldGet2(_activeTurnDiagnostics, this).delete(connection.id);
_classPrivateFieldGet2(_startupTokens, this).delete(connection.id);
_classPrivateFieldGet2(_callTokens, this).delete(connection.id);
_assertClassBrand(_VoiceAgentMixin_brand, this, _releaseKeepAlive).call(this, connection.id);
_classPrivateFieldGet2(_cm, this).cleanup(connection.id);
return _onClose?.(connection, ...rest);
};
this.onMessage = (connection, message) => {
if (message instanceof ArrayBuffer) {
_classPrivateFieldGet2(_cm, this).bufferAudio(connection.id, message);
return;
}
if (typeof message !== "string") return _onMessage?.(connection, message);
let parsed;
try {
parsed = JSON.parse(message);
} catch {
return _onMessage?.(connection, message);
}
if (_VOICE_MESSAGES._.has(parsed.type)) {
switch (parsed.type) {
case "hello": break;
case "start_call":
runBackground("start_call", () => _assertClassBrand(_VoiceAgentMixin_brand, this, _handleStartCall).call(this, connection, parsed.preferred_format));
break;
case "end_call":
runBackground("end_call", () => _assertClassBrand(_VoiceAgentMixin_brand, this, _handleEndCall).call(this, connection));
break;
case "start_of_speech":
case "end_of_speech": break;
case "interrupt":
runBackground("interrupt", () => _assertClassBrand(_VoiceAgentMixin_brand, this, _handleInterrupt).call(this, connection));
break;
case "text_message": {
const text = parsed.text;
if (typeof text === "string") runBackground("text_message", () => _assertClassBrand(_VoiceAgentMixin_brand, this, _handleTextMessage).call(this, connection, text));
break;
}
}
return;
}
return _onMessage?.(connection, message);
};
}
onTurn(_transcript, _context) {
throw new Error("VoiceAgent subclass must implement onTurn(). Return a string, AI SDK stream, AsyncIterable<string>, or ReadableStream.");
}
/**
* Override to create a transcriber dynamically per connection.
* Useful for runtime model switching (e.g. Flux vs Nova 3 dropdown).
* Return null to fall back to the `transcriber` property.
*/
createTranscriber(_connection) {
return null;
}
beforeCallStart(_connection) {
return true;
}
onCallStart(_connection) {}
onCallEnd(_connection) {}
onInterrupt(_connection) {}
afterTranscribe(transcript, _connection) {
return transcript;
}
beforeSynthesize(text, _connection) {
return text;
}
afterSynthesize(audio, _text, _connection) {
return audio;
}
saveMessage(role, text) {
_assertClassBrand(_VoiceAgentMixin_brand, this, _ensureSchema).call(this);
this.sql`
INSERT INTO cf_voice_messages (role, text, timestamp)
VALUES (${role}, ${text}, ${Date.now()})
`;
const maxMessages = opt("maxMessageCount", DEFAULT_MAX_MESSAGE_COUNT);
this.sql`
DELETE FROM cf_voice_messages
WHERE id NOT IN (
SELECT id FROM cf_voice_messages
ORDER BY id DESC LIMIT ${maxMessages}
)
`;
}
getConversationHistory(limit) {
_assertClassBrand(_VoiceAgentMixin_brand, this, _ensureSchema).call(this);
const historyLimit = limit ?? opt("historyLimit", DEFAULT_HISTORY_LIMIT);
return this.sql`
SELECT role, text FROM cf_voice_messages
ORDER BY id DESC LIMIT ${historyLimit}
`.reverse().map((row) => ({
role: row.role,
content: row.text
}));
}
forceEndCall(connection) {
if (!_classPrivateFieldGet2(_cm, this).isInCall(connection.id)) return;
_assertClassBrand(_VoiceAgentMixin_brand, this, _handleEndCall).call(this, connection);
}
async speak(connection, text) {
const signal = _classPrivateFieldGet2(_cm, this).createPipelineAbort(connection.id);
try {
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "transcript_start",
role: "assistant"
});
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "transcript_end",
text
});
const audio = await _assertClassBrand(_VoiceAgentMixin_brand, this, _synthesizeWithHooks).call(this, text, connection, signal);
if (audio && !signal.aborted) {
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "status",
status: "speaking"
});
_assertClassBrand(_VoiceAgentMixin_brand, this, _diagnose).call(this, connection, "audio.first_sent", { bytes: audio.byteLength });
connection.send(audio);
_assertClassBrand(_VoiceAgentMixin_brand, this, _diagnose).call(this, connection, "audio.completed", { bytes: audio.byteLength });
}
if (!signal.aborted) {
_classPrivateFieldGet2(_cm, this).updateAgentContext(connection.id, text);
this.saveMessage("assistant", text);
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "status",
status: "listening"
});
}
} finally {
_classPrivateFieldGet2(_cm, this).clearPipelineAbort(connection.id, signal);
}
}
async speakAll(text) {
this.saveMessage("assistant", text);
const connections = [...this.getConnections()];
if (connections.length === 0) return;
for (const connection of connections) {
const signal = _classPrivateFieldGet2(_cm, this).createPipelineAbort(connection.id);
try {
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "transcript_start",
role: "assistant"
});
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "transcript_end",
text
});
const audio = await _assertClassBrand(_VoiceAgentMixin_brand, this, _synthesizeWithHooks).call(this, text, connection, signal);
if (audio && !signal.aborted) {
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "status",
status: "speaking"
});
_assertClassBrand(_VoiceAgentMixin_brand, this, _diagnose).call(this, connection, "audio.first_sent", { bytes: audio.byteLength });
connection.send(audio);
_assertClassBrand(_VoiceAgentMixin_brand, this, _diagnose).call(this, connection, "audio.completed", { bytes: audio.byteLength });
}
if (!signal.aborted) {
_classPrivateFieldGet2(_cm, this).updateAgentContext(connection.id, text);
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "status",
status: "listening"
});
}
} finally {
_classPrivateFieldGet2(_cm, this).clearPipelineAbort(connection.id, signal);
}
}
}
}
function _ensureSchema() {
if (_classPrivateFieldGet2(_schemaReady, this)) return;
this.sql`
CREATE TABLE IF NOT EXISTS cf_voice_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
role TEXT NOT NULL,
text TEXT NOT NULL,
timestamp INTEGER NOT NULL
)
`;
_classPrivateFieldSet2(_schemaReady, this, true);
}
function _requireTTS() {
if (!this.tts) throw new Error("No TTS provider configured. Set 'tts' on your VoiceAgent subclass.");
return this.tts;
}
async function _synthesizeWithHooks(text, connection, signal, turn) {
const sentence = turn?.beginTtsSentence();
let sentenceOutcome = "completed";
try {
let textToSpeak;
try {
textToSpeak = await this.beforeSynthesize(text, connection);
} catch (error) {
const voiceError = toVoiceError(error, "TTS preparation failed");
_assertClassBrand(_VoiceAgentMixin_brand, this, _emitTurnDiagnostic).call(this, connection, turn, "tts.failed", {
stage: "before_synthesize",
error: voiceError
});
throw voiceError;
}
if (!textToSpeak) {
sentenceOutcome = "skipped";
_assertClassBrand(_VoiceAgentMixin_brand, this, _emitTurnDiagnostic).call(this, connection, turn, "tts.skipped", { reason: "before_synthesize" });
return null;
}
let tts;
try {
tts = _assertClassBrand(_VoiceAgentMixin_brand, this, _requireTTS).call(this);
} catch (error) {
const voiceError = toVoiceError(error, "TTS is not configured");
_assertClassBrand(_VoiceAgentMixin_brand, this, _emitTurnDiagnostic).call(this, connection, turn, "tts.failed", {
stage: "configuration",
error: voiceError
});
throw voiceError;
}
const startedAt = Date.now();
_assertClassBrand(_VoiceAgentMixin_brand, this, _emitTurnDiagnostic).call(this, connection, turn, "tts.started", { characters: textToSpeak.length });
sentence?.providerStarted();
try {
const rawAudio = await tts.synthesize(textToSpeak, signal);
const audio = await this.afterSynthesize(rawAudio, textToSpeak, connection);
_assertClassBrand(_VoiceAgentMixin_brand, this, _emitTurnDiagnostic).call(this, connection, turn, "tts.completed", {
duration_ms: Date.now() - startedAt,
outcome: audio ? "audio" : "no_audio",
bytes: audio?.byteLength ?? 0
});
return audio;
} catch (error) {
const voiceError = toVoiceError(error, "TTS failed");
_assertClassBrand(_VoiceAgentMixin_brand, this, _emitTurnDiagnostic).call(this, connection, turn, "tts.failed", {
duration_ms: Date.now() - startedAt,
error: voiceError
});
throw voiceError;
}
} catch (error) {
sentenceOutcome = "failed";
throw error;
} finally {
sentence?.settle(sentenceOutcome);
}
}
async function _handleStartCall(connection, _preferredFormat) {
if (_classPrivateFieldGet2(_cm, this).isInCall(connection.id)) {
_assertClassBrand(_VoiceAgentMixin_brand, this, _diagnose).call(this, connection, "call.start_ignored", { reason: "already_active" });
return;
}
_assertClassBrand(_VoiceAgentMixin_brand, this, _diagnose).call(this, connection, "call.starting");
_assertClassBrand(_VoiceAgentMixin_brand, this, _abortInputTurn).call(this, connection, "call_restarted");
const startupToken = Symbol(connection.id);
_classPrivateFieldGet2(_startupTokens, this).set(connection.id, startupToken);
_classPrivateFieldGet2(_callTokens, this).set(connection.id, startupToken);
_classPrivateFieldGet2(_cm, this).initConnection(connection.id);
let provider;
try {
const allowed = await this.beforeCallStart(connection);
if (!_assertClassBrand(_VoiceAgentMixin_brand, this, _isCurrentStartup).call(this, connection.id, startupToken)) return;
if (!allowed) {
await _assertClassBrand(_VoiceAgentMixin_brand, this, _handleStartupFailure).call(this, connection, startupToken, void 0, "Voice call was rejected", null);
return;
}
provider = this.createTranscriber(connection) ?? this.transcriber;
if (!provider) {
const message = "No transcriber configured. Set 'transcriber' on your VoiceAgent subclass or override createTranscriber().";
logVoiceError({
component: "VoiceAgent",
stage: "configuration",
message,
connectionId: connection.id,
error: /* @__PURE__ */ new Error(message)
});
await _assertClassBrand(_VoiceAgentMixin_brand, this, _handleStartupFailure).call(this, connection, startupToken, void 0, message, null);
return;
}
const dispose = await this.keepAlive();
if (!_assertClassBrand(_VoiceAgentMixin_brand, this, _isCurrentStartup).call(this, connection.id, startupToken)) {
dispose();
return;
}
_classPrivateFieldGet2(_keepAliveDispose, this).set(connection.id, dispose);
const configuredFormat = opt("audioFormat", "mp3");
const configuredSampleRate = opt("sampleRate", DEFAULT_SAMPLE_RATE);
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "audio_config",
format: configuredFormat,
sampleRate: configuredSampleRate
});
} catch (error) {
await _assertClassBrand(_VoiceAgentMixin_brand, this, _handleStartupFailure).call(this, connection, startupToken, toVoiceError(error, "Voice call failed to start"), "Voice call failed to start");
return;
}
if (!provider) return;
let session;
try {
_assertClassBrand(_VoiceAgentMixin_brand, this, _diagnose).call(this, connection, "stt.starting");
session = _classPrivateFieldGet2(_cm, this).startTranscriberSession(connection.id, provider, {
onInterim: (text) => {
if (_classPrivateFieldGet2(_callTokens, this).get(connection.id) !== startupToken) return;
_assertClassBrand(_VoiceAgentMixin_brand, this, _getOrCreateInputTurn).call(this, connection).firstInterim(text.length);
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "transcript_interim",
text
});
},
onSpeechStart: () => {
if (_classPrivateFieldGet2(_callTokens, this).get(connection.id) !== startupToken) return;
_assertClassBrand(_VoiceAgentMixin_brand, this, _replaceInputTurn).call(this, connection).speechStarted();
_assertClassBrand(_VoiceAgentMixin_brand, this, _handleBargeIn).call(this, connection);
},
onUtterance: (transcript) => {
if (_classPrivateFieldGet2(_callTokens, this).get(connection.id) !== startupToken) return;
const turn = _assertClassBrand(_VoiceAgentMixin_brand, this, _takeInputTurn).call(this, connection);
turn.finalInput(transcript.length);
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "transcript_interim",
text: ""
});
_assertClassBrand(_VoiceAgentMixin_brand, this, _runPipeline).call(this, connection, transcript, turn);
},
onFatalError: (error) => {
runBackground("transcriber_fatal", () => _assertClassBrand(_VoiceAgentMixin_brand, this, _handleTranscriberFatal).call(this, connection, startupToken, error));
}
});
await session.waitUntilReady?.();
} catch (error) {
await _assertClassBrand(_VoiceAgentMixin_brand, this, _handleTranscriberStartupFailure).call(this, connection, startupToken, toVoiceError(error, "Speech recognition failed to start"));
return;
}
if (!_assertClassBrand(_VoiceAgentMixin_brand, this, _isCurrentStartup).call(this, connection.id, startupToken)) return;
_classPrivateFieldGet2(_startupTokens, this).delete(connection.id);
_assertClassBrand(_VoiceAgentMixin_brand, this, _diagnose).call(this, connection, "stt.ready");
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "status",
status: "listening"
});
_assertClassBrand(_VoiceAgentMixin_brand, this, _diagnose).call(this, connection, "call.ready");
await this.onCallStart(connection);
}
function _isCurrentStartup(connectionId, startupToken) {
return _classPrivateFieldGet2(_startupTokens, this).get(connectionId) === startupToken && _classPrivateFieldGet2(_cm, this).isInCall(connectionId);
}
async function _handleTranscriberStartupFailure(connection, startupToken, error) {
await _assertClassBrand(_VoiceAgentMixin_brand, this, _handleStartupFailure).call(this, connection, startupToken, error, "Speech recognition failed to start", "transcriber_startup", {
code: "stt_startup_failed",
stage: "stt",
retryable: false
});
}
async function _handleStartupFailure(connection, startupToken, error, clientMessage, logStage = "call_startup", structuredError) {
if (!_assertClassBrand(_VoiceAgentMixin_brand, this, _isCurrentStartup).call(this, connection.id, startupToken)) return;
if (logStage && error !== void 0) logVoiceError({
component: "VoiceAgent",
stage: logStage,
message: clientMessage,
connectionId: connection.id,
error
});
_classPrivateFieldGet2(_startupTokens, this).delete(connection.id);
if (_classPrivateFieldGet2(_callTokens, this).get(connection.id) === startupToken) _classPrivateFieldGet2(_callTokens, this).delete(connection.id);
_assertClassBrand(_VoiceAgentMixin_brand, this, _diagnose).call(this, connection, "call.start_failed", {
stage: logStage ?? "authorization",
retryable: structuredError?.retryable ?? false,
...error === void 0 ? {} : { error }
});
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "error",
message: clientMessage,
...structuredError
});
_classPrivateFieldGet2(_cm, this).cleanup(connection.id);
_assertClassBrand(_VoiceAgentMixin_brand, this, _releaseKeepAlive).call(this, connection.id);
_assertClassBrand(_VoiceAgentMixin_brand, this, _diagnose).call(this, connection, "cleanup.completed");
_assertClassBrand(_VoiceAgentMixin_brand, this, _diagnose).call(this, connection, "call.ended", { reason: "startup_failed" });
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "status",
status: "idle"
});
await this.onCallEnd(connection);
}
async function _handleTranscriberFatal(connection, callToken, error) {
if (_classPrivateFieldGet2(_callTokens, this).get(connection.id) !== callToken || !_classPrivateFieldGet2(_cm, this).isInCall(connection.id)) return;
const isStarting = _classPrivateFieldGet2(_startupTokens, this).get(connection.id) === callToken;
const message = isStarting ? "Speech recognition failed to start" : "Speech recognition connection was lost";
logVoiceError({
component: "VoiceAgent",
stage: isStarting ? "transcriber_startup" : "transcriber_runtime",
message,
connectionId: connection.id,
error
});
_classPrivateFieldGet2(_startupTokens, this).delete(connection.id);
_classPrivateFieldGet2(_callTokens, this).delete(connection.id);
_assertClassBrand(_VoiceAgentMixin_brand, this, _abortInputTurn).call(this, connection, "stt_fatal");
_assertClassBrand(_VoiceAgentMixin_brand, this, _diagnose).call(this, connection, "stt.fatal", {
stage: isStarting ? "startup" : "runtime",
retryable: !isStarting,
error
});
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "error",
message,
code: isStarting ? "stt_startup_failed" : "stt_connection_lost",
stage: "stt",
retryable: !isStarting
});
_classPrivateFieldGet2(_cm, this).cleanup(connection.id);
_assertClassBrand(_VoiceAgentMixin_brand, this, _releaseKeepAlive).call(this, connection.id);
_assertClassBrand(_VoiceAgentMixin_brand, this, _diagnose).call(this, connection, "cleanup.completed");
_assertClassBrand(_VoiceAgentMixin_brand, this, _diagnose).call(this, connection, "call.ended", { reason: "stt_fatal" });
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "status",
status: "idle"
});
await this.onCallEnd(connection);
}
function _releaseKeepAlive(connectionId) {
const dispose = _classPrivateFieldGet2(_keepAliveDispose, this).get(connectionId);
if (dispose) {
dispose();
_classPrivateFieldGet2(_keepAliveDispose, this).delete(connectionId);
}
}
function _handleEndCall(connection) {
_assertClassBrand(_VoiceAgentMixin_brand, this, _diagnose).call(this, connection, "call.ended", { reason: "requested" });
_assertClassBrand(_VoiceAgentMixin_brand, this, _requestActiveTurnAbort).call(this, connection, "turn.abort_requested", "call_ended");
_classPrivateFieldGet2(_startupTokens, this).delete(connection.id);
_classPrivateFieldGet2(_callTokens, this).delete(connection.id);
_assertClassBrand(_VoiceAgentMixin_brand, this, _abortInputTurn).call(this, connection, "call_ended");
_classPrivateFieldGet2(_cm, this).cleanup(connection.id);
_assertClassBrand(_VoiceAgentMixin_brand, this, _releaseKeepAlive).call(this, connection.id);
_assertClassBrand(_VoiceAgentMixin_brand, this, _diagnose).call(this, connection, "cleanup.completed");
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "status",
status: "idle"
});
return this.onCallEnd(connection);
}
function _handleInterrupt(connection) {
_assertClassBrand(_VoiceAgentMixin_brand, this, _abortInputTurn).call(this, connection, "client_interrupt");
_assertClassBrand(_VoiceAgentMixin_brand, this, _requestActiveTurnAbort).call(this, connection, "turn.interrupt_requested", "client_interrupt");
_classPrivateFieldGet2(_cm, this).abortPipeline(connection.id);
_classPrivateFieldGet2(_cm, this).clearAudioBuffer(connection.id);
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "status",
status: "listening"
});
return this.onInterrupt(connection);
}
function _handleBargeIn(connection) {
_assertClassBrand(_VoiceAgentMixin_brand, this, _requestActiveTurnAbort).call(this, connection, "turn.abort_requested", "barge_in");
if (!_classPrivateFieldGet2(_cm, this).abortPipeline(connection.id)) return;
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, { type: "playback_interrupt" });
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "status",
status: "listening"
});
this.onInterrupt(connection);
}
function _createTurn(connection, source) {
var _this$turnSequence;
const turn = diagnostics.turn(connection, `turn_${_classPrivateFieldSet2(_turnSequence, this, (_this$turnSequence = _classPrivateFieldGet2(_turnSequence, this), ++_this$turnSequence)).toString(36)}`, source);
if (source === "text") turn.markTextInput();
turn.emit("turn.started", { source });
return turn;
}
function _replaceInputTurn(connection) {
_assertClassBrand(_VoiceAgentMixin_brand, this, _abortInputTurn).call(this, connection, "replaced");
const turn = _assertClassBrand(_VoiceAgentMixin_brand, this, _createTurn).call(this, connection, "speech");
_classPrivateFieldGet2(_inputTurns, this).set(connection.id, turn);
return turn;
}
function _getOrCreateInputTurn(connection) {
const current = _classPrivateFieldGet2(_inputTurns, this).get(connection.id);
if (current) return current;
const turn = _assertClassBrand(_VoiceAgentMixin_brand, this, _createTurn).call(this, connection, "speech");
_classPrivateFieldGet2(_inputTurns, this).set(connection.id, turn);
return turn;
}
function _takeInputTurn(connection) {
const turn = _assertClassBrand(_VoiceAgentMixin_brand, this, _getOrCreateInputTurn).call(this, connection);
_classPrivateFieldGet2(_inputTurns, this).delete(connection.id);
return turn;
}
function _abortInputTurn(connection, reason) {
const turn = _classPrivateFieldGet2(_inputTurns, this).get(connection.id);
if (!turn) return;
_classPrivateFieldGet2(_inputTurns, this).delete(connection.id);
turn.emit("turn.aborted", { reason });
turn.finish("aborted");
}
function _beginTurnDiagnostics(connection, source, turn = _assertClassBrand(_VoiceAgentMixin_brand, this, _createTurn).call(this, connection, source)) {
const previous = _classPrivateFieldGet2(_activeTurnDiagnostics, this).get(connection.id);
if (previous) {
previous.turn.emit("turn.abort_requested", { reason: "replaced" });
previous.model?.abort();
}
const active = {
signal: _classPrivateFieldGet2(_cm, this).createPipelineAbort(connection.id),
turn
};
_classPrivateFieldGet2(_activeTurnDiagnostics, this).set(connection.id, active);
return active;
}
function _requestActiveTurnAbort(connection, event, reason) {
const active = _classPrivateFieldGet2(_activeTurnDiagnostics, this).get(connection.id);
if (!active) return;
active.turn.emit(event, { reason });
active.model?.abort();
}
function _clearActiveTurn(connectionId, active) {
if (_classPrivateFieldGet2(_activeTurnDiagnostics, this).get(connectionId) === active) _classPrivateFieldGet2(_activeTurnDiagnostics, this).delete(connectionId);
}
function _emitTurnDiagnostic(connection, turn, event, data) {
if (turn) turn.emit(event, data);
else _assertClassBrand(_VoiceAgentMixin_brand, this, _diagnose).call(this, connection, event, data);
}
async function _handleTextMessage(connection, text) {
if (!text || text.trim().length === 0) return;
const userText = text.trim();
const pipelineStart = Date.now();
const active = _assertClassBrand(_VoiceAgentMixin_brand, this, _beginTurnDiagnostics).call(this, connection, "text");
const { signal, turn } = active;
let turnOutcome = "completed";
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "status",
status: "thinking"
});
const priorMessages = this.getConversationHistory();
this.saveMessage("user", userText);
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "transcript",
role: "user",
text: userText
});
try {
const context = {
connection,
messages: priorMessages,
signal
};
const model = turn.startModel();
active.model = model;
const turnResult = await this.onTurn(userText, context);
if (signal.aborted) return;
if (_classPrivateFieldGet2(_cm, this).isInCall(connection.id)) {
const { text: fullText, finishReason } = await _assertClassBrand(_VoiceAgentMixin_brand, this, _streamResponse).call(this, connection, turnResult, pipelineStart, signal, turn, model);
if (signal.aborted) return;
const hasOutput = fullText.trim().length > 0;
turnOutcome = stableTurnOutcome(finishReason, hasOutput);
if (turnOutcome === "completed" && turn.hasTtsFailures) turnOutcome = "tts_error";
if (hasOutput) {
_classPrivateFieldGet2(_cm, this).updateAgentContext(connection.id, fullText);
this.saveMessage("assistant", fullText);
}
const completionOutcome = createCompletionOutcome(finishReason, hasOutput);
if (completionOutcome) _assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "completion_outcome",
...completionOutcome
});
if (!hasOutput) _assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "error",
message: "No response generated"
});
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "status",
status: "listening"
});
} else {
let fullText = "";
let pendingText = "";
let transcriptStarted = false;
const sendAssistantDelta = (token) => {
if (!transcriptStarted) {
pendingText += token;
if (pendingText.trim().length === 0) return;
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "transcript_start",
role: "assistant"
});
transcriptStarted = true;
token = pendingText;
pendingText = "";
}
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "transcript_delta",
text: token
});
};
let finishReason;
for await (const event of iterateTextEvents(turnResult)) {
if (signal.aborted) break;
model.observe(event);
if (event.type === "finish") finishReason = event.finishReason;
else if (event.type === "error") {
if (transcriptStarted) _assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "transcript_end",
text: fullText
});
throw new ModelStreamError(event.error, fullText.trim().length > 0);
} else if (event.type === "text") {
fullText += event.text;
sendAssistantDelta(event.text);
}
}
const hasOutput = fullText.trim().length > 0;
model.complete(hasOutput ? "output" : "no_output", finishReason);
turnOutcome = stableTurnOutcome(finishReason, hasOutput);
if (hasOutput) {
if (transcriptStarted) _assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "transcript_end",
text: fullText
});
this.saveMessage("assistant", fullText);
}
const completionOutcome = createCompletionOutcome(finishReason, hasOutput);
if (completionOutcome) _assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "completion_outcome",
...completionOutcome
});
if (!hasOutput) _assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "error",
message: "No response generated"
});
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "status",
status: "idle"
});
}
} catch (error) {
if (signal.aborted) return;
turnOutcome = error instanceof ModelStreamError ? "model_error" : turn.hasTtsFailures ? "tts_error" : "error";
const pipelineError = error instanceof ModelStreamError ? error.streamError : toVoiceError(error, "Text turn failed");
active.model?.fail(pipelineError);
turn.emit("turn.error", {
stage: error instanceof ModelStreamError ? "model" : "pipeline",
error: pipelineError
});
if (error instanceof ModelStreamError) _assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "completion_outcome",
code: "model_error",
stage: "llm",
partialOutput: error.partialOutput
});
logVoiceError({
component: "VoiceAgent",
stage: "text_pipeline",
message: "Text pipeline failed",
connectionId: connection.id,
error: pipelineError
});
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "error",
message: voiceErrorMessage(pipelineError, "Text pipeline failed")
});
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "status",
status: _classPrivateFieldGet2(_cm, this).isInCall(connection.id) ? "listening" : "idle"
});
} finally {
if (signal.aborted) {
turnOutcome = "aborted";
active.model?.abort();
turn.emit("turn.aborted");
}
turn.finish(turnOutcome);
_classPrivateFieldGet2(_cm, this).clearPipelineAbort(connection.id, signal);
_assertClassBrand(_VoiceAgentMixin_brand, this, _clearActiveTurn).call(this, connection.id, active);
}
}
async function _runPipeline(connection, transcript, turn) {
const pipelineStart = Date.now();
const active = _assertClassBrand(_VoiceAgentMixin_brand, this, _beginTurnDiagnostics).call(this, connection, "speech", turn);
const { signal } = active;
let turnOutcome = "completed";
try {
const afterTranscribeStart = Date.now();
const userText = await this.afterTranscribe(transcript, connection);
turn.recordAfterTranscribe(Date.now() - afterTranscribeStart, userText ? "accepted" : "skipped", userText?.length ?? 0);
if (signal.aborted) return;
if (!userText) {
turnOutcome = "skipped";
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "status",
status: "listening"
});
return;
}
const priorMessages = this.getConversationHistory();
this.saveMessage("user", userText);
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "transcript",
role: "user",
text: userText
});
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "status",
status: "thinking"
});
const context = {
connection,
messages: priorMessages,
signal
};
const model = turn.startModel();
active.model = model;
const turnResult = await this.onTurn(userText, context);
if (signal.aborted) return;
const { text: fullText, llmMs, ttsMs, firstAudioMs, finishReason } = await _assertClassBrand(_VoiceAgentMixin_brand, this, _streamResponse).call(this, connection, turnResult, pipelineStart, signal, turn, model);
if (signal.aborted) return;
const hasOutput = fullText.trim().length > 0;
turnOutcome = stableTurnOutcome(finishReason, hasOutput);
if (turnOutcome === "completed" && turn.hasTtsFailures) turnOutcome = "tts_error";
if (!hasOutput) {
const completionOutcome = createCompletionOutcome(finishReason, false);
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "completion_outcome",
...completionOutcome
});
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "error",
message: "No response generated"
});
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "status",
status: "listening"
});
return;
}
const completionOutcome = createCompletionOutcome(finishReason, true);
if (completionOutcome) _assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "completion_outcome",
...completionOutcome
});
const totalMs = Date.now() - pipelineStart;
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "metrics",
llm_ms: llmMs,
tts_ms: ttsMs,
first_audio_ms: firstAudioMs,
total_ms: totalMs
});
_classPrivateFieldGet2(_cm, this).updateAgentContext(connection.id, fullText);
this.saveMessage("assistant", fullText);
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "status",
status: "listening"
});
} catch (error) {
if (signal.aborted) return;
turnOutcome = error instanceof ModelStreamError ? "model_error" : turn.hasTtsFailures ? "tts_error" : "error";
const pipelineError = error instanceof ModelStreamError ? error.streamError : toVoiceError(error, "Voice turn failed");
active.model?.fail(pipelineError);
turn.emit("turn.error", {
stage: error instanceof ModelStreamError ? "model" : "pipeline",
error: pipelineError
});
if (error instanceof ModelStreamError) _assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "completion_outcome",
code: "model_error",
stage: "llm",
partialOutput: error.partialOutput
});
logVoiceError({
component: "VoiceAgent",
stage: "pipeline",
message: "Voice pipeline failed",
connectionId: connection.id,
error: pipelineError
});
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "error",
message: voiceErrorMessage(pipelineError, "Voice pipeline failed")
});
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "status",
status: "listening"
});
} finally {
if (signal.aborted) {
turnOutcome = "aborted";
active.model?.abort();
turn.emit("turn.aborted");
}
turn.finish(turnOutcome);
_classPrivateFieldGet2(_cm, this).clearPipelineAbort(connection.id, signal);
_assertClassBrand(_VoiceAgentMixin_brand, this, _clearActiveTurn).call(this, connection.id, active);
}
}
async function _streamResponse(connection, response, pipelineStart, signal, turn, model) {
if (typeof response === "string") {
const llmMs = model.elapsedMs();
if (response.trim().length === 0) {
model.complete("no_output");
return {
text: response,
llmMs,
ttsMs: 0,
firstAudioMs: 0
};
}
model.observe({
type: "text",
text: response
});
model.complete("output");
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "transcript_start",
role: "assistant"
});
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "transcript_end",
text: response
});
const ttsStart = Date.now();
let audio;
try {
audio = await _assertClassBrand(_VoiceAgentMixin_brand, this, _synthesizeWithHooks).call(this, response, connection, void 0, turn);
} finally {
turn.finishTts();
}
const ttsMs = Date.now() - ttsStart;
let firstAudioMs = 0;
if (audio && !signal.aborted) {
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "status",
status: "speaking"
});
firstAudioMs = Date.now() - pipelineStart;
turn.emit("audio.first_sent", {
bytes: audio.byteLength,
elapsed_ms: firstAudioMs
});
turn.audioSent();
connection.send(audio);
turn.emit("audio.completed", { bytes: audio.byteLength });
}
return {
text: response,
llmMs,
ttsMs,
firstAudioMs
};
}
try {
return await _assertClassBrand(_VoiceAgentMixin_brand, this, _streamingTTSPipeline).call(this, connection, iterateTextEvents(response), pipelineStart, signal, turn, model);
} finally {
turn.finishTts();
}
}
async function _streamingTTSPipeline(connection, tokenStream, pipelineStart, signal, turn, model) {
const chunker = new SentenceChunker();
const ttsQueue = [];
let fullText = "";
let pendingTranscriptText = "";
let transcriptStarted = false;
let firstAudioSentAt = null;
let firstTtsStartedAt = null;
let cumulativeTtsMs = 0;
let totalAudioBytes = 0;
let skippedSentences = 0;
let ttsFailures = 0;
let finishReason;
let streamComplete = false;
let drainNotify = null;
let drainPending = false;
let drainedCount = 0;
const drainWaiters = /* @__PURE__ */ new Map();
const notifyDrain = () => {
if (drainNotify) {
const resolve = drainNotify;
drainNotify = null;
resolve();
} else drainPending = true;
};
const notifyDrained = () => {
for (const [target, waiters] of drainWaiters) {
if (drainedCount < target) continue;
drainWaiters.delete(target);
for (const resolve of waiters) resolve();
}
};
const waitForDrained = (target) => {
if (drainedCount >= target) return Promise.resolve();
return new Promise((resolve) => {
const waiters = drainWaiters.get(target) ?? [];
waiters.push(resolve);
drainWaiters.set(target, waiters);
});
};
const tts = _assertClassBrand(_VoiceAgentMixin_brand, this, _requireTTS).call(this);
const hasStreamingTTS = typeof tts.synthesizeStream === "function";
const drainPromise = (async () => {
let i = 0;
while (true) {
while (i >= ttsQueue.length) {
if (streamComplete && i >= ttsQueue.length) return;
if (drainPending) {
drainPending = false;
continue;
}
await new Promise((r) => {
drainNotify = r;
});
if (streamComplete && i >= ttsQueue.length) return;
}
if (signal.aborted) return;
try {
for await (const chunk of ttsQueue[i]) {
if (signal.aborted) return;
if (firstAudioSentAt === null) {
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "status",
status: "speaking"
});
firstAudioSentAt = Date.now();
turn.emit("audio.first_sent", {
bytes: chunk.byteLength,
elapsed_ms: firstAudioSentAt - pipelineStart
});
}
totalAudioBytes += chunk.byteLength;
turn.audioSent();
connection.send(chunk);
}
} catch (error) {
if (signal.aborted) return;
const voiceError = toVoiceError(error, "TTS sentence failed");
ttsFailures++;
turn.emit("tts.failed", { error: voiceError });
logVoiceError({
component: "VoiceAgent",
stage: "tts",
message: "TTS failed for a sentence",
connectionId: connection.id,
error: voiceError
});
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "error",
message: voiceErrorMessage(voiceError, "TTS failed for a sentence")
});
}
i++;
drainedCount = i;
notifyDrained();
}
})();
const makeSentenceTTS = (sentence) => {
const self = this;
async function* generate() {
const attempt = turn.beginTtsSentence();
let sentenceOutcome = "completed";
try {
const text = await self.beforeSynthesize(sentence, connection);
if (!text) {
sentenceOutcome = "skipped";
skippedSentences++;
return;
}
if (firstTtsStartedAt === null) {
firstTtsStartedAt = Date.now();
turn.emit("tts.started", {
mode: hasStreamingTTS ? "streaming" : "buffered",
characters: text.length
});
}
attempt.providerStarted();
if (hasStreamingTTS) for await (const chunk of tts.synthesizeStream(text, signal)) {
const processed = await self.afterSynthesize(chunk, text, connection);
if (processed) yield processed;
}
else {
const rawAudio = await tts.synthesize(text, signal);
const processed = await self.afterSynthesize(rawAudio, text, connection);
if (processed) yield processed;
}
} catch (error) {
sentenceOutcome = "failed";
throw error;
} finally {
cumulativeTtsMs += attempt.settle(sentenceOutcome);
}
}
return eagerAsyncIterable(generate());
};
const enqueueSentence = (sentence) => {
ttsQueue.push(makeSentenceTTS(sentence));
notifyDrain();
};
const sendAssistantDelta = (token) => {
if (!transcriptStarted) {
pendingTranscriptText += token;
if (pendingTranscriptText.trim().length === 0) return;
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "transcript_start",
role: "assistant"
});
transcriptStarted = true;
token = pendingTranscriptText;
pendingTranscriptText = "";
}
_assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "transcript_delta",
text: token
});
};
for await (const event of tokenStream) {
if (signal.aborted) break;
model.observe(event);
if (event.type === "boundary") {
for (const sentence of chunker.flush()) enqueueSentence(sentence);
await waitForDrained(ttsQueue.length);
continue;
}
if (event.type === "finish") {
finishReason = event.finishReason;
continue;
}
if (event.type === "error") {
for (const sentence of chunker.flush()) enqueueSentence(sentence);
await waitForDrained(ttsQueue.length);
if (transcriptStarted) _assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "transcript_end",
text: fullText
});
streamComplete = true;
notifyDrain();
await drainPromise;
throw new ModelStreamError(event.error, fullText.trim().length > 0);
}
if (event.type !== "text") continue;
const token = event.text;
fullText += token;
sendAssistantDelta(token);
const sentences = chunker.add(token);
for (const sentence of sentences) enqueueSentence(sentence);
}
const llmMs = model.elapsedMs();
model.complete(fullText.trim().length > 0 ? "output" : "no_output", finishReason);
const remaining = chunker.flush();
for (const sentence of remaining) enqueueSentence(sentence);
streamComplete = true;
notifyDrain();
if (transcriptStarted) _assertClassBrand(_VoiceAgentMixin_brand, this, _sendJSON).call(this, connection, {
type: "transcript_end",
text: fullText
});
await drainPromise;
if (firstTtsStartedAt === null) turn.emit("tts.skipped", {
reason: fullText.trim().length === 0 ? "no_output" : ttsFailures > 0 ? "preparation_failed" : "before_synthesize",
sentences: skippedSentences,
failures: ttsFailures
});
else turn.emit("tts.completed", {
duration_ms: Date.now() - firstTtsStartedAt,
outcome: totalAudioBytes > 0 ? ttsFailures > 0 ? "partial" : "audio" : ttsFailures > 0 ? "failed" : "no_audio",
bytes: totalAudioBytes,
failures: ttsFailures,
skipped_sentences: skippedSentences
});
if (totalAudioBytes > 0) turn.emit("audio.completed", { bytes: totalAudioBytes });
const firstAudioMs = firstAudioSentAt ? firstAudioSentAt - pipelineStart : 0;
return {
text: fullText,
llmMs,
ttsMs: cumulativeTtsMs,
firstAudioMs,
...finishReason === void 0 ? {} : { finishReason }
};
}
function _diagnose(connection, event, data) {
diagnostics.emit(connection, event, data);
}
function _sendJSON(connection, data) {
sendVoiceJSON(connection, data, "VoiceAgent", data.type === "transcript_delta");
}
var _VOICE_MESSAGES = { _: /* @__PURE__ */ new Set([
"hello",
"start_call",
"end_call",
"start_of_speech",
"end_of_speech",
"interrupt",
"text_message"
]) };
return VoiceAgentMixin;
}
function eagerAsyncIterable(source) {
const buffer = [];
let finished = false;
let error = null;
let waitResolve = null;
const notify = () => {
if (waitResolve) {
const resolve = waitResolve;
waitResolve = null;
resolve();
}
};
(async () => {
try {
for await (const item of source) {
buffer.push(item);
notify();
}
} catch (err) {
error = err;
} finally {
finished = true;
notify();
}
})();
return { [Symbol.asyncIterator]() {
let index = 0;
return { async next() {
while (index >= buffer.length && !finished) await new Promise((r) => {
waitResolve = r;
});
if (error) throw error;
if (index >= buffer.length) return {
done: true,
value: void 0
};
return {
done: false,
value: buffer[index++]
};
} };
} };
}
//#endregion
export { SentenceChunker, VOICE_PROTOCOL_VERSION, WorkersAIFluxSTT, WorkersAINova3STT, WorkersAITTS, addSFUTracks, createSFUSession, createSFUWebSocketAdapter, decodeVarint, downsample48kStereoTo16kMono, encodePayloadToProtobuf, encodeVarint, extractPayloadFromProtobuf, iterateText, renegotiateSFUSession, sfuFetch, upsample16kMonoTo48kStereo, withVoice, withVoiceInput };
//# sourceMappingURL=index.js.map