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.
This commit is contained in:
Michael Bolin authored and GitHub committed 2026-06-24 18:03:44 -07:00
1 parent f4e6aa70e5
commit f15df624a6
12 files changed
+145

No files matched your search

+5
View File
@@ -2860,6 +2860,11 @@
"enabled": { "enabled": {
"type": "boolean" "type": "boolean"
}, },
"guidance_message": {
"description": "Guidance appended to the context-window metadata in a developer message.",
"maxLength": 1000,
"type": "string"
},
"reminder_message_template": { "reminder_message_template": {
"description": "Reminder template. `{n_remaining}` is replaced with the tokens remaining before auto-compaction.", "description": "Reminder template. `{n_remaining}` is replaced with the tokens remaining before auto-compaction.",
"maxLength": 1000, "maxLength": 1000,
+2
View File
@@ -475,10 +475,12 @@ async fn load_config_resolves_token_budget_config() -> std::io::Result<()> {
enabled = true enabled = true
reminder_threshold_tokens = 16000 reminder_threshold_tokens = 16000
reminder_message_template = "Custom reminder: {n_remaining} tokens." reminder_message_template = "Custom reminder: {n_remaining} tokens."
guidance_message = "Preserve important state before compaction."
"#, "#,
TokenBudgetConfig { TokenBudgetConfig {
reminder_threshold_tokens: Some(16_000), reminder_threshold_tokens: Some(16_000),
reminder_message_template: "Custom reminder: {n_remaining} tokens.".to_string(), reminder_message_template: "Custom reminder: {n_remaining} tokens.".to_string(),
guidance_message: Some("Preserve important state before compaction.".to_string()),
}, },
), ),
] { ] {
+19
View File
@@ -1089,11 +1089,13 @@ pub(crate) const DEFAULT_TOKEN_BUDGET_REMINDER_MESSAGE_TEMPLATE: &str = concat!(
"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." "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."
); );
const TOKEN_BUDGET_REMINDER_MESSAGE_TEMPLATE_MAX_BYTES: usize = 1000; const TOKEN_BUDGET_REMINDER_MESSAGE_TEMPLATE_MAX_BYTES: usize = 1000;
const TOKEN_BUDGET_GUIDANCE_MESSAGE_MAX_BYTES: usize = 1000;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct TokenBudgetConfig { pub struct TokenBudgetConfig {
pub reminder_threshold_tokens: Option<i64>, pub reminder_threshold_tokens: Option<i64>,
pub reminder_message_template: String, pub reminder_message_template: String,
pub guidance_message: Option<String>,
} }
impl Default for TokenBudgetConfig { impl Default for TokenBudgetConfig {
@@ -1101,6 +1103,7 @@ impl Default for TokenBudgetConfig {
Self { Self {
reminder_threshold_tokens: None, reminder_threshold_tokens: None,
reminder_message_template: DEFAULT_TOKEN_BUDGET_REMINDER_MESSAGE_TEMPLATE.to_string(), reminder_message_template: DEFAULT_TOKEN_BUDGET_REMINDER_MESSAGE_TEMPLATE.to_string(),
guidance_message: None,
} }
} }
} }
@@ -2578,9 +2581,25 @@ fn resolve_token_budget_config(
)); ));
} }
let guidance_message = token_budget_config
.and_then(|config| config.guidance_message.clone())
.filter(|message| !message.trim().is_empty());
if guidance_message
.as_ref()
.is_some_and(|message| message.len() > TOKEN_BUDGET_GUIDANCE_MESSAGE_MAX_BYTES)
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"features.token_budget.guidance_message must not exceed {TOKEN_BUDGET_GUIDANCE_MESSAGE_MAX_BYTES} bytes"
),
));
}
Ok(Some(TokenBudgetConfig { Ok(Some(TokenBudgetConfig {
reminder_threshold_tokens, reminder_threshold_tokens,
reminder_message_template, reminder_message_template,
guidance_message,
})) }))
} }
+1
View File
@@ -71,6 +71,7 @@ pub(crate) use realtime_start_with_instructions::RealtimeStartWithInstructions;
pub(crate) use recommended_plugins_instructions::RecommendedPluginsInstructions; pub(crate) use recommended_plugins_instructions::RecommendedPluginsInstructions;
pub(crate) use rollout_budget::RolloutBudgetContext; pub(crate) use rollout_budget::RolloutBudgetContext;
pub(crate) use subagent_notification::SubagentNotification; pub(crate) use subagent_notification::SubagentNotification;
pub(crate) use token_budget_context::ContextWindowGuidance;
pub(crate) use token_budget_context::TokenBudgetContext; pub(crate) use token_budget_context::TokenBudgetContext;
pub(crate) use token_budget_context::TokenBudgetRemainingContext; pub(crate) use token_budget_context::TokenBudgetRemainingContext;
pub(crate) use token_budget_context::TokenBudgetReminder; pub(crate) use token_budget_context::TokenBudgetReminder;
@@ -1,6 +1,8 @@
use super::ContextualUserFragment; use super::ContextualUserFragment;
use codex_protocol::ThreadId; use codex_protocol::ThreadId;
use codex_protocol::protocol::CONTEXT_WINDOW_CLOSE_TAG; use codex_protocol::protocol::CONTEXT_WINDOW_CLOSE_TAG;
use codex_protocol::protocol::CONTEXT_WINDOW_GUIDANCE_CLOSE_TAG;
use codex_protocol::protocol::CONTEXT_WINDOW_GUIDANCE_OPEN_TAG;
use codex_protocol::protocol::CONTEXT_WINDOW_OPEN_TAG; use codex_protocol::protocol::CONTEXT_WINDOW_OPEN_TAG;
use uuid::Uuid; use uuid::Uuid;
@@ -63,6 +65,40 @@ impl ContextualUserFragment for TokenBudgetContext {
} }
} }
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ContextWindowGuidance {
message: String,
}
impl ContextWindowGuidance {
pub(crate) fn new(message: &str) -> Self {
Self {
message: message.to_string(),
}
}
}
impl ContextualUserFragment for ContextWindowGuidance {
fn role(&self) -> &'static str {
"developer"
}
fn markers(&self) -> (&'static str, &'static str) {
Self::type_markers()
}
fn type_markers() -> (&'static str, &'static str) {
(
CONTEXT_WINDOW_GUIDANCE_OPEN_TAG,
CONTEXT_WINDOW_GUIDANCE_CLOSE_TAG,
)
}
fn body(&self) -> String {
format!("\n{}\n", self.message)
}
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TokenBudgetRemainingContext { pub(crate) struct TokenBudgetRemainingContext {
tokens_left: Option<i64>, tokens_left: Option<i64>,
+2
View File
@@ -15,6 +15,7 @@ use codex_protocol::models::is_image_open_tag_text;
use codex_protocol::models::is_local_image_close_tag_text; use codex_protocol::models::is_local_image_close_tag_text;
use codex_protocol::models::is_local_image_open_tag_text; use codex_protocol::models::is_local_image_open_tag_text;
use codex_protocol::protocol::COLLABORATION_MODE_OPEN_TAG; use codex_protocol::protocol::COLLABORATION_MODE_OPEN_TAG;
use codex_protocol::protocol::CONTEXT_WINDOW_GUIDANCE_OPEN_TAG;
use codex_protocol::protocol::CONTEXT_WINDOW_OPEN_TAG; use codex_protocol::protocol::CONTEXT_WINDOW_OPEN_TAG;
use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG; use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG;
use codex_protocol::protocol::REALTIME_CONVERSATION_OPEN_TAG; use codex_protocol::protocol::REALTIME_CONVERSATION_OPEN_TAG;
@@ -38,6 +39,7 @@ const CONTEXTUAL_DEVELOPER_PREFIXES: &[&str] = &[
// Keep recognizing token-budget wrappers persisted by older versions. // Keep recognizing token-budget wrappers persisted by older versions.
"<token_budget>", "<token_budget>",
CONTEXT_WINDOW_OPEN_TAG, CONTEXT_WINDOW_OPEN_TAG,
CONTEXT_WINDOW_GUIDANCE_OPEN_TAG,
"<rollout_budget>", "<rollout_budget>",
]; ];
+14
View File
@@ -16,6 +16,8 @@ use codex_protocol::models::ReasoningItemReasoningSummary;
use codex_protocol::models::ResponseItem; use codex_protocol::models::ResponseItem;
use codex_protocol::models::WebSearchAction; use codex_protocol::models::WebSearchAction;
use codex_protocol::protocol::CONTEXT_WINDOW_CLOSE_TAG; use codex_protocol::protocol::CONTEXT_WINDOW_CLOSE_TAG;
use codex_protocol::protocol::CONTEXT_WINDOW_GUIDANCE_CLOSE_TAG;
use codex_protocol::protocol::CONTEXT_WINDOW_GUIDANCE_OPEN_TAG;
use codex_protocol::protocol::CONTEXT_WINDOW_OPEN_TAG; use codex_protocol::protocol::CONTEXT_WINDOW_OPEN_TAG;
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG; use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
@@ -55,6 +57,18 @@ Thread id: 00000000-0000-0000-0000-000000000000
assert!(!has_non_contextual_dev_message_content(&content)); assert!(!has_non_contextual_dev_message_content(&content));
} }
#[test]
fn recognizes_context_window_guidance_as_contextual_developer_content() {
let content = vec![ContentItem::InputText {
text: format!(
"{CONTEXT_WINDOW_GUIDANCE_OPEN_TAG}\nPreserve important state.\n{CONTEXT_WINDOW_GUIDANCE_CLOSE_TAG}"
),
}];
assert!(is_contextual_dev_message_content(&content));
assert!(!has_non_contextual_dev_message_content(&content));
}
#[test] #[test]
fn parses_user_message_with_text_and_two_images() { fn parses_user_message_with_text_and_two_images() {
let img1 = "https://example.com/one.png".to_string(); let img1 = "https://example.com/one.png".to_string();
+2
View File
@@ -248,6 +248,7 @@ mod tests {
config.token_budget = Some(crate::config::TokenBudgetConfig { config.token_budget = Some(crate::config::TokenBudgetConfig {
reminder_threshold_tokens: Some(16_000), reminder_threshold_tokens: Some(16_000),
reminder_message_template: "Locked reminder: {n_remaining} tokens.".to_string(), reminder_message_template: "Locked reminder: {n_remaining} tokens.".to_string(),
guidance_message: Some("Locked context-window guidance.".to_string()),
}); });
config config
.features .features
@@ -340,6 +341,7 @@ mod tests {
reminder_message_template: Some( reminder_message_template: Some(
"Locked reminder: {n_remaining} tokens.".to_string() "Locked reminder: {n_remaining} tokens.".to_string()
), ),
guidance_message: Some("Locked context-window guidance.".to_string()),
})) }))
); );
+10
View File
@@ -3339,6 +3339,16 @@ impl Session {
) )
.render(), .render(),
); );
if let Some(guidance_message) = turn_context
.config
.token_budget
.as_ref()
.and_then(|config| config.guidance_message.as_deref())
.filter(|message| !message.trim().is_empty())
{
developer_sections
.push(crate::context::ContextWindowGuidance::new(guidance_message).render());
}
} }
for fragment in world_state.render_full() { for fragment in world_state.render_full() {
match fragment.role() { match fragment.role() {
+48
View File
@@ -7,6 +7,8 @@ use codex_model_provider_info::built_in_model_providers;
use codex_protocol::config_types::AutoCompactTokenLimitScope; use codex_protocol::config_types::AutoCompactTokenLimitScope;
use codex_protocol::items::TurnItem; use codex_protocol::items::TurnItem;
use codex_protocol::protocol::CONTEXT_WINDOW_CLOSE_TAG; use codex_protocol::protocol::CONTEXT_WINDOW_CLOSE_TAG;
use codex_protocol::protocol::CONTEXT_WINDOW_GUIDANCE_CLOSE_TAG;
use codex_protocol::protocol::CONTEXT_WINDOW_GUIDANCE_OPEN_TAG;
use codex_protocol::protocol::CONTEXT_WINDOW_OPEN_TAG; use codex_protocol::protocol::CONTEXT_WINDOW_OPEN_TAG;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::HookEventName; use codex_protocol::protocol::HookEventName;
@@ -211,6 +213,52 @@ async fn token_budget_context_is_only_emitted_with_full_context() -> Result<()>
Ok(()) Ok(())
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn token_budget_guidance_follows_context_window() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let response = mount_sse_sequence(
&server,
vec![sse(vec![
ev_response_created("resp-1"),
ev_completed("resp-1"),
])],
)
.await;
let guidance_message = "Preserve important state before compaction.";
let test = test_codex()
.with_config(move |config| {
config.model_context_window = Some(CONFIGURED_CONTEXT_WINDOW);
config.token_budget = Some(TokenBudgetConfig {
guidance_message: Some(guidance_message.to_string()),
..TokenBudgetConfig::default()
});
config
.features
.enable(Feature::TokenBudget)
.expect("test config should allow token budget");
})
.build_with_auto_env(&server)
.await?;
test.submit_turn("inspect context guidance").await?;
let developer_texts = response.single_request().message_input_texts("developer");
let context_window_index = developer_texts
.iter()
.position(|text| text.starts_with(CONTEXT_WINDOW_OPEN_TAG))
.expect("context-window metadata should be present");
assert_eq!(
developer_texts.get(context_window_index + 1),
Some(&format!(
"{CONTEXT_WINDOW_GUIDANCE_OPEN_TAG}\n{guidance_message}\n{CONTEXT_WINDOW_GUIDANCE_CLOSE_TAG}"
))
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn token_budget_context_injects_plain_thread_hint_text() -> Result<()> { async fn token_budget_context_injects_plain_thread_hint_text() -> Result<()> {
skip_if_no_network!(Ok(())); skip_if_no_network!(Ok(()));
+4
View File
@@ -88,6 +88,10 @@ pub struct TokenBudgetConfigToml {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
#[schemars(length(min = 1, max = 1000))] #[schemars(length(min = 1, max = 1000))]
pub reminder_message_template: Option<String>, pub reminder_message_template: Option<String>,
/// Guidance appended to the context-window metadata in a developer message.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(length(max = 1000))]
pub guidance_message: Option<String>,
} }
impl FeatureConfig for TokenBudgetConfigToml { impl FeatureConfig for TokenBudgetConfigToml {
+2
View File
@@ -114,6 +114,8 @@ pub const REALTIME_CONVERSATION_OPEN_TAG: &str = "<realtime_conversation>";
pub const REALTIME_CONVERSATION_CLOSE_TAG: &str = "</realtime_conversation>"; pub const REALTIME_CONVERSATION_CLOSE_TAG: &str = "</realtime_conversation>";
pub const CONTEXT_WINDOW_OPEN_TAG: &str = "<context_window>"; pub const CONTEXT_WINDOW_OPEN_TAG: &str = "<context_window>";
pub const CONTEXT_WINDOW_CLOSE_TAG: &str = "</context_window>"; pub const CONTEXT_WINDOW_CLOSE_TAG: &str = "</context_window>";
pub const CONTEXT_WINDOW_GUIDANCE_OPEN_TAG: &str = "<context_window_guidance>";
pub const CONTEXT_WINDOW_GUIDANCE_CLOSE_TAG: &str = "</context_window_guidance>";
pub const USER_MESSAGE_BEGIN: &str = "## My request for Codex:"; pub const USER_MESSAGE_BEGIN: &str = "## My request for Codex:";
// TODO(anp): Replace `TurnEnvironmentSelection` with `PathUri` once path URIs carry environment // TODO(anp): Replace `TurnEnvironmentSelection` with `PathUri` once path URIs carry environment