AIニュース最前線
最新ニュースAI日報Hacker日報週報動画AIツールトレンド企業

AIニュース最前線

世界中のAI最新情報を日本語で毎時更新

最新ニュース日報トレンド企業プレミアムRSS
© 2026 ainew.jp特定商取引法に基づく表記
ニュース一覧元記事を開く
Cloudflare Blog·2026年4月15日 22:00·約17分で読める

エージェントに音声機能を追加

#AIエージェント#リアルタイム音声処理#Cloudflare Agents SDK#Deepgram#モジュール型設計
TL;DR

CloudflareがAgents SDKに実験的な音声パイプライン「@cloudflare/voice」を追加し、既存のアーキテクチャを維持したままリアルタイム音声対話型AIエージェントの開発を可能にした。

AI深層分析2026年4月15日 23:48
4
重要/ 5段階
深度40%
3
関連度30%
4
実用性20%
4
革新性10%
3

キーポイント

1

既存アーキテクチャとのシームレス統合

Durable ObjectやWebSocket接続モデル、SQLite履歴を維持したまま、エージェントに音声機能を追加できる設計を採用。

2

多様な開発者向けAPIとフック提供

会話型エージェント用「withVoice」、音声入力専用「withVoiceInput」、Reactフック、フレームワーク非依存クライアントを提供。

3

Workers AI経由のビルトイン音声プロバイダー

DeepgramのSTT(Flux/Nova 3)とTTS(Aura)をWorkers AIに統合し、外部APIキーなしで開発を開始可能にした。

4

モジュール型プロバイダインターフェース設計

音声・電話・トランスポートのプロバイダインターフェースを小型化し、開発者が用途に応じてコンポーネントを自由に組み合せることを重視。

5

要点タイトル

1-2文の説明

6

withVoiceInputによる入力特化型設計

辞書入力や音声検索など、返答に音声が不要なケース向けに設計されており、会話ループを簡素化できる。

7

単一接続でのマルチモーダル処理

STTをバイパスしてテキストを送信しつつ、音声応答も同時に処理できるため、実装を分けることなく統合されたエージェントが構築可能。

影響分析・編集コメントを表示

影響分析

Cloudflareのこのアップデートは、AIエージェント開発における「音声インターフェースの実装コスト」を大幅に削減する。既存のサーバーレス/エッジアーキテクチャとシームレスに統合できるため、開発者は複雑な音声フレームワークの移行なしに双方向音声アプリを構築可能になる。ただし実験的パッケージであり、プロダクション利用には安定性検証とカスタムプロバイダ実装の学習コストが伴う点に注意が必要だ。

編集コメント

音声エージェントの実装ハードルを下げつつ、Cloudflareのサーバーレスエコシステムへの統合を促す戦略的なリリースと言える。実験段階ではあるものの、モジュール設計が業界標準の「エージェントOS」構築に寄与する可能性を秘めている。

私たち多くの人が AI エージェントと接する最初の経験は、チャットボックスに入力することでした。そして、日常的にエージェントを使用している私たちにとって、それらを誘導するために詳細なプロンプトやマークダウンファイルを書くことに長けているはずです。

しかし、エージェントが最も有用となる瞬間は、必ずしもテキスト中心ではない場合があります。長時間の通勤中である場合、複数のオープンセッションを同時に処理している場合、あるいは単にエージェントと自然に会話し、その返答を受け取り、対話を継続したい場合などです。

エージェントに音声機能を追加するために、そのエージェントを別の音声フレームワークに移行する必要はありません。本日、Agents SDK 用の実験的な音声パイプラインをリリースします。

@cloudflare/voice を使用すれば、すでに使用している同じエージェントアーキテクチャにリアルタイム音声を追加できます。音声は、単に同じ Durable Object に対して、Agents SDK が既に提供しているのと同じツール、永続性、WebSocket 接続モデルを用いて対話する別の方法となります。

@cloudflare/voice は、Agents SDK 用の実験的なパッケージであり、以下の機能を提供します:

完全な会話型音声エージェント向けの withVoice(Agent)

Dictation や音声検索など、テキストへの音声変換(Speech-to-Text)のみのユースケース向けの withVoiceInput(Agent)

React アプリケーション向けの useVoiceAgent および useVoiceInput フック

フレームワーク非依存のクライアント向けの VoiceClient

外部 API キーなしで開始できるようにする、組み込みの Workers AI プロバイダー:

Deepgram Flux による継続的な STT(Speech-to-Text)

Deepgram Nova 3 による継続的な STT(Speech-to-Text)

Deepgram Aura による Text-to-Speech

