コンテンツにスキップ

音声エージェントの構築

デフォルトの OpenAIRealtimeWebRTC のように、一部のトランスポート層では音声の入力と出力が自動で処理されます。一方、OpenAIRealtimeWebSocket のような別のトランスポート機構では、セッション音声を自分で処理する必要があります:

import {
RealtimeAgent,
RealtimeSession,
TransportLayerAudio,
} from '@openai/agents/realtime';
const agent = new RealtimeAgent({ name: 'My agent' });
const session = new RealtimeSession(agent);
const newlyRecordedAudio = new ArrayBuffer(0);
session.on('audio', (event: TransportLayerAudio) => {
// play your audio
});
// send new audio to the agent
session.sendAudio(newlyRecordedAudio);

基盤となるトランスポートが対応している場合、session.muted は現在のミュート状態を返し、session.mute(true | false) でマイク入力の取得を切り替えられます。OpenAIRealtimeWebSocket はミュートを実装していません。session.mutednull を返し、session.mute() は例外を投げます。そのため、 websocket の構成では自分の側で入力取得を一時停止し、マイクを再び有効にするまで sendAudio() の呼び出しを止めてください。

RealtimeSession を作成する際に、通常は model オプションと config オブジェクトを通じて、セッション自体を構成します。connect(...) は任意のセッションフィールドではなく、資格情報、エンドポイント URL、 SIP 通話への接続といった接続時の関心事に使います。

import { RealtimeAgent, RealtimeSession } from '@openai/agents/realtime';
const agent = new RealtimeAgent({
name: 'Greeter',
instructions: 'Greet the user with cheer and answer questions.',
});
const session = new RealtimeSession(agent, {
model: 'gpt-realtime-1.5',
config: {
outputModalities: ['audio'],
audio: {
input: {
format: 'pcm16',
transcription: {
model: 'gpt-4o-mini-transcribe',
},
},
output: {
format: 'pcm16',
},
},
},
});

内部的には、 SDK はこの構成を Realtime の session.update 形式に正規化します。RealtimeSessionConfig に対応するプロパティがない生のセッションフィールドが必要な場合は、providerData を使うか、session.transport.sendEvent(...) で生の session.update を送信してください。

新しい SDK の構成形式である outputModalitiesaudio.inputaudio.output を優先してください。古い SDK の別名である modalitiesinputAudioFormatoutputAudioFormatinputAudioTranscriptionturnDetection も後方互換性のために引き続き正規化されますが、新しいコードではここで示しているネストされた audio 構造を使うべきです。

音声対音声のセッションでは、通常 outputModalities: ['audio'] を選びます。これにより、音声出力に加えて文字起こしも得られます。テキストのみの応答が必要な場合に限り ['text'] に切り替えてください。

新しいパラメーターで、RealtimeSessionConfig に対応するパラメーターがない場合は、providerData を使えます。providerData に渡したものはすべて、生の session オブジェクトの一部として転送されます。

構築時に設定できる追加の RealtimeSession オプション:

OptionTypePurpose
contextTContextセッションコンテキストにマージされる追加のローカルコンテキスト
historyStoreAudiobooleanローカル履歴スナップショットに音声データを保存する(デフォルトでは無効)
outputGuardrailsRealtimeOutputGuardrail[]セッションの出力ガードレール(Guardrails を参照)
outputGuardrailSettings{ debounceTextLength?: number }ガードレールの実行間隔。デフォルトは 100 で、全文テキストが利用可能になった時点でのみ実行するには -1 を使用
tracingDisabledbooleanセッションのトレーシングを無効化
groupIdstringセッションやバックエンド実行をまたいでトレースをグループ化。workflowName が必要
traceMetadataRecord<string, any>セッショントレースに付加するカスタムメタデータ。workflowName が必要
workflowNamestringトレースワークフローのわかりやすい名前
automaticallyTriggerResponseForMcpToolCallsbooleanMCP ツール呼び出し完了時にモデル応答を自動トリガーする(デフォルト: true
toolErrorFormatterToolErrorFormatterモデルに返すツール承認拒否メッセージをカスタマイズ

connect(...) のオプション:

OptionTypePurpose
apiKeystring | (() => string | Promise<string>)この接続で使う API キー(または遅延ローダー)
modelOpenAIRealtimeModels | stringトランスポートレベルのオプション型に存在します。RealtimeSession ではコンストラクターでモデルを設定し、生のトランスポートでは接続時にもモデルを使えます
urlstring任意のカスタム Realtime エンドポイント URL
callIdstring既存の SIP 開始済み通話 / セッションに接続

RealtimeSession は長寿命の Realtime 接続の上に成り立っています。会話履歴のローカルコピーを保持し、トランスポートイベントを監視し、ツールと出力ガードレールを実行し、アクティブなエージェント構成をトランスポートと同期し続けます。

それでも、基盤 API の挙動は重要です:

  • 接続が成功すると session.created イベントで開始し、その後の構成変更では session.updated が生成されます
  • 多くのセッションプロパティは時間とともに変更できますが、model は会話の途中で変更できず、voice はセッションが音声出力を生成する前にしか変更できません。また、Realtime API では有効化後にトレーシングを変更できないため、トレーシングは最初に決めておくべきです
  • Realtime API は現在、1 つのセッションを 60 分に制限しています
  • 入力音声の文字起こしは非同期のため、最新の発話のトランスクリプトは、応答生成がすでに始まった後に届くことがあります

SDK 層では、await session.connect() は「会話を始められる程度にトランスポートの準備ができた」という意味ですが、正確な時点はトランスポートによって異なります:

  • デフォルトのブラウザ WebRTC トランスポートでは、データチャネルが開くとすぐに SDK が最初の session.update を送信し、通常は対応する session.updated イベントを待ってから connect() を解決しようとします。これは、 instructions、tools、modalities が適用される前に音声がサーバーへ届くのを避けるためです。その確認応答が届かない場合、connect() は短いタイムアウト後に解決するようフォールバックします
  • デフォルトのサーバー側 WebSocket トランスポートでは、ソケットが開いて最初の構成が送信された時点で connect() が解決します。そのため、対応する session.updated イベントは connect() の解決後に届くことがあります

生のイベントモデルが必要な場合は、このページとあわせて公式の Realtime conversations guide を読んでください。

ターン検出と音声アクティビティ検出

Section titled “ターン検出と音声アクティビティ検出”

デフォルトでは、Realtime セッションは組み込みの音声アクティビティ検出( VAD )を使い、 API がユーザーの発話開始 / 終了のタイミングや、いつ応答を作るべきかを判断します。SDK ではこれを audio.input.turnDetection を通じて公開しています。

import { RealtimeSession } from '@openai/agents/realtime';
import { agent } from './agent';
const session = new RealtimeSession(agent, {
model: 'gpt-realtime-1.5',
config: {
audio: {
input: {
turnDetection: {
type: 'semantic_vad',
eagerness: 'medium',
createResponse: true,
interruptResponse: true,
},
},
},
},
});

よく使われる 2 つのモードは次のとおりです:

  • semantic_vad: より自然なターン境界を目指し、ユーザーがまだ話し終えていないように聞こえる場合には少し長く待てます
  • server_vad: しきい値ベースの性質がより強く、thresholdprefixPaddingMssilenceDurationMsidleTimeoutMs といった設定を公開します

ターン境界を自分で管理したい場合は、audio.input.turnDetectionnull に設定してください。公式の voice activity detection guideRealtime conversations guide には、基盤の挙動がより詳しく説明されています。

VAD が有効な場合、エージェントにかぶせて話すと現在の応答を割り込めます。WebSocket トランスポートでは、SDK は input_audio_buffer.speech_started を監視し、ユーザーが実際に聞いた分までアシスタント音声を切り詰めて、audio_interrupted イベントを発行します。このイベントは、特に WebSocket 構成で再生を自分で管理している場合に便利です。

import { session } from './agent';
session.on('audio_interrupted', () => {
// handle local playback interruption
});

手動の停止ボタンを提供したい場合は、自分で interrupt() を呼び出してください:

import { session } from './agent';
session.interrupt();
// this will still trigger the `audio_interrupted` event for you
// to cut off the audio playback when using WebSockets

WebRTC でも WebSocket でも進行中の応答は停止しますが、低レベルの仕組みはトランスポートごとに異なります。WebRTC はバッファされた出力音声を自動でクリアします。WebSocket 構成ではローカル再生を自分で停止する必要があり、対応する切り詰めイベントと会話イベントがトランスポートから戻ってきた時点でローカル履歴が更新されます。

入力中の会話にタイプ入力や追加の構造化されたユーザーコンテンツを送る場合は、sendMessage() を使います。

import { RealtimeSession, RealtimeAgent } from '@openai/agents/realtime';
const agent = new RealtimeAgent({
name: 'Assistant',
});
const session = new RealtimeSession(agent, {
model: 'gpt-realtime-1.5',
});
session.sendMessage('Hello, how are you?');

これは、テキストと音声を混在させた UI、帯域外のコンテキスト注入、あるいは話し言葉の入力に明示的なタイプ補足を組み合わせる場合に便利です。

Realtime の音声対音声セッションには画像も含められます。SDK では、現在の会話に画像を添付するために addImage() を使います。

import { RealtimeAgent, RealtimeSession } from '@openai/agents/realtime';
const agent = new RealtimeAgent({
name: 'Assistant',
});
const session = new RealtimeSession(agent, {
model: 'gpt-realtime-1.5',
});
const imageDataUrl = 'data:image/png;base64,...';
session.addImage(imageDataUrl, { triggerResponse: false });
session.sendMessage('Describe what is in this image.');

triggerResponse: false を渡すと、モデルに応答を求める前に、その画像を後続のテキストまたは音声ターンとまとめて送れます。これは公式の Realtime conversations image input guidance と一致しています。

より高レベルな SDK 層では、sendMessage()addImage() はデフォルトで応答をトリガーします。手動での応答制御が重要になるのは、生のトランスポートイベント、プッシュツートークのフロー、またはカスタムのモデレーション / 検証ステップを扱う場合です。

import { RealtimeAgent, RealtimeSession } from '@openai/agents/realtime';
const agent = new RealtimeAgent({
name: 'Greeter',
instructions: 'Greet the user with cheer and answer questions.',
});
const session = new RealtimeSession(agent, {
model: 'gpt-realtime-1.5',
});
session.transport.on('*', (event) => {
// JSON parsed version of the event received on the connection
});
// Send any valid event as JSON. For example triggering a new response
session.transport.sendEvent({
type: 'response.create',
// ...
});

よくあるケースは 2 つあります:

  1. audio.input.turnDetection = null で VAD を完全に無効にした場合、音声ターンの確定と、その後の response.create の送信は自分で行う必要があります
  2. VAD を有効のままにしつつ、turnDetection.interruptResponse = falseturnDetection.createResponse = false を設定した場合、API は引き続きターンを検出しますが、応答作成は自分に委ねられます

2 つ目のパターンは、モデルが応答する前にユーザー入力を確認またはモデレーションしたい場合に便利です。これは公式の Realtime conversations guidance on disabling automatic responses と対応しています。

通常のエージェントと同様に、ハンドオフを使って 1 つのエージェントを複数のエージェントに分割し、その間をオーケストレーションすることで、性能を改善し、問題のスコープをより適切に絞れます。

import { RealtimeAgent } from '@openai/agents/realtime';
const mathTutorAgent = new RealtimeAgent({
name: 'Math Tutor',
handoffDescription: 'Specialist agent for math questions',
instructions:
'You provide help with math problems. Explain your reasoning at each step and include examples',
});
const agent = new RealtimeAgent({
name: 'Greeter',
instructions: 'Greet the user with cheer and answer questions.',
handoffs: [mathTutorAgent],
});

ただし、Realtime Agents では通常のエージェントと少し異なる挙動になります。ハンドオフが実行されると、進行中のセッションが新しいエージェント構成で更新されます。そのため、新しいエージェントは進行中の会話履歴に自動的にアクセスでき、現在は入力フィルターは適用されません。

セッションはライブのまま維持されるため、ハンドオフ中にそのセッションのモデルは変わりません。音声の変更は基盤の Realtime API ルールに従い、セッションが音声出力を生成する前にしか機能しません。Realtime のハンドオフは主に、同一セッション上で RealtimeAgent の構成を切り替えるためのものです。たとえば gpt-5.4 のような推論モデルなど、別のモデルを使う必要がある場合や、非リアルタイムのバックエンドエージェントに委譲したい場合は、ツール経由の委譲 を使ってください。

通常のエージェントと同様に、Realtime Agents もアクション実行のためにツールを呼び出せます。Realtime は 関数ツール(ローカル実行)と hosted MCP tools(Realtime API によるリモート実行)をサポートします。関数ツールは、通常のエージェントで使うのと同じ tool() ヘルパーで定義できます。

import { tool, RealtimeAgent } from '@openai/agents/realtime';
import { z } from 'zod';
const getWeather = tool({
name: 'get_weather',
description: 'Return the weather for a city.',
parameters: z.object({ city: z.string() }),
async execute({ city }) {
return `The weather in ${city} is sunny.`;
},
});
const weatherAgent = new RealtimeAgent({
name: 'Weather assistant',
instructions: 'Answer weather questions.',
tools: [getWeather],
});

関数ツールは RealtimeSession と同じ環境で実行されます。つまり、セッションをブラウザで動かしている場合、ツールもブラウザで実行されます。機密性の高い操作を行う必要がある場合は、ツールの中からバックエンドを呼び出し、権限の必要な処理はサーバーに行わせてください。

これにより、ブラウザ側ツールをサーバー側ロジックへの薄いバックチャネルとして使えます。たとえば、examples/realtime-next ではブラウザ内で refundBackchannel ツールを定義し、リクエストと現在の会話履歴をサーバー上の handleRefundRequest(...) に転送します。そこで別の Runner が別エージェントまたは別モデルを使って返金可否を評価し、その結果を音声セッションへ返します。

Hosted MCP tools は hostedMcpTool で構成でき、リモートで実行されます。MCP ツールの利用可能性が変わると、セッションは mcp_tools_changed を発行します。MCP ツール呼び出し完了後にセッションがモデル応答を自動トリガーしないようにするには、automaticallyTriggerResponseForMcpToolCalls: false を設定してください。

現在フィルター後の MCP ツール一覧は session.availableMcpTools としても利用できます。このプロパティと mcp_tools_changed イベントはどちらも、アクティブなエージェントで有効な hosted MCP サーバーのみを反映し、エージェント構成の allowed_tools フィルター適用後の状態を示します。

Hosted MCP のセットアップは、安全なサーバー選択、ヘッダー、承認を接続前構成として扱うと理解しやすくなります。RealtimeSession.connect() がトランスポートを開く前に、SDK はアクティブなエージェントの hosted MCP ツール定義を解決し、サポートされている MCP フィールドを、Realtime API に送る初期セッション構成に含めます。

このタイミングが特に重要なのは、ブラウザ WebRTC アプリです。一時的な client secret は常にサーバー上で発行されるため、秘密にしておく必要がある hosted MCP の認証情報やカスタム headers は、サーバー側の POST /v1/realtime/client_secrets リクエストで初期 session ペイロードの一部として付加する必要があります。長期有効な認証情報をブラウザコードに置いて、connect() 開始後に後から追加する前提にはしないでください。

Realtime API レベルでは、後続の session.update 呼び出しでツールやその他の可変セッションフィールドを変更することは引き続き可能であり、SDK 自体もアクティブなエージェントが変わると session.update を送信します。ただしブラウザアプリでは、安全な Hosted MCP 初期化はサーバー側の接続前の関心事として扱い、ブラウザ側の RealtimeSession 構成はサーバーが発行した内容と整合させてください。

ツールの実行中、エージェントはユーザーからの新しいリクエストを処理できません。体験を改善する 1 つの方法は、ツールを実行しようとしていることを告げたり、ツール実行の時間を稼ぐ特定のフレーズを話したりするよう、エージェントに指示することです。

関数ツールが、ただちに別のモデル応答をトリガーせずに完了すべき場合は、@openai/agents/realtime から backgroundResult(output) を返してください。これにより、応答トリガーの制御を自分に残したまま、ツール出力をセッションへ返せます。

関数ツールのタイムアウトオプション(timeoutMstimeoutBehaviortimeoutErrorFunction)は Realtime セッションでも同じように機能します。デフォルトの error_as_result では、タイムアウトメッセージはツール出力として送信されます。raise_exception では、セッションは ToolTimeoutError を伴う error イベントを発行し、その呼び出しに対してツール出力は送信しません。

エージェントが特定のツールを呼び出した際の引数に加えて、Realtime Session が追跡している現在の会話履歴のスナップショットにもアクセスできます。これは、会話の現在状態に基づいてより複雑なアクションを実行したい場合や、委譲のためのツール を使う予定がある場合に便利です。

import {
tool,
RealtimeContextData,
RealtimeItem,
} from '@openai/agents/realtime';
import { z } from 'zod';
const parameters = z.object({
request: z.string(),
});
const refundTool = tool<typeof parameters, RealtimeContextData>({
name: 'Refund Expert',
description: 'Evaluate a refund',
parameters,
execute: async ({ request }, details) => {
// The history might not be available
const history: RealtimeItem[] = details?.context?.history ?? [];
// making your call to process the refund request
},
});

needsApproval: true を指定してツールを定義すると、エージェントはツール実行前に tool_approval_requested イベントを発行します。

このイベントを監視することで、ユーザーにツール呼び出しを承認または拒否する UI を表示できます。

リクエストは await session.approve(request.approvalItem) または await session.reject(request.approvalItem) で解決します。関数ツールでは、セッションの残りの間に繰り返される呼び出しでも同じ判断を再利用するために { alwaysApprove: true } または { alwaysReject: true } を渡せます。また、session.reject(request.approvalItem, { message: '...' }) を使うと、その特定の呼び出しに対するカスタム拒否メッセージをモデルへ返せます。Hosted MCP の承認では固定の承認 / 拒否はサポートされないため、代わりに hosted MCP の allowedTools 構成でそれらのツールを制限してください。

呼び出しごとの拒否 message を渡さない場合、セッションは toolErrorFormatter(設定されている場合)を使い、その次に SDK のデフォルト拒否テキストへフォールバックします。

import { session } from './agent';
session.on('tool_approval_requested', (_context, _agent, request) => {
// show a UI to the user to approve or reject the tool call
// you can use the `session.approve(...)` or `session.reject(...)` methods to approve or reject the tool call
session.approve(request.approvalItem); // or session.reject(request.approvalItem);
});

ガードレールは、エージェントの発話内容がルール群に違反していないかを監視し、違反した場合にただちに応答を打ち切るための方法です。これらのチェックは、エージェント応答のトランスクリプトストリームに対して実行されます。音声セッションでは、SDK は出力音声のトランスクリプトとトランスクリプト差分を使うため、重要な前提条件は別個のテキスト出力モダリティではなく、トランスクリプトが利用可能であることです。

指定したガードレールは、モデル応答が返される間に非同期で実行されるため、たとえば「特定の禁止語に言及した」といった事前定義の分類トリガーに基づいて応答を打ち切れます。

ガードレールが発動すると、セッションは guardrail_tripped イベントを発行します。このイベントは、ガードレールを発動させた itemId を含む details オブジェクトも提供します。

import {
RealtimeOutputGuardrail,
RealtimeAgent,
RealtimeSession,
} from '@openai/agents/realtime';
const agent = new RealtimeAgent({
name: 'Greeter',
instructions: 'Greet the user with cheer and answer questions.',
});
const guardrails: RealtimeOutputGuardrail[] = [
{
name: 'No mention of Dom',
async execute({ agentOutput }) {
const domInOutput = agentOutput.includes('Dom');
return {
tripwireTriggered: domInOutput,
outputInfo: { domInOutput },
};
},
},
];
const guardedSession = new RealtimeSession(agent, {
outputGuardrails: guardrails,
});

デフォルトでは、ガードレールは 100 文字ごとに実行され、さらに最終トランスクリプトが利用可能になったときにも実行されます。通常、テキストを話す時間のほうがトランスクリプト生成より長いため、これによりユーザーが聞く前に安全でない出力を打ち切れることがよくあります。

この挙動を変更したい場合は、セッションに outputGuardrailSettings オブジェクトを渡してください。

応答の最後で、完全に生成されたトランスクリプトを 1 回だけ評価したい場合は、debounceTextLength: -1 を設定します。

import { RealtimeAgent, RealtimeSession } from '@openai/agents/realtime';
const agent = new RealtimeAgent({
name: 'Greeter',
instructions: 'Greet the user with cheer and answer questions.',
});
const guardedSession = new RealtimeSession(agent, {
outputGuardrails: [
/*...*/
],
outputGuardrailSettings: {
debounceTextLength: 500, // run guardrail every 500 characters or set it to -1 to run it only at the end
},
});

RealtimeSession は、ユーザーメッセージ、アシスタント出力、ツール呼び出し、切り詰め状態を追跡するローカルな history スナップショットを自動的に維持します。これを UI に表示したり、ツール内で調べたり、項目を修正または削除する必要があるときに更新したりできます。

会話が変化すると、セッションは history_updated を発行します。履歴変更を要求する必要がある場合は、updateHistory() を使います。これにより、トランスポートに現在の履歴との差分計算を依頼し、必要な削除 / 作成イベントを送信します。ローカルの session.history ビューは、対応する会話イベントが戻ってきた時点で更新されます。

import { RealtimeSession, RealtimeAgent } from '@openai/agents/realtime';
const agent = new RealtimeAgent({
name: 'Assistant',
});
const session = new RealtimeSession(agent, {
model: 'gpt-realtime-1.5',
});
await session.connect({ apiKey: '<client-api-key>' });
// listening to the history_updated event
session.on('history_updated', (history) => {
// returns the full history of the session
console.log(history);
});
// Option 1: explicit setting
session.updateHistory([
/* specific history */
]);
// Option 2: override based on current state like removing all agent messages
session.updateHistory((currentHistory) => {
return currentHistory.filter(
(item) => !(item.type === 'message' && item.role === 'assistant'),
);
});
  1. 現在のところ、後から関数ツール呼び出しを編集することはできません
  2. 履歴内のアシスタントテキストは、output_audio.transcript を含む利用可能なトランスクリプトに依存します
  3. 割り込みによって切り詰められた応答には、最終トランスクリプトが保持されません
  4. 入力音声の文字起こしは、モデルが音声をどう解釈したかの正確なコピーではなく、ユーザーが何を言ったかのおおまかな目安として扱うのが適切です

ツール経由の委譲

会話履歴とツール呼び出しを組み合わせることで、会話を別のバックエンドエージェントに委譲して、より複雑なアクションを実行させ、その結果をユーザーへの応答として返せます。

import {
RealtimeAgent,
RealtimeContextData,
tool,
} from '@openai/agents/realtime';
import { handleRefundRequest } from './serverAgent';
import z from 'zod';
const refundSupervisorParameters = z.object({
request: z.string(),
});
const refundSupervisor = tool<
typeof refundSupervisorParameters,
RealtimeContextData
>({
name: 'escalateToRefundSupervisor',
description: 'Escalate a refund request to the refund supervisor',
parameters: refundSupervisorParameters,
execute: async ({ request }, details) => {
// This will execute on the server
return handleRefundRequest(request, details?.context?.history ?? []);
},
});
const agent = new RealtimeAgent({
name: 'Customer Support',
instructions:
'You are a customer support agent. If you receive any requests for refunds, you need to delegate to your supervisor.',
tools: [refundSupervisor],
});

次のコードはサーバー上で実行されます。この例では Next.js Server Action を通じて実行しています。

// This runs on the server
import 'server-only';
import { Agent, run } from '@openai/agents';
import type { RealtimeItem } from '@openai/agents/realtime';
import z from 'zod';
const agent = new Agent({
name: 'Refund Expert',
instructions:
'You are a refund expert. You are given a request to process a refund and you need to determine if the request is valid.',
model: 'gpt-5.4',
outputType: z.object({
reasong: z.string(),
refundApproved: z.boolean(),
}),
});
export async function handleRefundRequest(
request: string,
history: RealtimeItem[],
) {
const input = `
The user has requested a refund.
The request is: ${request}
Current conversation history:
${JSON.stringify(history, null, 2)}
`.trim();
const result = await run(agent, input);
return JSON.stringify(result.finalOutput, null, 2);
}