INTEGRITY Документация

Голос

Создавайте голосовые агенты в реальном времени с распознаванием речи, синтезом речи и сохранением разговора. Аудио передаётся потоком через WebSocket: SFU или инфраструктура для встреч не нужны. Бета

Обзор

@cloudflare/voice предоставляет два серверных mixin и соответствующие клиентские библиотеки:

Экспорт Импорт Назначение
withVoice @cloudflare/voice Полноценный голосовой агент: STT, LLM, TTS, сохранение состояния
withVoiceInput @cloudflare/voice Только STT: транскрипция без ответа
useVoiceAgent @cloudflare/voice/react React-хук для withVoice агенты
useVoiceInput @cloudflare/voice/react React-хук для withVoiceInput агенты
VoiceClient @cloudflare/voice/client Клиент, не привязанный к фреймворку

Построено на Cloudflare Durable Objects, поэтому вы получаете:

быстрый старт

Установить

npm install @cloudflare/voice agents

Сервер

import { Agent } from "agents";
import { withVoice, WorkersAIFluxSTT, WorkersAITTS } from "@cloudflare/voice";

const VoiceAgent = withVoice(Agent);

export class MyAgent extends VoiceAgent {
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new WorkersAITTS(this.env.AI);

	async onTurn(transcript, context) {
		return "Hello! I heard you say: " + transcript;
	}
}
import { Agent } from "agents";
import {
	withVoice,
	WorkersAIFluxSTT,
	WorkersAITTS,
	type VoiceTurnContext,
} from "@cloudflare/voice";

const VoiceAgent = withVoice(Agent);

export class MyAgent extends VoiceAgent<Env> {
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new WorkersAITTS(this.env.AI);

	async onTurn(transcript: string, context: VoiceTurnContext) {
		return "Hello! I heard you say: " + transcript;
	}
}

Клиент (React)

import { useVoiceAgent } from "@cloudflare/voice/react";

function VoiceUI() {
	const {
		status,
		transcript,
		interimTranscript,
		audioLevel,
		isMuted,
		startCall,
		endCall,
		toggleMute,
	} = useVoiceAgent({ agent: "MyAgent" });

	return (
		<div>
			<p>Status: {status}</p>

			<button onClick={status === "idle" ? startCall : endCall}>
				{status === "idle" ? "Start Call" : "End Call"}
			</button>

			<button onClick={toggleMute}>{isMuted ? "Unmute" : "Mute"}</button>

			{interimTranscript && (
				<p>
					<em>{interimTranscript}</em>
				</p>
			)}

			{transcript.map((msg, i) => (
				<p key={i}>
					<strong>{msg.role}:</strong> {msg.text}
				</p>
			))}
		</div>
	);
}

Конфигурация Wrangler

{
	"ai": {
		"binding": "AI"
	},
	"durable_objects": {
		"bindings": [
			{
				"name": "MyAgent",
				"class_name": "MyAgent"
			}
		]
	},
	"migrations": [
		{
			"tag": "v1",
			"new_sqlite_classes": ["MyAgent"]
		}
	]
}
[ai]
binding = "AI"

[[durable_objects.bindings]]
name = "MyAgent"
class_name = "MyAgent"

[[migrations]]
tag = "v1"
new_sqlite_classes = [ "MyAgent" ]

Как это работает

Browser                              Durable Object (withVoice)
┌──────────┐                         ┌──────────────────────────┐
│ Mic      │   binary PCM (16kHz)    │ Transcriber session      │
│          │ ──────────────────────► │ (per-call, continuous)   │
│          │                         │   ↓ model detects turn   │
│          │   JSON: transcript      │ onTurn() → your LLM code │
│          │ ◄────────────────────── │   ↓ (sentence chunking)  │
│          │   binary: audio         │ TTS                      │
│ Speaker  │ ◄────────────────────── │                          │
└──────────┘                         └──────────────────────────┘
  1. Клиент захватывает звук с микрофона и отправляет его в виде бинарных кадров WebSocket (16 кГц, моно, 16 бит PCM).
  2. Аудио непрерывно передаётся потоком в сессию транскрибера (созданную в start_call, существует на протяжении всего вызова).
  3. Модель STT определяет момент, когда пользователь заканчивает высказывание, и вызывает onUtterance. Все провайдеры используют определение хода на основе модели : клиенту не нужно сигнализировать об окончании речи для STT.
  4. Ваш onTurn() метод выполняется, обычно это вызов LLM.
  5. Ответ разбивается на предложения и синтезируется через TTS.
  6. Аудио передаётся потоком обратно клиенту для воспроизведения.

