UNPKG

agents

Version:

A home for your AI agents

1,812 lines 76.4 kB
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