これは、単一の WebSocket 接続を通じてユーザーとリアルタイムに会話できるエージェントを構築可能であることを意味します。同時に、同じ Agent クラス、Durable Object インスタンス、そして SQLite をバックエンドとする会話履歴を維持したままです。

それと同様に重要なのは、これが単一の固定されたデフォルトスタックに限定されないことです。@cloudflare/voice 内のプロバイダーインターフェースは意図的に小さく設計されており、音声、テレフォニー、トランスポートのプロバイダーが私たちと共に開発することを望んでいます。これにより、開発者は単一の音声アーキテクチャに縛られることなく、ユースケースに適したコンポーネントを自由に組み合わせることができます。

音声機能の始め方

Agents SDK における音声エージェントの最小限のサーバーサイドパターンは以下の通りです:

import { Agent, routeAgentRequest } from "agents";

import {

withVoice,

WorkersAIFluxSTT,

WorkersAITTS,

type VoiceTurnContext

} 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: string, context: VoiceTurnContext) {

return You said: ${transcript};

}

}

export default {

async fetch(request: Request, env: Env) {

return (

(await routeAgentRequest(request, env)) ??

new Response("Not found", { status: 404 })

);

}

} satisfies ExportedHandler;

これがサーバー全体の構成です。ここに連続トランスクリプター(continuous transcriber)とテキスト読み上げプロバイダー(text-to-speech provider)を追加し、onTurn() を実装します。

クライアント側では、React フックを使用して接続できます:

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

function App() {

const {

status,

transcript,

interimTranscript,

startCall,

endCall,

toggleMute

} = useVoiceAgent({ agent: "my-agent" });

return (

Status: {status}

{interimTranscript && *{interimTranscript}*

}

{transcript.map((msg, i) => (

  • {msg.role}: {msg.text}

))}

Start Call

End Call

Mute / Unmute

);

}

React を使用していない場合は、@cloudflare/voice/client から直接 VoiceClient を使用できます。

How the voice pipeline works

Agents SDK では、すべてのエージェントは Durable Object(耐久性のあるオブジェクト)です。これは、独自の SQLite データベース、WebSocket 接続、およびアプリケーションロジックを持つ、状態を保持しアドレス可能なサーバーインスタンスです。音声パイプラインはこのモデルを置き換えるのではなく、拡張するものです。

高レベルでは、フローは以下のようになります:

imageimage

パイプラインの各ステップは以下の通り分解されます:

オーディオ転送:ブラウザはマイクからの音声をキャプチャし、エージェントが既に使用している同じ WebSocket 接続を通じて、16 kHz モノラル PCM でストリーミングします。

STT セッションのセットアップ:通話開始時、エージェントは通話全体を通じて存続する継続的なトランスクリバー・セッションを作成します。

STT 入力:音声ストリームは、そのセッションに継続的に流入します。

STT 話者検出(Turn Detection):音声からテキストへの変換モデル自体が、ユーザーの発話が完了したと判断し、その話者(ターン)に対する安定した文字起こし結果を出力します。

LLM/アプリケーションロジック:音声パイプラインは、その文字起こし結果をあなたの onTurn() メソッドに渡します。

TTS 出力:応答は音声に合成され、クライアントへ送信されます。onTurn() がストリームを返す場合、パイプラインはそれを文単位に分割し、文が準備されるたびに音声の送信を開始します。

永続化:ユーザーとエージェントのメッセージは SQLite に保存されるため、再接続やデプロイ後も会話履歴が保持されます。

なぜ音声はエージェントの他の機能ととも成長すべきか

多くの音声フレームワークは、音声ループそのものに焦点を当てています:入力された音声を文字起こしし、モデルの応答を得て、出力する音声へ変換するという一連の流れです。これらは重要な基本要素ですが、エージェントには音声以外にも多くの側面があります。

本番環境で動作する実際のエージェントは成長していきます。それらは状態管理、スケジューリング、永続化、ツール、ワークフロー、電話回線(テレフォニー)の機能、そしてそれらすべての整合性をチャネル間で維持する方法を必要とします。エージェントの複雑さが増すにつれて、音声は単独の機能ではなく、より大きなシステムの一部分となります。

私たちは、Agents SDK における音声機能もその前提から始める必要がありました。音声を別のスタックとして構築するのではなく、同じ Durable Object ベースのエージェントプラットフォームの上に構築したため、後でアプリケーションを再設計することなく、必要な他のプリミティブを引き継ぐことができます。

音声とテキストは同じ状態を共有します

