← Создание агентов на Cloudflare / agents / communication-channels
Голос
Создавайте голосовые агенты в реальном времени с распознаванием речи, синтезом речи и сохранением разговора. Аудио передаётся потоком через 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, поэтому вы получаете:
- Аудио в реальном времени : аудио с микрофона передаётся потоком в виде бинарных кадров WebSocket, аудио TTS передаётся обратно
- Автоматическое сохранение диалога : сообщения, хранящиеся в SQLite, переживают перезапуски
- Потоковая передача TTS : токены LLM разбиваются на предложения и синтезируются параллельно
- Обработка прерываний : речь пользователя во время воспроизведения отменяет текущий ответ
- Непрерывный STT : отдельная сессия транскрибера для каждого вызова, определение конца хода берёт на себя модель
- Хуки pipeline : перехватывает и преобразует текст на каждом этапе
быстрый старт
Установить
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 │ ◄────────────────────── │ │
└──────────┘ └──────────────────────────┘- Клиент захватывает звук с микрофона и отправляет его в виде бинарных кадров WebSocket (16 кГц, моно, 16 бит PCM).
- Аудио непрерывно передаётся потоком в сессию транскрибера (созданную в
start_call, существует на протяжении всего вызова). - Модель STT определяет момент, когда пользователь заканчивает высказывание, и вызывает
onUtterance. Все провайдеры используют определение хода на основе модели : клиенту не нужно сигнализировать об окончании речи для STT. - Ваш
onTurn()метод выполняется, обычно это вызов LLM. - Ответ разбивается на предложения и синтезируется через TTS.
- Аудио передаётся потоком обратно клиенту для воспроизведения.
Клиент получает 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:
beforeCallStart(connection), возвращаютfalseчтобы отклонитьonCallStart(connection),onCallEnd(connection),onInterrupt(connection)createTranscriber(connection): переопределение для переключения модели во время выполненияafterTranscribe(transcript, connection): фильтрует или преобразует транскрипты
Это делает не имеют хуки 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 → VoiceAgentWorkersAITTS возвращает 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 чтобы предотвратить вытеснение во время активных вызовов.