Skip to content

Commit f15df62

Browse files
authored
core: add configurable <context_window_guidance> message (#29936)
## Why This PR adds a configurable `<context_window_guidance>` developer section immediately after `<context_window>`. Harness integrations need this section to give the model deployment-specific instructions for preparing for context-window transitions. ## What changed - Add an optional `features.token_budget.guidance_message` config with a 1,000-byte runtime cap and generated schema support. - Render configured guidance as a developer `ContextualUserFragment` wrapped in `<context_window_guidance>` immediately after `<context_window>`. - Omit the section when guidance is unset, empty, or whitespace-only. - Preserve the resolved value in config locks and classify persisted guidance as contextual developer content. - Add integration coverage for rendered content and ordering.
1 parent f4e6aa7 commit f15df62

12 files changed

Lines changed: 145 additions & 0 deletions

File tree

codex-rs/core/config.schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2860,6 +2860,11 @@
28602860
"enabled": {
28612861
"type": "boolean"
28622862
},
2863+
"guidance_message": {
2864+
"description": "Guidance appended to the context-window metadata in a developer message.",
2865+
"maxLength": 1000,
2866+
"type": "string"
2867+
},
28632868
"reminder_message_template": {
28642869
"description": "Reminder template. `{n_remaining}` is replaced with the tokens remaining before auto-compaction.",
28652870
"maxLength": 1000,

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,10 +475,12 @@ async fn load_config_resolves_token_budget_config() -> std::io::Result<()> {
475475
enabled = true
476476
reminder_threshold_tokens = 16000
477477
reminder_message_template = "Custom reminder: {n_remaining} tokens."
478+
guidance_message = "Preserve important state before compaction."
478479
"#,
479480
TokenBudgetConfig {
480481
reminder_threshold_tokens: Some(16_000),
481482
reminder_message_template: "Custom reminder: {n_remaining} tokens.".to_string(),
483+
guidance_message: Some("Preserve important state before compaction.".to_string()),
482484
},
483485
),
484486
] {

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,18 +1089,21 @@ pub(crate) const DEFAULT_TOKEN_BUDGET_REMINDER_MESSAGE_TEMPLATE: &str = concat!(
10891089
"Once reset, message items in current context window will be cleared in the new window, but notes and history items will be persistent across windows."
10901090
);
10911091
const TOKEN_BUDGET_REMINDER_MESSAGE_TEMPLATE_MAX_BYTES: usize = 1000;
1092+
const TOKEN_BUDGET_GUIDANCE_MESSAGE_MAX_BYTES: usize = 1000;
10921093

10931094
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
10941095
pub struct TokenBudgetConfig {
10951096
pub reminder_threshold_tokens: Option<i64>,
10961097
pub reminder_message_template: String,
1098+
pub guidance_message: Option<String>,
10971099
}
10981100

10991101
impl Default for TokenBudgetConfig {
11001102
fn default() -> Self {
11011103
Self {
11021104
reminder_threshold_tokens: None,
11031105
reminder_message_template: DEFAULT_TOKEN_BUDGET_REMINDER_MESSAGE_TEMPLATE.to_string(),
1106+
guidance_message: None,
11041107
}
11051108
}
11061109
}
@@ -2578,9 +2581,25 @@ fn resolve_token_budget_config(
25782581
));
25792582
}
25802583

2584+
let guidance_message = token_budget_config
2585+
.and_then(|config| config.guidance_message.clone())
2586+
.filter(|message| !message.trim().is_empty());
2587+
if guidance_message
2588+
.as_ref()
2589+
.is_some_and(|message| message.len() > TOKEN_BUDGET_GUIDANCE_MESSAGE_MAX_BYTES)
2590+
{
2591+
return Err(std::io::Error::new(
2592+
std::io::ErrorKind::InvalidInput,
2593+
format!(
2594+
"features.token_budget.guidance_message must not exceed {TOKEN_BUDGET_GUIDANCE_MESSAGE_MAX_BYTES} bytes"
2595+
),
2596+
));
2597+
}
2598+
25812599
Ok(Some(TokenBudgetConfig {
25822600
reminder_threshold_tokens,
25832601
reminder_message_template,
2602+
guidance_message,
25842603
}))
25852604
}
25862605

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ pub(crate) use realtime_start_with_instructions::RealtimeStartWithInstructions;
7171
pub(crate) use recommended_plugins_instructions::RecommendedPluginsInstructions;
7272
pub(crate) use rollout_budget::RolloutBudgetContext;
7373
pub(crate) use subagent_notification::SubagentNotification;
74+
pub(crate) use token_budget_context::ContextWindowGuidance;
7475
pub(crate) use token_budget_context::TokenBudgetContext;
7576
pub(crate) use token_budget_context::TokenBudgetRemainingContext;
7677
pub(crate) use token_budget_context::TokenBudgetReminder;