ユーザーはテキスト入力から始め、音声に切り替え、再びテキストに戻る可能性があります。Agents SDK では、これらはすべて同じエージェントに対する異なる入力に過ぎません。会話履歴は SQLite に保存され、利用可能なツールも同じです。これにより、より明確なメンタルモデルと、推論が容易な非常にシンプルなアプリケーションアーキテクチャが得られます。

低レイテンシは…

短いネットワークパスから生まれます

音声体験は、心地よいものか不快なものかがすぐに判断されます。ユーザーが話しを終えた後、システムは文字起こしを行い、思考し、会話らしく聞こえる速さで応答を話し始める必要があります。

音声レイテンシの多くは、モデル処理時間そのものではありません。それは、異なる場所にある異なるサービス間でオーディオとテキストを行き来するコストです。オーディオは STT(Speech-to-Text)に送信され、文字起こし結果は LLM に渡され、応答は TTS(Text-to-Speech)モデルに送られます。各ハンドオフにはネットワークオーバーヘッドが加算されます。

エージェント SDK の音声パイプラインを使用すると、エージェントは Cloudflare のネットワーク上で実行され、組み込みプロバイダーは Workers AI バインディングを利用します。これによりパイプラインが簡素化され、自分で構築する必要のあるインフラストラクチャの量を削減できます。

組み込みストリーミング

エージェントが最初の文を素早く話す(Time-to-First Audio、初回音声到達時間とも呼ばれる)と、対話体験はより自然に感じられます。onTurn() がストリームを返す場合、パイプラインはそれを文単位に分割し、文が完成するたびに音声合成を開始します。つまり、残りの部分がまだ生成されている間に、ユーザーは回答の冒頭を聞くことができます。

より現実的なバックエンド

以下は、LLM の応答をストリーミングし、文単位でそれを音声として返すより完全な例です:

import { Agent, routeAgentRequest } from "agents";

import {

withVoice,

WorkersAIFluxSTT,

WorkersAITTS,

type VoiceTurnContext

} from "@cloudflare/voice";

import { streamText } from "ai";

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

const VoiceAgent = withVoice(Agent);

export class MyAgent extends VoiceAgent {

transcriber = new WorkersAIFluxSTT(this.env.AI);

tts = new WorkersAITTS(this.env.AI);

async onTurn(transcript: string, context: VoiceTurnContext) {

const ai = createWorkersAI({ binding: this.env.AI });

const result = streamText({

model: ai("@cf/cloudflare/gpt-oss-20b"),

system: "You are a helpful voice assistant. Be concise.",

messages: [

...context.messages.map((m) => ({

role: m.role as "user" | "assistant",

content: m.content

})),

{ role: "user" as const, content: transcript }

],

abortSignal: context.signal

});

return result.textStream;

}

}

export default {

async fetch(request: Request, env: Env) {

return (

(await routeAgentRequest(request, env)) ??

new Response("Not found", { status: 404 })

);

}

} satisfies ExportedHandler;

Context.messages gives you recent SQLite-backed conversation history, and context.signal lets the pipeline abort the LLM call if the user interrupts.

Voice as an input: withVoiceInput

Not every speech interface needs to speak back. Sometimes you might want dictation, transcription, or voice search. For these use cases, you can use withVoiceInput

import { Agent, type Connection } 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: string, _connection: Connection) {

console.log("User said:", text);

}

}

クライアント側では、useVoiceInput を使用して、文字起こしを中核とした軽量インターフェースを取得できます:

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

const { transcript, interimTranscript, isListening, start, stop, clear } =

useVoiceInput({ agent: "DictationAgent" });

これは、音声が入力方法として機能し、完全な会話ループを必要としない場合に有用です。

同一接続上での音声とテキスト

同じクライアントは sendText("What’s the weather?") を呼び出すことができ、これは STT(音声認識)をバイパスしてテキストを直接 onTurn() に送信します。アクティブな通話中、応答は音声として再生され、同時にテキストとしても表示されます。通話外では、テキストのみの状態を維持することも可能です。

これにより、実装を異なるコードパスに分割することなく、真の意味でマルチモーダルなエージェントを実現できます。

その他に何を作れるか?

音声エージェントも依然としてエージェントであるため、通常の Agents SDK の機能はすべて適用されます。

ツールとスケジューリング

セッション開始時に発信者を歓迎する処理を行うことができます:

import { Agent, type Connection } 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: string) {

return You said: ${transcript};

}

async onCallStart(connection: Connection) {

await this.speak(connection, "Hi! How can I help you today?");

}

}

あなたは、他のエージェントと同様に、音声によるリマインダーをスケジュールしたり、LLM(大規模言語モデル)に対してツールを公開したりできます:

import { Agent } from "agents";

