diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock
index 69a9f51b0e..f9cf38bc90 100644
--- a/codex-rs/Cargo.lock
+++ b/codex-rs/Cargo.lock
@@ -1947,6 +1947,7 @@ dependencies = [
"codex-otel",
"codex-plugin",
"codex-protocol",
+ "codex-realtime",
"codex-response-debug-context",
"codex-rmcp-client",
"codex-rollout",
@@ -2677,6 +2678,33 @@ dependencies = [
"uuid",
]
+[[package]]
+name = "codex-realtime"
+version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "async-channel",
+ "base64 0.22.1",
+ "chrono",
+ "codex-api",
+ "codex-config",
+ "codex-exec-server",
+ "codex-git-utils",
+ "codex-login",
+ "codex-model-provider-info",
+ "codex-protocol",
+ "codex-thread-store",
+ "codex-utils-absolute-path",
+ "codex-utils-output-truncation",
+ "http 1.4.0",
+ "pretty_assertions",
+ "serde_json",
+ "tempfile",
+ "tokio",
+ "tracing",
+ "whoami",
+]
+
[[package]]
name = "codex-realtime-webrtc"
version = "0.0.0"
diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml
index fd203e6259..5decb33ff2 100644
--- a/codex-rs/Cargo.toml
+++ b/codex-rs/Cargo.toml
@@ -50,6 +50,7 @@ members = [
"ollama",
"process-hardening",
"protocol",
+ "realtime",
"realtime-webrtc",
"rollout",
"rmcp-client",
@@ -159,6 +160,7 @@ codex-plugin = { path = "plugin" }
codex-model-provider = { path = "model-provider" }
codex-process-hardening = { path = "process-hardening" }
codex-protocol = { path = "protocol" }
+codex-realtime = { path = "realtime" }
codex-realtime-webrtc = { path = "realtime-webrtc" }
codex-responses-api-proxy = { path = "responses-api-proxy" }
codex-response-debug-context = { path = "response-debug-context" }
diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml
index 8770aaf487..2da8f3ba6e 100644
--- a/codex-rs/core/Cargo.toml
+++ b/codex-rs/core/Cargo.toml
@@ -55,6 +55,7 @@ codex-plugin = { workspace = true }
codex-model-provider = { workspace = true }
codex-protocol = { workspace = true }
codex-response-debug-context = { workspace = true }
+codex-realtime = { workspace = true }
codex-rollout = { workspace = true }
codex-rmcp-client = { workspace = true }
codex-sandboxing = { workspace = true }
diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs
index 1809789a8e..d2c8cf5b03 100644
--- a/codex-rs/core/src/lib.rs
+++ b/codex-rs/core/src/lib.rs
@@ -13,7 +13,6 @@ mod client;
mod client_common;
mod realtime_context;
mod realtime_conversation;
-mod realtime_prompt;
pub(crate) mod session;
pub use session::SteerInputError;
mod codex_thread;
diff --git a/codex-rs/core/src/realtime_context.rs b/codex-rs/core/src/realtime_context.rs
index 6dfb56a8a3..d176b3b777 100644
--- a/codex-rs/core/src/realtime_context.rs
+++ b/codex-rs/core/src/realtime_context.rs
@@ -1,128 +1,35 @@
use crate::compact::content_items_to_text;
use crate::event_mapping::is_contextual_user_message_content;
use crate::session::session::Session;
-use chrono::Utc;
-use codex_exec_server::LOCAL_FS;
-use codex_git_utils::resolve_root_git_project_for_trust;
use codex_protocol::models::ResponseItem;
+use codex_realtime::RealtimeStartupContext;
+use codex_realtime::RealtimeStartupContextTurn;
use codex_thread_store::ListThreadsParams;
use codex_thread_store::SortDirection;
use codex_thread_store::StoredThread;
use codex_thread_store::ThreadSortKey;
use codex_thread_store::ThreadStore;
-use codex_utils_absolute_path::AbsolutePathBuf;
-use codex_utils_output_truncation::TruncationPolicy;
-use codex_utils_output_truncation::truncate_text;
use dirs::home_dir;
-use std::cmp::Reverse;
-use std::collections::HashMap;
-use std::collections::HashSet;
-use std::ffi::OsStr;
-use std::fs::DirEntry;
-use std::io;
use std::mem::take;
-use std::path::Path;
-use std::path::PathBuf;
-use tracing::debug;
-use tracing::info;
use tracing::warn;
-const STARTUP_CONTEXT_HEADER: &str = "Startup context from Codex.\nThis is background context about recent work and machine/workspace layout. It may be incomplete or stale. Use it to inform responses, and do not repeat it back unless relevant.";
-const STARTUP_CONTEXT_OPEN_TAG: &str = "";
-const STARTUP_CONTEXT_CLOSE_TAG: &str = "";
-const CURRENT_THREAD_SECTION_TOKEN_BUDGET: usize = 1_200;
-const RECENT_WORK_SECTION_TOKEN_BUDGET: usize = 2_200;
-const WORKSPACE_SECTION_TOKEN_BUDGET: usize = 1_600;
-const NOTES_SECTION_TOKEN_BUDGET: usize = 300;
-pub(crate) const REALTIME_TURN_TOKEN_BUDGET: usize = 300;
const MAX_RECENT_THREADS: usize = 40;
-const MAX_RECENT_WORK_GROUPS: usize = 8;
-const MAX_CURRENT_CWD_ASKS: usize = 8;
-const MAX_OTHER_CWD_ASKS: usize = 5;
-const MAX_ASK_CHARS: usize = 240;
-const TREE_MAX_DEPTH: usize = 2;
-const DIR_ENTRY_LIMIT: usize = 20;
-const APPROX_BYTES_PER_TOKEN: usize = 4;
-const NOISY_DIR_NAMES: &[&str] = &[
- ".git",
- ".next",
- ".pytest_cache",
- ".ruff_cache",
- "__pycache__",
- "build",
- "dist",
- "node_modules",
- "out",
- "target",
-];
pub(crate) async fn build_realtime_startup_context(
sess: &Session,
budget_tokens: usize,
) -> Option {
let config = sess.get_config().await;
- let cwd = config.cwd.clone();
let history = sess.clone_history().await;
- let current_thread_section = build_current_thread_section(history.raw_items());
+ let current_thread_turns = current_thread_turns(history.raw_items());
let recent_threads = load_recent_threads(sess).await;
- let recent_work_section = build_recent_work_section(&cwd, &recent_threads).await;
- let workspace_section = build_workspace_section_with_user_root(&cwd, home_dir()).await;
-
- if current_thread_section.is_none()
- && recent_work_section.is_none()
- && workspace_section.is_none()
- {
- debug!("realtime startup context unavailable; skipping injection");
- return None;
- }
-
- let mut parts = vec![STARTUP_CONTEXT_HEADER.to_string()];
-
- let has_current_thread_section = current_thread_section.is_some();
- let has_recent_work_section = recent_work_section.is_some();
- let has_workspace_section = workspace_section.is_some();
-
- if let Some(section) = format_section(
- "Current Thread",
- current_thread_section,
- CURRENT_THREAD_SECTION_TOKEN_BUDGET,
- ) {
- parts.push(section);
- }
- if let Some(section) = format_section(
- "Recent Work",
- recent_work_section,
- RECENT_WORK_SECTION_TOKEN_BUDGET,
- ) {
- parts.push(section);
- }
- if let Some(section) = format_section(
- "Machine / Workspace Map",
- workspace_section,
- WORKSPACE_SECTION_TOKEN_BUDGET,
- ) {
- parts.push(section);
- }
- if let Some(section) = format_section(
- "Notes",
- Some("Built at realtime startup from the current thread history, local thread metadata, and a bounded local workspace scan. This excludes repo memory instructions, AGENTS files, project-doc prompt blends, and memory summaries.".to_string()),
- NOTES_SECTION_TOKEN_BUDGET,
- ) {
- parts.push(section);
- }
-
- let context = format_startup_context_blob(&parts.join("\n\n"));
- debug!(
- approx_tokens = approx_token_count(&context),
- requested_budget_tokens = budget_tokens,
- bytes = context.len(),
- has_current_thread_section,
- has_recent_work_section,
- has_workspace_section,
- "built realtime startup context"
- );
- info!("realtime startup context: {context}");
- Some(context)
+ let context = RealtimeStartupContext {
+ cwd: config.cwd.clone(),
+ current_thread_turns,
+ recent_threads,
+ user_root: home_dir(),
+ };
+ codex_realtime::build_realtime_startup_context(&context, budget_tokens).await
}
async fn load_recent_threads(sess: &Session) -> Vec {
@@ -149,61 +56,7 @@ async fn load_recent_threads(sess: &Session) -> Vec {
}
}
-async fn build_recent_work_section(
- cwd: &AbsolutePathBuf,
- recent_threads: &[StoredThread],
-) -> Option {
- let mut groups: HashMap> = HashMap::new();
- for entry in recent_threads {
- let group = match AbsolutePathBuf::from_absolute_path(entry.cwd.as_path()) {
- Ok(entry_cwd) => resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &entry_cwd)
- .await
- .map(AbsolutePathBuf::into_path_buf)
- .unwrap_or_else(|| entry.cwd.clone()),
- Err(_) => entry.cwd.clone(),
- };
- groups.entry(group).or_default().push(entry);
- }
-
- let current_group = resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), cwd)
- .await
- .map(AbsolutePathBuf::into_path_buf)
- .unwrap_or_else(|| cwd.clone().into_path_buf());
- let mut groups = groups.into_iter().collect::>();
- groups.sort_by(|(left_group, left_entries), (right_group, right_entries)| {
- let left_latest = left_entries
- .iter()
- .map(|entry| entry.updated_at)
- .max()
- .unwrap_or_else(Utc::now);
- let right_latest = right_entries
- .iter()
- .map(|entry| entry.updated_at)
- .max()
- .unwrap_or_else(Utc::now);
- (
- *left_group != current_group,
- Reverse(left_latest),
- left_group.as_os_str(),
- )
- .cmp(&(
- *right_group != current_group,
- Reverse(right_latest),
- right_group.as_os_str(),
- ))
- });
-
- let mut sections = Vec::new();
- for (group, mut entries) in groups.into_iter().take(MAX_RECENT_WORK_GROUPS) {
- entries.sort_by_key(|entry| Reverse(entry.updated_at));
- if let Some(section) = format_thread_group(¤t_group, &group, entries).await {
- sections.push(section);
- }
- }
- (!sections.is_empty()).then(|| sections.join("\n\n"))
-}
-
-fn build_current_thread_section(items: &[ResponseItem]) -> Option {
+fn current_thread_turns(items: &[ResponseItem]) -> Vec {
let mut turns = Vec::new();
let mut current_user = Vec::new();
let mut current_assistant = Vec::new();
@@ -221,7 +74,10 @@ fn build_current_thread_section(items: &[ResponseItem]) -> Option {
continue;
};
if !current_user.is_empty() || !current_assistant.is_empty() {
- turns.push((take(&mut current_user), take(&mut current_assistant)));
+ turns.push(RealtimeStartupContextTurn {
+ user_messages: take(&mut current_user),
+ assistant_messages: take(&mut current_assistant),
+ });
}
current_user.push(text);
}
@@ -242,321 +98,40 @@ fn build_current_thread_section(items: &[ResponseItem]) -> Option {
}
if !current_user.is_empty() || !current_assistant.is_empty() {
- turns.push((current_user, current_assistant));
+ turns.push(RealtimeStartupContextTurn {
+ user_messages: current_user,
+ assistant_messages: current_assistant,
+ });
}
- if turns.is_empty() {
- return None;
- }
-
- let mut lines = vec![
- "Most recent user/assistant turns from this exact thread. Use them for continuity when responding.".to_string(),
- ];
- let mut remaining_budget =
- CURRENT_THREAD_SECTION_TOKEN_BUDGET.saturating_sub(approx_token_count(&lines.join("\n")));
- let mut retained_turn_count = 0;
-
- for (index, (user_messages, assistant_messages)) in turns.into_iter().rev().enumerate() {
- if remaining_budget == 0 {
- break;
- }
-
- let mut turn_lines = Vec::new();
- if index == 0 {
- turn_lines.push("### Latest turn".to_string());
- } else {
- turn_lines.push(format!("### Previous turn {index}"));
- }
-
- if !user_messages.is_empty() {
- turn_lines.push("User:".to_string());
- turn_lines.push(user_messages.join("\n\n"));
- }
- if !assistant_messages.is_empty() {
- turn_lines.push(String::new());
- turn_lines.push("Assistant:".to_string());
- turn_lines.push(assistant_messages.join("\n\n"));
- }
-
- let turn_budget = REALTIME_TURN_TOKEN_BUDGET.min(remaining_budget);
- let turn_text = turn_lines.join("\n");
- let turn_text = truncate_realtime_text_to_token_budget(&turn_text, turn_budget);
- let turn_tokens = approx_token_count(&turn_text);
- if turn_tokens == 0 {
- continue;
- }
-
- lines.push(String::new());
- lines.push(turn_text);
- remaining_budget = remaining_budget.saturating_sub(turn_tokens);
- retained_turn_count += 1;
- }
-
- (retained_turn_count > 0).then(|| lines.join("\n"))
+ turns
}
-pub(crate) fn truncate_realtime_text_to_token_budget(text: &str, budget_tokens: usize) -> String {
- let mut truncation_budget = budget_tokens;
- loop {
- let candidate = truncate_text(text, TruncationPolicy::Tokens(truncation_budget));
- let candidate_tokens = approx_token_count(&candidate);
- if candidate_tokens <= budget_tokens {
- break candidate;
- }
-
- // The shared truncator adds its marker after choosing preserved
- // content, so tighten the content budget until the rendered turn
- // itself fits the per-turn cap.
- let excess_tokens = candidate_tokens.saturating_sub(budget_tokens);
- let next_budget = truncation_budget.saturating_sub(excess_tokens.max(1));
- if next_budget == 0 {
- let candidate = truncate_text(text, TruncationPolicy::Tokens(0));
- if approx_token_count(&candidate) <= budget_tokens {
- break candidate;
- }
- break String::new();
- }
- truncation_budget = next_budget;
- }
+#[cfg(test)]
+fn build_current_thread_section(items: &[ResponseItem]) -> Option {
+ codex_realtime::build_current_thread_section(¤t_thread_turns(items))
}
-async fn build_workspace_section_with_user_root(
- cwd: &AbsolutePathBuf,
- user_root: Option,
-) -> Option {
- let cwd_path = cwd.as_path();
- let git_root = resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), cwd).await;
- let cwd_tree = render_tree(cwd_path);
- let git_root_tree = git_root
- .as_ref()
- .filter(|git_root| git_root.as_path() != cwd_path)
- .and_then(|git_root| render_tree(git_root.as_path()));
- let user_root_tree = user_root
- .as_ref()
- .filter(|user_root| user_root.as_path() != cwd_path)
- .filter(|user_root| {
- git_root
- .as_ref()
- .is_none_or(|git_root| git_root.as_path() != user_root.as_path())
- })
- .and_then(|user_root| render_tree(user_root));
-
- if cwd_tree.is_none() && git_root.is_none() && user_root_tree.is_none() {
- return None;
- }
-
- let mut lines = vec![
- format!("Current working directory: {}", cwd_path.display()),
- format!("Working directory name: {}", file_name_string(cwd_path)),
- ];
-
- if let Some(git_root) = &git_root {
- lines.push(format!("Git root: {}", git_root.display()));
- lines.push(format!("Git project: {}", file_name_string(git_root)));
- }
- if let Some(user_root) = &user_root {
- lines.push(format!("User root: {}", user_root.display()));
- }
-
- if let Some(tree) = cwd_tree {
- lines.push(String::new());
- lines.push("Working directory tree:".to_string());
- lines.extend(tree);
- }
-
- if let Some(tree) = git_root_tree {
- lines.push(String::new());
- lines.push("Git root tree:".to_string());
- lines.extend(tree);
- }
-
- if let Some(tree) = user_root_tree {
- lines.push(String::new());
- lines.push("User root tree:".to_string());
- lines.extend(tree);
- }
-
- Some(lines.join("\n"))
-}
-
-fn render_tree(root: &Path) -> Option> {
- if !root.is_dir() {
- return None;
- }
-
- let mut lines = Vec::new();
- collect_tree_lines(root, /*depth*/ 0, &mut lines);
- (!lines.is_empty()).then_some(lines)
-}
-
-fn collect_tree_lines(dir: &Path, depth: usize, lines: &mut Vec) {
- if depth >= TREE_MAX_DEPTH {
- return;
- }
-
- let entries = match read_sorted_entries(dir) {
- Ok(entries) => entries,
- Err(_) => return,
- };
- let total_entries = entries.len();
-
- for entry in entries.into_iter().take(DIR_ENTRY_LIMIT) {
- let Ok(file_type) = entry.file_type() else {
- continue;
- };
- let name = file_name_string(&entry.path());
- let indent = " ".repeat(depth);
- let suffix = if file_type.is_dir() { "/" } else { "" };
- lines.push(format!("{indent}- {name}{suffix}"));
- if file_type.is_dir() {
- collect_tree_lines(&entry.path(), depth + 1, lines);
- }
- }
-
- if total_entries > DIR_ENTRY_LIMIT {
- lines.push(format!(
- "{}- ... {} more entries",
- " ".repeat(depth),
- total_entries - DIR_ENTRY_LIMIT
- ));
- }
-}
-
-fn read_sorted_entries(dir: &Path) -> io::Result> {
- let mut entries = std::fs::read_dir(dir)?
- .filter_map(Result::ok)
- .filter(|entry| !is_noisy_name(&entry.file_name()))
- .collect::>();
- entries.sort_by(|left, right| {
- let left_is_dir = left
- .file_type()
- .map(|file_type| file_type.is_dir())
- .unwrap_or(false);
- let right_is_dir = right
- .file_type()
- .map(|file_type| file_type.is_dir())
- .unwrap_or(false);
- (!left_is_dir, file_name_string(&left.path()))
- .cmp(&(!right_is_dir, file_name_string(&right.path())))
- });
- Ok(entries)
-}
-
-fn is_noisy_name(name: &OsStr) -> bool {
- let name = name.to_string_lossy();
- name.starts_with('.') || NOISY_DIR_NAMES.iter().any(|noisy| *noisy == name)
-}
-
-fn format_section(title: &str, body: Option, budget_tokens: usize) -> Option {
- let body = body?;
- let body = body.trim();
- if body.is_empty() {
- return None;
- }
-
- let heading = format!("## {title}\n");
- let body_budget = budget_tokens.saturating_sub(approx_token_count(&heading));
- if body_budget == 0 {
- return None;
- }
-
- let body = truncate_realtime_text_to_token_budget(body, body_budget);
- if body.is_empty() {
- return None;
- }
-
- Some(format!("{heading}{body}"))
-}
-
-fn format_startup_context_blob(body: &str) -> String {
- format!("{STARTUP_CONTEXT_OPEN_TAG}\n{body}\n{STARTUP_CONTEXT_CLOSE_TAG}")
-}
-
-async fn format_thread_group(
- current_group: &Path,
- group: &Path,
- entries: Vec<&StoredThread>,
-) -> Option {
- let latest = entries.first()?;
- let group_label =
- if let Ok(latest_cwd) = AbsolutePathBuf::from_absolute_path(latest.cwd.as_path()) {
- if resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &latest_cwd)
- .await
- .is_some()
- {
- format!("### Git repo: {}", group.display())
- } else {
- format!("### Directory: {}", group.display())
- }
- } else {
- format!("### Directory: {}", group.display())
- };
- let mut lines = vec![
- group_label,
- format!("Recent sessions: {}", entries.len()),
- format!("Latest activity: {}", latest.updated_at.to_rfc3339()),
- ];
-
- if let Some(git_branch) = latest
- .git_info
- .as_ref()
- .and_then(|git| git.branch.as_deref())
- .filter(|git_branch| !git_branch.is_empty())
- {
- lines.push(format!("Latest branch: {git_branch}"));
- }
-
- lines.push(String::new());
- lines.push("User asks:".to_string());
-
- let mut seen = HashSet::new();
- let max_asks = if group == current_group {
- MAX_CURRENT_CWD_ASKS
- } else {
- MAX_OTHER_CWD_ASKS
- };
-
- for entry in entries {
- let Some(first_user_message) = entry.first_user_message.as_deref() else {
- continue;
- };
- let ask = first_user_message
- .split_whitespace()
- .collect::>()
- .join(" ");
- let dedupe_key = format!("{}:{ask}", entry.cwd.display());
- if ask.is_empty() || !seen.insert(dedupe_key) {
- continue;
- }
- let ask = if ask.chars().count() > MAX_ASK_CHARS {
- format!(
- "{}...",
- ask.chars()
- .take(MAX_ASK_CHARS.saturating_sub(3))
- .collect::()
- )
- } else {
- ask
- };
- lines.push(format!("- {}: {ask}", entry.cwd.display()));
- if seen.len() == max_asks {
- break;
- }
- }
-
- (lines.len() > 5).then(|| lines.join("\n"))
-}
-
-fn file_name_string(path: &Path) -> String {
- path.file_name()
- .and_then(OsStr::to_str)
- .map(str::to_owned)
- .unwrap_or_else(|| path.display().to_string())
-}
-
-fn approx_token_count(text: &str) -> usize {
- text.len().div_ceil(APPROX_BYTES_PER_TOKEN)
-}
+#[cfg(test)]
+pub(crate) use codex_realtime::CURRENT_THREAD_SECTION_TOKEN_BUDGET;
+#[cfg(test)]
+pub(crate) use codex_realtime::NOTES_SECTION_TOKEN_BUDGET;
+pub(crate) use codex_realtime::REALTIME_TURN_TOKEN_BUDGET;
+#[cfg(test)]
+pub(crate) use codex_realtime::RECENT_WORK_SECTION_TOKEN_BUDGET;
+#[cfg(test)]
+pub(crate) use codex_realtime::STARTUP_CONTEXT_HEADER;
+#[cfg(test)]
+pub(crate) use codex_realtime::WORKSPACE_SECTION_TOKEN_BUDGET;
+#[cfg(test)]
+use codex_realtime::build_recent_work_section;
+#[cfg(test)]
+use codex_realtime::build_workspace_section_with_user_root;
+#[cfg(test)]
+use codex_realtime::format_section;
+#[cfg(test)]
+use codex_realtime::format_startup_context_blob;
+pub(crate) use codex_realtime::truncate_realtime_text_to_token_budget;
#[cfg(test)]
#[path = "realtime_context_tests.rs"]
diff --git a/codex-rs/core/src/realtime_conversation.rs b/codex-rs/core/src/realtime_conversation.rs
index 0ca7c1c169..cb10541fa3 100644
--- a/codex-rs/core/src/realtime_conversation.rs
+++ b/codex-rs/core/src/realtime_conversation.rs
@@ -1,33 +1,13 @@
-use crate::client::ModelClient;
+use crate::config::Config;
use crate::realtime_context::build_realtime_startup_context;
-use crate::realtime_prompt::prepare_realtime_backend_prompt;
use crate::session::session::Session;
-use anyhow::Context;
-use async_channel::Receiver;
-use async_channel::RecvError;
-use async_channel::Sender;
-use async_channel::TrySendError;
-use base64::Engine;
-use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
-use codex_api::ApiError;
-use codex_api::Provider as ApiProvider;
-use codex_api::RealtimeAudioFrame;
use codex_api::RealtimeEvent;
-use codex_api::RealtimeEventParser;
use codex_api::RealtimeSessionConfig;
-use codex_api::RealtimeSessionMode;
use codex_api::RealtimeWebsocketClient;
-use codex_api::RealtimeWebsocketEvents;
-use codex_api::RealtimeWebsocketWriter;
use codex_api::map_api_error;
use codex_app_server_protocol::AuthMode;
-use codex_config::config_toml::RealtimeWsMode;
use codex_config::config_toml::RealtimeWsVersion;
-use codex_login::CodexAuth;
use codex_login::default_client::default_headers;
-use codex_login::read_openai_api_key_from_env;
-use codex_model_provider_info::ModelProviderInfo;
-use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::protocol::CodexErrorInfo;
use codex_protocol::protocol::ConversationAudioParams;
@@ -41,38 +21,31 @@ use codex_protocol::protocol::RealtimeConversationClosedEvent;
use codex_protocol::protocol::RealtimeConversationRealtimeEvent;
use codex_protocol::protocol::RealtimeConversationSdpEvent;
use codex_protocol::protocol::RealtimeConversationStartedEvent;
-use codex_protocol::protocol::RealtimeHandoffRequested;
use codex_protocol::protocol::RealtimeOutputModality;
use codex_protocol::protocol::RealtimeVoice;
-use codex_protocol::protocol::RealtimeVoicesList;
-use http::HeaderMap;
-use http::HeaderValue;
-use http::header::AUTHORIZATION;
-use serde_json::json;
use std::sync::Arc;
-use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
-use tokio::sync::Mutex;
-use tokio::task::JoinHandle;
use tracing::debug;
use tracing::error;
use tracing::info;
use tracing::warn;
-const AUDIO_IN_QUEUE_CAPACITY: usize = 256;
-const USER_TEXT_IN_QUEUE_CAPACITY: usize = 64;
-const HANDOFF_OUT_QUEUE_CAPACITY: usize = 64;
-const OUTPUT_EVENTS_QUEUE_CAPACITY: usize = 256;
+pub(crate) use codex_realtime::REALTIME_USER_TEXT_PREFIX;
+pub(crate) use codex_realtime::RealtimeConversationManager;
+use codex_realtime::RealtimeFeaturesConfig;
+use codex_realtime::RealtimeStart;
+use codex_realtime::RealtimeStartOutput;
+use codex_realtime::build_realtime_session_config as build_session_config;
+pub(crate) use codex_realtime::prefix_realtime_v2_text;
+use codex_realtime::realtime_api_key;
+use codex_realtime::realtime_delegation_from_handoff;
+use codex_realtime::realtime_request_headers;
+#[cfg(test)]
+use codex_realtime::realtime_text_from_handoff_request;
+#[cfg(test)]
+use codex_realtime::wrap_realtime_delegation_input;
+
const REALTIME_STARTUP_CONTEXT_TOKEN_BUDGET: usize = 5_300;
-const DEFAULT_REALTIME_MODEL: &str = "gpt-realtime-1.5";
-pub(crate) const REALTIME_USER_TEXT_PREFIX: &str = "[USER] ";
-pub(crate) const REALTIME_BACKEND_TEXT_PREFIX: &str = "[BACKEND] ";
-const REALTIME_V2_HANDOFF_COMPLETE_ACKNOWLEDGEMENT: &str =
- "Background agent finished. Use the preceding [BACKEND] messages as the result.";
-const REALTIME_V2_STEER_ACKNOWLEDGEMENT: &str =
- "This was sent to steer the previous background agent task.";
-const REALTIME_ACTIVE_RESPONSE_ERROR_PREFIX: &str =
- "Conversation already has an active response in progress:";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RealtimeConversationEnd {
@@ -81,477 +54,6 @@ enum RealtimeConversationEnd {
Error,
}
-enum RealtimeFanoutTaskStop {
- Abort,
- Detach,
-}
-
-pub(crate) struct RealtimeConversationManager {
- state: Mutex