codex-rs/core/src/context/token_budget_context.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use super::ContextualUserFragment;
22
use codex_protocol::ThreadId;
33
use codex_protocol::protocol::CONTEXT_WINDOW_CLOSE_TAG;
4+
use codex_protocol::protocol::CONTEXT_WINDOW_GUIDANCE_CLOSE_TAG;
5+
use codex_protocol::protocol::CONTEXT_WINDOW_GUIDANCE_OPEN_TAG;
46
use codex_protocol::protocol::CONTEXT_WINDOW_OPEN_TAG;
57
use uuid::Uuid;
68

@@ -63,6 +65,40 @@ impl ContextualUserFragment for TokenBudgetContext {
6365
}
6466
}
6567

68+
#[derive(Debug, Clone, PartialEq, Eq)]
69+
pub(crate) struct ContextWindowGuidance {
70+
message: String,
71+
}
72+
73+
impl ContextWindowGuidance {
74+
pub(crate) fn new(message: &str) -> Self {
75+
Self {
76+
message: message.to_string(),
77+
}
78+
}
79+
}
80+
81+
impl ContextualUserFragment for ContextWindowGuidance {
82+
fn role(&self) -> &'static str {
83+
"developer"
84+
}
85+
86+
fn markers(&self) -> (&'static str, &'static str) {
87+
Self::type_markers()
88+
}
89+
90+
fn type_markers() -> (&'static str, &'static str) {
91+
(
92+
CONTEXT_WINDOW_GUIDANCE_OPEN_TAG,
93+
CONTEXT_WINDOW_GUIDANCE_CLOSE_TAG,
94+
)
95+
}
96+
97+
fn body(&self) -> String {
98+
format!("\n{}\n", self.message)
99+
}
100+
}
101+
66102
#[derive(Debug, Clone, PartialEq, Eq)]
67103
pub(crate) struct TokenBudgetRemainingContext {
68104
tokens_left: Option<i64>,

codex-rs/core/src/event_mapping.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use codex_protocol::models::is_image_open_tag_text;
1515
use codex_protocol::models::is_local_image_close_tag_text;
1616
use codex_protocol::models::is_local_image_open_tag_text;
1717
use codex_protocol::protocol::COLLABORATION_MODE_OPEN_TAG;
18+
use codex_protocol::protocol::CONTEXT_WINDOW_GUIDANCE_OPEN_TAG;
1819
use codex_protocol::protocol::CONTEXT_WINDOW_OPEN_TAG;
1920
use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG;
2021
use codex_protocol::protocol::REALTIME_CONVERSATION_OPEN_TAG;
@@ -38,6 +39,7 @@ const CONTEXTUAL_DEVELOPER_PREFIXES: &[&str] = &[
3839
// Keep recognizing token-budget wrappers persisted by older versions.
3940
"<token_budget>",
4041
CONTEXT_WINDOW_OPEN_TAG,
42+
CONTEXT_WINDOW_GUIDANCE_OPEN_TAG,
4143
"<rollout_budget>",
4244
];
4345

codex-rs/core/src/event_mapping_tests.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ use codex_protocol::models::ReasoningItemReasoningSummary;
1616
use codex_protocol::models::ResponseItem;
1717
use codex_protocol::models::WebSearchAction;
1818
use codex_protocol::protocol::CONTEXT_WINDOW_CLOSE_TAG;
19+
use codex_protocol::protocol::CONTEXT_WINDOW_GUIDANCE_CLOSE_TAG;
20+
use codex_protocol::protocol::CONTEXT_WINDOW_GUIDANCE_OPEN_TAG;
1921
use codex_protocol::protocol::CONTEXT_WINDOW_OPEN_TAG;
2022
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG;
2123
use codex_protocol::user_input::UserInput;
@@ -55,6 +57,18 @@ Thread id: 00000000-0000-0000-0000-000000000000
5557
assert!(!has_non_contextual_dev_message_content(&content));
5658
}
5759