import {

withVoice,

WorkersAIFluxSTT,

WorkersAITTS,

type VoiceTurnContext

} from "@cloudflare/voice";

import { streamText, tool } from "ai";

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

import { z } from "zod";

const VoiceAgent = withVoice(Agent);

export class MyAgent extends VoiceAgent {

transcriber = new WorkersAIFluxSTT(this.env.AI);

tts = new WorkersAITTS(this.env.AI);

async speakReminder(payload: { message: string }) {

await this.speakAll(Reminder: ${payload.message});

}

async onTurn(transcript: string, context: VoiceTurnContext) {

const ai = createWorkersAI({ binding: this.env.AI });

const result = streamText({

model: ai("@cf/cloudflare/gpt-oss-20b"),

messages: [

...context.messages.map((m) => ({

role: m.role as "user" | "assistant",

content: m.content

})),

{ role: "user" as const, content: transcript }

],

tools: {

set_reminder: tool({

description: "Set a spoken reminder after a delay",

inputSchema: z.object({

message: z.string(),

delay_seconds: z.number()

}),

execute: async ({ message, delay_seconds }) => {

await this.schedule(delay_seconds, "speakReminder", { message });

return { confirmed: true };

}

})

},

abortSignal: context.signal

});

return result.textStream;

}

}

Runtime model switching

The voice pipeline also lets you choose a transcription model dynamically per connection.

For example, you might prefer Flux for conversational turn-taking and Nova 3 for higher-accuracy dictation. You can switch at runtime by overriding createTranscriber():

import { Agent, type Connection } from "agents";

import {

withVoice,

WorkersAIFluxSTT,

WorkersAINova3STT,

WorkersAITTS,

type Transcriber

} from "@cloudflare/voice";

export class MyAgent extends VoiceAgent {

tts = new WorkersAITTS(this.env.AI);

createTranscriber(connection: Connection): Transcriber {

const url = new URL(connection.url ?? "http://localhost");

const model = url.searchParams.get("model");

if (model === "nova-3") {

return new WorkersAINova3STT(this.env.AI);

}

return new WorkersAIFluxSTT(this.env.AI);

}

}

On the client, you can pass query parameters through the hook:

const voiceAgent = useVoiceAgent({

agent: "my-voice-agent",

query: { model: "nova-3" }

});

Pipeline hooks

You can also intercept data between stages:

afterTranscribe(transcript, connection)

beforeSynthesize(text, connection)

afterSynthesize(audio, text, connection)

These hooks are useful for content filtering, text normalization, language-specific transformations, or custom logging.

Telephone and transport options

By default, the voice pipeline uses a single WebSocket connection as the simplest path for 1:1 voice agents. But that’s not the only option.

Phone calls via Twilio

You can connect phone calls to the same agent using the Twilio adapter:

import { TwilioAdapter } from "@cloudflare/voice-twilio";

export default {

async fetch(request: Request, env: Env) {

if (new URL(request.url).pathname === "/twilio") {

return TwilioAdapter.handleRequest(request, env, "MyAgent");

}

return (

(await routeAgentRequest(request, env)) ??

new Response("Not found", { status: 404 })

);

}

};

これにより、同じエージェントがウェブ音声、テキスト入力、および電話通話を処理できるようになります。

1 つの注意点として、デフォルトの Workers AI TTS(Text-to-Speech:テキスト読み上げ)プロバイダーは MP3 を返しますが、Twilio は mulaw 8kHz のオーディオを期待しています。本番環境の電話回線では、PCM または mulaw を直接出力する TTS プロバイダーを使用することをお勧めします。

WebRTC

過酷なネットワーク条件により適している、または複数の参加者を含む必要があるトランスポートが必要な場合、音声パッケージには SFU(Selective Forwarding Unit:選択中継ユニット)ユーティリティも含まれており、カスタムトランスポートをサポートしています。デフォルトのモデルは今日では WebSocket ネイティブですが、グローバルな SFU インフラストラクチャに接続するためのより多くのアダプターを開発する予定です。

一緒に構築しよう

音声パイプラインは設計上、プロバイダーに依存しません。

内部では、各ステージは小さなインターフェースによって定義されます。トランスクリバー(文字起こしエンジン)は継続的なセッションを開き、到着するオーディオフレームを受け取ります。一方、TTS プロバイダーはテキストを取り込み、オーディオを返します。プロバイダーがオーディオ出力をストリーミングできる場合、パイプラインはそれを使用することもできます。

interface Transcriber {

createSession(options?: TranscriberSessionOptions): TranscriberSession;

}

