mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
f1945de3b7
Token-budget initial context carries thread and context-window lineage that the model should treat as one structured context-window block. Wrapping it in `<context_window>` makes that boundary explicit while preserving the existing window id content. Before this change, the window identifiers were injected as an untagged developer text fragment: ```text Thread id <THREAD_ID>. First context window id: <FIRST_WINDOW_ID> Current context window id: <WINDOW_ID> Previous context window id: <PREVIOUS_WINDOW_ID> ``` After this change, the same payload is wrapped as a context-window block: ```text <context_window> Thread id: <THREAD_ID> First context window id: <FIRST_WINDOW_ID> Current context window id: <WINDOW_ID> Previous context window id: <PREVIOUS_WINDOW_ID> </context_window> ``` This adds shared `CONTEXT_WINDOW_*_TAG` protocol constants, updates `TokenBudgetContext` to render with those markers, treats the new wrapper as contextual developer content when mapping history, and refreshes the token-budget request-shape assertions and snapshot. Verification: - `just test -p codex-core token_budget` - `just test -p codex-core recognizes_context_window_as_contextual_developer_content`
583 lines
20 KiB
Rust
583 lines
20 KiB
Rust
use super::has_non_contextual_dev_message_content;
|
|
use super::is_contextual_dev_message_content;
|
|
use super::parse_turn_item;
|
|
use crate::context::ContextualUserFragment;
|
|
use crate::context::InternalContextSource;
|
|
use crate::context::InternalModelContextFragment;
|
|
use codex_protocol::items::AgentMessageContent;
|
|
use codex_protocol::items::HookPromptFragment;
|
|
use codex_protocol::items::TurnItem;
|
|
use codex_protocol::items::WebSearchItem;
|
|
use codex_protocol::items::build_hook_prompt_message;
|
|
use codex_protocol::models::ContentItem;
|
|
use codex_protocol::models::DEFAULT_IMAGE_DETAIL;
|
|
use codex_protocol::models::ReasoningItemContent;
|
|
use codex_protocol::models::ReasoningItemReasoningSummary;
|
|
use codex_protocol::models::ResponseItem;
|
|
use codex_protocol::models::WebSearchAction;
|
|
use codex_protocol::protocol::CONTEXT_WINDOW_CLOSE_TAG;
|
|
use codex_protocol::protocol::CONTEXT_WINDOW_OPEN_TAG;
|
|
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG;
|
|
use codex_protocol::user_input::UserInput;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
#[test]
|
|
fn recognizes_skills_instructions_as_contextual_developer_content() {
|
|
assert!(is_contextual_dev_message_content(&[
|
|
ContentItem::InputText {
|
|
text: format!("{SKILLS_INSTRUCTIONS_OPEN_TAG}\n## Skills"),
|
|
},
|
|
]));
|
|
}
|
|
|
|
#[test]
|
|
fn recognizes_legacy_token_budget_as_contextual_developer_content() {
|
|
let content = vec![ContentItem::InputText {
|
|
text: "<token_budget>\nYou have 710 tokens left in this context window.\n</token_budget>"
|
|
.to_string(),
|
|
}];
|
|
|
|
assert!(is_contextual_dev_message_content(&content));
|
|
assert!(!has_non_contextual_dev_message_content(&content));
|
|
}
|
|
|
|
#[test]
|
|
fn recognizes_context_window_as_contextual_developer_content() {
|
|
let content = vec![ContentItem::InputText {
|
|
text: format!(
|
|
r#"{CONTEXT_WINDOW_OPEN_TAG}
|
|
Thread id: 00000000-0000-0000-0000-000000000000
|
|
{CONTEXT_WINDOW_CLOSE_TAG}"#
|
|
),
|
|
}];
|
|
|
|
assert!(is_contextual_dev_message_content(&content));
|
|
assert!(!has_non_contextual_dev_message_content(&content));
|
|
}
|
|
|
|
#[test]
|
|
fn parses_user_message_with_text_and_two_images() {
|
|
let img1 = "https://example.com/one.png".to_string();
|
|
let img2 = "https://example.com/two.jpg".to_string();
|
|
|
|
let item = ResponseItem::Message {
|
|
id: None,
|
|
role: "user".to_string(),
|
|
content: vec![
|
|
ContentItem::InputText {
|
|
text: "Hello world".to_string(),
|
|
},
|
|
ContentItem::InputImage {
|
|
image_url: img1.clone(),
|
|
detail: Some(DEFAULT_IMAGE_DETAIL),
|
|
},
|
|
ContentItem::InputImage {
|
|
image_url: img2.clone(),
|
|
detail: Some(DEFAULT_IMAGE_DETAIL),
|
|
},
|
|
],
|
|
phase: None,
|
|
internal_chat_message_metadata_passthrough: None,
|
|
};
|
|
|
|
let turn_item = parse_turn_item(&item).expect("expected user message turn item");
|
|
|
|
match turn_item {
|
|
TurnItem::UserMessage(user) => {
|
|
let expected_content = vec![
|
|
UserInput::Text {
|
|
text: "Hello world".to_string(),
|
|
text_elements: Vec::new(),
|
|
},
|
|
UserInput::Image {
|
|
image_url: img1,
|
|
detail: Some(DEFAULT_IMAGE_DETAIL),
|
|
},
|
|
UserInput::Image {
|
|
image_url: img2,
|
|
detail: Some(DEFAULT_IMAGE_DETAIL),
|
|
},
|
|
];
|
|
assert_eq!(user.content, expected_content);
|
|
}
|
|
other => panic!("expected TurnItem::UserMessage, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn skips_local_image_label_text() {
|
|
let image_url = "data:image/png;base64,abc".to_string();
|
|
let label = r#"<image name=[Image #1] path="/tmp/local.png">"#.to_string();
|
|
let user_text = "Please review this image.".to_string();
|
|
|
|
let item = ResponseItem::Message {
|
|
id: None,
|
|
role: "user".to_string(),
|
|
content: vec![
|
|
ContentItem::InputText { text: label },
|
|
ContentItem::InputImage {
|
|
image_url: image_url.clone(),
|
|
detail: Some(DEFAULT_IMAGE_DETAIL),
|
|
},
|
|
ContentItem::InputText {
|
|
text: "</image>".to_string(),
|
|
},
|
|
ContentItem::InputText {
|
|
text: user_text.clone(),
|
|
},
|
|
],
|
|
phase: None,
|
|
internal_chat_message_metadata_passthrough: None,
|
|
};
|
|
|
|
let turn_item = parse_turn_item(&item).expect("expected user message turn item");
|
|
|
|
match turn_item {
|
|
TurnItem::UserMessage(user) => {
|
|
let expected_content = vec![
|
|
UserInput::Image {
|
|
image_url,
|
|
detail: Some(DEFAULT_IMAGE_DETAIL),
|
|
},
|
|
UserInput::Text {
|
|
text: user_text,
|
|
text_elements: Vec::new(),
|
|
},
|
|
];
|
|
assert_eq!(user.content, expected_content);
|
|
}
|
|
other => panic!("expected TurnItem::UserMessage, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parses_assistant_message_input_text_for_backward_compatibility() {
|
|
let item = ResponseItem::Message {
|
|
id: None,
|
|
role: "assistant".to_string(),
|
|
content: vec![ContentItem::InputText {
|
|
text: "author: /root\nrecipient: /root/worker\nother_recipients: []\nContent: continue"
|
|
.to_string(),
|
|
}],
|
|
phase: None,
|
|
internal_chat_message_metadata_passthrough: None,
|
|
};
|
|
|
|
let turn_item = parse_turn_item(&item).expect("expected assistant message turn item");
|
|
|
|
match turn_item {
|
|
TurnItem::AgentMessage(message) => {
|
|
let rendered = message
|
|
.content
|
|
.into_iter()
|
|
.map(|content| {
|
|
let AgentMessageContent::Text { text } = content;
|
|
text
|
|
})
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(
|
|
rendered,
|
|
vec![
|
|
"author: /root\nrecipient: /root/worker\nother_recipients: []\nContent: continue"
|
|
.to_string()
|
|
]
|
|
);
|
|
}
|
|
other => panic!("expected TurnItem::AgentMessage, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn skips_unnamed_image_label_text() {
|
|
let image_url = "data:image/png;base64,abc".to_string();
|
|
let label = codex_protocol::models::image_open_tag_text();
|
|
let user_text = "Please review this image.".to_string();
|
|
|
|
let item = ResponseItem::Message {
|
|
id: None,
|
|
role: "user".to_string(),
|
|
content: vec![
|
|
ContentItem::InputText { text: label },
|
|
ContentItem::InputImage {
|
|
image_url: image_url.clone(),
|
|
detail: Some(DEFAULT_IMAGE_DETAIL),
|
|
},
|
|
ContentItem::InputText {
|
|
text: codex_protocol::models::image_close_tag_text(),
|
|
},
|
|
ContentItem::InputText {
|
|
text: user_text.clone(),
|
|
},
|
|
],
|
|
phase: None,
|
|
internal_chat_message_metadata_passthrough: None,
|
|
};
|
|
|
|
let turn_item = parse_turn_item(&item).expect("expected user message turn item");
|
|
|
|
match turn_item {
|
|
TurnItem::UserMessage(user) => {
|
|
let expected_content = vec![
|
|
UserInput::Image {
|
|
image_url,
|
|
detail: Some(DEFAULT_IMAGE_DETAIL),
|
|
},
|
|
UserInput::Text {
|
|
text: user_text,
|
|
text_elements: Vec::new(),
|
|
},
|
|
];
|
|
assert_eq!(user.content, expected_content);
|
|
}
|
|
other => panic!("expected TurnItem::UserMessage, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn skips_user_instructions_and_env() {
|
|
let items = vec![
|
|
ResponseItem::Message {
|
|
id: None,
|
|
role: "user".to_string(),
|
|
content: vec![ContentItem::InputText {
|
|
text: "# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>".to_string(),
|
|
}],
|
|
phase: None,
|
|
internal_chat_message_metadata_passthrough: None,},
|
|
ResponseItem::Message {
|
|
id: None,
|
|
role: "user".to_string(),
|
|
content: vec![ContentItem::InputText {
|
|
text: "<environment_context>test_text</environment_context>".to_string(),
|
|
}],
|
|
phase: None,
|
|
internal_chat_message_metadata_passthrough: None,},
|
|
ResponseItem::Message {
|
|
id: None,
|
|
role: "user".to_string(),
|
|
content: vec![ContentItem::InputText {
|
|
text: "# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>".to_string(),
|
|
}],
|
|
phase: None,
|
|
internal_chat_message_metadata_passthrough: None,},
|
|
ResponseItem::Message {
|
|
id: None,
|
|
role: "user".to_string(),
|
|
content: vec![ContentItem::InputText {
|
|
text: "<skill>\n<name>demo</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>"
|
|
.to_string(),
|
|
}],
|
|
phase: None,
|
|
internal_chat_message_metadata_passthrough: None,},
|
|
ResponseItem::Message {
|
|
id: None,
|
|
role: "user".to_string(),
|
|
content: vec![ContentItem::InputText {
|
|
text: "<user_shell_command>echo 42</user_shell_command>".to_string(),
|
|
}],
|
|
phase: None,
|
|
internal_chat_message_metadata_passthrough: None,},
|
|
ResponseItem::Message {
|
|
id: None,
|
|
role: "user".to_string(),
|
|
content: vec![
|
|
ContentItem::InputText {
|
|
text: "<environment_context>ctx</environment_context>".to_string(),
|
|
},
|
|
ContentItem::InputText {
|
|
text:
|
|
"# AGENTS.md instructions for dir\n\n<INSTRUCTIONS>\nbody\n</INSTRUCTIONS>"
|
|
.to_string(),
|
|
},
|
|
],
|
|
phase: None,
|
|
internal_chat_message_metadata_passthrough: None,},
|
|
];
|
|
|
|
for item in items {
|
|
let turn_item = parse_turn_item(&item);
|
|
assert!(turn_item.is_none(), "expected none, got {turn_item:?}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parses_hook_prompt_message_as_distinct_turn_item() {
|
|
let item = build_hook_prompt_message(&[HookPromptFragment::from_single_hook(
|
|
"Retry with exactly the phrase meow meow meow.",
|
|
"hook-run-1",
|
|
)])
|
|
.expect("hook prompt message");
|
|
|
|
let turn_item = parse_turn_item(&item).expect("expected hook prompt turn item");
|
|
|
|
match turn_item {
|
|
TurnItem::HookPrompt(hook_prompt) => {
|
|
assert_eq!(hook_prompt.fragments.len(), 1);
|
|
assert_eq!(
|
|
hook_prompt.fragments[0],
|
|
HookPromptFragment {
|
|
text: "Retry with exactly the phrase meow meow meow.".to_string(),
|
|
hook_run_id: "hook-run-1".to_string(),
|
|
}
|
|
);
|
|
}
|
|
other => panic!("expected TurnItem::HookPrompt, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parses_hook_prompt_and_hides_other_contextual_fragments() {
|
|
let item = ResponseItem::Message {
|
|
id: Some("msg-1".to_string()),
|
|
role: "user".to_string(),
|
|
content: vec![
|
|
ContentItem::InputText {
|
|
text: "<environment_context>ctx</environment_context>".to_string(),
|
|
},
|
|
ContentItem::InputText {
|
|
text:
|
|
"<hook_prompt hook_run_id=\"hook-run-1\">Retry with care & joy.</hook_prompt>"
|
|
.to_string(),
|
|
},
|
|
],
|
|
phase: None,
|
|
internal_chat_message_metadata_passthrough: None,};
|
|
|
|
let turn_item = parse_turn_item(&item).expect("expected hook prompt turn item");
|
|
|
|
match turn_item {
|
|
TurnItem::HookPrompt(hook_prompt) => {
|
|
assert_eq!(hook_prompt.id, "msg-1");
|
|
assert_eq!(
|
|
hook_prompt.fragments,
|
|
vec![HookPromptFragment {
|
|
text: "Retry with care & joy.".to_string(),
|
|
hook_run_id: "hook-run-1".to_string(),
|
|
}]
|
|
);
|
|
}
|
|
other => panic!("expected TurnItem::HookPrompt, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn internal_model_context_does_not_parse_as_visible_turn_item() {
|
|
let item = ResponseItem::Message {
|
|
id: Some("msg-1".to_string()),
|
|
role: "user".to_string(),
|
|
content: vec![ContentItem::InputText {
|
|
text: InternalModelContextFragment::new(
|
|
InternalContextSource::from_static("extension"),
|
|
"Internal steering.",
|
|
)
|
|
.render(),
|
|
}],
|
|
phase: None,
|
|
internal_chat_message_metadata_passthrough: None,
|
|
};
|
|
|
|
assert!(parse_turn_item(&item).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn parses_agent_message() {
|
|
let item = ResponseItem::Message {
|
|
id: Some("msg-1".to_string()),
|
|
role: "assistant".to_string(),
|
|
content: vec![ContentItem::OutputText {
|
|
text: "Hello from Codex".to_string(),
|
|
}],
|
|
phase: None,
|
|
internal_chat_message_metadata_passthrough: None,
|
|
};
|
|
|
|
let turn_item = parse_turn_item(&item).expect("expected agent message turn item");
|
|
|
|
match turn_item {
|
|
TurnItem::AgentMessage(message) => {
|
|
let Some(AgentMessageContent::Text { text }) = message.content.first() else {
|
|
panic!("expected agent message text content");
|
|
};
|
|
assert_eq!(text, "Hello from Codex");
|
|
}
|
|
other => panic!("expected TurnItem::AgentMessage, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parses_reasoning_summary_and_raw_content() {
|
|
let item = ResponseItem::Reasoning {
|
|
id: Some("reasoning_1".to_string()),
|
|
summary: vec![
|
|
ReasoningItemReasoningSummary::SummaryText {
|
|
text: "Step 1".to_string(),
|
|
},
|
|
ReasoningItemReasoningSummary::SummaryText {
|
|
text: "Step 2".to_string(),
|
|
},
|
|
],
|
|
content: Some(vec![ReasoningItemContent::ReasoningText {
|
|
text: "raw details".to_string(),
|
|
}]),
|
|
encrypted_content: None,
|
|
internal_chat_message_metadata_passthrough: None,
|
|
};
|
|
|
|
let turn_item = parse_turn_item(&item).expect("expected reasoning turn item");
|
|
|
|
match turn_item {
|
|
TurnItem::Reasoning(reasoning) => {
|
|
assert_eq!(
|
|
reasoning.summary_text,
|
|
vec!["Step 1".to_string(), "Step 2".to_string()]
|
|
);
|
|
assert_eq!(reasoning.raw_content, vec!["raw details".to_string()]);
|
|
}
|
|
other => panic!("expected TurnItem::Reasoning, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parses_reasoning_including_raw_content() {
|
|
let item = ResponseItem::Reasoning {
|
|
id: Some("reasoning_2".to_string()),
|
|
summary: vec![ReasoningItemReasoningSummary::SummaryText {
|
|
text: "Summarized step".to_string(),
|
|
}],
|
|
content: Some(vec![
|
|
ReasoningItemContent::ReasoningText {
|
|
text: "raw step".to_string(),
|
|
},
|
|
ReasoningItemContent::Text {
|
|
text: "final thought".to_string(),
|
|
},
|
|
]),
|
|
encrypted_content: None,
|
|
internal_chat_message_metadata_passthrough: None,
|
|
};
|
|
|
|
let turn_item = parse_turn_item(&item).expect("expected reasoning turn item");
|
|
|
|
match turn_item {
|
|
TurnItem::Reasoning(reasoning) => {
|
|
assert_eq!(reasoning.summary_text, vec!["Summarized step".to_string()]);
|
|
assert_eq!(
|
|
reasoning.raw_content,
|
|
vec!["raw step".to_string(), "final thought".to_string()]
|
|
);
|
|
}
|
|
other => panic!("expected TurnItem::Reasoning, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parses_web_search_call() {
|
|
let item = ResponseItem::WebSearchCall {
|
|
id: Some("ws_1".to_string()),
|
|
status: Some("completed".to_string()),
|
|
action: Some(WebSearchAction::Search {
|
|
query: Some("weather".to_string()),
|
|
queries: None,
|
|
}),
|
|
internal_chat_message_metadata_passthrough: None,
|
|
};
|
|
|
|
let turn_item = parse_turn_item(&item).expect("expected web search turn item");
|
|
|
|
match turn_item {
|
|
TurnItem::WebSearch(search) => assert_eq!(
|
|
search,
|
|
WebSearchItem {
|
|
id: "ws_1".to_string(),
|
|
query: "weather".to_string(),
|
|
action: WebSearchAction::Search {
|
|
query: Some("weather".to_string()),
|
|
queries: None,
|
|
},
|
|
}
|
|
),
|
|
other => panic!("expected TurnItem::WebSearch, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parses_web_search_open_page_call() {
|
|
let item = ResponseItem::WebSearchCall {
|
|
id: Some("ws_open".to_string()),
|
|
status: Some("completed".to_string()),
|
|
action: Some(WebSearchAction::OpenPage {
|
|
url: Some("https://example.com".to_string()),
|
|
}),
|
|
internal_chat_message_metadata_passthrough: None,
|
|
};
|
|
|
|
let turn_item = parse_turn_item(&item).expect("expected web search turn item");
|
|
|
|
match turn_item {
|
|
TurnItem::WebSearch(search) => assert_eq!(
|
|
search,
|
|
WebSearchItem {
|
|
id: "ws_open".to_string(),
|
|
query: "https://example.com".to_string(),
|
|
action: WebSearchAction::OpenPage {
|
|
url: Some("https://example.com".to_string()),
|
|
},
|
|
}
|
|
),
|
|
other => panic!("expected TurnItem::WebSearch, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parses_web_search_find_in_page_call() {
|
|
let item = ResponseItem::WebSearchCall {
|
|
id: Some("ws_find".to_string()),
|
|
status: Some("completed".to_string()),
|
|
action: Some(WebSearchAction::FindInPage {
|
|
url: Some("https://example.com".to_string()),
|
|
pattern: Some("needle".to_string()),
|
|
}),
|
|
internal_chat_message_metadata_passthrough: None,
|
|
};
|
|
|
|
let turn_item = parse_turn_item(&item).expect("expected web search turn item");
|
|
|
|
match turn_item {
|
|
TurnItem::WebSearch(search) => assert_eq!(
|
|
search,
|
|
WebSearchItem {
|
|
id: "ws_find".to_string(),
|
|
query: "'needle' in https://example.com".to_string(),
|
|
action: WebSearchAction::FindInPage {
|
|
url: Some("https://example.com".to_string()),
|
|
pattern: Some("needle".to_string()),
|
|
},
|
|
}
|
|
),
|
|
other => panic!("expected TurnItem::WebSearch, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parses_partial_web_search_call_without_action_as_other() {
|
|
let item = ResponseItem::WebSearchCall {
|
|
id: Some("ws_partial".to_string()),
|
|
status: Some("in_progress".to_string()),
|
|
action: None,
|
|
internal_chat_message_metadata_passthrough: None,
|
|
};
|
|
|
|
let turn_item = parse_turn_item(&item).expect("expected web search turn item");
|
|
match turn_item {
|
|
TurnItem::WebSearch(search) => assert_eq!(
|
|
search,
|
|
WebSearchItem {
|
|
id: "ws_partial".to_string(),
|
|
query: String::new(),
|
|
action: WebSearchAction::Other,
|
|
}
|
|
),
|
|
other => panic!("expected TurnItem::WebSearch, got {other:?}"),
|
|
}
|
|
}
|