Skip to content

Commit f4f9695

Browse files
authored
feat: compaction prompt configurable (#5959)
``` codex -c compact_prompt="Summarize in bullet points" ```
1 parent 5fcc380 commit f4f9695

File tree

11 files changed

+198
-22
lines changed

11 files changed

+198
-22
lines changed

codex-rs/app-server-protocol/src/protocol.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,10 @@ pub struct NewConversationParams {
321321
#[serde(skip_serializing_if = "Option::is_none")]
322322
pub base_instructions: Option<String>,
323323

324+
/// Prompt used during conversation compaction.
325+
#[serde(skip_serializing_if = "Option::is_none")]
326+
pub compact_prompt: Option<String>,
327+
324328
/// Whether to include the apply patch tool in the conversation.
325329
#[serde(skip_serializing_if = "Option::is_none")]
326330
pub include_apply_patch_tool: Option<bool>,
@@ -1125,6 +1129,7 @@ mod tests {
11251129
sandbox: None,
11261130
config: None,
11271131
base_instructions: None,
1132+
compact_prompt: None,
11281133
include_apply_patch_tool: None,
11291134
},
11301135
};

codex-rs/app-server/src/codex_message_processor.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1760,6 +1760,7 @@ async fn derive_config_from_params(
17601760
sandbox: sandbox_mode,
17611761
config: cli_overrides,
17621762
base_instructions,
1763+
compact_prompt,
17631764
include_apply_patch_tool,
17641765
} = params;
17651766
let overrides = ConfigOverrides {
@@ -1772,6 +1773,7 @@ async fn derive_config_from_params(
17721773
model_provider,
17731774
codex_linux_sandbox_exe,
17741775
base_instructions,
1776+
compact_prompt,
17751777
include_apply_patch_tool,
17761778
include_view_image_tool: None,
17771779
show_raw_agent_reasoning: None,

codex-rs/core/src/codex.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ impl Codex {
173173
model_reasoning_summary: config.model_reasoning_summary,
174174
user_instructions,
175175
base_instructions: config.base_instructions.clone(),
176+
compact_prompt: config.compact_prompt.clone(),
176177
approval_policy: config.approval_policy,
177178
sandbox_policy: config.sandbox_policy.clone(),
178179
cwd: config.cwd.clone(),
@@ -265,6 +266,7 @@ pub(crate) struct TurnContext {
265266
/// instead of `std::env::current_dir()`.
266267
pub(crate) cwd: PathBuf,
267268
pub(crate) base_instructions: Option<String>,
269+
pub(crate) compact_prompt: Option<String>,
268270
pub(crate) user_instructions: Option<String>,
269271
pub(crate) approval_policy: AskForApproval,
270272
pub(crate) sandbox_policy: SandboxPolicy,
@@ -281,6 +283,12 @@ impl TurnContext {
281283
.map(PathBuf::from)
282284
.map_or_else(|| self.cwd.clone(), |p| self.cwd.join(p))
283285
}
286+
287+
pub(crate) fn compact_prompt(&self) -> &str {
288+
self.compact_prompt
289+
.as_deref()
290+
.unwrap_or(compact::SUMMARIZATION_PROMPT)
291+
}
284292
}
285293

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

312+
/// Compact prompt override.
313+
compact_prompt: Option<String>,
314+
304315
/// When to escalate for approval for execution
305316
approval_policy: AskForApproval,
306317
/// How to sandbox commands executed in the system
@@ -407,6 +418,7 @@ impl Session {
407418
client,
408419
cwd: session_configuration.cwd.clone(),
409420
base_instructions: session_configuration.base_instructions.clone(),
421+
compact_prompt: session_configuration.compact_prompt.clone(),
410422
user_instructions: session_configuration.user_instructions.clone(),
411423
approval_policy: session_configuration.approval_policy,
412424
sandbox_policy: session_configuration.sandbox_policy.clone(),
@@ -1313,7 +1325,7 @@ mod handlers {
13131325
use crate::codex::Session;
13141326
use crate::codex::SessionSettingsUpdate;
13151327
use crate::codex::TurnContext;
1316-
use crate::codex::compact;
1328+
13171329
use crate::codex::spawn_review_thread;
13181330
use crate::config::Config;
13191331
use crate::mcp::auth::compute_auth_statuses;
@@ -1540,7 +1552,7 @@ mod handlers {
15401552
// Attempt to inject input into current task
15411553
if let Err(items) = sess
15421554
.inject_input(vec![UserInput::Text {
1543-
text: compact::SUMMARIZATION_PROMPT.to_string(),
1555+
text: turn_context.compact_prompt().to_string(),
15441556
}])
15451557
.await
15461558
{
@@ -1664,6 +1676,7 @@ async fn spawn_review_thread(
16641676
tools_config,
16651677
user_instructions: None,
16661678
base_instructions: Some(base_instructions.clone()),
1679+
compact_prompt: parent_turn_context.compact_prompt.clone(),
16671680
approval_policy: parent_turn_context.approval_policy,
16681681
sandbox_policy: parent_turn_context.sandbox_policy.clone(),
16691682
shell_environment_policy: parent_turn_context.shell_environment_policy.clone(),
@@ -2500,6 +2513,7 @@ mod tests {
25002513
model_reasoning_summary: config.model_reasoning_summary,
25012514
user_instructions: config.user_instructions.clone(),
25022515
base_instructions: config.base_instructions.clone(),
2516+
compact_prompt: config.compact_prompt.clone(),
25032517
approval_policy: config.approval_policy,
25042518
sandbox_policy: config.sandbox_policy.clone(),
25052519
cwd: config.cwd.clone(),
@@ -2574,6 +2588,7 @@ mod tests {
25742588
model_reasoning_summary: config.model_reasoning_summary,
25752589
user_instructions: config.user_instructions.clone(),
25762590
base_instructions: config.base_instructions.clone(),
2591+
compact_prompt: config.compact_prompt.clone(),
25772592
approval_policy: config.approval_policy,
25782593
sandbox_policy: config.sandbox_policy.clone(),
25792594
cwd: config.cwd.clone(),

codex-rs/core/src/codex/compact.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,8 @@ pub(crate) async fn run_inline_auto_compact_task(
3939
sess: Arc<Session>,
4040
turn_context: Arc<TurnContext>,
4141
) {
42-
let input = vec![UserInput::Text {
43-
text: SUMMARIZATION_PROMPT.to_string(),
44-
}];
42+
let prompt = turn_context.compact_prompt().to_string();
43+
let input = vec![UserInput::Text { text: prompt }];
4544
run_compact_task_inner(sess, turn_context, input).await;
4645
}
4746

codex-rs/core/src/config/mod.rs

Lines changed: 99 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ pub struct Config {
128128
/// Base instructions override.
129129
pub base_instructions: Option<String>,
130130

131+
/// Compact prompt override.
132+
pub compact_prompt: Option<String>,
133+
131134
/// Optional external notifier command. When set, Codex will spawn this
132135
/// program after each completed *turn* (i.e. when the agent finishes
133136
/// processing a user submission). The value must be the full command
@@ -540,6 +543,8 @@ pub struct ConfigToml {
540543

541544
/// System instructions.
542545
pub instructions: Option<String>,
546+
/// Compact prompt used for history compaction.
547+
pub compact_prompt: Option<String>,
543548

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

645650
/// Legacy, now use features
646651
pub experimental_instructions_file: Option<PathBuf>,
652+
pub experimental_compact_prompt_file: Option<PathBuf>,
647653
pub experimental_use_exec_command_tool: Option<bool>,
648654
pub experimental_use_unified_exec_tool: Option<bool>,
649655
pub experimental_use_rmcp_client: Option<bool>,
@@ -824,6 +830,7 @@ pub struct ConfigOverrides {
824830
pub config_profile: Option<String>,
825831
pub codex_linux_sandbox_exe: Option<PathBuf>,
826832
pub base_instructions: Option<String>,
833+
pub compact_prompt: Option<String>,
827834
pub include_apply_patch_tool: Option<bool>,
828835
pub include_view_image_tool: Option<bool>,
829836
pub show_raw_agent_reasoning: Option<bool>,
@@ -854,6 +861,7 @@ impl Config {
854861
config_profile: config_profile_key,
855862
codex_linux_sandbox_exe,
856863
base_instructions,
864+
compact_prompt,
857865
include_apply_patch_tool: include_apply_patch_tool_override,
858866
include_view_image_tool: include_view_image_tool_override,
859867
show_raw_agent_reasoning,
@@ -1030,17 +1038,40 @@ impl Config {
10301038
.and_then(|info| info.auto_compact_token_limit)
10311039
});
10321040

1041+
let compact_prompt = compact_prompt.or(cfg.compact_prompt).and_then(|value| {
1042+
let trimmed = value.trim();
1043+
if trimmed.is_empty() {
1044+
None
1045+
} else {
1046+
Some(trimmed.to_string())
1047+
}
1048+
});
1049+
10331050
// Load base instructions override from a file if specified. If the
10341051
// path is relative, resolve it against the effective cwd so the
10351052
// behaviour matches other path-like config values.
10361053
let experimental_instructions_path = config_profile
10371054
.experimental_instructions_file
10381055
.as_ref()
10391056
.or(cfg.experimental_instructions_file.as_ref());
1040-
let file_base_instructions =
1041-
Self::get_base_instructions(experimental_instructions_path, &resolved_cwd)?;
1057+
let file_base_instructions = Self::load_override_from_file(
1058+
experimental_instructions_path,
1059+
&resolved_cwd,
1060+
"experimental instructions file",
1061+
)?;
10421062
let base_instructions = base_instructions.or(file_base_instructions);
10431063

1064+
let experimental_compact_prompt_path = config_profile
1065+
.experimental_compact_prompt_file
1066+
.as_ref()
1067+
.or(cfg.experimental_compact_prompt_file.as_ref());
1068+
let file_compact_prompt = Self::load_override_from_file(
1069+
experimental_compact_prompt_path,
1070+
&resolved_cwd,
1071+
"experimental compact prompt file",
1072+
)?;
1073+
let compact_prompt = compact_prompt.or(file_compact_prompt);
1074+
10441075
// Default review model when not set in config; allow CLI override to take precedence.
10451076
let review_model = override_review_model
10461077
.or(cfg.review_model)
@@ -1064,6 +1095,7 @@ impl Config {
10641095
notify: cfg.notify,
10651096
user_instructions,
10661097
base_instructions,
1098+
compact_prompt,
10671099
// The config.toml omits "_mode" because it's a config file. However, "_mode"
10681100
// is important in code to differentiate the mode from the store implementation.
10691101
cli_auth_credentials_store_mode: cfg.cli_auth_credentials_store.unwrap_or_default(),
@@ -1160,18 +1192,15 @@ impl Config {
11601192
None
11611193
}
11621194

1163-
fn get_base_instructions(
1195+
fn load_override_from_file(
11641196
path: Option<&PathBuf>,
11651197
cwd: &Path,
1198+
description: &str,
11661199
) -> std::io::Result<Option<String>> {
1167-
let p = match path.as_ref() {
1168-
None => return Ok(None),
1169-
Some(p) => p,
1200+
let Some(p) = path else {
1201+
return Ok(None);
11701202
};
11711203

1172-
// Resolve relative paths against the provided cwd to make CLI
1173-
// overrides consistent regardless of where the process was launched
1174-
// from.
11751204
let full_path = if p.is_relative() {
11761205
cwd.join(p)
11771206
} else {
@@ -1181,21 +1210,15 @@ impl Config {
11811210
let contents = std::fs::read_to_string(&full_path).map_err(|e| {
11821211
std::io::Error::new(
11831212
e.kind(),
1184-
format!(
1185-
"failed to read experimental instructions file {}: {e}",
1186-
full_path.display()
1187-
),
1213+
format!("failed to read {description} {}: {e}", full_path.display()),
11881214
)
11891215
})?;
11901216

11911217
let s = contents.trim().to_string();
11921218
if s.is_empty() {
11931219
Err(std::io::Error::new(
11941220
std::io::ErrorKind::InvalidData,
1195-
format!(
1196-
"experimental instructions file is empty: {}",
1197-
full_path.display()
1198-
),
1221+
format!("{description} is empty: {}", full_path.display()),
11991222
))
12001223
} else {
12011224
Ok(Some(s))
@@ -2653,6 +2676,61 @@ model = "gpt-5-codex"
26532676
}
26542677
}
26552678

2679+
#[test]
2680+
fn cli_override_sets_compact_prompt() -> std::io::Result<()> {
2681+
let codex_home = TempDir::new()?;
2682+
let overrides = ConfigOverrides {
2683+
compact_prompt: Some("Use the compact override".to_string()),
2684+
..Default::default()
2685+
};
2686+
2687+
let config = Config::load_from_base_config_with_overrides(
2688+
ConfigToml::default(),
2689+
overrides,
2690+
codex_home.path().to_path_buf(),
2691+
)?;
2692+
2693+
assert_eq!(
2694+
config.compact_prompt.as_deref(),
2695+
Some("Use the compact override")
2696+
);
2697+
2698+
Ok(())
2699+
}
2700+
2701+
#[test]
2702+
fn loads_compact_prompt_from_file() -> std::io::Result<()> {
2703+
let codex_home = TempDir::new()?;
2704+
let workspace = codex_home.path().join("workspace");
2705+
std::fs::create_dir_all(&workspace)?;
2706+
2707+
let prompt_path = workspace.join("compact_prompt.txt");
2708+
std::fs::write(&prompt_path, " summarize differently ")?;
2709+
2710+
let cfg = ConfigToml {
2711+
experimental_compact_prompt_file: Some(PathBuf::from("compact_prompt.txt")),
2712+
..Default::default()
2713+
};
2714+
2715+
let overrides = ConfigOverrides {
2716+
cwd: Some(workspace),
2717+
..Default::default()
2718+
};
2719+
2720+
let config = Config::load_from_base_config_with_overrides(
2721+
cfg,
2722+
overrides,
2723+
codex_home.path().to_path_buf(),
2724+
)?;
2725+
2726+
assert_eq!(
2727+
config.compact_prompt.as_deref(),
2728+
Some("summarize differently")
2729+
);
2730+
2731+
Ok(())
2732+
}
2733+
26562734
fn create_test_fixture() -> std::io::Result<PrecedenceTestFixture> {
26572735
let toml = r#"
26582736
model = "o3"
@@ -2808,6 +2886,7 @@ model_verbosity = "high"
28082886
model_verbosity: None,
28092887
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
28102888
base_instructions: None,
2889+
compact_prompt: None,
28112890
forced_chatgpt_workspace_id: None,
28122891
forced_login_method: None,
28132892
include_apply_patch_tool: false,
@@ -2879,6 +2958,7 @@ model_verbosity = "high"
28792958
model_verbosity: None,
28802959
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
28812960
base_instructions: None,
2961+
compact_prompt: None,
28822962
forced_chatgpt_workspace_id: None,
28832963
forced_login_method: None,
28842964
include_apply_patch_tool: false,
@@ -2965,6 +3045,7 @@ model_verbosity = "high"
29653045
model_verbosity: None,
29663046
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
29673047
base_instructions: None,
3048+
compact_prompt: None,
29683049
forced_chatgpt_workspace_id: None,
29693050
forced_login_method: None,
29703051
include_apply_patch_tool: false,
@@ -3037,6 +3118,7 @@ model_verbosity = "high"
30373118
model_verbosity: Some(Verbosity::High),
30383119
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
30393120
base_instructions: None,
3121+
compact_prompt: None,
30403122
forced_chatgpt_workspace_id: None,
30413123
forced_login_method: None,
30423124
include_apply_patch_tool: false,

codex-rs/core/src/config/profile.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub struct ConfigProfile {
2222
pub model_verbosity: Option<Verbosity>,
2323
pub chatgpt_base_url: Option<String>,
2424
pub experimental_instructions_file: Option<PathBuf>,
25+
pub experimental_compact_prompt_file: Option<PathBuf>,
2526
pub include_apply_patch_tool: Option<bool>,
2627
pub include_view_image_tool: Option<bool>,
2728
pub experimental_use_unified_exec_tool: Option<bool>,

0 commit comments

Comments
 (0)