interface TranscriberSession {

feed(chunk: ArrayBuffer): void;

close(): void;

}

interface TTSProvider {

synthesize(text: string, signal?: AbortSignal): Promise;

}

Agents SDK で音声サポートを、1 つの固定されたモデルとトランスポートの組み合わせだけで動作させるつもりはありませんでした。エコシステムが拡大する中で、デフォルトのパスをシンプルに保ちつつ、他のプロバイダーを簡単に組み込めるようにすることが私たちの目標でした。

ビルトインのプロバイダーは Workers AI を使用しているため、外部の API キーなしで開始できます:

会話型のストリーミング STT 用 WorkersAIFluxSTT

Dictation スタイルのストリーミング STT 用 WorkersAINova3STT

テキスト読み上げ(Text-to-Speech)用 WorkersAITTS

しかし、より大きな目標は相互運用性です。音声やボイスサービスを維持している場合、これらのインターフェースは小さ enough で、SDK の内部構造のすべてを理解する必要なく実装できます。ストリーミング音声を受け入れ、発話の境界を検出できる STT プロバイダーであれば、トランスクリバーインターフェースを満たすことができます。ストリーミング音声出力ができる TTS プロバイダーであれば、さらに理想的です。

以下の相互運用性について協力したいと考えています:

AssemblyAI、Rev.ai、Speechmatics などの STT プロバイダー、またはリアルタイム文字起こし API を提供するあらゆるサービス

PlayHT、LMNT、Cartesia、Coqui、Amazon Polly、Google Cloud TTS などの TTS プロバイダー

Vonage、Telnyx、Bandwidth などのプラットフォーム向けのテレフォニーアダプター

WebRTC データチャネル、SFU ブリッジ、その他の音声トランスポート層向けのトランスポート実装

また、個々のプロバイダーを超えたコラボレーションにも興味があります:

STT + LLM + TTS の組み合わせにおけるレイテンシベンチマーク

英語以外の音声エージェントに対する多言語サポートと、より良いドキュメント化

アクセシビリティの取り組み、特にマルチモーダルインターフェースや音声障害に関する部分について

音声インフラストラクチャを構築しており、第一級統合を確認したい場合は、PR(プルリクエスト)を開くか、お問い合わせください。

今すぐ試す

音声パイプラインは、現在実験的なパッケージとして利用可能です:

npm create cloudflare@latest -- --template cloudflare/agents-starter

@cloudflare/voice を追加し、エージェントにトランスクリバー(文字起こし機能)と TTS(テキスト読み上げ)プロバイダーを与え、デプロイして会話を開始してください。API リファレンスもご参照いただけます。

興味深いものを作成した場合は、github.com/cloudflare/agents 上で issue または PR を開いてください。音声には別個のスタックを必要とすべきではなく、私たちは、他のすべてのものと同じ耐久性のあるアプリケーションモデル(durable application model)上で構築された最高の音声エージェントが生まれると考えています。

imageimage

原文を表示

For many of us, our first experiences with AI agents have been through typing into a chat box. And for those of us using agents day to day, we have likely gotten very good at writing detailed prompts or markdown files to guide them.

But some of the moments where agents would be most useful are not always text-first. You might be on a long commute, juggling a few open sessions, or just wanting to speak naturally to an agent, have it speak back, and continue the interaction.

Adding voice to an agent should not require moving that agent into a separate voice framework. Today, we are releasing an experimental voice pipeline for the Agents SDK.

With @cloudflare/voice, you can add real-time voice to the same Agent architecture you already use. Voice just becomes another way you can talk to the same Durable Object, with the same tools, persistence, and WebSocket connection model that the Agents SDK already provides.

@cloudflare/voice is an experimental package for the Agents SDK that provides:

withVoice(Agent) for full conversation voice agents

withVoiceInput(Agent) for speech-to-text-only use cases, like dictation or voice search

useVoiceAgent and useVoiceInput hooks for React apps

VoiceClient for framework-agnostic clients

Built-in Workers AI providers, so that you can get started without external API keys:

Continuous STT with Deepgram Flux

Continuous STT with Deepgram Nova 3

Text-to-speech with Deepgram Aura

This means you can now build an agent that users can talk to in real time over a single WebSocket connection, while keeping the same Agent class, Durable Object instance, and the same SQLite-backed conversation history.

Just as importantly, we want this to be bigger than one fixed default stack. The provider interfaces in @cloudflare/voice are intentionally small, and we want speech, telephony, and transport providers to build with us, so developers can mix and match the right components for their use case, instead of being locked into a single voice architecture.

Get started with voice

Here’s the minimal server-side pattern for a voice agent in the Agents SDK:

