mirror of
https://github.com/openai/codex.git
synced 2026-04-26 23:55:25 +00:00
Move default realtime prompt into core (#17165)
- Adds a core-owned realtime backend prompt template and preparation path. - Makes omitted realtime start prompts use the core default, while null or empty prompts intentionally send empty instructions. - Covers the core realtime path and app-server v2 path with integration coverage. --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -48,6 +48,8 @@ use wiremock::matchers::method;
|
||||
use wiremock::matchers::path_regex;
|
||||
|
||||
const STARTUP_CONTEXT_HEADER: &str = "Startup context from Codex.";
|
||||
const REALTIME_BACKEND_PROMPT: &str = include_str!("../../templates/realtime/backend_prompt.md");
|
||||
const USER_FIRST_NAME_PLACEHOLDER: &str = "{{ user_first_name }}";
|
||||
const MEMORY_PROMPT_PHRASE: &str =
|
||||
"You have access to a memory folder with guidance from prior runs.";
|
||||
const REALTIME_CONVERSATION_TEST_SUBPROCESS_ENV_VAR: &str =
|
||||
@@ -101,6 +103,20 @@ fn websocket_request_instructions(
|
||||
.map(str::to_owned)
|
||||
}
|
||||
|
||||
fn expected_realtime_backend_prompt() -> String {
|
||||
REALTIME_BACKEND_PROMPT
|
||||
.trim_end()
|
||||
.replace(USER_FIRST_NAME_PLACEHOLDER, &test_user_first_name())
|
||||
}
|
||||
|
||||
fn test_user_first_name() -> String {
|
||||
[whoami::realname(), whoami::username()]
|
||||
.into_iter()
|
||||
.filter_map(|name| name.split_whitespace().next().map(str::to_string))
|
||||
.find(|name| !name.is_empty())
|
||||
.unwrap_or_else(|| "there".to_string())
|
||||
}
|
||||
|
||||
async fn wait_for_matching_websocket_request<F>(
|
||||
server: &core_test_support::responses::WebSocketTestServer,
|
||||
description: &str,
|
||||
@@ -224,7 +240,7 @@ async fn conversation_start_audio_text_close_round_trip() -> Result<()> {
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
prompt: Some(Some("backend prompt".to_string())),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
@@ -375,7 +391,7 @@ async fn conversation_webrtc_start_posts_generated_session() -> Result<()> {
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
prompt: Some(Some("backend prompt".to_string())),
|
||||
session_id: None,
|
||||
transport: Some(ConversationStartTransport::Webrtc {
|
||||
sdp: "v=offer\r\n".to_string(),
|
||||
@@ -511,7 +527,7 @@ async fn conversation_start_uses_openai_env_key_fallback_with_chatgpt_auth() ->
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
prompt: Some(Some("backend prompt".to_string())),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
@@ -571,7 +587,7 @@ async fn conversation_transport_close_emits_closed_event() -> Result<()> {
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
prompt: Some(Some("backend prompt".to_string())),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
@@ -655,7 +671,7 @@ async fn conversation_start_preflight_failure_emits_realtime_error_only() -> Res
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
prompt: Some(Some("backend prompt".to_string())),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
@@ -696,7 +712,7 @@ async fn conversation_start_connect_failure_emits_realtime_error_only() -> Resul
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
prompt: Some(Some("backend prompt".to_string())),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
@@ -785,7 +801,7 @@ async fn conversation_second_start_replaces_runtime() -> Result<()> {
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "old".to_string(),
|
||||
prompt: Some(Some("old".to_string())),
|
||||
session_id: Some("conv_old".to_string()),
|
||||
transport: None,
|
||||
}))
|
||||
@@ -802,7 +818,7 @@ async fn conversation_second_start_replaces_runtime() -> Result<()> {
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "new".to_string(),
|
||||
prompt: Some(Some("new".to_string())),
|
||||
session_id: Some("conv_new".to_string()),
|
||||
transport: None,
|
||||
}))
|
||||
@@ -889,7 +905,7 @@ async fn conversation_uses_experimental_realtime_ws_base_url_override() -> Resul
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
prompt: Some(Some("backend prompt".to_string())),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
@@ -919,6 +935,132 @@ async fn conversation_uses_experimental_realtime_ws_base_url_override() -> Resul
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn conversation_uses_default_realtime_backend_prompt() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_websocket_server(vec![
|
||||
vec![],
|
||||
vec![vec![json!({
|
||||
"type": "session.updated",
|
||||
"session": { "id": "sess_default", "instructions": "default" }
|
||||
})]],
|
||||
])
|
||||
.await;
|
||||
|
||||
let mut builder = test_codex().with_config(|config| {
|
||||
config.experimental_realtime_ws_startup_context =
|
||||
Some("controlled startup context".to_string());
|
||||
});
|
||||
let test = builder.build_with_websocket_server(&server).await?;
|
||||
assert!(
|
||||
server
|
||||
.wait_for_handshakes(/*expected*/ 1, Duration::from_secs(2))
|
||||
.await
|
||||
);
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: None,
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
let session_updated = wait_for_event_match(&test.codex, |msg| match msg {
|
||||
EventMsg::RealtimeConversationRealtime(RealtimeConversationRealtimeEvent {
|
||||
payload: RealtimeEvent::SessionUpdated { session_id, .. },
|
||||
}) => Some(session_id.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
assert_eq!(session_updated, "sess_default");
|
||||
|
||||
let connections = server.connections();
|
||||
assert_eq!(connections.len(), 2);
|
||||
let instructions =
|
||||
websocket_request_instructions(&connections[1][0]).expect("default session instructions");
|
||||
assert_eq!(
|
||||
instructions,
|
||||
format!(
|
||||
"{}\n\ncontrolled startup context",
|
||||
expected_realtime_backend_prompt()
|
||||
)
|
||||
);
|
||||
|
||||
server.shutdown().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn conversation_uses_empty_instructions_for_null_or_empty_prompt() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_websocket_server(vec![
|
||||
vec![],
|
||||
vec![vec![json!({
|
||||
"type": "session.updated",
|
||||
"session": { "id": "sess_null", "instructions": "" }
|
||||
})]],
|
||||
vec![vec![json!({
|
||||
"type": "session.updated",
|
||||
"session": { "id": "sess_empty", "instructions": "" }
|
||||
})]],
|
||||
])
|
||||
.await;
|
||||
|
||||
let mut builder = test_codex().with_config(|config| {
|
||||
config.experimental_realtime_ws_startup_context = Some(String::new());
|
||||
});
|
||||
let test = builder.build_with_websocket_server(&server).await?;
|
||||
assert!(
|
||||
server
|
||||
.wait_for_handshakes(/*expected*/ 1, Duration::from_secs(2))
|
||||
.await
|
||||
);
|
||||
|
||||
for (prompt, expected_session_id) in [
|
||||
(Some(None), "sess_null"),
|
||||
(Some(Some(String::new())), "sess_empty"),
|
||||
] {
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt,
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
let session_updated = wait_for_event_match(&test.codex, |msg| match msg {
|
||||
EventMsg::RealtimeConversationRealtime(RealtimeConversationRealtimeEvent {
|
||||
payload: RealtimeEvent::SessionUpdated { session_id, .. },
|
||||
}) => Some(session_id.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
assert_eq!(session_updated, expected_session_id);
|
||||
|
||||
test.codex.submit(Op::RealtimeConversationClose).await?;
|
||||
let _closed = wait_for_event_match(&test.codex, |msg| match msg {
|
||||
EventMsg::RealtimeConversationClosed(closed) => Some(closed.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
let connections = server.connections();
|
||||
assert_eq!(connections.len(), 3);
|
||||
let null_instructions =
|
||||
websocket_request_instructions(&connections[1][0]).expect("null prompt instructions");
|
||||
let empty_instructions =
|
||||
websocket_request_instructions(&connections[2][0]).expect("empty prompt instructions");
|
||||
assert_eq!(null_instructions, "");
|
||||
assert_eq!(empty_instructions, "");
|
||||
|
||||
server.shutdown().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn conversation_uses_experimental_realtime_ws_backend_prompt_override() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
@@ -944,7 +1086,7 @@ async fn conversation_uses_experimental_realtime_ws_backend_prompt_override() ->
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "prompt from op".to_string(),
|
||||
prompt: Some(Some("prompt from op".to_string())),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
@@ -1007,7 +1149,7 @@ async fn conversation_uses_experimental_realtime_ws_startup_context_override() -
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "prompt from op".to_string(),
|
||||
prompt: Some(Some("prompt from op".to_string())),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
@@ -1068,7 +1210,7 @@ async fn conversation_disables_realtime_startup_context_with_empty_override() ->
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "prompt from op".to_string(),
|
||||
prompt: Some(Some("prompt from op".to_string())),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
@@ -1122,7 +1264,7 @@ async fn conversation_start_injects_startup_context_from_thread_history() -> Res
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
prompt: Some(Some("backend prompt".to_string())),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
@@ -1176,7 +1318,7 @@ async fn conversation_startup_context_falls_back_to_workspace_map() -> Result<()
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
prompt: Some(Some("backend prompt".to_string())),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
@@ -1228,7 +1370,7 @@ async fn conversation_startup_context_is_truncated_and_sent_once_per_start() ->
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
prompt: Some(Some("backend prompt".to_string())),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
@@ -1313,7 +1455,7 @@ async fn conversation_mirrors_assistant_message_text_to_realtime_handoff() -> Re
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
prompt: Some(Some("backend prompt".to_string())),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
@@ -1440,7 +1582,7 @@ async fn conversation_handoff_persists_across_item_done_until_turn_complete() ->
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
prompt: Some(Some("backend prompt".to_string())),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
@@ -1582,7 +1724,7 @@ async fn inbound_handoff_request_starts_turn() -> Result<()> {
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
prompt: Some(Some("backend prompt".to_string())),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
@@ -1677,7 +1819,7 @@ async fn inbound_handoff_request_uses_active_transcript() -> Result<()> {
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
prompt: Some(Some("backend prompt".to_string())),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
@@ -1770,7 +1912,7 @@ async fn inbound_handoff_request_clears_active_transcript_after_each_handoff() -
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
prompt: Some(Some("backend prompt".to_string())),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
@@ -1870,7 +2012,7 @@ async fn inbound_conversation_item_does_not_start_turn_and_still_forwards_audio(
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
prompt: Some(Some("backend prompt".to_string())),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
@@ -1983,7 +2125,7 @@ async fn delegated_turn_user_role_echo_does_not_redelegate_and_still_forwards_au
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
prompt: Some(Some("backend prompt".to_string())),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
@@ -2126,7 +2268,7 @@ async fn inbound_handoff_request_does_not_block_realtime_event_forwarding() -> R
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
prompt: Some(Some("backend prompt".to_string())),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
@@ -2253,7 +2395,7 @@ async fn inbound_handoff_request_steers_active_turn() -> Result<()> {
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
prompt: Some(Some("backend prompt".to_string())),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
@@ -2395,7 +2537,7 @@ async fn inbound_handoff_request_starts_turn_and_does_not_block_realtime_audio()
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
prompt: Some(Some("backend prompt".to_string())),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user