agents 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/dist/{agent-routing-CnkaHb-v.d.ts → agent-routing-DE5zmCQ8.d.ts} +1038 -394
- package/dist/agent-routing.d.ts +1 -1
- package/dist/agent-tool-types.d.ts +26 -26
- package/dist/{agent-tools-C0XJqWzB.d.ts → agent-tools-DtXMTDGM.d.ts} +7 -7
- package/dist/agent-tools.d.ts +21 -21
- package/dist/browser/ai.js +4 -1
- package/dist/browser/ai.js.map +1 -1
- package/dist/browser/tanstack-ai.js.map +1 -1
- package/dist/callable-decorator-DP__HhBA.d.ts +72 -0
- package/dist/callable-decorator.d.ts +18 -0
- package/dist/callable-decorator.js +71 -0
- package/dist/callable-decorator.js.map +1 -0
- package/dist/capability-BjSKYpzg.js.map +1 -1
- package/dist/capability-runner-Be_-PLR1.d.ts +459 -0
- package/dist/channel-Bnm4S7T2.d.ts +491 -0
- package/dist/channels/ai-sdk.d.ts +46 -0
- package/dist/channels/ai-sdk.js +120 -0
- package/dist/channels/ai-sdk.js.map +1 -0
- package/dist/channels/email.d.ts +95 -0
- package/dist/channels/email.js +323 -0
- package/dist/channels/email.js.map +1 -0
- package/dist/channels/index.d.ts +233 -0
- package/dist/channels/index.js +608 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/channels/slack.d.ts +140 -0
- package/dist/channels/slack.js +614 -0
- package/dist/channels/slack.js.map +1 -0
- package/dist/channels/tanstack-ai.d.ts +39 -0
- package/dist/channels/tanstack-ai.js +17 -0
- package/dist/channels/tanstack-ai.js.map +1 -0
- package/dist/channels/telegram.d.ts +106 -0
- package/dist/channels/telegram.js +427 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/channels/voice.d.ts +45 -0
- package/dist/channels/voice.js +122 -0
- package/dist/channels/voice.js.map +1 -0
- package/dist/chat/index.d.ts +2325 -2019
- package/dist/chat/index.js +888 -518
- package/dist/chat/index.js.map +1 -1
- package/dist/chat-sdk/index.d.ts +7 -7
- package/dist/chat-sdk/index.js +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/context/index.d.ts +216 -0
- package/dist/context/index.js +454 -0
- package/dist/context/index.js.map +1 -0
- package/dist/{current-agent-CuMErtly.d.ts → current-agent-Da_C9a3b.d.ts} +90 -107
- package/dist/current-agent-DhoDkSnH.js.map +1 -1
- package/dist/{diagnostics-CaBjfz4J.js → diagnostics-BzvaX2UT.js} +5 -1
- package/dist/diagnostics-BzvaX2UT.js.map +1 -0
- package/dist/diagnostics-C4jcz3VK.js +360 -0
- package/dist/diagnostics-C4jcz3VK.js.map +1 -0
- package/dist/index-BB0kqhIz.d.ts +101 -0
- package/dist/index-BVVgDSdq.d.ts +1 -0
- package/dist/index-XDkuQ7zm.d.ts +89 -0
- package/dist/{index-DcSAZKsB.d.ts → index-YSKgfgg9.d.ts} +3 -1
- package/dist/index.d.ts +91 -82
- package/dist/index.js +3 -2
- package/dist/ingress-BfetZbMO.js +83 -0
- package/dist/ingress-BfetZbMO.js.map +1 -0
- package/dist/internal-CYlgHl1l.js +59 -0
- package/dist/internal-CYlgHl1l.js.map +1 -0
- package/dist/internal_context.d.ts +1 -1
- package/dist/lifecycle/index.d.ts +35 -17
- package/dist/lifecycle/index.js +1 -1
- package/dist/lifecycle-CMRGjZdw.js +1299 -0
- package/dist/lifecycle-CMRGjZdw.js.map +1 -0
- package/dist/mcp/client/index.d.ts +20 -20
- package/dist/mcp/index.d.ts +35 -35
- package/dist/mcp/index.js +1 -1
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +1 -1
- package/dist/react.d.ts +4 -4
- package/dist/{retries-CAvxtG9d.d.ts → retries-D9Ds-1lz.d.ts} +17 -6
- package/dist/retries.d.ts +8 -6
- package/dist/retries.js +13 -1
- package/dist/retries.js.map +1 -1
- package/dist/routing/index.d.ts +137 -0
- package/dist/routing/index.js +244 -0
- package/dist/routing/index.js.map +1 -0
- package/dist/sanitize-D9TujEK8.js +79 -0
- package/dist/sanitize-D9TujEK8.js.map +1 -0
- package/dist/scheduler-DD9NdYbF.js +665 -0
- package/dist/scheduler-DD9NdYbF.js.map +1 -0
- package/dist/{scheduler-DQoTGoAW.d.ts → scheduler-Dwh85ZGl.d.ts} +21 -22
- package/dist/schedules/index.d.ts +1 -1
- package/dist/schedules/index.js +1 -1
- package/dist/sentence-chunker-BAidJ4DA.d.ts +68 -0
- package/dist/serializable.d.ts +1 -1
- package/dist/sessions/index.d.ts +441 -0
- package/dist/sessions/index.js +2063 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/skills/index.d.ts +99 -0
- package/dist/skills/index.js +254 -5
- package/dist/skills/index.js.map +1 -1
- package/dist/{src-5W6JNKVb.js → src-DlSHshb2.js} +1460 -1110
- package/dist/src-DlSHshb2.js.map +1 -0
- package/dist/streams/index.d.ts +120 -0
- package/dist/streams/index.js +107 -0
- package/dist/streams/index.js.map +1 -0
- package/dist/streams-D6tJ0NN9.d.ts +370 -0
- package/dist/streams-DZKgAj9b.js +709 -0
- package/dist/streams-DZKgAj9b.js.map +1 -0
- package/dist/sub-routing.d.ts +12 -12
- package/dist/surface-bZZJqBka.js +17 -0
- package/dist/surface-bZZJqBka.js.map +1 -0
- package/dist/tasks/index.d.ts +64 -0
- package/dist/tasks/index.js +2 -0
- package/dist/tasks-BRJ5zgya.d.ts +517 -0
- package/dist/tasks-ylZgBjhj.js +1656 -0
- package/dist/tasks-ylZgBjhj.js.map +1 -0
- package/dist/text-segment-joiner-BtAFQSA_.js +57 -0
- package/dist/text-segment-joiner-BtAFQSA_.js.map +1 -0
- package/dist/text-stream-CpdiKrJB.js +272 -0
- package/dist/text-stream-CpdiKrJB.js.map +1 -0
- package/dist/tokens-nHAKcN6M.js +52 -0
- package/dist/tokens-nHAKcN6M.js.map +1 -0
- package/dist/tool-schema-CBjGPrsQ.js +31 -0
- package/dist/tool-schema-CBjGPrsQ.js.map +1 -0
- package/dist/types-B7LojTe4.d.ts +202 -0
- package/dist/types-_Faxb570.d.ts +439 -0
- package/dist/voice/client.d.ts +226 -0
- package/dist/voice/client.js +932 -0
- package/dist/voice/client.js.map +1 -0
- package/dist/voice/errors.d.ts +43 -0
- package/dist/voice/errors.js +41 -0
- package/dist/voice/errors.js.map +1 -0
- package/dist/voice/index.d.ts +271 -0
- package/dist/voice/index.js +1812 -0
- package/dist/voice/index.js.map +1 -0
- package/dist/voice/react.d.ts +167 -0
- package/dist/voice/react.js +234 -0
- package/dist/voice/react.js.map +1 -0
- package/dist/voice/sfu.d.ts +71 -0
- package/dist/voice/sfu.js +157 -0
- package/dist/voice/sfu.js.map +1 -0
- package/dist/voice/text.d.ts +6 -0
- package/dist/voice/text.js +2 -0
- package/dist/voice/types.d.ts +58 -0
- package/dist/voice/types.js +18 -0
- package/dist/voice/types.js.map +1 -0
- package/dist/voice/workers-ai.d.ts +136 -0
- package/dist/voice/workers-ai.js +568 -0
- package/dist/voice/workers-ai.js.map +1 -0
- package/dist/websockets/index.d.ts +192 -0
- package/dist/websockets/index.js +2 -0
- package/dist/websockets-DUfRHPRq.js +502 -0
- package/dist/websockets-DUfRHPRq.js.map +1 -0
- package/dist/workflow-types.d.ts +25 -25
- package/dist/workflows.d.ts +21 -21
- package/dist/workflows.js +1 -1
- package/docs/agent-class.md +2 -2
- package/docs/agent-tools.md +2 -1
- package/docs/channels.md +323 -0
- package/docs/chat-agents.md +6 -13
- package/docs/context.md +131 -0
- package/docs/index.md +15 -12
- package/docs/lifecycle.md +102 -55
- package/docs/long-running-agents.md +2 -2
- package/docs/mcp-servers.md +5 -1
- package/docs/resumable-streaming.md +1 -1
- package/docs/routing.md +105 -0
- package/docs/sessions.md +237 -871
- package/docs/streams.md +213 -0
- package/docs/sub-agents.md +184 -124
- package/docs/tasks.md +246 -0
- package/docs/voice.md +745 -0
- package/package.json +115 -13
- package/dist/capability-runner-CvHGZqUu.d.ts +0 -150
- package/dist/compaction-helpers-iiKMr2TQ.js +0 -340
- package/dist/compaction-helpers-iiKMr2TQ.js.map +0 -1
- package/dist/compaction-helpers-wUz6M3us.d.ts +0 -621
- package/dist/diagnostics-CaBjfz4J.js.map +0 -1
- package/dist/durable-object-lifecycle-D6nNQJJd.js +0 -862
- package/dist/durable-object-lifecycle-D6nNQJJd.js.map +0 -1
- package/dist/experimental/memory/session/index.d.ts +0 -671
- package/dist/experimental/memory/session/index.js +0 -2379
- package/dist/experimental/memory/session/index.js.map +0 -1
- package/dist/experimental/memory/utils/index.d.ts +0 -96
- package/dist/experimental/memory/utils/index.js +0 -79
- package/dist/experimental/memory/utils/index.js.map +0 -1
- package/dist/scheduler-CR9RHGos.js +0 -857
- package/dist/scheduler-CR9RHGos.js.map +0 -1
- package/dist/src-5W6JNKVb.js.map +0 -1
- package/dist/tool-output-truncation-CNnnGZQ3.js +0 -98
- package/dist/tool-output-truncation-CNnnGZQ3.js.map +0 -1
package/docs/voice.md
ADDED
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
# Voice Agents
|
|
2
|
+
|
|
3
|
+
Build real-time voice agents with speech-to-text, text-to-speech, and conversation persistence. Audio streams over WebSocket — no SFU or meeting infrastructure required.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`agents/voice` provides two server-side mixins and matching React hooks:
|
|
8
|
+
|
|
9
|
+
| Export | Import | Purpose |
|
|
10
|
+
| ---------------- | --------------------- | -------------------------------------------- |
|
|
11
|
+
| `withVoice` | `agents/voice` | Full voice agent: STT, LLM, TTS, persistence |
|
|
12
|
+
| `withVoiceInput` | `agents/voice` | STT-only: transcription without response |
|
|
13
|
+
| `useVoiceAgent` | `agents/voice/react` | React hook for `withVoice` agents |
|
|
14
|
+
| `useVoiceInput` | `agents/voice/react` | React hook for `withVoiceInput` agents |
|
|
15
|
+
| `VoiceClient` | `agents/voice/client` | Framework-agnostic client |
|
|
16
|
+
|
|
17
|
+
Built on Cloudflare Durable Objects, you get:
|
|
18
|
+
|
|
19
|
+
- **Real-time audio** — mic audio streams as binary WebSocket frames, TTS audio streams back
|
|
20
|
+
- **Automatic conversation persistence** — messages stored in SQLite, survive restarts
|
|
21
|
+
- **Streaming TTS** — LLM tokens are sentence-chunked and synthesized concurrently
|
|
22
|
+
- **Interruption handling** — user speech during playback cancels the current response
|
|
23
|
+
- **Continuous STT** — per-call transcriber session, model handles turn detection
|
|
24
|
+
- **Pipeline hooks** — intercept and transform text at every stage
|
|
25
|
+
|
|
26
|
+
> **Experimental.** This API is under active development and will break between releases. Pin your version.
|
|
27
|
+
|
|
28
|
+
The Voice mixins extend the Agents SDK. Its documentation is available at
|
|
29
|
+
`agents/docs/index.md`.
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
### Install
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npm install agents
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Server
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { Agent } from "agents";
|
|
43
|
+
import {
|
|
44
|
+
withVoice,
|
|
45
|
+
WorkersAIFluxSTT,
|
|
46
|
+
WorkersAITTS,
|
|
47
|
+
type VoiceTurnContext
|
|
48
|
+
} from "agents/voice";
|
|
49
|
+
|
|
50
|
+
const VoiceAgent = withVoice(Agent);
|
|
51
|
+
|
|
52
|
+
export class MyAgent extends VoiceAgent<Env> {
|
|
53
|
+
transcriber = new WorkersAIFluxSTT(this.env.AI);
|
|
54
|
+
tts = new WorkersAITTS(this.env.AI);
|
|
55
|
+
|
|
56
|
+
async onTurn(transcript: string, context: VoiceTurnContext) {
|
|
57
|
+
return "Hello! I heard you say: " + transcript;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Client (React)
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import { useVoiceAgent } from "agents/voice/react";
|
|
66
|
+
|
|
67
|
+
function VoiceUI() {
|
|
68
|
+
const {
|
|
69
|
+
status,
|
|
70
|
+
transcript,
|
|
71
|
+
interimTranscript,
|
|
72
|
+
audioLevel,
|
|
73
|
+
isMuted,
|
|
74
|
+
startCall,
|
|
75
|
+
endCall,
|
|
76
|
+
toggleMute
|
|
77
|
+
} = useVoiceAgent({ agent: "MyAgent" });
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div>
|
|
81
|
+
<p>Status: {status}</p>
|
|
82
|
+
|
|
83
|
+
<button onClick={status === "idle" ? startCall : endCall}>
|
|
84
|
+
{status === "idle" ? "Start Call" : "End Call"}
|
|
85
|
+
</button>
|
|
86
|
+
|
|
87
|
+
<button onClick={toggleMute}>{isMuted ? "Unmute" : "Mute"}</button>
|
|
88
|
+
|
|
89
|
+
{interimTranscript && (
|
|
90
|
+
<p>
|
|
91
|
+
<em>{interimTranscript}</em>
|
|
92
|
+
</p>
|
|
93
|
+
)}
|
|
94
|
+
|
|
95
|
+
{transcript.map((msg, i) => (
|
|
96
|
+
<p key={i}>
|
|
97
|
+
<strong>{msg.role}:</strong> {msg.text}
|
|
98
|
+
</p>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Wrangler Config
|
|
106
|
+
|
|
107
|
+
```jsonc
|
|
108
|
+
// wrangler.jsonc
|
|
109
|
+
{
|
|
110
|
+
"ai": { "binding": "AI" },
|
|
111
|
+
"durable_objects": {
|
|
112
|
+
"bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }]
|
|
113
|
+
},
|
|
114
|
+
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyAgent"] }]
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## How It Works
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
Browser Durable Object (withVoice)
|
|
122
|
+
┌──────────┐ ┌──────────────────────────┐
|
|
123
|
+
│ Mic │ binary PCM (16kHz) │ Transcriber session │
|
|
124
|
+
│ │ ──────────────────────► │ (per-call, continuous) │
|
|
125
|
+
│ │ │ ↓ model detects turn │
|
|
126
|
+
│ │ JSON: transcript │ onTurn() → your LLM code │
|
|
127
|
+
│ │ ◄────────────────────── │ ↓ (sentence chunking) │
|
|
128
|
+
│ │ binary: audio │ TTS │
|
|
129
|
+
│ Speaker │ ◄────────────────────── │ │
|
|
130
|
+
└──────────┘ └──────────────────────────┘
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
1. The client captures mic audio and sends it as binary WebSocket frames (16kHz mono 16-bit PCM)
|
|
134
|
+
2. Audio streams continuously to the transcriber session (created at `start_call`, lives for the entire call)
|
|
135
|
+
3. The STT model detects when the user finishes an utterance and fires `onUtterance`
|
|
136
|
+
4. Your `onTurn()` method runs — typically an LLM call
|
|
137
|
+
5. The response is sentence-chunked and synthesized via TTS
|
|
138
|
+
6. Audio streams back to the client for playback
|
|
139
|
+
|
|
140
|
+
## Server API: `withVoice`
|
|
141
|
+
|
|
142
|
+
`withVoice(Agent)` adds the full voice pipeline to an Agent class.
|
|
143
|
+
|
|
144
|
+
### Providers
|
|
145
|
+
|
|
146
|
+
Set providers as class properties. Class field initializers run after `super()`, so `this.env` is available.
|
|
147
|
+
|
|
148
|
+
| Property | Type | Required | Description |
|
|
149
|
+
| ------------- | ------------- | -------- | -------------------------------- |
|
|
150
|
+
| `transcriber` | `Transcriber` | Yes | Continuous per-call STT provider |
|
|
151
|
+
| `tts` | `TTSProvider` | Yes | Text-to-speech |
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { withVoice, WorkersAIFluxSTT, WorkersAITTS } from "agents/voice";
|
|
155
|
+
|
|
156
|
+
const VoiceAgent = withVoice(Agent);
|
|
157
|
+
|
|
158
|
+
export class MyAgent extends VoiceAgent<Env> {
|
|
159
|
+
transcriber = new WorkersAIFluxSTT(this.env.AI);
|
|
160
|
+
tts = new WorkersAITTS(this.env.AI);
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
For runtime model switching (e.g. Flux vs Nova 3 dropdown), override `createTranscriber`:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
export class MyAgent extends VoiceAgent<Env> {
|
|
168
|
+
tts = new WorkersAITTS(this.env.AI);
|
|
169
|
+
|
|
170
|
+
createTranscriber(connection: Connection): Transcriber {
|
|
171
|
+
return new WorkersAIFluxSTT(this.env.AI);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Custom transcriber sessions can implement `waitUntilReady(): Promise<void>` when they open an upstream streaming connection asynchronously. `withVoice()` waits for this optional method before sending `listening` or running `onCallStart()`. Resolve it when the session can accept audio and emit transcripts; reject it when startup failed and the call should return to `idle`. If `close()` abandons startup while readiness is pending, settle the readiness promise so server startup work does not wait forever. Providers that are ready synchronously can omit the method.
|
|
177
|
+
|
|
178
|
+
### `onTurn(transcript, context)`
|
|
179
|
+
|
|
180
|
+
**Required.** Called when the user finishes speaking and the transcript is ready. `context.messages` contains completed conversation history before this transcript. Append `transcript` exactly once when constructing an LLM message list.
|
|
181
|
+
|
|
182
|
+
Return a `string`, `AsyncIterable<string>`, or `ReadableStream` for streaming responses:
|
|
183
|
+
|
|
184
|
+
**Simple response:**
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
async onTurn(transcript: string, context: VoiceTurnContext) {
|
|
188
|
+
return "You said: " + transcript;
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Streaming response (recommended for LLM):**
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
import { streamText, convertToModelMessages } from "ai";
|
|
196
|
+
import { createWorkersAI } from "workers-ai-provider";
|
|
197
|
+
|
|
198
|
+
async onTurn(transcript: string, context: VoiceTurnContext) {
|
|
199
|
+
const workersai = createWorkersAI({ binding: this.env.AI });
|
|
200
|
+
|
|
201
|
+
const result = streamText({
|
|
202
|
+
model: workersai("@cf/moonshotai/kimi-k2.7-code"),
|
|
203
|
+
system: "You are a helpful voice assistant. Keep responses concise.",
|
|
204
|
+
messages: [
|
|
205
|
+
...context.messages.map(m => ({
|
|
206
|
+
role: m.role as "user" | "assistant",
|
|
207
|
+
content: m.content
|
|
208
|
+
})),
|
|
209
|
+
{ role: "user", content: transcript }
|
|
210
|
+
],
|
|
211
|
+
abortSignal: context.signal
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return result.fullStream;
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The `context` object provides:
|
|
219
|
+
|
|
220
|
+
| Field | Type | Description |
|
|
221
|
+
| ------------ | ------------------------------------------ | --------------------------------------- |
|
|
222
|
+
| `connection` | `Connection` | The WebSocket connection |
|
|
223
|
+
| `messages` | `Array<{ role: string; content: string }>` | Completed history before the transcript |
|
|
224
|
+
| `signal` | `AbortSignal` | Aborted on interrupt or disconnect |
|
|
225
|
+
|
|
226
|
+
### Lifecycle Hooks
|
|
227
|
+
|
|
228
|
+
| Method | Description |
|
|
229
|
+
| ----------------------------- | ------------------------------------------- |
|
|
230
|
+
| `beforeCallStart(connection)` | Return `false` to reject the call |
|
|
231
|
+
| `onCallStart(connection)` | Called after a call is accepted |
|
|
232
|
+
| `onCallEnd(connection)` | Called when a call ends |
|
|
233
|
+
| `onInterrupt(connection)` | Called when user interrupts during playback |
|
|
234
|
+
|
|
235
|
+
If startup is rejected or fails, the server sends the client back to `idle` and calls `onCallEnd()` so cleanup logic can run.
|
|
236
|
+
|
|
237
|
+
### Pipeline Hooks
|
|
238
|
+
|
|
239
|
+
Intercept and transform data at each pipeline stage. Return `null` to skip the current utterance.
|
|
240
|
+
|
|
241
|
+
| Method | Receives | Can skip? |
|
|
242
|
+
| ------------------------------------------ | --------------- | --------- |
|
|
243
|
+
| `afterTranscribe(transcript, connection)` | STT text | Yes |
|
|
244
|
+
| `beforeSynthesize(text, connection)` | Text before TTS | Yes |
|
|
245
|
+
| `afterSynthesize(audio, text, connection)` | Audio after TTS | Yes |
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
export class MyAgent extends VoiceAgent<Env> {
|
|
249
|
+
// Filter out short/noise transcripts
|
|
250
|
+
afterTranscribe(transcript: string, connection: Connection) {
|
|
251
|
+
if (transcript.length < 3) return null; // skip
|
|
252
|
+
return transcript;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Add SSML or modify text before TTS
|
|
256
|
+
beforeSynthesize(text: string, connection: Connection) {
|
|
257
|
+
return text.replace(/\bAI\b/g, "A.I."); // improve pronunciation
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Convenience Methods
|
|
263
|
+
|
|
264
|
+
| Method | Description |
|
|
265
|
+
| -------------------------- | -------------------------------------------- |
|
|
266
|
+
| `speak(connection, text)` | Synthesize and send audio to one connection |
|
|
267
|
+
| `speakAll(text)` | Synthesize and send audio to all connections |
|
|
268
|
+
| `forceEndCall(connection)` | Programmatically end a call |
|
|
269
|
+
| `saveMessage(role, text)` | Persist a message to conversation history |
|
|
270
|
+
| `getConversationHistory()` | Retrieve conversation history from SQLite |
|
|
271
|
+
|
|
272
|
+
### Configuration Options
|
|
273
|
+
|
|
274
|
+
Pass options to `withVoice()` as the second argument:
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
const VoiceAgent = withVoice(Agent, {
|
|
278
|
+
historyLimit: 20, // Max messages loaded for context (default: 20)
|
|
279
|
+
audioFormat: "mp3", // Audio format sent to client (default: "mp3")
|
|
280
|
+
sampleRate: 16000, // Sample rate (Hz) for raw pcm16 payloads (default: 16000)
|
|
281
|
+
maxMessageCount: 1000, // Max messages in SQLite (default: 1000)
|
|
282
|
+
diagnostics: {
|
|
283
|
+
browserConsole: false // forward server diagnostics to browser console (default: false)
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Server API: `withVoiceInput`
|
|
289
|
+
|
|
290
|
+
`withVoiceInput(Agent)` adds STT-only voice input — no TTS, no LLM, no response generation. Use this for dictation, search-by-voice, or any UI where you need speech-to-text without a conversational agent.
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { Agent } from "agents";
|
|
294
|
+
import { withVoiceInput, WorkersAINova3STT } from "agents/voice";
|
|
295
|
+
|
|
296
|
+
const InputAgent = withVoiceInput(Agent);
|
|
297
|
+
|
|
298
|
+
export class DictationAgent extends InputAgent<Env> {
|
|
299
|
+
transcriber = new WorkersAINova3STT(this.env.AI);
|
|
300
|
+
|
|
301
|
+
onTranscript(text: string, connection: Connection) {
|
|
302
|
+
console.log("User said:", text);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### `onTranscript(text, connection)`
|
|
308
|
+
|
|
309
|
+
Called after each utterance is transcribed. Override this to process the transcript.
|
|
310
|
+
|
|
311
|
+
### Hooks
|
|
312
|
+
|
|
313
|
+
`withVoiceInput` supports the same lifecycle hooks as `withVoice`:
|
|
314
|
+
|
|
315
|
+
- `beforeCallStart(connection)` — return `false` to reject
|
|
316
|
+
- `onCallStart(connection)`, `onCallEnd(connection)`, `onInterrupt(connection)`
|
|
317
|
+
- `createTranscriber(connection)` — override for runtime model switching
|
|
318
|
+
- `afterTranscribe(transcript, connection)` — filter or transform transcripts
|
|
319
|
+
|
|
320
|
+
If startup is rejected or fails, the server sends the client back to `idle` and calls `onCallEnd()` so cleanup logic can run.
|
|
321
|
+
|
|
322
|
+
It does **not** have TTS hooks (`beforeSynthesize`, `afterSynthesize`) or `onTurn`.
|
|
323
|
+
|
|
324
|
+
## Client API: React Hooks
|
|
325
|
+
|
|
326
|
+
### `useVoiceAgent`
|
|
327
|
+
|
|
328
|
+
Wraps `VoiceClient` for `withVoice` agents. Manages connection, mic capture, playback, silence detection, and interrupt detection.
|
|
329
|
+
|
|
330
|
+
```tsx
|
|
331
|
+
import { useVoiceAgent } from "agents/voice/react";
|
|
332
|
+
|
|
333
|
+
const selectedSpeakerId = "default";
|
|
334
|
+
|
|
335
|
+
const {
|
|
336
|
+
status, // "idle" | "listening" | "thinking" | "speaking"
|
|
337
|
+
transcript, // TranscriptMessage[] — conversation history
|
|
338
|
+
interimTranscript, // string | null — real-time partial transcript
|
|
339
|
+
turnMetrics, // VoiceTurnMetrics | null (latest stable terminal summary)
|
|
340
|
+
audioLevel, // number (0–1) — current mic RMS level
|
|
341
|
+
isMuted, // boolean
|
|
342
|
+
connected, // boolean — WebSocket connected
|
|
343
|
+
error, // string | null
|
|
344
|
+
outputDeviceError, // string | null — non-fatal speaker routing issue
|
|
345
|
+
startCall, // () => Promise<void>
|
|
346
|
+
endCall, // () => void
|
|
347
|
+
toggleMute, // () => void
|
|
348
|
+
sendText, // (text: string) => void — bypass STT
|
|
349
|
+
sendJSON, // (data: Record<string, unknown>) => void
|
|
350
|
+
lastCustomMessage // unknown — last non-voice message from server
|
|
351
|
+
} = useVoiceAgent({
|
|
352
|
+
agent: "MyAgent", // Required: Durable Object class name
|
|
353
|
+
name: "default", // Instance name (default: "default")
|
|
354
|
+
host: window.location.host, // Host to connect to
|
|
355
|
+
outputDeviceId: selectedSpeakerId, // Optional audiooutput device ID
|
|
356
|
+
enabled: true // Set false to delay connecting until prerequisites are ready
|
|
357
|
+
});
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Use `enabled: false` when the app must wait for async connection prerequisites, such as a user-scoped capability token. While disabled, the hook does not create or connect a `VoiceClient`, returns the idle/disconnected state, and action callbacks such as `startCall()`, `sendText()`, and `sendJSON()` are safe no-ops. When `enabled` changes to `true`, the hook connects with the current options. The first enable is treated as an initial connection, so `onReconnect` only fires for later connection identity changes while the hook remains enabled.
|
|
361
|
+
|
|
362
|
+
#### Output Device Selection
|
|
363
|
+
|
|
364
|
+
Pass `outputDeviceId` to route assistant playback to a selected speaker when the browser supports `HTMLMediaElement.setSinkId()`:
|
|
365
|
+
|
|
366
|
+
```tsx
|
|
367
|
+
const [outputDeviceId, setOutputDeviceId] = useState("default");
|
|
368
|
+
|
|
369
|
+
const voice = useVoiceAgent({
|
|
370
|
+
agent: "MyAgent",
|
|
371
|
+
outputDeviceId
|
|
372
|
+
});
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Use a `MediaDeviceInfo.deviceId` from `navigator.mediaDevices.enumerateDevices()` where `kind === "audiooutput"`. `"default"` and `undefined` use the system default output. Browsers without sink selection support continue playing through the default output and set `outputDeviceError` when a non-default output is requested. Device labels may be blank until the user grants microphone permission, so refresh device lists after `startCall()` if you show a speaker picker.
|
|
376
|
+
|
|
377
|
+
#### Tuning Options
|
|
378
|
+
|
|
379
|
+
| Option | Type | Default | Description |
|
|
380
|
+
| -------------------- | -------- | ------- | ------------------------------------------------ |
|
|
381
|
+
| `silenceThreshold` | `number` | `0.04` | RMS below this is silence |
|
|
382
|
+
| `silenceDurationMs` | `number` | `500` | Silence duration before `end_of_speech` (ms) |
|
|
383
|
+
| `interruptThreshold` | `number` | `0.05` | RMS to detect speech during playback |
|
|
384
|
+
| `interruptChunks` | `number` | `2` | Consecutive high-RMS chunks to trigger interrupt |
|
|
385
|
+
|
|
386
|
+
Changing tuning options triggers a client reconnect (the connection key includes them).
|
|
387
|
+
|
|
388
|
+
### `useVoiceInput`
|
|
389
|
+
|
|
390
|
+
Lightweight hook for dictation / voice-to-text. Accumulates user transcripts into a single string.
|
|
391
|
+
|
|
392
|
+
```tsx
|
|
393
|
+
import { useVoiceInput } from "agents/voice/react";
|
|
394
|
+
|
|
395
|
+
function Dictation() {
|
|
396
|
+
const {
|
|
397
|
+
transcript, // string — accumulated text from all utterances
|
|
398
|
+
interimTranscript, // string | null — current partial transcript
|
|
399
|
+
turnMetrics, // VoiceTurnMetrics | null — latest terminal STT summary
|
|
400
|
+
isListening, // boolean
|
|
401
|
+
audioLevel, // number (0–1)
|
|
402
|
+
isMuted, // boolean
|
|
403
|
+
error, // string | null
|
|
404
|
+
start, // () => Promise<void>
|
|
405
|
+
stop, // () => void
|
|
406
|
+
toggleMute, // () => void
|
|
407
|
+
clear // () => void — clear accumulated transcript
|
|
408
|
+
} = useVoiceInput({ agent: "DictationAgent" });
|
|
409
|
+
|
|
410
|
+
return (
|
|
411
|
+
<div>
|
|
412
|
+
<textarea
|
|
413
|
+
value={transcript + (interimTranscript ? " " + interimTranscript : "")}
|
|
414
|
+
readOnly
|
|
415
|
+
/>
|
|
416
|
+
<button onClick={isListening ? stop : start}>
|
|
417
|
+
{isListening ? "Stop" : "Dictate"}
|
|
418
|
+
</button>
|
|
419
|
+
</div>
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
## Client API: `VoiceClient`
|
|
425
|
+
|
|
426
|
+
Framework-agnostic client for environments without React.
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
import { VoiceClient } from "agents/voice/client";
|
|
430
|
+
|
|
431
|
+
const client = new VoiceClient({ agent: "MyAgent" });
|
|
432
|
+
|
|
433
|
+
client.addEventListener("statuschange", (status) => {
|
|
434
|
+
console.log("Status:", status);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
client.addEventListener("transcriptchange", (messages) => {
|
|
438
|
+
console.log("Transcript:", messages);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
client.addEventListener("error", (err) => {
|
|
442
|
+
console.error("Error:", err);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
client.connect();
|
|
446
|
+
await client.startCall();
|
|
447
|
+
|
|
448
|
+
// Switch assistant playback without reconnecting the call.
|
|
449
|
+
await client.setOutputDevice(selectedSpeakerId);
|
|
450
|
+
|
|
451
|
+
// Later:
|
|
452
|
+
client.endCall();
|
|
453
|
+
client.disconnect();
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Events
|
|
457
|
+
|
|
458
|
+
| Event | Data Type | Description |
|
|
459
|
+
| ------------------- | --------------------- | ---------------------------------------- |
|
|
460
|
+
| `statuschange` | `VoiceStatus` | Pipeline state changed |
|
|
461
|
+
| `transcriptchange` | `TranscriptMessage[]` | Transcript updated |
|
|
462
|
+
| `interimtranscript` | `string \| null` | Interim transcript from streaming STT |
|
|
463
|
+
| `turnmetrics` | `VoiceTurnMetrics` | Stable terminal per-turn summary metrics |
|
|
464
|
+
| `audiolevelchange` | `number` | Mic audio level (0–1) |
|
|
465
|
+
| `connectionchange` | `boolean` | WebSocket connected/disconnected |
|
|
466
|
+
| `mutechange` | `boolean` | Mute state changed |
|
|
467
|
+
| `error` | `string \| null` | Error occurred |
|
|
468
|
+
| `outputdeviceerror` | `string \| null` | Non-fatal speaker routing issue |
|
|
469
|
+
| `custommessage` | `unknown` | Non-voice message from server |
|
|
470
|
+
|
|
471
|
+
### Advanced Options
|
|
472
|
+
|
|
473
|
+
| Option | Type | Description |
|
|
474
|
+
| ----------------- | ------------------ | ----------------------------------------------------- |
|
|
475
|
+
| `transport` | `VoiceTransport` | Custom transport (default: WebSocket via PartySocket) |
|
|
476
|
+
| `audioInput` | `VoiceAudioInput` | Custom mic capture (default: built-in AudioWorklet) |
|
|
477
|
+
| `preferredFormat` | `VoiceAudioFormat` | Hint for server audio format (advisory only) |
|
|
478
|
+
| `outputDeviceId` | `string` | Preferred `audiooutput` device for assistant playback |
|
|
479
|
+
|
|
480
|
+
## Providers
|
|
481
|
+
|
|
482
|
+
### Built-in (Workers AI)
|
|
483
|
+
|
|
484
|
+
No API keys required — use your Workers AI binding:
|
|
485
|
+
|
|
486
|
+
| Class | Type | Default Model | Recommended for |
|
|
487
|
+
| ------------------- | -------------- | --------------------- | ---------------- |
|
|
488
|
+
| `WorkersAIFluxSTT` | Continuous STT | `@cf/deepgram/flux` | `withVoice` |
|
|
489
|
+
| `WorkersAINova3STT` | Continuous STT | `@cf/deepgram/nova-3` | `withVoiceInput` |
|
|
490
|
+
| `WorkersAITTS` | TTS | `@cf/deepgram/aura-1` | Both |
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
import {
|
|
494
|
+
WorkersAIFluxSTT,
|
|
495
|
+
WorkersAINova3STT,
|
|
496
|
+
WorkersAITTS
|
|
497
|
+
} from "agents/voice";
|
|
498
|
+
|
|
499
|
+
transcriber = new WorkersAIFluxSTT(this.env.AI);
|
|
500
|
+
tts = new WorkersAITTS(this.env.AI);
|
|
501
|
+
|
|
502
|
+
// Custom options
|
|
503
|
+
transcriber = new WorkersAIFluxSTT(this.env.AI, {
|
|
504
|
+
eotThreshold: 0.8,
|
|
505
|
+
keyterms: ["Cloudflare", "Workers"]
|
|
506
|
+
});
|
|
507
|
+
tts = new WorkersAITTS(this.env.AI, {
|
|
508
|
+
model: "@cf/deepgram/aura-1",
|
|
509
|
+
speaker: "asteria"
|
|
510
|
+
});
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### Third-Party Providers
|
|
514
|
+
|
|
515
|
+
| Package | Class | Description |
|
|
516
|
+
| ------------------------------ | -------------------------------- | ------------------------------------------- |
|
|
517
|
+
| `@cloudflare/voice-assemblyai` | `AssemblyAISTT` | Continuous STT (Universal 3.5 Pro Realtime) |
|
|
518
|
+
| `@cloudflare/voice-deepgram` | `DeepgramSTT` | Continuous STT |
|
|
519
|
+
| `@cloudflare/voice-elevenlabs` | `ElevenLabsSTT`, `ElevenLabsTTS` | Continuous STT and high-quality TTS |
|
|
520
|
+
| `@cloudflare/voice-telnyx` | `TelnyxSTT`, `TelnyxTTS` | Continuous STT, TTS, and phone transport |
|
|
521
|
+
| `@cloudflare/voice-twilio` | Twilio adapter | Telephony (phone calls) |
|
|
522
|
+
|
|
523
|
+
**AssemblyAI STT:**
|
|
524
|
+
|
|
525
|
+
```typescript
|
|
526
|
+
import { AssemblyAISTT } from "@cloudflare/voice-assemblyai";
|
|
527
|
+
|
|
528
|
+
export class MyAgent extends VoiceAgent<Env> {
|
|
529
|
+
transcriber = new AssemblyAISTT({
|
|
530
|
+
apiKey: this.env.ASSEMBLYAI_API_KEY
|
|
531
|
+
});
|
|
532
|
+
tts = new WorkersAITTS(this.env.AI);
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
After each agent reply, the pipeline automatically feeds the spoken text back to AssemblyAI as conversational context (`agent_context`), improving recognition of short answers like "yes" or "7pm". See the [package README](https://github.com/cloudflare/agents/tree/main/voice-providers/assemblyai) for the full options table.
|
|
537
|
+
|
|
538
|
+
**ElevenLabs STT:**
|
|
539
|
+
|
|
540
|
+
```typescript
|
|
541
|
+
import { ElevenLabsSTT } from "@cloudflare/voice-elevenlabs";
|
|
542
|
+
|
|
543
|
+
export class MyAgent extends VoiceAgent<Env> {
|
|
544
|
+
transcriber = new ElevenLabsSTT({
|
|
545
|
+
apiKey: this.env.ELEVENLABS_API_KEY,
|
|
546
|
+
keyterms: ["Cloudflare", "Workers AI"]
|
|
547
|
+
});
|
|
548
|
+
tts = new WorkersAITTS(this.env.AI);
|
|
549
|
+
}
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
**ElevenLabs TTS:**
|
|
553
|
+
|
|
554
|
+
```typescript
|
|
555
|
+
import { ElevenLabsTTS } from "@cloudflare/voice-elevenlabs";
|
|
556
|
+
|
|
557
|
+
export class MyAgent extends VoiceAgent<Env> {
|
|
558
|
+
transcriber = new WorkersAIFluxSTT(this.env.AI);
|
|
559
|
+
tts = new ElevenLabsTTS({
|
|
560
|
+
apiKey: this.env.ELEVENLABS_API_KEY,
|
|
561
|
+
voiceId: "21m00Tcm4TlvDq8ikWAM"
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
**Deepgram STT:**
|
|
567
|
+
|
|
568
|
+
```typescript
|
|
569
|
+
import { DeepgramSTT } from "@cloudflare/voice-deepgram";
|
|
570
|
+
|
|
571
|
+
export class MyAgent extends VoiceAgent<Env> {
|
|
572
|
+
transcriber = new DeepgramSTT({
|
|
573
|
+
apiKey: this.env.DEEPGRAM_API_KEY
|
|
574
|
+
});
|
|
575
|
+
tts = new WorkersAITTS(this.env.AI);
|
|
576
|
+
}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
## Continuous STT
|
|
580
|
+
|
|
581
|
+
The transcriber session is created at `start_call` and lives for the entire call. All audio is fed continuously — the model handles speech boundary detection (turn detection). The client receives `transcript_interim` messages with partial results as the user speaks.
|
|
582
|
+
|
|
583
|
+
```typescript
|
|
584
|
+
export class MyAgent extends VoiceAgent<Env> {
|
|
585
|
+
transcriber = new DeepgramSTT({
|
|
586
|
+
apiKey: this.env.DEEPGRAM_API_KEY
|
|
587
|
+
});
|
|
588
|
+
tts = new WorkersAITTS(this.env.AI);
|
|
589
|
+
|
|
590
|
+
async onTurn(transcript: string, context: VoiceTurnContext) {
|
|
591
|
+
return "You said: " + transcript;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
The client displays interim transcripts automatically:
|
|
597
|
+
|
|
598
|
+
```tsx
|
|
599
|
+
const { interimTranscript, transcript } = useVoiceAgent({ agent: "MyAgent" });
|
|
600
|
+
|
|
601
|
+
// interimTranscript updates in real time as the user speaks
|
|
602
|
+
// transcript contains finalized messages
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
All transcriber providers use **model-driven turn detection** — the model detects when the user has finished speaking and triggers the pipeline. The client does not need to send `end_of_speech` for STT; `start_of_speech` and `end_of_speech` are only used for client-side UI state (speaking indicators, audio level).
|
|
606
|
+
|
|
607
|
+
## Text Messages
|
|
608
|
+
|
|
609
|
+
`withVoice` agents can also receive text messages, bypassing STT entirely. This is useful for chat-style input alongside voice.
|
|
610
|
+
|
|
611
|
+
**Client:**
|
|
612
|
+
|
|
613
|
+
```tsx
|
|
614
|
+
const { sendText } = useVoiceAgent({ agent: "MyAgent" });
|
|
615
|
+
|
|
616
|
+
// Send text — goes straight to onTurn() without STT
|
|
617
|
+
sendText("What is the weather like today?");
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
Text messages work both during and outside of active calls. During a call, the response is spoken aloud via TTS. Outside a call, the response is sent as text-only transcript messages.
|
|
621
|
+
|
|
622
|
+
## Custom Messages
|
|
623
|
+
|
|
624
|
+
Send and receive application-level JSON messages alongside voice protocol messages. Non-voice messages pass through to your `onMessage` handler on the server and emit `custommessage` events on the client.
|
|
625
|
+
|
|
626
|
+
**Server:**
|
|
627
|
+
|
|
628
|
+
```typescript
|
|
629
|
+
export class MyAgent extends VoiceAgent<Env> {
|
|
630
|
+
onMessage(connection: Connection, message: WSMessage) {
|
|
631
|
+
const data = JSON.parse(message as string);
|
|
632
|
+
if (data.type === "kick_speaker") {
|
|
633
|
+
this.forceEndCall(connection);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
**Client:**
|
|
640
|
+
|
|
641
|
+
```tsx
|
|
642
|
+
const { sendJSON, lastCustomMessage } = useVoiceAgent({ agent: "MyAgent" });
|
|
643
|
+
|
|
644
|
+
// Send custom JSON
|
|
645
|
+
sendJSON({ type: "kick_speaker" });
|
|
646
|
+
|
|
647
|
+
// Receive custom messages
|
|
648
|
+
useEffect(() => {
|
|
649
|
+
if (lastCustomMessage) {
|
|
650
|
+
console.log("Custom message:", lastCustomMessage);
|
|
651
|
+
}
|
|
652
|
+
}, [lastCustomMessage]);
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
## Single-Speaker Enforcement
|
|
656
|
+
|
|
657
|
+
Use `beforeCallStart` to restrict who can start a call. This example enforces single-speaker — only one connection can be the active speaker at a time:
|
|
658
|
+
|
|
659
|
+
```typescript
|
|
660
|
+
export class MyAgent extends VoiceAgent<Env> {
|
|
661
|
+
#speakerId: string | null = null;
|
|
662
|
+
|
|
663
|
+
beforeCallStart(connection: Connection) {
|
|
664
|
+
if (this.#speakerId !== null) {
|
|
665
|
+
return false; // reject — someone else is speaking
|
|
666
|
+
}
|
|
667
|
+
this.#speakerId = connection.id;
|
|
668
|
+
return true;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
onCallEnd(connection: Connection) {
|
|
672
|
+
if (this.#speakerId === connection.id) {
|
|
673
|
+
this.#speakerId = null;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
## Telephony (Twilio)
|
|
680
|
+
|
|
681
|
+
Connect phone calls to your voice agent using the Twilio adapter:
|
|
682
|
+
|
|
683
|
+
```sh
|
|
684
|
+
npm install @cloudflare/voice-twilio
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
The adapter bridges Twilio Media Streams to your VoiceAgent:
|
|
688
|
+
|
|
689
|
+
```
|
|
690
|
+
Phone → Twilio → WebSocket → TwilioAdapter → WebSocket → VoiceAgent
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
**Important:** `WorkersAITTS` returns MP3, which cannot be decoded to PCM in the Workers runtime. When using the Twilio adapter, use a TTS provider that outputs raw PCM (for example, ElevenLabs with `outputFormat: "pcm_16000"`).
|
|
694
|
+
|
|
695
|
+
## Pipeline metrics
|
|
696
|
+
|
|
697
|
+
`VoiceTurnMetrics` is emitted once for every allocated speech or text turn, including aborted, skipped, empty, model-error, and TTS-error outcomes. `turnId`, `source`, and `outcome` are dimensions used to correlate and interpret the timing fields; they are not measurements. Unreached timings are omitted rather than set to zero. All durations use the Worker clock, overlap, and are not additive.
|
|
698
|
+
|
|
699
|
+
```typescript
|
|
700
|
+
client.addEventListener("turnmetrics", (turnMetrics) => {
|
|
701
|
+
console.log(turnMetrics.turnId, turnMetrics.outcome);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
client.turnMetrics; // VoiceTurnMetrics | null, the last terminal summary
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
`useVoiceAgent()` and `useVoiceInput()` expose the same last value as `turnMetrics`. `withVoiceInput` emits the speech, `afterTranscribe`, and total timings it can measure. Model and TTS timings remain absent.
|
|
708
|
+
|
|
709
|
+
| Kind | Stable fields when available |
|
|
710
|
+
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
711
|
+
| Dimensions | `turnId`, `source`, `outcome` |
|
|
712
|
+
| Speech timing | `speechStartToFirstInterimMs`, `speechStartToFinalMs` |
|
|
713
|
+
| Turn and model timing | `afterTranscribeMs`, `modelToFirstTextMs`, `exposedReasoningMs`, `modelStreamConsumptionMs`, `finalInputToFirstAudioMs`, `turnTotalMs` |
|
|
714
|
+
| TTS timing | `ttsToFirstAudioMs`, `ttsWallMs`, cumulative overlapping `ttsWorkMs` |
|
|
715
|
+
|
|
716
|
+
Terminal outcomes are `completed`, `no_output`, `output_limit`, `content_filtered`, `model_error`, `tts_error`, `aborted`, `skipped`, and `error`.
|
|
717
|
+
|
|
718
|
+
```tsx
|
|
719
|
+
const { turnMetrics } = useVoiceAgent({ agent: "MyAgent" });
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
## Conversation History
|
|
723
|
+
|
|
724
|
+
`withVoice` automatically persists conversation messages to SQLite. In `onTurn()`, `context.messages` is a snapshot of the completed history before the current transcript. The pipeline persists the current transcript before invoking the hook, so a direct `getConversationHistory()` call inside `onTurn()` includes it.
|
|
725
|
+
|
|
726
|
+
```typescript
|
|
727
|
+
// Get stored history, including the current transcript during onTurn()
|
|
728
|
+
const history = this.getConversationHistory(20);
|
|
729
|
+
|
|
730
|
+
// Manually save a message
|
|
731
|
+
this.saveMessage("assistant", "Welcome! How can I help?");
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
History survives Durable Object restarts and client reconnections. Voice agents use `keepAlive` to prevent eviction during active calls.
|
|
735
|
+
|
|
736
|
+
## Examples
|
|
737
|
+
|
|
738
|
+
- [`examples/voice-agent`](https://github.com/cloudflare/agents/tree/main/examples/voice-agent) — full voice agent with Workers AI, AssemblyAI, Telnyx, and ElevenLabs STT provider options
|
|
739
|
+
- [`examples/voice-input`](https://github.com/cloudflare/agents/tree/main/examples/voice-input) — voice input (dictation) example
|
|
740
|
+
|
|
741
|
+
## Related
|
|
742
|
+
|
|
743
|
+
- [Agent Class](https://github.com/cloudflare/agents/blob/main/docs/agents/agent-class.md) — understanding the base Agent class
|
|
744
|
+
- [Chat Agents](https://github.com/cloudflare/agents/blob/main/docs/agents/chat-agents.md) — text-based AI chat agents
|
|
745
|
+
- [State Management](https://github.com/cloudflare/agents/blob/main/docs/agents/state.md) — managing agent state
|