import { Agent, routeAgentRequest } from "agents";

import {

withVoice,

WorkersAIFluxSTT,

WorkersAITTS,

type VoiceTurnContext

} 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: string, context: VoiceTurnContext) {

return You said: ${transcript};

}

}

export default {

async fetch(request: Request, env: Env) {

return (

(await routeAgentRequest(request, env)) ??

new Response("Not found", { status: 404 })

);

}

} satisfies ExportedHandler;

That’s the whole server. You add a continuous transcriber, a text-to-speech provider, and implement onTurn().

On the client side, you can connect to it with a React hook:

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

function App() {

const {

status,

transcript,

interimTranscript,

startCall,

endCall,

toggleMute

} = useVoiceAgent({ agent: "my-agent" });

return (

Status: {status}

{interimTranscript && *{interimTranscript}*

}

{transcript.map((msg, i) => (

  • {msg.role}: {msg.text}

))}

Start Call

End Call

Mute / Unmute

);

}

If you are not using React, you can use VoiceClient directly from @cloudflare/voice/client.

How the voice pipeline works

With the Agents SDK, every agent is a Durable Object — a stateful, addressable server instance with its own SQLite database, WebSocket connections, and application logic. The voice pipeline extends this model instead of replacing it.

At a high level, the flow looks like this:

image
image

Here’s how the pipeline breaks down, step by step:

Audio transport: The browser captures microphone audio and streams 16 kHz mono PCM over the same WebSocket connection the agent already uses.

STT session setup:  When the call starts, the agent creates a continuous transcriber session that lives for the duration of the call.

STT input: Audio streams continuously into that session.

STT turn detection: The speech-to-text model itself decides when the user has finished an utterance and emits a stable transcript for that turn.

LLM/application logic: The voice pipeline passes that transcript to your onTurn() method.

TTS output: Your response is synthesized to audio and sent back to the client. If onTurn() returns a stream, the pipeline sentence-chunks it and starts sending audio as sentences are ready.

Persistence: The user and agent messages are persisted in SQLite, so conversation history survives reconnections and deployments.

Why voice should grow with the rest of your agent

Many voice frameworks focus on the voice loop itself: audio in, transcription, model response, audio out. Those are important primitives, but there’s a lot more to an agent than just voice.

Real agents running in production will grow. They need state, scheduling, persistence, tools, workflows, telephony, and ways to keep all of that consistent across channels. As your agent grows in complexity, voice stops being a standalone feature and becomes part of a larger system.

We wanted voice in the Agents SDK to start from that assumption. Instead of building voice as a separate stack, we built it on top of the same Durable Object-based agent platform, so you can pull in the rest of the primitives you need without re-architecting the application later.

Voice and text share the same state

A user might start by typing, switch to voice, and go back to text. With Agents SDK, these are all just different inputs to the same agent. The same conversation history lives in SQLite, and the same tools are available. This gives you both a cleaner mental model and a much simpler application architecture to reason about.

Lower latency comes from...

a shorter network path

Voice experiences feel good or bad very quickly. Once a user stops speaking, the system needs to transcribe, think, and start speaking back fast enough to feel conversational.

A lot of voice latency is not pure model time. It’s the cost of bouncing audio and text between different services in different places. Audio needs to go to STT, transcripts go to an LLM, and responses go to a TTS model – and each handoff adds network overhead.

With the Agents SDK voice pipeline, the agent runs on Cloudflare’s network, and the built-in providers use Workers AI bindings. That keeps the pipeline tighter and reduces the amount of infrastructure you have to stitch together yourself.

built-in streaming

A voice agent interaction feels much more natural if it speaks the first sentence quickly (also called Time-to-First Audio). When onTurn() returns a stream, the pipeline chunks it into sentences and starts synthesis as sentences complete. That means the user can hear the beginning of the answer while the rest is still being generated.

A more realistic backend

Here is a fuller example that streams an LLM response and starts speaking it back, sentence by sentence:

import { Agent, routeAgentRequest } from "agents";

import {

withVoice,

WorkersAIFluxSTT,

WorkersAITTS,

type VoiceTurnContext

} from "@cloudflare/voice";

import { streamText } from "ai";

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

const VoiceAgent = withVoice(Agent);

export class MyAgent extends VoiceAgent {

transcriber = new WorkersAIFluxSTT(this.env.AI);

tts = new WorkersAITTS(this.env.AI);

async onTurn(transcript: string, context: VoiceTurnContext) {

const ai = createWorkersAI({ binding: this.env.AI });

const result = streamText({

model: ai("@cf/cloudflare/gpt-oss-20b"),

system: "You are a helpful voice assistant. Be concise.",

messages: [

...context.messages.map((m) => ({

role: m.role as "user" | "assistant",

content: m.content

})),

{ role: "user" as const, content: transcript }

],

abortSignal: context.signal

});

return result.textStream;

}

}

