Rename Realtime V2 tool to background_agent (#17278)

Rename the Realtime V2 delegation tool and parser constant to
background_agent, and update the tool description and fixtures to match.

Validation: just fmt; cargo check -p codex-api; git diff --check

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Ahmed IbrahimandCodex authored and GitHub committed 2026-04-09 22:17:55 -07:00
1 parent 9f2a585153
commit 60236e8c92
5 files changed
+34 -32

No files matched your search

@@ -165,14 +165,15 @@ struct StartedWebrtcRealtime {
sdp: ThreadRealtimeSdpNotification, sdp: ThreadRealtimeSdpNotification,
} }
// Scripted SSE responses for the normal Codex agent loop. Realtime can ask for a delegated // Scripted SSE responses for the normal background agent loop. Realtime can ask for a delegated
// Codex turn; that turn talks to this mock `/responses` endpoint and may request ordinary tools. // background agent turn; that turn talks to this mock `/responses` endpoint and may request
// ordinary tools.
struct MainLoopResponsesScript { struct MainLoopResponsesScript {
responses: Vec<String>, responses: Vec<String>,
} }
// Scripted server events for the direct realtime sideband WebSocket. This mock is the realtime // Scripted server events for the direct realtime sideband WebSocket. This mock is the realtime
// session app-server joins after call creation; it is not the main-loop Responses stream. // session app-server joins after call creation; it is not the background agent Responses stream.
struct RealtimeSidebandScript { struct RealtimeSidebandScript {
connections: Vec<WebSocketConnectionConfig>, connections: Vec<WebSocketConnectionConfig>,
} }
@@ -425,13 +426,13 @@ fn session_updated(session_id: &str) -> Value {
}) })
} }
fn v2_codex_tool_call(call_id: &str, prompt: &str) -> Value { fn v2_background_agent_tool_call(call_id: &str, prompt: &str) -> Value {
json!({ json!({
"type": "conversation.item.done", "type": "conversation.item.done",
"item": { "item": {
"id": format!("item_{call_id}"), "id": format!("item_{call_id}"),
"type": "function_call", "type": "function_call",
"name": "codex", "name": "background_agent",
"call_id": call_id, "call_id": call_id,
"arguments": json!({ "prompt": prompt }).to_string() "arguments": json!({ "prompt": prompt }).to_string()
} }
@@ -478,7 +479,7 @@ async fn realtime_conversation_streams_v2_notifications() -> Result<()> {
"item": { "item": {
"id": "item_2", "id": "item_2",
"type": "function_call", "type": "function_call",
"name": "codex", "name": "background_agent",
"call_id": "handoff_1", "call_id": "handoff_1",
"arguments": "{\"input_transcript\":\"delegate now\"}" "arguments": "{\"input_transcript\":\"delegate now\"}"
} }
@@ -973,7 +974,7 @@ async fn realtime_webrtc_start_emits_sdp_notification() -> Result<()> {
Some("multipart/form-data; boundary=codex-realtime-call-boundary") Some("multipart/form-data; boundary=codex-realtime-call-boundary")
); );
let body = String::from_utf8(request.body).context("multipart body should be utf-8")?; let body = String::from_utf8(request.body).context("multipart body should be utf-8")?;
let session = r#"{"tool_choice":"auto","type":"realtime","model":"gpt-realtime-1.5","instructions":"backend prompt\n\nstartup context","output_modalities":["audio"],"audio":{"input":{"format":{"type":"audio/pcm","rate":24000},"noise_reduction":{"type":"near_field"},"turn_detection":{"type":"server_vad","interrupt_response":true,"create_response":true}},"output":{"format":{"type":"audio/pcm","rate":24000},"voice":"marin"}},"tools":[{"type":"function","name":"codex","description":"Delegate a request to Codex and return the final result to the user. Use this as the default action. If the user asks to do something next, later, after this, or once current work finishes, call this tool so the work is actually queued instead of merely promising to do it later.","parameters":{"type":"object","properties":{"prompt":{"type":"string","description":"The user request to delegate to Codex."}},"required":["prompt"],"additionalProperties":false}}]}"#; let session = r#"{"tool_choice":"auto","type":"realtime","model":"gpt-realtime-1.5","instructions":"backend prompt\n\nstartup context","output_modalities":["audio"],"audio":{"input":{"format":{"type":"audio/pcm","rate":24000},"noise_reduction":{"type":"near_field"},"turn_detection":{"type":"server_vad","interrupt_response":true,"create_response":true}},"output":{"format":{"type":"audio/pcm","rate":24000},"voice":"marin"}},"tools":[{"type":"function","name":"background_agent","description":"Send a user request to the background agent. Use this as the default action. If the background agent is idle, this starts a new task and returns the final result to the user. If the background agent is already working on a task, this sends the request as guidance to steer that previous task. If the user asks to do something next, later, after this, or once current work finishes, call this tool so the work is actually queued instead of merely promising to do it later.","parameters":{"type":"object","properties":{"prompt":{"type":"string","description":"The user request to delegate to the background agent."}},"required":["prompt"],"additionalProperties":false}}]}"#;
assert_eq!( assert_eq!(
body, body,
format!( format!(
@@ -1089,7 +1090,7 @@ async fn webrtc_v1_handoff_request_delegates_and_appends_result() -> Result<()>
let started = harness.start_webrtc_realtime("v=offer\r\n").await?; let started = harness.start_webrtc_realtime("v=offer\r\n").await?;
assert_eq!(started.started.version, RealtimeConversationVersion::V1); assert_eq!(started.started.version, RealtimeConversationVersion::V1);
// Phase 2: wait for the delegated Codex turn that is launched by the handoff request. // Phase 2: wait for the delegated background agent turn that is launched by the handoff request.
let turn_started = harness let turn_started = harness
.read_notification::<TurnStartedNotification>("turn/started") .read_notification::<TurnStartedNotification>("turn/started")
.await?; .await?;
@@ -1204,11 +1205,12 @@ async fn webrtc_v2_forwards_audio_and_text_between_client_and_sideband() -> Resu
} }
#[tokio::test] #[tokio::test]
async fn webrtc_v2_codex_tool_call_delegates_and_returns_function_output() -> Result<()> { async fn webrtc_v2_background_agent_tool_call_delegates_and_returns_function_output() -> Result<()>
{
skip_if_no_network!(Ok(())); skip_if_no_network!(Ok(()));
// Phase 1: script a v2 codex function call and a delegated Responses turn that returns final // Phase 1: script a v2 background agent function call and a delegated Responses turn that
// assistant text. // returns final assistant text.
let mut harness = RealtimeE2eHarness::new( let mut harness = RealtimeE2eHarness::new(
RealtimeTestVersion::V2, RealtimeTestVersion::V2,
main_loop_responses(vec![create_final_assistant_message_sse_response( main_loop_responses(vec![create_final_assistant_message_sse_response(
@@ -1217,7 +1219,7 @@ async fn webrtc_v2_codex_tool_call_delegates_and_returns_function_output() -> Re
realtime_sideband(vec![realtime_sideband_connection(vec![ realtime_sideband(vec![realtime_sideband_connection(vec![
vec![ vec![
session_updated("sess_v2_tool"), session_updated("sess_v2_tool"),
v2_codex_tool_call("call_v2", "delegate from v2"), v2_background_agent_tool_call("call_v2", "delegate from v2"),
], ],
vec![], vec![],
])]), ])]),
@@ -1259,8 +1261,8 @@ async fn webrtc_v2_tool_call_delegated_turn_can_execute_shell_tool() -> Result<(
skip_if_no_network!(Ok(())); skip_if_no_network!(Ok(()));
// Phase 1: keep the two mocked OpenAI conversations explicit. The realtime sideband only // Phase 1: keep the two mocked OpenAI conversations explicit. The realtime sideband only
// calls the `codex` function; the shell command is requested by the delegated main-loop // calls the `background_agent` function; the shell command is requested by the delegated
// Responses turn that app-server starts after receiving that function call. // background agent Responses turn that app-server starts after receiving that function call.
let main_loop = main_loop_responses(vec![ let main_loop = main_loop_responses(vec![
create_shell_command_sse_response( create_shell_command_sse_response(
realtime_tool_ok_command(), realtime_tool_ok_command(),
@@ -1273,7 +1275,7 @@ async fn webrtc_v2_tool_call_delegated_turn_can_execute_shell_tool() -> Result<(
let realtime = realtime_sideband(vec![realtime_sideband_connection(vec![ let realtime = realtime_sideband(vec![realtime_sideband_connection(vec![
vec![ vec![
session_updated("sess_v2_shell"), session_updated("sess_v2_shell"),
v2_codex_tool_call("call_shell", "run shell through delegated turn"), v2_background_agent_tool_call("call_shell", "run shell through delegated turn"),
], ],
vec![], vec![],
])]); ])]);
@@ -1288,7 +1290,7 @@ async fn webrtc_v2_tool_call_delegated_turn_can_execute_shell_tool() -> Result<(
let _ = harness.start_webrtc_realtime("v=offer\r\n").await?; let _ = harness.start_webrtc_realtime("v=offer\r\n").await?;
// Phase 2: observe the delegated main-loop turn executing the requested shell command. // Phase 2: observe the delegated background agent turn executing the requested shell command.
let started_command = wait_for_started_command_execution(&mut harness.mcp).await?; let started_command = wait_for_started_command_execution(&mut harness.mcp).await?;
let ThreadItem::CommandExecution { id, status, .. } = started_command.item else { let ThreadItem::CommandExecution { id, status, .. } = started_command.item else {
unreachable!("helper returns command execution items"); unreachable!("helper returns command execution items");
@@ -1367,7 +1369,7 @@ async fn webrtc_v2_tool_call_does_not_block_sideband_audio() -> Result<()> {
realtime_sideband(vec![realtime_sideband_connection(vec![ realtime_sideband(vec![realtime_sideband_connection(vec![
vec![ vec![
session_updated("sess_v2_nonblocking"), session_updated("sess_v2_nonblocking"),
v2_codex_tool_call("call_audio", "delegate while audio continues"), v2_background_agent_tool_call("call_audio", "delegate while audio continues"),
json!({ json!({
"type": "response.output_audio.delta", "type": "response.output_audio.delta",
"delta": "CQoL", "delta": "CQoL",
@@ -1669,7 +1671,7 @@ fn assert_v2_session_update(request: &Value) -> Result<()> {
); );
assert_eq!( assert_eq!(
request["session"]["tools"][0]["name"].as_str(), request["session"]["tools"][0]["name"].as_str(),
Some("codex") Some("background_agent")
); );
Ok(()) Ok(())
} }
@@ -868,7 +868,7 @@ mod tests {
"item": { "item": {
"id": "item_123", "id": "item_123",
"type": "function_call", "type": "function_call",
"name": "codex", "name": "background_agent",
"call_id": "call_123", "call_id": "call_123",
"arguments": "{\"prompt\":\"delegate this\"}" "arguments": "{\"prompt\":\"delegate this\"}"
} }
@@ -989,7 +989,7 @@ mod tests {
"output": [{ "output": [{
"id": "item_123", "id": "item_123",
"type": "function_call", "type": "function_call",
"name": "codex", "name": "background_agent",
"call_id": "call_123", "call_id": "call_123",
"arguments": "{\"prompt\":\"delegate from done\"}" "arguments": "{\"prompt\":\"delegate from done\"}"
}] }]
@@ -1283,7 +1283,7 @@ mod tests {
assert_eq!(fourth_json["handoff_id"], "handoff_1"); assert_eq!(fourth_json["handoff_id"], "handoff_1");
assert_eq!( assert_eq!(
fourth_json["output_text"], fourth_json["output_text"],
"\"Agent Final Message\":\n\nhello from codex" "\"Agent Final Message\":\n\nhello from background agent"
); );
ws.send(Message::Text( ws.send(Message::Text(
@@ -1407,7 +1407,7 @@ mod tests {
connection connection
.send_conversation_handoff_append( .send_conversation_handoff_append(
"handoff_1".to_string(), "handoff_1".to_string(),
"hello from codex".to_string(), "hello from background agent".to_string(),
) )
.await .await
.expect("send handoff"); .expect("send handoff");
@@ -1493,7 +1493,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
async fn realtime_v2_session_update_includes_codex_tool_and_handoff_output_item() { async fn realtime_v2_session_update_includes_background_agent_tool_and_handoff_output_item() {
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind"); let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("local addr"); let addr = listener.local_addr().expect("local addr");
@@ -1553,7 +1553,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
first_json["session"]["tools"][0]["name"], first_json["session"]["tools"][0]["name"],
Value::String("codex".to_string()) Value::String("background_agent".to_string())
); );
assert_eq!( assert_eq!(
first_json["session"]["tools"][0]["parameters"]["required"], first_json["session"]["tools"][0]["parameters"]["required"],
@@ -27,8 +27,8 @@ use serde_json::json;
const REALTIME_V2_OUTPUT_MODALITY_AUDIO: &str = "audio"; const REALTIME_V2_OUTPUT_MODALITY_AUDIO: &str = "audio";
const REALTIME_V2_TOOL_CHOICE: &str = "auto"; const REALTIME_V2_TOOL_CHOICE: &str = "auto";
const REALTIME_V2_CODEX_TOOL_NAME: &str = "codex"; const REALTIME_V2_BACKGROUND_AGENT_TOOL_NAME: &str = "background_agent";
const REALTIME_V2_CODEX_TOOL_DESCRIPTION: &str = "Delegate a request to Codex and return the final result to the user. Use this as the default action. If the user asks to do something next, later, after this, or once current work finishes, call this tool so the work is actually queued instead of merely promising to do it later."; const REALTIME_V2_BACKGROUND_AGENT_TOOL_DESCRIPTION: &str = "Send a user request to the background agent. Use this as the default action. If the background agent is idle, this starts a new task and returns the final result to the user. If the background agent is already working on a task, this sends the request as guidance to steer that previous task. If the user asks to do something next, later, after this, or once current work finishes, call this tool so the work is actually queued instead of merely promising to do it later.";
pub(super) fn conversation_item_create_message(text: String) -> RealtimeOutboundMessage { pub(super) fn conversation_item_create_message(text: String) -> RealtimeOutboundMessage {
RealtimeOutboundMessage::ConversationItemCreate { RealtimeOutboundMessage::ConversationItemCreate {
@@ -93,14 +93,14 @@ pub(super) fn session_update_session(
}, },
tools: Some(vec![SessionFunctionTool { tools: Some(vec![SessionFunctionTool {
r#type: SessionToolType::Function, r#type: SessionToolType::Function,
name: REALTIME_V2_CODEX_TOOL_NAME.to_string(), name: REALTIME_V2_BACKGROUND_AGENT_TOOL_NAME.to_string(),
description: REALTIME_V2_CODEX_TOOL_DESCRIPTION.to_string(), description: REALTIME_V2_BACKGROUND_AGENT_TOOL_DESCRIPTION.to_string(),
parameters: json!({ parameters: json!({
"type": "object", "type": "object",
"properties": { "properties": {
"prompt": { "prompt": {
"type": "string", "type": "string",
"description": "The user request to delegate to Codex." "description": "The user request to delegate to the background agent."
} }
}, },
"required": ["prompt"], "required": ["prompt"],
@@ -11,7 +11,7 @@ use serde_json::Map as JsonMap;
use serde_json::Value; use serde_json::Value;
use tracing::debug; use tracing::debug;
const CODEX_TOOL_NAME: &str = "codex"; const BACKGROUND_AGENT_TOOL_NAME: &str = "background_agent";
const DEFAULT_AUDIO_SAMPLE_RATE: u32 = 24_000; const DEFAULT_AUDIO_SAMPLE_RATE: u32 = 24_000;
const DEFAULT_AUDIO_CHANNELS: u16 = 1; const DEFAULT_AUDIO_CHANNELS: u16 = 1;
const TOOL_ARGUMENT_KEYS: [&str; 5] = ["input_transcript", "input", "text", "prompt", "query"]; const TOOL_ARGUMENT_KEYS: [&str; 5] = ["input_transcript", "input", "text", "prompt", "query"];
@@ -118,7 +118,7 @@ fn parse_conversation_item_done_event(parsed: &Value) -> Option<RealtimeEvent> {
fn parse_handoff_requested_event(item: &JsonMap<String, Value>) -> Option<RealtimeEvent> { fn parse_handoff_requested_event(item: &JsonMap<String, Value>) -> Option<RealtimeEvent> {
let item_type = item.get("type").and_then(Value::as_str); let item_type = item.get("type").and_then(Value::as_str);
let item_name = item.get("name").and_then(Value::as_str); let item_name = item.get("name").and_then(Value::as_str);
if item_type != Some("function_call") || item_name != Some(CODEX_TOOL_NAME) { if item_type != Some("function_call") || item_name != Some(BACKGROUND_AGENT_TOOL_NAME) {
return None; return None;
} }
@@ -493,7 +493,7 @@ async fn realtime_ws_e2e_realtime_v2_parser_emits_handoff_requested() {
"item": { "item": {
"id": "item_123", "id": "item_123",
"type": "function_call", "type": "function_call",
"name": "codex", "name": "background_agent",
"call_id": "call_123", "call_id": "call_123",
"arguments": "{\"prompt\":\"delegate now\"}" "arguments": "{\"prompt\":\"delegate now\"}"
} }