Клиент получает transcript_interim сообщения с промежуточными результатами по мере того, как говорит пользователь, чтобы вы могли показывать обратную связь в интерфейсе в режиме реального времени.

Server API: withVoice

withVoice(Agent) добавляет полный голосовой конвейер в класс Agent.

Провайдеры

Устанавливайте провайдеров как свойства класса. Инициализаторы полей класса выполняются после super(), поэтому this.env доступен.

Свойство Тип Обязательный Описание
transcriber Transcriber Да Провайдер непрерывного STT для каждого вызова
tts TTSProvider Да Синтез речи
import { withVoice, WorkersAIFluxSTT, WorkersAITTS } from "@cloudflare/voice";

const VoiceAgent = withVoice(Agent);

export class MyAgent extends VoiceAgent {
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new WorkersAITTS(this.env.AI);
}
import { withVoice, WorkersAIFluxSTT, WorkersAITTS } from "@cloudflare/voice";

const VoiceAgent = withVoice(Agent);

export class MyAgent extends VoiceAgent<Env> {
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new WorkersAITTS(this.env.AI);
}

Для переключения модели во время выполнения (например, выпадающий список Flux или Nova 3) переопределите createTranscriber:

export class MyAgent extends VoiceAgent {
	tts = new WorkersAITTS(this.env.AI);

	createTranscriber(connection) {
		return new WorkersAIFluxSTT(this.env.AI);
	}
}
export class MyAgent extends VoiceAgent<Env> {
	tts = new WorkersAITTS(this.env.AI);

	createTranscriber(connection: Connection): Transcriber {
		return new WorkersAIFluxSTT(this.env.AI);
	}
}

onTurn(transcript, context)

Обязательно. Вызывается, когда пользователь заканчивает говорить и транскрипт готов.

Возвращает string, AsyncIterable<string>, или ReadableStream для потоковых ответов.

Простой ответ:

export class MyAgent extends VoiceAgent {
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new WorkersAITTS(this.env.AI);

	async onTurn(transcript, context) {
		return "You said: " + transcript;
	}
}
export class MyAgent extends VoiceAgent<Env> {
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new WorkersAITTS(this.env.AI);

	async onTurn(transcript: string, context: VoiceTurnContext) {
		return "You said: " + transcript;
	}
}

Потоковый ответ (рекомендуется для LLM):

import { streamText } from "ai";
import { createWorkersAI } from "workers-ai-provider";

export class MyAgent extends VoiceAgent {
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new WorkersAITTS(this.env.AI);

	async onTurn(transcript, context) {
		const workersai = createWorkersAI({ binding: this.env.AI });

		const result = streamText({
			model: workersai("@cf/moonshotai/kimi-k2.6"),
			system: "You are a helpful voice assistant. Keep responses concise.",
			messages: [
				...context.messages.map((m) => ({
					role: m.role,
					content: m.content,
				})),
				{ role: "user", content: transcript },
			],
			abortSignal: context.signal,
		});

		return result.textStream;
	}
}
import { streamText } from "ai";
import { createWorkersAI } from "workers-ai-provider";

export class MyAgent extends VoiceAgent<Env> {
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new WorkersAITTS(this.env.AI);

	async onTurn(transcript: string, context: VoiceTurnContext) {
		const workersai = createWorkersAI({ binding: this.env.AI });

		const result = streamText({
			model: workersai("@cf/moonshotai/kimi-k2.6"),
			system: "You are a helpful voice assistant. Keep responses concise.",
			messages: [
				...context.messages.map((m) => ({
					role: m.role as "user" | "assistant",
					content: m.content,
				})),
				{ role: "user", content: transcript },
			],
			abortSignal: context.signal,
		});

		return result.textStream;
	}
}

context объект предоставляет:

Поле Тип Описание
connection Connection Соединение WebSocket
messages Array<{ role: string; content: string }> История разговора из SQLite
signal AbortSignal Останавливается при прерывании или разрыве соединения

Хуки жизненного цикла

Метод Описание
beforeCallStart(connection) Верните false чтобы отклонить вызов
onCallStart(connection) Вызывается после принятия звонка
onCallEnd(connection) Вызывается при завершении звонка
onInterrupt(connection) Вызывается, когда пользователь прерывает воспроизведение

Хуки pipeline