export default {

async fetch(request: Request, env: Env) {

return (

(await routeAgentRequest(request, env)) ??

new Response("Not found", { status: 404 })

);

}

} satisfies ExportedHandler;

Context.messages gives you recent SQLite-backed conversation history, and context.signal lets the pipeline abort the LLM call if the user interrupts.

Voice as an input: withVoiceInput

Not every speech interface needs to speak back. Sometimes you might want dictation, transcription, or voice search. For these use cases, you can use withVoiceInput

import { Agent, type Connection } 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: string, _connection: Connection) {

console.log("User said:", text);

}

}

On the client, useVoiceInput gives you a lightweight interface centered on transcriptions:

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

const { transcript, interimTranscript, isListening, start, stop, clear } =

useVoiceInput({ agent: "DictationAgent" });

This is useful when speech is an input method, and you don’t need a full conversational loop.

Voice and text on the same connection

The same client can call sendText(“What’s the weather?”), which bypasses STT and sends the text directly to onTurn(). During an active call, the response can be spoken and shown as text. Outside a call, it can remain text-only.

This gives you a genuinely multimodal agent, without splitting the implementation into different code paths.

What else can you build?

Because a voice agent is still an agent, all the normal Agents SDK capabilities still apply.

Tools and scheduling

You can greet a caller when a session starts:

import { Agent, type Connection } 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: string) {

return You said: ${transcript};

}

async onCallStart(connection: Connection) {

await this.speak(connection, "Hi! How can I help you today?");

}

}

You can schedule spoken reminders and expose tools to your LLM just like any other agent:

import { Agent } from "agents";

import {

withVoice,

WorkersAIFluxSTT,

WorkersAITTS,

type VoiceTurnContext

} from "@cloudflare/voice";

import { streamText, tool } from "ai";

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

import { z } from "zod";

const VoiceAgent = withVoice(Agent);

export class MyAgent extends VoiceAgent {

transcriber = new WorkersAIFluxSTT(this.env.AI);

tts = new WorkersAITTS(this.env.AI);

async speakReminder(payload: { message: string }) {

await this.speakAll(Reminder: ${payload.message});

}

async onTurn(transcript: string, context: VoiceTurnContext) {

const ai = createWorkersAI({ binding: this.env.AI });

const result = streamText({

model: ai("@cf/cloudflare/gpt-oss-20b"),

messages: [

...context.messages.map((m) => ({

role: m.role as "user" | "assistant",

content: m.content

})),

{ role: "user" as const, content: transcript }

],

tools: {

set_reminder: tool({

description: "Set a spoken reminder after a delay",

inputSchema: z.object({

message: z.string(),

delay_seconds: z.number()

}),

execute: async ({ message, delay_seconds }) => {

await this.schedule(delay_seconds, "speakReminder", { message });

return { confirmed: true };

}

})

},

abortSignal: context.signal

});

return result.textStream;

}

}

Runtime model switching

The voice pipeline also lets you choose a transcription model dynamically per connection.

For example, you might prefer Flux for conversational turn-taking and Nova 3 for higher-accuracy dictation. You can switch at runtime by overriding createTranscriber():

import { Agent, type Connection } from "agents";

import {

withVoice,

WorkersAIFluxSTT,

WorkersAINova3STT,

WorkersAITTS,

type Transcriber

} from "@cloudflare/voice";

export class MyAgent extends VoiceAgent {

tts = new WorkersAITTS(this.env.AI);

createTranscriber(connection: Connection): Transcriber {

const url = new URL(connection.url ?? "http://localhost");

const model = url.searchParams.get("model");

if (model === "nova-3") {

return new WorkersAINova3STT(this.env.AI);

}

return new WorkersAIFluxSTT(this.env.AI);

}

}

On the client, you can pass query parameters through the hook:

const voiceAgent = useVoiceAgent({

agent: "my-voice-agent",

query: { model: "nova-3" }

});

Pipeline hooks

You can also intercept data between stages:

afterTranscribe(transcript, connection)

beforeSynthesize(text, connection)

afterSynthesize(audio, text, connection)

These hooks are useful for content filtering, text normalization, language-specific transformations, or custom logging.

Telephone and transport options

By default, the voice pipeline uses a single WebSocket connection as the simplest path for 1:1 voice agents. But that’s not the only option.

Phone calls via Twilio

