Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions codex-rs/app-server-protocol/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,10 @@ pub struct NewConversationParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub base_instructions: Option<String>,

/// Prompt used during conversation compaction.
#[serde(skip_serializing_if = "Option::is_none")]
pub compact_prompt: Option<String>,

/// Whether to include the apply patch tool in the conversation.
#[serde(skip_serializing_if = "Option::is_none")]
pub include_apply_patch_tool: Option<bool>,
Expand Down Expand Up @@ -1125,6 +1129,7 @@ mod tests {
sandbox: None,
config: None,
base_instructions: None,
compact_prompt: None,
include_apply_patch_tool: None,
},
};
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/app-server/src/codex_message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1760,6 +1760,7 @@ async fn derive_config_from_params(
sandbox: sandbox_mode,
config: cli_overrides,
base_instructions,
compact_prompt,
include_apply_patch_tool,
} = params;
let overrides = ConfigOverrides {
Expand All @@ -1772,6 +1773,7 @@ async fn derive_config_from_params(
model_provider,
codex_linux_sandbox_exe,
base_instructions,
compact_prompt,
include_apply_patch_tool,
include_view_image_tool: None,
show_raw_agent_reasoning: None,
Expand Down
19 changes: 17 additions & 2 deletions codex-rs/core/src/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ impl Codex {
model_reasoning_summary: config.model_reasoning_summary,
user_instructions,
base_instructions: config.base_instructions.clone(),
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.approval_policy,
sandbox_policy: config.sandbox_policy.clone(),
cwd: config.cwd.clone(),
Expand Down Expand Up @@ -265,6 +266,7 @@ pub(crate) struct TurnContext {
/// instead of `std::env::current_dir()`.
pub(crate) cwd: PathBuf,
pub(crate) base_instructions: Option<String>,
pub(crate) compact_prompt: Option<String>,
pub(crate) user_instructions: Option<String>,
pub(crate) approval_policy: AskForApproval,
pub(crate) sandbox_policy: SandboxPolicy,
Expand All @@ -281,6 +283,12 @@ impl TurnContext {
.map(PathBuf::from)
.map_or_else(|| self.cwd.clone(), |p| self.cwd.join(p))
}

pub(crate) fn compact_prompt(&self) -> &str {
self.compact_prompt
.as_deref()
.unwrap_or(compact::SUMMARIZATION_PROMPT)
}
}

#[allow(dead_code)]
Expand All @@ -301,6 +309,9 @@ pub(crate) struct SessionConfiguration {
/// Base instructions override.
base_instructions: Option<String>,

/// Compact prompt override.
compact_prompt: Option<String>,

/// When to escalate for approval for execution
approval_policy: AskForApproval,
/// How to sandbox commands executed in the system
Expand Down Expand Up @@ -407,6 +418,7 @@ impl Session {
client,
cwd: session_configuration.cwd.clone(),
base_instructions: session_configuration.base_instructions.clone(),
compact_prompt: session_configuration.compact_prompt.clone(),
user_instructions: session_configuration.user_instructions.clone(),
approval_policy: session_configuration.approval_policy,
sandbox_policy: session_configuration.sandbox_policy.clone(),
Expand Down Expand Up @@ -1313,7 +1325,7 @@ mod handlers {
use crate::codex::Session;
use crate::codex::SessionSettingsUpdate;
use crate::codex::TurnContext;
use crate::codex::compact;

use crate::codex::spawn_review_thread;
use crate::config::Config;
use crate::mcp::auth::compute_auth_statuses;
Expand Down Expand Up @@ -1540,7 +1552,7 @@ mod handlers {
// Attempt to inject input into current task
if let Err(items) = sess
.inject_input(vec![UserInput::Text {
text: compact::SUMMARIZATION_PROMPT.to_string(),
text: turn_context.compact_prompt().to_string(),
}])
.await
{
Expand Down Expand Up @@ -1664,6 +1676,7 @@ async fn spawn_review_thread(
tools_config,
user_instructions: None,
base_instructions: Some(base_instructions.clone()),
compact_prompt: parent_turn_context.compact_prompt.clone(),
approval_policy: parent_turn_context.approval_policy,
sandbox_policy: parent_turn_context.sandbox_policy.clone(),
shell_environment_policy: parent_turn_context.shell_environment_policy.clone(),
Expand Down Expand Up @@ -2500,6 +2513,7 @@ mod tests {
model_reasoning_summary: config.model_reasoning_summary,
user_instructions: config.user_instructions.clone(),
base_instructions: config.base_instructions.clone(),
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.approval_policy,
sandbox_policy: config.sandbox_policy.clone(),
cwd: config.cwd.clone(),
Expand Down Expand Up @@ -2574,6 +2588,7 @@ mod tests {
model_reasoning_summary: config.model_reasoning_summary,
user_instructions: config.user_instructions.clone(),
base_instructions: config.base_instructions.clone(),
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.approval_policy,
sandbox_policy: config.sandbox_policy.clone(),
cwd: config.cwd.clone(),
Expand Down
5 changes: 2 additions & 3 deletions codex-rs/core/src/codex/compact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,8 @@ pub(crate) async fn run_inline_auto_compact_task(
sess: Arc<Session>,
turn_context: Arc<TurnContext>,
) {
let input = vec![UserInput::Text {
text: SUMMARIZATION_PROMPT.to_string(),
}];
let prompt = turn_context.compact_prompt().to_string();
let input = vec![UserInput::Text { text: prompt }];
run_compact_task_inner(sess, turn_context, input).await;
}

Expand Down
116 changes: 99 additions & 17 deletions codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ pub struct Config {
/// Base instructions override.
pub base_instructions: Option<String>,

/// Compact prompt override.
pub compact_prompt: Option<String>,

/// Optional external notifier command. When set, Codex will spawn this
/// program after each completed *turn* (i.e. when the agent finishes
/// processing a user submission). The value must be the full command
Expand Down Expand Up @@ -540,6 +543,8 @@ pub struct ConfigToml {

/// System instructions.
pub instructions: Option<String>,
/// Compact prompt used for history compaction.
pub compact_prompt: Option<String>,

/// When set, restricts ChatGPT login to a specific workspace identifier.
#[serde(default)]
Expand Down Expand Up @@ -644,6 +649,7 @@ pub struct ConfigToml {

/// Legacy, now use features
pub experimental_instructions_file: Option<PathBuf>,
pub experimental_compact_prompt_file: Option<PathBuf>,
pub experimental_use_exec_command_tool: Option<bool>,
pub experimental_use_unified_exec_tool: Option<bool>,
pub experimental_use_rmcp_client: Option<bool>,
Expand Down Expand Up @@ -824,6 +830,7 @@ pub struct ConfigOverrides {
pub config_profile: Option<String>,
pub codex_linux_sandbox_exe: Option<PathBuf>,
pub base_instructions: Option<String>,
pub compact_prompt: Option<String>,
pub include_apply_patch_tool: Option<bool>,
pub include_view_image_tool: Option<bool>,
pub show_raw_agent_reasoning: Option<bool>,
Expand Down Expand Up @@ -854,6 +861,7 @@ impl Config {
config_profile: config_profile_key,
codex_linux_sandbox_exe,
base_instructions,
compact_prompt,
include_apply_patch_tool: include_apply_patch_tool_override,
include_view_image_tool: include_view_image_tool_override,
show_raw_agent_reasoning,
Expand Down Expand Up @@ -1030,17 +1038,40 @@ impl Config {
.and_then(|info| info.auto_compact_token_limit)
});

let compact_prompt = compact_prompt.or(cfg.compact_prompt).and_then(|value| {
let trimmed = value.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
});

// Load base instructions override from a file if specified. If the
// path is relative, resolve it against the effective cwd so the
// behaviour matches other path-like config values.
let experimental_instructions_path = config_profile
.experimental_instructions_file
.as_ref()
.or(cfg.experimental_instructions_file.as_ref());
let file_base_instructions =
Self::get_base_instructions(experimental_instructions_path, &resolved_cwd)?;
let file_base_instructions = Self::load_override_from_file(
experimental_instructions_path,
&resolved_cwd,
"experimental instructions file",
)?;
let base_instructions = base_instructions.or(file_base_instructions);

let experimental_compact_prompt_path = config_profile
.experimental_compact_prompt_file
.as_ref()
.or(cfg.experimental_compact_prompt_file.as_ref());
let file_compact_prompt = Self::load_override_from_file(
experimental_compact_prompt_path,
&resolved_cwd,
"experimental compact prompt file",
)?;
let compact_prompt = compact_prompt.or(file_compact_prompt);

// Default review model when not set in config; allow CLI override to take precedence.
let review_model = override_review_model
.or(cfg.review_model)
Expand All @@ -1064,6 +1095,7 @@ impl Config {
notify: cfg.notify,
user_instructions,
base_instructions,
compact_prompt,
// The config.toml omits "_mode" because it's a config file. However, "_mode"
// is important in code to differentiate the mode from the store implementation.
cli_auth_credentials_store_mode: cfg.cli_auth_credentials_store.unwrap_or_default(),
Expand Down Expand Up @@ -1160,18 +1192,15 @@ impl Config {
None
}

fn get_base_instructions(
fn load_override_from_file(
path: Option<&PathBuf>,
cwd: &Path,
description: &str,
) -> std::io::Result<Option<String>> {
let p = match path.as_ref() {
None => return Ok(None),
Some(p) => p,
let Some(p) = path else {
return Ok(None);
};

// Resolve relative paths against the provided cwd to make CLI
// overrides consistent regardless of where the process was launched
// from.
let full_path = if p.is_relative() {
cwd.join(p)
} else {
Expand All @@ -1181,21 +1210,15 @@ impl Config {
let contents = std::fs::read_to_string(&full_path).map_err(|e| {
std::io::Error::new(
e.kind(),
format!(
"failed to read experimental instructions file {}: {e}",
full_path.display()
),
format!("failed to read {description} {}: {e}", full_path.display()),
)
})?;

let s = contents.trim().to_string();
if s.is_empty() {
Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"experimental instructions file is empty: {}",
full_path.display()
),
format!("{description} is empty: {}", full_path.display()),
))
} else {
Ok(Some(s))
Expand Down Expand Up @@ -2653,6 +2676,61 @@ model = "gpt-5-codex"
}
}

#[test]
fn cli_override_sets_compact_prompt() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let overrides = ConfigOverrides {
compact_prompt: Some("Use the compact override".to_string()),
..Default::default()
};

let config = Config::load_from_base_config_with_overrides(
ConfigToml::default(),
overrides,
codex_home.path().to_path_buf(),
)?;

assert_eq!(
config.compact_prompt.as_deref(),
Some("Use the compact override")
);

Ok(())
}

#[test]
fn loads_compact_prompt_from_file() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let workspace = codex_home.path().join("workspace");
std::fs::create_dir_all(&workspace)?;

let prompt_path = workspace.join("compact_prompt.txt");
std::fs::write(&prompt_path, " summarize differently ")?;

let cfg = ConfigToml {
experimental_compact_prompt_file: Some(PathBuf::from("compact_prompt.txt")),
..Default::default()
};

let overrides = ConfigOverrides {
cwd: Some(workspace),
..Default::default()
};

let config = Config::load_from_base_config_with_overrides(
cfg,
overrides,
codex_home.path().to_path_buf(),
)?;

assert_eq!(
config.compact_prompt.as_deref(),
Some("summarize differently")
);

Ok(())
}

fn create_test_fixture() -> std::io::Result<PrecedenceTestFixture> {
let toml = r#"
model = "o3"
Expand Down Expand Up @@ -2808,6 +2886,7 @@ model_verbosity = "high"
model_verbosity: None,
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
base_instructions: None,
compact_prompt: None,
forced_chatgpt_workspace_id: None,
forced_login_method: None,
include_apply_patch_tool: false,
Expand Down Expand Up @@ -2879,6 +2958,7 @@ model_verbosity = "high"
model_verbosity: None,
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
base_instructions: None,
compact_prompt: None,
forced_chatgpt_workspace_id: None,
forced_login_method: None,
include_apply_patch_tool: false,
Expand Down Expand Up @@ -2965,6 +3045,7 @@ model_verbosity = "high"
model_verbosity: None,
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
base_instructions: None,
compact_prompt: None,
forced_chatgpt_workspace_id: None,
forced_login_method: None,
include_apply_patch_tool: false,
Expand Down Expand Up @@ -3037,6 +3118,7 @@ model_verbosity = "high"
model_verbosity: Some(Verbosity::High),
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
base_instructions: None,
compact_prompt: None,
forced_chatgpt_workspace_id: None,
forced_login_method: None,
include_apply_patch_tool: false,
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/config/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub struct ConfigProfile {
pub model_verbosity: Option<Verbosity>,
pub chatgpt_base_url: Option<String>,
pub experimental_instructions_file: Option<PathBuf>,
pub experimental_compact_prompt_file: Option<PathBuf>,
pub include_apply_patch_tool: Option<bool>,
pub include_view_image_tool: Option<bool>,
pub experimental_use_unified_exec_tool: Option<bool>,
Expand Down
Loading
Loading