Перехватывайте и преобразуйте данные на каждом этапе конвейера. Верните null чтобы пропустить текущее высказывание.

Метод Получает Можно пропустить?
afterTranscribe(transcript, connection) Текст STT Да
beforeSynthesize(text, connection) Текст перед TTS Да
afterSynthesize(audio, text, connection) Аудио после TTS Да
import {} from "agents";

export class MyAgent extends VoiceAgent {
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new WorkersAITTS(this.env.AI);

	afterTranscribe(transcript, connection) {
		if (transcript.length < 3) return null;
		return transcript;
	}

	beforeSynthesize(text, connection) {
		return text.replace(/\bAI\b/g, "A.I.");
	}

	async onTurn(transcript, context) {
		return transcript;
	}
}
import { type Connection } from "agents";

export class MyAgent extends VoiceAgent<Env> {
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new WorkersAITTS(this.env.AI);

	afterTranscribe(transcript: string, connection: Connection) {
		if (transcript.length < 3) return null;
		return transcript;
	}

	beforeSynthesize(text: string, connection: Connection) {
		return text.replace(/\bAI\b/g, "A.I.");
	}

	async onTurn(transcript: string, context: VoiceTurnContext) {
		return transcript;
	}
}

Вспомогательные методы

Метод Описание
speak(connection, text) Синтезирует аудио и отправляет его одному подключению
speakAll(text) Синтезирует аудио и рассылает его всем подключениям
forceEndCall(connection) Программное завершение звонка
saveMessage(role, text) Сохранение сообщения в историю разговора
getConversationHistory() Получить историю диалога из SQLite

Параметры настройки

Передайте параметры в withVoice() в качестве второго аргумента:

const VoiceAgent = withVoice(Agent, {
	historyLimit: 20,
	audioFormat: "mp3",
	maxMessageCount: 1000,
});
const VoiceAgent = withVoice(Agent, {
	historyLimit: 20,
	audioFormat: "mp3",
	maxMessageCount: 1000,
});
Опция Тип По умолчанию Описание
historyLimit number 20 Максимальное количество сообщений, загружаемых в контекст
audioFormat string "mp3" Формат аудио, отправляемого клиенту
maxMessageCount number 1000 Максимальное количество сообщений, хранимых в SQLite

Server API: withVoiceInput

withVoiceInput(Agent) добавляет только голосовой ввод STT: без TTS, без LLM, без генерации ответов. Используйте это для диктовки, голосового поиска или любого интерфейса, где нужно распознавание речи без диалогового агента.

import { Agent } from "agents";
import { withVoiceInput, WorkersAINova3STT } from "@cloudflare/voice";

const InputAgent = withVoiceInput(Agent);

export class DictationAgent extends InputAgent {
	transcriber = new WorkersAINova3STT(this.env.AI);

	onTranscript(text, connection) {
		console.log("User said:", text);
	}
}
import { Agent } from "agents";
import { withVoiceInput, WorkersAINova3STT } from "@cloudflare/voice";

const InputAgent = withVoiceInput(Agent);

export class DictationAgent extends InputAgent<Env> {
	transcriber = new WorkersAINova3STT(this.env.AI);

	onTranscript(text: string, connection: Connection) {
		console.log("User said:", text);
	}
}

onTranscript(text, connection)

Вызывается после расшифровки каждой реплики. Переопределите этот метод для обработки транскрипта.

Хуки

withVoiceInput поддерживает те же хуки жизненного цикла, что и withVoice:

Это делает не имеют хуки TTS (beforeSynthesize, afterSynthesize) или onTurn.

Клиентское API: React hooks

useVoiceAgent

Оборачивает VoiceClient для withVoice агенты. Управляет соединением, захватом микрофона, воспроизведением, обнаружением тишины и обнаружением прерываний.

import { useVoiceAgent } from "@cloudflare/voice/react";

const selectedSpeakerId = "default";

const {
	status, // "idle" | "listening" | "thinking" | "speaking"
	transcript, // TranscriptMessage[] — conversation history
	interimTranscript, // string | null — real-time partial transcript
	metrics, // VoicePipelineMetrics | null
	audioLevel, // number (0–1) — current mic RMS level
	isMuted, // boolean
	connected, // boolean — WebSocket connected
	error, // string | null
	outputDeviceError, // string | null — non-fatal speaker routing issue
	startCall, // () => Promise<void>
	endCall, // () => void
	toggleMute, // () => void
	sendText, // (text: string) => void — bypass STT
	sendJSON, // (data: Record<string, unknown>) => void
	lastCustomMessage, // unknown — last non-voice message from server
} = useVoiceAgent({
	agent: "MyAgent",
	name: "default",
	host: window.location.host,
	outputDeviceId: selectedSpeakerId, // Optional audiooutput device ID
	enabled: true,
});