You can connect phone calls to the same agent using the Twilio adapter:

import { TwilioAdapter } from "@cloudflare/voice-twilio";

export default {

async fetch(request: Request, env: Env) {

if (new URL(request.url).pathname === "/twilio") {

return TwilioAdapter.handleRequest(request, env, "MyAgent");

}

return (

(await routeAgentRequest(request, env)) ??

new Response("Not found", { status: 404 })

);

}

};

This lets the same agent handle web voice, text input, and phone calls.

One caveat: the default Workers AI TTS provider returns MP3, while Twilio expects mulaw 8kHz audio. For production telephony, you may want to use a TTS provider that outputs PCM or mulaw directly.

WebRTC

If you need a transport that is better suited to difficult network conditions or will include multiple participants, the voice package also includes SFU utilities and supports custom transports. The default model is WebSocket-native today, but we plan to develop more adapters to connect to our global SFU infrastructure.

Build with us

The voice pipeline is provider-agnostic by design.

Under the hood, each stage is defined by a small interface: a transcriber opens a continuous session and accepts audio frames as they arrive, while a TTS provider takes text and returns audio. If a provider can stream audio output, the pipeline can use that too.

interface Transcriber {

createSession(options?: TranscriberSessionOptions): TranscriberSession;

}

interface TranscriberSession {

feed(chunk: ArrayBuffer): void;

close(): void;

}

interface TTSProvider {

synthesize(text: string, signal?: AbortSignal): Promise;

}

We didn’t want voice support in Agents SDK to only work with one fixed combination of models and transports. We wanted the default path to be simple, while still making it easy to plug in other providers as the ecosystem grows.

The built-in providers use Workers AI, so you can get started without external API keys:

WorkersAIFluxSTT for conversational streaming STT

WorkersAINova3STT for dictation-style streaming STT

WorkersAITTS for text-to-speech

But the bigger goal is interoperability. If you maintain a speech or voice service, these interfaces are small enough to implement without needing to understand the rest of the SDK internals. If your STT provider accepts streaming audio and can detect utterance boundaries, it can satisfy the transcriber interface. If your TTS provider can stream audio output, even better.

We would love to work on interoperability with:

STT providers like AssemblyAI, Rev.ai, Speechmatics, or any service with a real-time transcription API

TTS providers like PlayHT, LMNT, Cartesia, Coqui, Amazon Polly, or Google Cloud TTS

telephony adapters for platforms like Vonage, Telnyx, or Bandwidth

transport implementations for WebRTC data channels, SFU bridges, and other audio transport layers

We are also interested in collaborations that go beyond individual providers:

latency benchmarking across STT + LLM + TTS combinations

multilingual support and better documentation for non-English voice agents

accessibility work, especially around multimodal interfaces and speech impairments

If you are building voice infrastructure and want to see a first-class integration, open a PR or reach out.

Try it now

The voice pipeline is available today as an experimental package:

npm create cloudflare@latest -- --template cloudflare/agents-starter

Add @cloudflare/voice, give your agent a transcriber and a TTS provider, deploy it, and start talking to it. You can also read the API reference.

If you build something interesting, open an issue or PR on github.com/cloudflare/agents. Voice should not require a separate stack, and we think the best voice agents will be the ones built on the same durable application model as everything else.

image
image
この記事をシェア

関連記事

Ars Technica AI★42026年5月5日 04:03

教育におけるChatGPTの有用性を主張した研究が撤回される

Springer Nature は、OpenAI の ChatGPT が学習成果にプラスの影響を与えると主張した研究について、分析上の不整合と結論への信頼性欠如を理由に撤回を発表しました。この論文は出版後約1年で数百件の引用を集め、SNS でも話題となりましたが、著者による注目すべき主張には問題があったことが判明しました。

404 Media★42026年5月5日 02:56

Nature が ChatGPT の教育効果に関する論文を撤回

学術誌 Nature は、AI が学生の学習成績や思考力にプラスの影響を与えると主張したメタ分析論文を撤回しました。この論文は 5 月に発表され、ChatGPT の教育的利点を示す根拠として引用されていましたが、調査の結果問題が発覚し取り下げられました。

TLDR AI★32026年5月4日 09:00

OpenAI、Codex にアニメーションペットと設定ファイル自動インポート機能を追加

OpenAI は開発ツール「Codex」を更新し、画面にオーバーレイ表示されるアニメーションペット機能や、他コードエージェントからの設定ファイル自動インポート、音声入力精度向上のための辞書機能を追加した。これによりデスクトップアプリとしての利便性と魅力が強化された。

ニュース一覧に戻る元記事を読む