60+
#[test]
61+
fn recognizes_context_window_guidance_as_contextual_developer_content() {
62+
let content = vec![ContentItem::InputText {
63+
text: format!(
64+
"{CONTEXT_WINDOW_GUIDANCE_OPEN_TAG}\nPreserve important state.\n{CONTEXT_WINDOW_GUIDANCE_CLOSE_TAG}"
65+
),
66+
}];
67+
68+
assert!(is_contextual_dev_message_content(&content));
69+
assert!(!has_non_contextual_dev_message_content(&content));
70+
}
71+
5872
#[test]
5973
fn parses_user_message_with_text_and_two_images() {
6074
let img1 = "https://example.com/one.png".to_string();

codex-rs/core/src/session/config_lock.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ mod tests {
248248
config.token_budget = Some(crate::config::TokenBudgetConfig {
249249
reminder_threshold_tokens: Some(16_000),
250250
reminder_message_template: "Locked reminder: {n_remaining} tokens.".to_string(),
251+
guidance_message: Some("Locked context-window guidance.".to_string()),
251252
});
252253
config
253254
.features
@@ -340,6 +341,7 @@ mod tests {
340341
reminder_message_template: Some(
341342
"Locked reminder: {n_remaining} tokens.".to_string()
342343
),
344+
guidance_message: Some("Locked context-window guidance.".to_string()),
343345
}))
344346
);
345347

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3339,6 +3339,16 @@ impl Session {
33393339
)
33403340
.render(),
33413341
);
3342+
if let Some(guidance_message) = turn_context
3343+
.config
3344+
.token_budget
3345+
.as_ref()
3346+
.and_then(|config| config.guidance_message.as_deref())
3347+
.filter(|message| !message.trim().is_empty())
3348+
{
3349+
developer_sections
3350+
.push(crate::context::ContextWindowGuidance::new(guidance_message).render());
3351+
}
33423352
}
33433353
for fragment in world_state.render_full() {
33443354
match fragment.role() {

codex-rs/core/tests/suite/token_budget.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use codex_model_provider_info::built_in_model_providers;
77
use codex_protocol::config_types::AutoCompactTokenLimitScope;
88
use codex_protocol::items::TurnItem;
99
use codex_protocol::protocol::CONTEXT_WINDOW_CLOSE_TAG;
10+
use codex_protocol::protocol::CONTEXT_WINDOW_GUIDANCE_CLOSE_TAG;
11+
use codex_protocol::protocol::CONTEXT_WINDOW_GUIDANCE_OPEN_TAG;
1012
use codex_protocol::protocol::CONTEXT_WINDOW_OPEN_TAG;
1113
use codex_protocol::protocol::EventMsg;
1214
use codex_protocol::protocol::HookEventName;
@@ -211,6 +213,52 @@ async fn token_budget_context_is_only_emitted_with_full_context() -> Result<()>
211213
Ok(())
212214
}
213215

216+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
217+
async fn token_budget_guidance_follows_context_window() -> Result<()> {
218+
skip_if_no_network!(Ok(()));
219+
220+
let server = start_mock_server().await;
221+
let response = mount_sse_sequence(
222+
&server,
223+
vec![sse(vec![
224+
ev_response_created("resp-1"),
225+
ev_completed("resp-1"),
226+
])],
227+
)
228+
.await;
229+
let guidance_message = "Preserve important state before compaction.";
230+
let test = test_codex()
231+
.with_config(move |config| {
232+
config.model_context_window = Some(CONFIGURED_CONTEXT_WINDOW);
233+
config.token_budget = Some(TokenBudgetConfig {
234+
guidance_message: Some(guidance_message.to_string()),
235+
..TokenBudgetConfig::default()
236+
});
237+
config
238+
.features
239+
.enable(Feature::TokenBudget)
240+
.expect("test config should allow token budget");
241+
})
242+
.build_with_auto_env(&server)
243+
.await?;
244+
245+
test.submit_turn("inspect context guidance").await?;
246+
247+
let developer_texts = response.single_request().message_input_texts("developer");
248+
let context_window_index = developer_texts
249+
.iter()
250+
.position(|text| text.starts_with(CONTEXT_WINDOW_OPEN_TAG))
251+
.expect("context-window metadata should be present");
252+
assert_eq!(
253+
developer_texts.get(context_window_index + 1),
254+
Some(&format!(
255+
"{CONTEXT_WINDOW_GUIDANCE_OPEN_TAG}\n{guidance_message}\n{CONTEXT_WINDOW_GUIDANCE_CLOSE_TAG}"
256+
))
257+
);
258+
259+
Ok(())
260+
}
261+
214262
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
215263
async fn token_budget_context_injects_plain_thread_hint_text() -> Result<()> {
216264
skip_if_no_network!(Ok(()));

0 commit comments

Comments
 (0)