Используйте enabled: false когда приложению нужно дождаться асинхронных предварительных условий подключения, например пользовательского токена доступа. Пока хук отключён, он не создаёт и не подключает VoiceClient, возвращает состояние простоя без подключения, а колбэки действий, такие как startCall(), sendText(), а также sendJSON() безопасны и не выполняют никаких действий.

Когда enabled меняется на true, хук подключается с текущими параметрами. Первое включение считается первоначальным подключением, поэтому onReconnect срабатывает только при последующих изменениях идентичности соединения, пока хук остаётся включённым.

Выбор устройства вывода

Передайте outputDeviceId чтобы направлять воспроизведение ответа ассистента на выбранное устройство вывода звука, если браузер поддерживает HTMLMediaElement.setSinkId():

const [outputDeviceId, setOutputDeviceId] = useState("default");

const voice = useVoiceAgent({
	agent: "MyAgent",
	outputDeviceId,
});
const [outputDeviceId, setOutputDeviceId] = useState("default");

const voice = useVoiceAgent({
	agent: "MyAgent",
	outputDeviceId,
});

Используйте MediaDeviceInfo.deviceId от navigator.mediaDevices.enumerateDevices() где kind === "audiooutput". "default" и undefined используют устройство вывода по умолчанию в системе. Браузеры без поддержки выбора устройства вывода продолжают воспроизведение через устройство по умолчанию и устанавливают outputDeviceError когда запрошено устройство вывода, отличное от используемого по умолчанию. Названия устройств могут быть пустыми, пока пользователь не предоставит разрешение на использование микрофона, поэтому обновляйте списки устройств после startCall() если вы отображаете выбор спикера.

Параметры настройки

Опция Тип По умолчанию Описание
enabled boolean true Отложить создание клиента и подключение, если значение false
silenceThreshold number 0.04 RMS ниже этого значения считается тишиной
silenceDurationMs number 500 Продолжительность тишины перед end_of_speech (мс)
interruptThreshold number 0.05 RMS для обнаружения речи во время воспроизведения
interruptChunks number 2 Количество последовательных фрагментов с высоким RMS для срабатывания прерывания

Изменение параметров настройки вызывает переподключение клиента (ключ подключения включает эти параметры).

useVoiceInput

Лёгкий хук для диктовки и преобразования речи в текст. Собирает транскрипты пользователя в единую строку.

import { useVoiceInput } from "@cloudflare/voice/react";

function Dictation() {
	const {
		transcript, // string — accumulated text from all utterances
		interimTranscript, // string | null — current partial transcript
		isListening, // boolean
		audioLevel, // number (0–1)
		isMuted, // boolean
		error, // string | null
		start, // () => Promise<void>
		stop, // () => void
		toggleMute, // () => void
		clear, // () => void — clear accumulated transcript
	} = useVoiceInput({ agent: "DictationAgent" });

	return (
		<div>
			<textarea
				value={transcript + (interimTranscript ? " " + interimTranscript : "")}
				readOnly
			/>
			<button onClick={isListening ? stop : start}>
				{isListening ? "Stop" : "Dictate"}
			</button>
		</div>
	);
}

Клиентское API: VoiceClient

Клиент, не привязанный к фреймворку, для окружений без React.

import { VoiceClient } from "@cloudflare/voice/client";

const client = new VoiceClient({ agent: "MyAgent" });

client.addEventListener("statuschange", (status) => {
	console.log("Status:", status);
});

client.addEventListener("transcriptchange", (messages) => {
	console.log("Transcript:", messages);
});

client.addEventListener("error", (err) => {
	console.error("Error:", err);
});

client.connect();
await client.startCall();

// Switch assistant playback without reconnecting the call.
await client.setOutputDevice(selectedSpeakerId);

// Later:
client.endCall();
client.disconnect();
import { VoiceClient } from "@cloudflare/voice/client";

const client = new VoiceClient({ agent: "MyAgent" });

client.addEventListener("statuschange", (status) => {
	console.log("Status:", status);
});

client.addEventListener("transcriptchange", (messages) => {
	console.log("Transcript:", messages);
});

client.addEventListener("error", (err) => {
	console.error("Error:", err);
});

