Current section
Files
Jump to
Current section
Files
examples/realtime_handoffs.exs
#!/usr/bin/env mix run
# Realtime Handoffs Example
#
# Demonstrates agent-to-agent handoffs in realtime sessions.
# TLS note: set CODEX_CA_CERTIFICATE to a PEM bundle to override trust roots for HTTPS/WSS.
# If unset, SSL_CERT_FILE is used instead; blank values are ignored.
#
# Usage:
# mix run examples/realtime_handoffs.exs
defmodule RealtimeHandoffsExample do
@moduledoc false
alias Codex.Realtime
alias Codex.Realtime.Config.RunConfig
alias Codex.Realtime.Config.SessionModelSettings
alias Codex.Realtime.Diagnostics
alias Codex.Realtime.Events
@probe_timeout_ms 8_000
def main do
case run() do
:ok ->
:ok
{:skip, reason} ->
IO.puts("SKIPPED: #{reason}")
{:error, reason} ->
IO.puts("Failed: #{inspect(reason)}")
System.halt(1)
end
end
def run do
IO.puts("=== Realtime Handoffs Example ===\n")
unless fetch_api_key() do
{:error, "no API key found (CODEX_API_KEY, auth.json OPENAI_API_KEY, or OPENAI_API_KEY)"}
else
with :ok <- ensure_realtime_api_available(),
{:ok, session} <- start_session() do
Realtime.subscribe(session, self())
result =
try do
Process.sleep(500)
IO.puts("\nSending message that should trigger transfer_to_techsupport...")
Realtime.send_message(
session,
"I have a technical problem with my software. Use transfer_to_techsupport."
)
handle_events(30_000, %{
handoff_seen?: false,
tool_calls: 0,
skip_reason: nil
})
after
safe_close(session)
end
case result do
%{skip_reason: reason} when is_binary(reason) ->
{:skip, reason}
%{handoff_seen?: true} ->
:ok
%{handoff_seen?: false, tool_calls: tool_calls} ->
IO.puts("No handoff observed (tool_calls=#{tool_calls}).")
{:error, {:handoff_not_observed, tool_calls}}
end
else
{:skip, _reason} = skip -> skip
{:error, reason} -> maybe_skip_quota(reason)
end
end
end
defp ensure_realtime_api_available do
case Diagnostics.probe_text_turn(timeout_ms: @probe_timeout_ms) do
{:ok, _proof} ->
:ok
{:error, {:upstream_server_error, proof}} ->
{:skip, Diagnostics.format_probe_failure(proof)}
{:error, {:realtime_probe_failed, %{error: error}}} ->
case Diagnostics.skip_reason_for_error(error) do
nil -> {:error, {:realtime_probe_failed, error}}
skip_reason -> {:skip, skip_reason}
end
{:error, reason} ->
case Diagnostics.skip_reason_for_error(reason) do
nil -> {:error, reason}
skip_reason -> {:skip, skip_reason}
end
end
end
defp start_session do
tech_support =
Realtime.agent(
name: "TechSupport",
handoff_description: "Handles technical support issues and troubleshooting",
instructions: """
You are a technical support specialist. Help users with technical issues.
Be patient and concise.
"""
)
billing_agent =
Realtime.agent(
name: "BillingAgent",
handoff_description: "Handles billing inquiries and account questions",
instructions: """
You handle billing and account inquiries.
Be professional and concise.
"""
)
greeter =
Realtime.agent(
name: "Greeter",
instructions: """
You are a friendly greeter. Welcome users and route to specialists.
Use transfer_to_techsupport for technical issues.
Use transfer_to_billingagent for billing issues.
""",
handoffs: [tech_support, billing_agent]
)
IO.puts("Created agent network:")
IO.puts(" - #{greeter.name} (entry point)")
IO.puts(" -> #{tech_support.name}: #{tech_support.handoff_description}")
IO.puts(" -> #{billing_agent.name}: #{billing_agent.handoff_description}")
IO.puts("\nStarting realtime session...")
config = %RunConfig{
model_settings: %SessionModelSettings{
voice: "alloy",
# Force a deterministic handoff tool call for demo stability.
tool_choice: {:function, "transfer_to_techsupport"}
}
}
Realtime.run(greeter, config: config)
end
defp handle_events(timeout, state) do
if is_binary(state.skip_reason) do
state
else
receive do
{:session_event, %Events.HandoffEvent{from_agent: from, to_agent: to}} ->
IO.puts("\n[Handoff] #{from.name} -> #{to.name}")
handle_events(timeout, %{state | handoff_seen?: true})
{:session_event, %Events.ToolStartEvent{tool: tool}} ->
tool_name = Map.get(tool || %{}, "name") || Map.get(tool || %{}, :name) || inspect(tool)
IO.puts("\n[Tool Call] #{tool_name}")
handle_events(timeout, %{state | tool_calls: state.tool_calls + 1})
{:session_event, %Events.AgentStartEvent{agent: agent}} ->
IO.puts("\n[Agent] Now speaking with: #{agent.name}")
handle_events(timeout, state)
{:session_event, %Events.AgentEndEvent{agent: agent}} ->
IO.puts("[Agent] #{agent.name} turn ended")
handle_events(timeout, state)
{:session_event, %Events.AudioEvent{}} ->
IO.write(".")
handle_events(timeout, state)
{:session_event, %Events.ErrorEvent{error: error}} ->
case skip_reason_for_error(error) do
reason when is_binary(reason) ->
IO.puts("\n[Error] #{reason} from API")
%{state | skip_reason: reason}
_ ->
IO.puts("\n[Error] #{inspect(error)}")
handle_events(timeout, state)
end
{:session_event, event} ->
IO.puts("[Event] #{inspect(event.__struct__)}")
handle_events(timeout, state)
after
timeout ->
IO.puts("\n[Timeout] Event handling complete")
state
end
end
end
defp safe_close(session) do
IO.puts("\nClosing session...")
Realtime.close(session)
rescue
_ -> :ok
end
defp maybe_skip_quota(reason) do
case Diagnostics.skip_reason_for_error(reason) do
nil -> {:error, reason}
skip_reason -> {:skip, skip_reason}
end
end
defp skip_reason_for_error(error), do: Diagnostics.skip_reason_for_error(error)
defp fetch_api_key, do: Codex.Auth.direct_api_key()
end
RealtimeHandoffsExample.main()