使用语音转文本、文本转语音和对话持久化构建实时语音 Agent。音频通过 WebSocket 流式传输——无需 SFU 或会议基础设施。Beta
@cloudflare/voice 提供两个服务端 mixin 及匹配的客户端库:
| 导出 | 导入 | 用途 |
|---|---|---|
withVoice |
@cloudflare/voice |
完整语音 Agent:STT、LLM、TTS、持久化 |
withVoiceInput |
@cloudflare/voice |
仅 STT:转录而不生成回复 |
useVoiceAgent |
@cloudflare/voice/react |
用于 withVoice Agent 的 React hook |
useVoiceInput |
@cloudflare/voice/react |
用于 withVoiceInput Agent 的 React hook |
VoiceClient |
@cloudflare/voice/client |
与框架无关的客户端 |
基于 Cloudflare Durable Objects,你将获得:
- 实时音频 — 麦克风音频以二进制 WebSocket 帧流式传输,TTS 音频流式返回
- 自动对话持久化 — 消息存储在 SQLite 中,重启后仍然保留
- 流式 TTS — LLM token 按句子分块并并发合成
- 中断处理 — 播放期间的用户语音可取消当前回复
- 连续 STT — 每次通话的转录会话,模型处理话轮检测
- 管道钩子 — 在每个阶段拦截并转换文本
npm install @cloudflare/voice agentsimport { 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;
}
}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>
);
}{
"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 帧(16kHz 单声道 16 位 PCM)发送。
- 音频持续流式传输到 transcriber 会话(在
start_call时创建,贯穿整个通话)。 - STT model 检测用户何时结束一次发言并触发
onUtterance。所有 provider 均使用 model 驱动的话轮检测——客户端无需为 STT 发送 end-of-speech 信号。 - 你的
onTurn()方法运行——通常是 LLM 调用。 - 响应按句子分块并通过 TTS 合成。
- 音频流式返回客户端播放。
客户端在用户说话时收到带部分结果的 transcript_interim 消息,可在 UI 中显示实时反馈。
withVoice(Agent) 为 Agent 类添加完整语音管道。
将 provider 设为类属性。类字段初始化在 super() 之后运行,因此 this.env 可用。
| 属性 | 类型 | 必需 | 描述 |
|---|---|---|---|
transcriber |
Transcriber |
是 | 每次通话的连续 STT provider |
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);
}要在运行时切换 model(例如 Flux 与 Nova 3 下拉菜单),可 override 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);
}
}必需。 用户说完且 transcript 就绪时调用。
返回 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 |
中断或断开时 abort |
| 方法 | 描述 |
|---|---|
beforeCallStart(connection) |
返回 false 拒绝通话 |
onCallStart(connection) |
通话被接受后调用 |
onCallEnd(connection) |
通话结束时调用 |
onInterrupt(connection) |
播放期间用户中断时调用 |
在各管道阶段 intercept 并 transform 数据。返回 null 跳过当前 utterance。
| 方法 | 接收 | 可跳过? |
|---|---|---|
afterTranscribe(transcript, connection) |
STT 文本 | 是 |
beforeSynthesize(text, connection) |
TTS 前文本 | 是 |
afterSynthesize(audio, text, connection) |
TTS 后 audio | 是 |
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) |
向单个连接合成并发送 audio |
speakAll(text) |
向所有连接合成并发送 audio |
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" |
发送给客户端的 audio 格式 |
maxMessageCount |
number |
1000 |
SQLite 中存储的最大消息数 |
withVoiceInput(Agent) 仅添加 STT 语音输入——无 TTS、无 LLM、无响应生成。适用于听写、语音搜索或任何只需 speech-to-text 而不需对话 Agent 的 UI。
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);
}
}每次话语转录完成后调用。重写此方法以处理转写文本。
withVoiceInput 支持与 withVoice 相同的生命周期钩子:
beforeCallStart(connection)— 返回false拒绝onCallStart(connection)、onCallEnd(connection)、onInterrupt(connection)createTranscriber(connection)— override 以在运行时切换 modelafterTranscribe(transcript, connection)— 过滤或 transform transcript
不包含 TTS hook(beforeSynthesize、afterSynthesize)或 onTurn。
为 withVoice Agent 封装 VoiceClient。管理连接、麦克风采集、播放、静音检测与中断检测。
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,
});当应用必须等待异步连接前提(例如用户 scope 的 capability token)时,使用 enabled: false。禁用期间 hook 不会创建或连接 VoiceClient,返回 idle 断开 state,且 startCall()、sendText()、sendJSON() 等操作回调为安全 no-op。
enabled 变为 true 时,hook 以当前选项连接。首次 enable 视为初始连接,因此 onReconnect 仅在 hook 保持 enabled 时后续连接 identity 变化时触发。
当浏览器支持 HTMLMediaElement.setSinkId() 时,传入 outputDeviceId 将 assistant 播放路由到所选扬声器:
const [outputDeviceId, setOutputDeviceId] = useState("default");
const voice = useVoiceAgent({
agent: "MyAgent",
outputDeviceId,
});const [outputDeviceId, setOutputDeviceId] = useState("default");
const voice = useVoiceAgent({
agent: "MyAgent",
outputDeviceId,
});使用 navigator.mediaDevices.enumerateDevices() 中 kind === "audiooutput" 的 MediaDeviceInfo.deviceId。"default" 与 undefined 使用系统默认输出。不支持 sink 选择的浏览器继续通过默认输出播放,请求非默认输出时设置 outputDeviceError。设备标签在用户授予麦克风权限前可能为空,若显示扬声器选择器,请在 startCall() 后刷新设备列表。
| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
enabled |
boolean |
true |
为 false 时延迟客户端创建与连接 |
silenceThreshold |
number |
0.04 |
低于此 RMS 视为静音 |
silenceDurationMs |
number |
500 |
end_of_speech 前的静音时长(ms) |
interruptThreshold |
number |
0.05 |
播放期间检测语音的 RMS |
interruptChunks |
number |
2 |
触发 interrupt 的连续高 RMS 块数 |
更改调优选项会触发客户端重连(连接 key 包含这些选项)。
用于听写与语音转文字的轻量 hook。将用户 transcript 累积为单个字符串。
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>
);
}适用于无 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 |
管道 state 变化 |
transcriptchange |
TranscriptMessage[] |
Transcript 更新 |
interimtranscript |
string | null |
流式 STT 的 interim transcript |
metricschange |
VoicePipelineMetrics |
管道计时指标 |
audiolevelchange |
number |
麦克风 audio 电平(0–1) |
connectionchange |
boolean |
WebSocket 连接/断开 |
mutechange |
boolean |
静音 state 变化 |
error |
string | null |
发生错误 |
outputdeviceerror |
string | null |
非致命扬声器路由问题 |
custommessage |
unknown |
来自服务端的非语音消息 |
| 选项 | 类型 | 描述 |
|---|---|---|
transport |
VoiceTransport |
自定义传输(默认:通过 PartySocket 的 WebSocket) |
audioInput |
VoiceAudioInput |
自定义麦克风采集(默认:内置 AudioWorklet) |
preferredFormat |
VoiceAudioFormat |
服务端 audio 格式提示(仅供参考) |
outputDeviceId |
string |
assistant 播放的首选 audiooutput 设备 |
无需 API 密钥——使用 Workers AI 绑定(binding):
| 类 | 类型 | 默认 model | 推荐用于 |
|---|---|---|---|
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 adapter 将电话通话连接到 voice agent:
npm install @cloudflare/voice-twilioAdapter 在 Twilio Media Streams 与 VoiceAgent 之间桥接:
Phone → Twilio → WebSocket → TwilioAdapter → WebSocket → VoiceAgentWorkersAITTS 返回 MP3,无法在 Workers 运行时解码为 PCM。使用 Twilio adapter 时,请使用输出 raw PCM 的 TTS provider(例如 ElevenLabs 配合 outputFormat: "pcm_16000")。
withVoice Agent 也可接收文本消息,完全绕过 STT。适用于与语音并存的聊天式输入。
const { sendText } = useVoiceAgent({ agent: "MyAgent" });
// Send text — goes straight to onTurn() without STT
sendText("What is the weather like today?");文本消息在活跃通话期间与通话外均可用。通话期间,响应通过 TTS 朗读。通话外,响应以纯文本 transcript 消息发送。
在语音协议消息旁发送与接收应用级 JSON 消息。非语音消息传递到服务端 onMessage handler,并在客户端触发 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;
}
}
}withVoice Agent 在每轮后发出计时指标:
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 重启与客户端重连后仍然保留。Voice Agent 使用 keepAlive 防止活跃通话期间被驱逐。