Skip to content

Commit 611e00c

Browse files
jif-oaipakrym-oai
andauthored
feat: compactor 2 (#6027)
Co-authored-by: pakrym-oai <[email protected]>
1 parent c8ebb2a commit 611e00c

File tree

7 files changed

+571
-137
lines changed

7 files changed

+571
-137
lines changed

codex-rs/Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/core/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ toml_edit = { workspace = true }
8080
tracing = { workspace = true, features = ["log"] }
8181
tree-sitter = { workspace = true }
8282
tree-sitter-bash = { workspace = true }
83-
uuid = { workspace = true, features = ["serde", "v4"] }
83+
uuid = { workspace = true, features = ["serde", "v4", "v5"] }
8484
which = { workspace = true }
8585
wildmatch = { workspace = true }
8686
codex_windows_sandbox = { package = "codex-windows-sandbox", path = "../windows-sandbox-rs" }

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

Lines changed: 68 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ use crate::protocol::TurnContextItem;
1616
use crate::protocol::WarningEvent;
1717
use crate::truncate::truncate_middle;
1818
use crate::util::backoff;
19-
use askama::Template;
2019
use codex_protocol::items::TurnItem;
2120
use codex_protocol::models::ContentItem;
2221
use codex_protocol::models::ResponseInputItem;
@@ -29,13 +28,6 @@ use tracing::error;
2928
pub const SUMMARIZATION_PROMPT: &str = include_str!("../../templates/compact/prompt.md");
3029
const COMPACT_USER_MESSAGE_MAX_TOKENS: usize = 20_000;
3130

32-
#[derive(Template)]
33-
#[template(path = "compact/history_bridge.md", escape = "none")]
34-
struct HistoryBridgeTemplate<'a> {
35-
user_messages_text: &'a str,
36-
summary_text: &'a str,
37-
}
38-
3931
pub(crate) async fn run_inline_auto_compact_task(
4032
sess: Arc<Session>,
4133
turn_context: Arc<TurnContext>,
@@ -150,6 +142,7 @@ async fn run_compact_task_inner(
150142
let history_snapshot = sess.clone_history().await.get_history();
151143
let summary_text = get_last_assistant_message_from_turn(&history_snapshot).unwrap_or_default();
152144
let user_messages = collect_user_messages(&history_snapshot);
145+
153146
let initial_context = sess.build_initial_context(turn_context.as_ref());
154147
let mut new_history = build_compacted_history(initial_context, &user_messages, &summary_text);
155148
let ghost_snapshots: Vec<ResponseItem> = history_snapshot
@@ -224,33 +217,47 @@ fn build_compacted_history_with_limit(
224217
summary_text: &str,
225218
max_bytes: usize,
226219
) -> Vec<ResponseItem> {
227-
let mut user_messages_text = if user_messages.is_empty() {
228-
"(none)".to_string()
229-
} else {
230-
user_messages.join("\n\n")
231-
};
232-
// Truncate the concatenated prior user messages so the bridge message
233-
// stays well under the context window (approx. 4 bytes/token).
234-
if user_messages_text.len() > max_bytes {
235-
user_messages_text = truncate_middle(&user_messages_text, max_bytes).0;
220+
let mut selected_messages: Vec<String> = Vec::new();
221+
if max_bytes > 0 {
222+
let mut remaining = max_bytes;
223+
for message in user_messages.iter().rev() {
224+
if remaining == 0 {
225+
break;
226+
}
227+
if message.len() <= remaining {
228+
selected_messages.push(message.clone());
229+
remaining = remaining.saturating_sub(message.len());
230+
} else {
231+
let (truncated, _) = truncate_middle(message, remaining);
232+
selected_messages.push(truncated);
233+
break;
234+
}
235+
}
236+
selected_messages.reverse();
237+
}
238+
239+
for message in &selected_messages {
240+
history.push(ResponseItem::Message {
241+
id: None,
242+
role: "user".to_string(),
243+
content: vec![ContentItem::InputText {
244+
text: message.clone(),
245+
}],
246+
});
236247
}
248+
237249
let summary_text = if summary_text.is_empty() {
238250
"(no summary available)".to_string()
239251
} else {
240252
summary_text.to_string()
241253
};
242-
let Ok(bridge) = HistoryBridgeTemplate {
243-
user_messages_text: &user_messages_text,
244-
summary_text: &summary_text,
245-
}
246-
.render() else {
247-
return vec![];
248-
};
254+
249255
history.push(ResponseItem::Message {
250256
id: None,
251257
role: "user".to_string(),
252-
content: vec![ContentItem::InputText { text: bridge }],
258+
content: vec![ContentItem::InputText { text: summary_text }],
253259
});
260+
254261
history
255262
}
256263

@@ -390,30 +397,55 @@ mod tests {
390397
"SUMMARY",
391398
max_bytes,
392399
);
400+
assert_eq!(history.len(), 2);
393401

394-
// Expect exactly one bridge message added to history (plus any initial context we provided, which is none).
395-
assert_eq!(history.len(), 1);
402+
let truncated_message = &history[0];
403+
let summary_message = &history[1];
396404

397-
// Extract the text content of the bridge message.
398-
let bridge_text = match &history[0] {
405+
let truncated_text = match truncated_message {
399406
ResponseItem::Message { role, content, .. } if role == "user" => {
400407
content_items_to_text(content).unwrap_or_default()
401408
}
402409
other => panic!("unexpected item in history: {other:?}"),
403410
};
404411

405-
// The bridge should contain the truncation marker and not the full original payload.
406412
assert!(
407-
bridge_text.contains("tokens truncated"),
408-
"expected truncation marker in bridge message"
413+
truncated_text.contains("tokens truncated"),
414+
"expected truncation marker in truncated user message"
409415
);
410416
assert!(
411-
!bridge_text.contains(&big),
412-
"bridge should not include the full oversized user text"
417+
!truncated_text.contains(&big),
418+
"truncated user message should not include the full oversized user text"
413419
);
420+
421+
let summary_text = match summary_message {
422+
ResponseItem::Message { role, content, .. } if role == "user" => {
423+
content_items_to_text(content).unwrap_or_default()
424+
}
425+
other => panic!("unexpected item in history: {other:?}"),
426+
};
427+
assert_eq!(summary_text, "SUMMARY");
428+
}
429+
430+
#[test]
431+
fn build_compacted_history_appends_summary_message() {
432+
let initial_context: Vec<ResponseItem> = Vec::new();
433+
let user_messages = vec!["first user message".to_string()];
434+
let summary_text = "summary text";
435+
436+
let history = build_compacted_history(initial_context, &user_messages, summary_text);
414437
assert!(
415-
bridge_text.contains("SUMMARY"),
416-
"bridge should include the provided summary text"
438+
!history.is_empty(),
439+
"expected compacted history to include summary"
417440
);
441+
442+
let last = history.last().expect("history should have a summary entry");
443+
let summary = match last {
444+
ResponseItem::Message { role, content, .. } if role == "user" => {
445+
content_items_to_text(content).unwrap_or_default()
446+
}
447+
other => panic!("expected summary message, found {other:?}"),
448+
};
449+
assert_eq!(summary, summary_text);
418450
}
419451
}

codex-rs/core/templates/compact/history_bridge.md

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
You have exceeded the maximum number of tokens, please stop coding and instead write a short memento message for the next agent. Your note should:
2-
- Summarize what you finished and what still needs work. If there was a recent update_plan call, repeat its steps verbatim.
3-
- List outstanding TODOs with file paths / line numbers so they're easy to find.
4-
- Flag code that needs more tests (edge cases, performance, integration, etc.).
5-
- Record any open bugs, quirks, or setup steps that will make it easier for the next agent to pick up where you left off.
1+
You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.
2+
3+
Include:
4+
- Current progress and key decisions made
5+
- Important context, constraints, or user preferences
6+
- What remains to be done (clear next steps)
7+
- Any critical data, examples, or references needed to continue
8+
9+
Be concise, structured, and focused on helping the next LLM seamlessly continue the work.

0 commit comments

Comments
 (0)