client.connect();
await client.startCall();

// Switch assistant playback without reconnecting the call.
await client.setOutputDevice(selectedSpeakerId);

// Later:
client.endCall();
client.disconnect();

События

Событие Тип данных Описание
statuschange VoiceStatus Состояние pipeline изменилось
transcriptchange TranscriptMessage[] Транскрипт обновлён
interimtranscript string | null Промежуточный транскрипт из потокового STT
metricschange VoicePipelineMetrics Метрики времени выполнения pipeline
audiolevelchange number Уровень звука микрофона (от 0 до 1)
connectionchange boolean WebSocket подключён/отключён
mutechange boolean Изменение состояния отключения звука
error string | null Произошла ошибка
outputdeviceerror string | null Некритичная проблема маршрутизации спикера
custommessage unknown Неголосовое сообщение от сервера

Дополнительные параметры

Опция Тип Описание
transport VoiceTransport Пользовательский транспорт (по умолчанию: WebSocket через PartySocket)
audioInput VoiceAudioInput Пользовательский захват микрофона (по умолчанию: встроенный AudioWorklet)
preferredFormat VoiceAudioFormat Подсказка для формата аудио на сервере (только рекомендательная)
outputDeviceId string Предпочитаемый audiooutput устройство для воспроизведения ответов ассистента

Провайдеры

Встроенный (Workers AI)

API-ключи не требуются, используйте свою привязку Workers AI:

Класс Тип Модель по умолчанию Рекомендуется для
WorkersAIFluxSTT Непрерывный STT @cf/deepgram/flux withVoice
WorkersAINova3STT Непрерывный STT @cf/deepgram/nova-3 withVoiceInput
WorkersAITTS TTS @cf/deepgram/aura-1 Оба
import { Agent } from "agents";
import {
	withVoice,
	WorkersAIFluxSTT,
	WorkersAINova3STT,
	WorkersAITTS,
} from "@cloudflare/voice";

const VoiceAgent = withVoice(Agent);

// Default usage
export class MyAgent extends VoiceAgent {
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new WorkersAITTS(this.env.AI);
}

// Custom options
export class CustomAgent extends VoiceAgent {
	transcriber = new WorkersAIFluxSTT(this.env.AI, {
		eotThreshold: 0.8,
		keyterms: ["Cloudflare", "Workers"],
	});
	tts = new WorkersAITTS(this.env.AI, {
		model: "@cf/deepgram/aura-1",
		speaker: "asteria",
	});
}
import { Agent } from "agents";
import {
	withVoice,
	WorkersAIFluxSTT,
	WorkersAINova3STT,
	WorkersAITTS,
} from "@cloudflare/voice";

const VoiceAgent = withVoice(Agent);

// Default usage
export class MyAgent extends VoiceAgent<Env> {
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new WorkersAITTS(this.env.AI);
}

// Custom options
export class CustomAgent extends VoiceAgent<Env> {
	transcriber = new WorkersAIFluxSTT(this.env.AI, {
		eotThreshold: 0.8,
		keyterms: ["Cloudflare", "Workers"],
	});
	tts = new WorkersAITTS(this.env.AI, {
		model: "@cf/deepgram/aura-1",
		speaker: "asteria",
	});
}

Сторонние провайдеры

Пакет Класс Описание
@cloudflare/voice-deepgram DeepgramSTT Непрерывный STT
@cloudflare/voice-elevenlabs ElevenLabsTTS Высококачественный TTS
@cloudflare/voice-twilio TwilioAdapter Телефония (телефонные звонки)

ElevenLabs TTS:

import { ElevenLabsTTS } from "@cloudflare/voice-elevenlabs";

export class MyAgent extends VoiceAgent {
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new ElevenLabsTTS({
		apiKey: this.env.ELEVENLABS_API_KEY,
		voiceId: "21m00Tcm4TlvDq8ikWAM",
	});
}
import { ElevenLabsTTS } from "@cloudflare/voice-elevenlabs";

export class MyAgent extends VoiceAgent<Env> {
	transcriber = new WorkersAIFluxSTT(this.env.AI);
	tts = new ElevenLabsTTS({
		apiKey: this.env.ELEVENLABS_API_KEY,
		voiceId: "21m00Tcm4TlvDq8ikWAM",
	});
}

Deepgram STT:

import { DeepgramSTT } from "@cloudflare/voice-deepgram";

