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.
Files changed (186) hide show
  1. package/README.md +20 -1
  2. package/dist/{agent-routing-CnkaHb-v.d.ts → agent-routing-DE5zmCQ8.d.ts} +1038 -394
  3. package/dist/agent-routing.d.ts +1 -1
  4. package/dist/agent-tool-types.d.ts +26 -26
  5. package/dist/{agent-tools-C0XJqWzB.d.ts → agent-tools-DtXMTDGM.d.ts} +7 -7
  6. package/dist/agent-tools.d.ts +21 -21
  7. package/dist/browser/ai.js +4 -1
  8. package/dist/browser/ai.js.map +1 -1
  9. package/dist/browser/tanstack-ai.js.map +1 -1
  10. package/dist/callable-decorator-DP__HhBA.d.ts +72 -0
  11. package/dist/callable-decorator.d.ts +18 -0
  12. package/dist/callable-decorator.js +71 -0
  13. package/dist/callable-decorator.js.map +1 -0
  14. package/dist/capability-BjSKYpzg.js.map +1 -1
  15. package/dist/capability-runner-Be_-PLR1.d.ts +459 -0
  16. package/dist/channel-Bnm4S7T2.d.ts +491 -0
  17. package/dist/channels/ai-sdk.d.ts +46 -0
  18. package/dist/channels/ai-sdk.js +120 -0
  19. package/dist/channels/ai-sdk.js.map +1 -0
  20. package/dist/channels/email.d.ts +95 -0
  21. package/dist/channels/email.js +323 -0
  22. package/dist/channels/email.js.map +1 -0
  23. package/dist/channels/index.d.ts +233 -0
  24. package/dist/channels/index.js +608 -0
  25. package/dist/channels/index.js.map +1 -0
  26. package/dist/channels/slack.d.ts +140 -0
  27. package/dist/channels/slack.js +614 -0
  28. package/dist/channels/slack.js.map +1 -0
  29. package/dist/channels/tanstack-ai.d.ts +39 -0
  30. package/dist/channels/tanstack-ai.js +17 -0
  31. package/dist/channels/tanstack-ai.js.map +1 -0
  32. package/dist/channels/telegram.d.ts +106 -0
  33. package/dist/channels/telegram.js +427 -0
  34. package/dist/channels/telegram.js.map +1 -0
  35. package/dist/channels/voice.d.ts +45 -0
  36. package/dist/channels/voice.js +122 -0
  37. package/dist/channels/voice.js.map +1 -0
  38. package/dist/chat/index.d.ts +2325 -2019
  39. package/dist/chat/index.js +888 -518
  40. package/dist/chat/index.js.map +1 -1
  41. package/dist/chat-sdk/index.d.ts +7 -7
  42. package/dist/chat-sdk/index.js +1 -1
  43. package/dist/client.d.ts +1 -1
  44. package/dist/context/index.d.ts +216 -0
  45. package/dist/context/index.js +454 -0
  46. package/dist/context/index.js.map +1 -0
  47. package/dist/{current-agent-CuMErtly.d.ts → current-agent-Da_C9a3b.d.ts} +90 -107
  48. package/dist/current-agent-DhoDkSnH.js.map +1 -1
  49. package/dist/{diagnostics-CaBjfz4J.js → diagnostics-BzvaX2UT.js} +5 -1
  50. package/dist/diagnostics-BzvaX2UT.js.map +1 -0
  51. package/dist/diagnostics-C4jcz3VK.js +360 -0
  52. package/dist/diagnostics-C4jcz3VK.js.map +1 -0
  53. package/dist/index-BB0kqhIz.d.ts +101 -0
  54. package/dist/index-BVVgDSdq.d.ts +1 -0
  55. package/dist/index-XDkuQ7zm.d.ts +89 -0
  56. package/dist/{index-DcSAZKsB.d.ts → index-YSKgfgg9.d.ts} +3 -1
  57. package/dist/index.d.ts +91 -82
  58. package/dist/index.js +3 -2
  59. package/dist/ingress-BfetZbMO.js +83 -0
  60. package/dist/ingress-BfetZbMO.js.map +1 -0
  61. package/dist/internal-CYlgHl1l.js +59 -0
  62. package/dist/internal-CYlgHl1l.js.map +1 -0
  63. package/dist/internal_context.d.ts +1 -1
  64. package/dist/lifecycle/index.d.ts +35 -17
  65. package/dist/lifecycle/index.js +1 -1
  66. package/dist/lifecycle-CMRGjZdw.js +1299 -0
  67. package/dist/lifecycle-CMRGjZdw.js.map +1 -0
  68. package/dist/mcp/client/index.d.ts +20 -20
  69. package/dist/mcp/index.d.ts +35 -35
  70. package/dist/mcp/index.js +1 -1
  71. package/dist/observability/index.d.ts +1 -1
  72. package/dist/observability/index.js +1 -1
  73. package/dist/react.d.ts +4 -4
  74. package/dist/{retries-CAvxtG9d.d.ts → retries-D9Ds-1lz.d.ts} +17 -6
  75. package/dist/retries.d.ts +8 -6
  76. package/dist/retries.js +13 -1
  77. package/dist/retries.js.map +1 -1
  78. package/dist/routing/index.d.ts +137 -0
  79. package/dist/routing/index.js +244 -0
  80. package/dist/routing/index.js.map +1 -0
  81. package/dist/sanitize-D9TujEK8.js +79 -0
  82. package/dist/sanitize-D9TujEK8.js.map +1 -0
  83. package/dist/scheduler-DD9NdYbF.js +665 -0
  84. package/dist/scheduler-DD9NdYbF.js.map +1 -0
  85. package/dist/{scheduler-DQoTGoAW.d.ts → scheduler-Dwh85ZGl.d.ts} +21 -22
  86. package/dist/schedules/index.d.ts +1 -1
  87. package/dist/schedules/index.js +1 -1
  88. package/dist/sentence-chunker-BAidJ4DA.d.ts +68 -0
  89. package/dist/serializable.d.ts +1 -1
  90. package/dist/sessions/index.d.ts +441 -0
  91. package/dist/sessions/index.js +2063 -0
  92. package/dist/sessions/index.js.map +1 -0
  93. package/dist/skills/index.d.ts +99 -0
  94. package/dist/skills/index.js +254 -5
  95. package/dist/skills/index.js.map +1 -1
  96. package/dist/{src-5W6JNKVb.js → src-DlSHshb2.js} +1460 -1110
  97. package/dist/src-DlSHshb2.js.map +1 -0
  98. package/dist/streams/index.d.ts +120 -0
  99. package/dist/streams/index.js +107 -0
  100. package/dist/streams/index.js.map +1 -0
  101. package/dist/streams-D6tJ0NN9.d.ts +370 -0
  102. package/dist/streams-DZKgAj9b.js +709 -0
  103. package/dist/streams-DZKgAj9b.js.map +1 -0
  104. package/dist/sub-routing.d.ts +12 -12
  105. package/dist/surface-bZZJqBka.js +17 -0
  106. package/dist/surface-bZZJqBka.js.map +1 -0
  107. package/dist/tasks/index.d.ts +64 -0
  108. package/dist/tasks/index.js +2 -0
  109. package/dist/tasks-BRJ5zgya.d.ts +517 -0
  110. package/dist/tasks-ylZgBjhj.js +1656 -0
  111. package/dist/tasks-ylZgBjhj.js.map +1 -0
  112. package/dist/text-segment-joiner-BtAFQSA_.js +57 -0
  113. package/dist/text-segment-joiner-BtAFQSA_.js.map +1 -0
  114. package/dist/text-stream-CpdiKrJB.js +272 -0
  115. package/dist/text-stream-CpdiKrJB.js.map +1 -0
  116. package/dist/tokens-nHAKcN6M.js +52 -0
  117. package/dist/tokens-nHAKcN6M.js.map +1 -0
  118. package/dist/tool-schema-CBjGPrsQ.js +31 -0
  119. package/dist/tool-schema-CBjGPrsQ.js.map +1 -0
  120. package/dist/types-B7LojTe4.d.ts +202 -0
  121. package/dist/types-_Faxb570.d.ts +439 -0
  122. package/dist/voice/client.d.ts +226 -0
  123. package/dist/voice/client.js +932 -0
  124. package/dist/voice/client.js.map +1 -0
  125. package/dist/voice/errors.d.ts +43 -0
  126. package/dist/voice/errors.js +41 -0
  127. package/dist/voice/errors.js.map +1 -0
  128. package/dist/voice/index.d.ts +271 -0
  129. package/dist/voice/index.js +1812 -0
  130. package/dist/voice/index.js.map +1 -0
  131. package/dist/voice/react.d.ts +167 -0
  132. package/dist/voice/react.js +234 -0
  133. package/dist/voice/react.js.map +1 -0
  134. package/dist/voice/sfu.d.ts +71 -0
  135. package/dist/voice/sfu.js +157 -0
  136. package/dist/voice/sfu.js.map +1 -0
  137. package/dist/voice/text.d.ts +6 -0
  138. package/dist/voice/text.js +2 -0
  139. package/dist/voice/types.d.ts +58 -0
  140. package/dist/voice/types.js +18 -0
  141. package/dist/voice/types.js.map +1 -0
  142. package/dist/voice/workers-ai.d.ts +136 -0
  143. package/dist/voice/workers-ai.js +568 -0
  144. package/dist/voice/workers-ai.js.map +1 -0
  145. package/dist/websockets/index.d.ts +192 -0
  146. package/dist/websockets/index.js +2 -0
  147. package/dist/websockets-DUfRHPRq.js +502 -0
  148. package/dist/websockets-DUfRHPRq.js.map +1 -0
  149. package/dist/workflow-types.d.ts +25 -25
  150. package/dist/workflows.d.ts +21 -21
  151. package/dist/workflows.js +1 -1
  152. package/docs/agent-class.md +2 -2
  153. package/docs/agent-tools.md +2 -1
  154. package/docs/channels.md +323 -0
  155. package/docs/chat-agents.md +6 -13
  156. package/docs/context.md +131 -0
  157. package/docs/index.md +15 -12
  158. package/docs/lifecycle.md +102 -55
  159. package/docs/long-running-agents.md +2 -2
  160. package/docs/mcp-servers.md +5 -1
  161. package/docs/resumable-streaming.md +1 -1
  162. package/docs/routing.md +105 -0
  163. package/docs/sessions.md +237 -871
  164. package/docs/streams.md +213 -0
  165. package/docs/sub-agents.md +184 -124
  166. package/docs/tasks.md +246 -0
  167. package/docs/voice.md +745 -0
  168. package/package.json +115 -13
  169. package/dist/capability-runner-CvHGZqUu.d.ts +0 -150
  170. package/dist/compaction-helpers-iiKMr2TQ.js +0 -340
  171. package/dist/compaction-helpers-iiKMr2TQ.js.map +0 -1
  172. package/dist/compaction-helpers-wUz6M3us.d.ts +0 -621
  173. package/dist/diagnostics-CaBjfz4J.js.map +0 -1
  174. package/dist/durable-object-lifecycle-D6nNQJJd.js +0 -862
  175. package/dist/durable-object-lifecycle-D6nNQJJd.js.map +0 -1
  176. package/dist/experimental/memory/session/index.d.ts +0 -671
  177. package/dist/experimental/memory/session/index.js +0 -2379
  178. package/dist/experimental/memory/session/index.js.map +0 -1
  179. package/dist/experimental/memory/utils/index.d.ts +0 -96
  180. package/dist/experimental/memory/utils/index.js +0 -79
  181. package/dist/experimental/memory/utils/index.js.map +0 -1
  182. package/dist/scheduler-CR9RHGos.js +0 -857
  183. package/dist/scheduler-CR9RHGos.js.map +0 -1
  184. package/dist/src-5W6JNKVb.js.map +0 -1
  185. package/dist/tool-output-truncation-CNnnGZQ3.js +0 -98
  186. 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