export class MyAgent extends VoiceAgent {
	transcriber = new DeepgramSTT({
		apiKey: this.env.DEEPGRAM_API_KEY,
	});
	tts = new WorkersAITTS(this.env.AI);
}
import { DeepgramSTT } from "@cloudflare/voice-deepgram";

export class MyAgent extends VoiceAgent<Env> {
	transcriber = new DeepgramSTT({
		apiKey: this.env.DEEPGRAM_API_KEY,
	});
	tts = new WorkersAITTS(this.env.AI);
}

Телефония (Twilio)

Подключайте телефонные звонки к своему голосовому агенту с помощью адаптера Twilio:

npm install @cloudflare/voice-twilio

Адаптер связывает Twilio Media Streams с вашим VoiceAgent:

Phone → Twilio → WebSocket → TwilioAdapter → WebSocket → VoiceAgent

WorkersAITTS возвращает MP3, который нельзя декодировать в PCM в среде выполнения Workers. При использовании адаптера Twilio используйте провайдера TTS, который выдаёт необработанный PCM (например, ElevenLabs с outputFormat: "pcm_16000").

Текстовые сообщения

withVoice агенты также могут получать текстовые сообщения, полностью минуя STT. Это полезно для ввода в стиле чата наряду с голосовым.

const { sendText } = useVoiceAgent({ agent: "MyAgent" });

// Send text — goes straight to onTurn() without STT
sendText("What is the weather like today?");

Текстовые сообщения работают как во время активных звонков, так и вне их. Во время звонка ответ озвучивается через TTS. Вне звонка ответ отправляется в виде текстовых сообщений транскрипта.

Пользовательские сообщения

Отправляйте и получайте JSON-сообщения уровня приложения наряду с сообщениями голосового протокола. Сообщения, не относящиеся к голосовому протоколу, передаются в ваш onMessage обработчик на сервере и генерируйте custommessage события на клиенте.

Сервер:

export class MyAgent extends VoiceAgent {
	onMessage(connection, message) {
		const data = JSON.parse(message);
		if (data.type === "kick_speaker") {
			this.forceEndCall(connection);
		}
	}
}
export class MyAgent extends VoiceAgent<Env> {
	onMessage(connection: Connection, message: WSMessage) {
		const data = JSON.parse(message as string);
		if (data.type === "kick_speaker") {
			this.forceEndCall(connection);
		}
	}
}

Клиент:

const { sendJSON, lastCustomMessage } = useVoiceAgent({ agent: "MyAgent" });

sendJSON({ type: "kick_speaker" });

useEffect(() => {
	if (lastCustomMessage) {
		console.log("Custom message:", lastCustomMessage);
	}
}, [lastCustomMessage]);

Ограничение одним говорящим

Используйте beforeCallStart чтобы ограничить, кто может начинать вызов. В этом примере реализован режим одного докладчика: активным докладчиком может быть только одно подключение одновременно:

import {} from "agents";

export class MyAgent extends VoiceAgent {
	#speakerId = null;

	beforeCallStart(connection) {
		if (this.#speakerId !== null) {
			return false;
		}
		this.#speakerId = connection.id;
		return true;
	}

	onCallEnd(connection) {
		if (this.#speakerId === connection.id) {
			this.#speakerId = null;
		}
	}
}
import { type Connection } from "agents";

export class MyAgent extends VoiceAgent<Env> {
	#speakerId: string | null = null;

	beforeCallStart(connection: Connection) {
		if (this.#speakerId !== null) {
			return false;
		}
		this.#speakerId = connection.id;
		return true;
	}

	onCallEnd(connection: Connection) {
		if (this.#speakerId === connection.id) {
			this.#speakerId = null;
		}
	}
}

Метрики pipeline

withVoice агенты отправляют метрики времени после каждого хода:

const { metrics } = useVoiceAgent({ agent: "MyAgent" });

// metrics: {
//   llm_ms: 850,
//   tts_ms: 200,
//   first_audio_ms: 950,
//   total_ms: 1200,
// }

История разговора

withVoice автоматически сохраняет сообщения разговора в SQLite. Обращайтесь к истории в своём onTurn через context.messages, либо напрямую:

const history = this.getConversationHistory(20);

this.saveMessage("assistant", "Welcome! How can I help?");
const history = this.getConversationHistory(20);

this.saveMessage("assistant", "Welcome! How can I help?");

История сохраняется при перезапуске Durable Object и переподключении клиента. Голосовые агенты используют keepAlive чтобы предотвратить вытеснение во время активных вызовов.