39 KiB
PR #1527: Add /compact
- URL: https://github.com/openai/codex/pull/1527
- Author: aibrahim-oai
- Created: 2025-07-11 02:38:19 UTC
- Updated: 2025-08-01 04:34:39 UTC
- Changes: +474/-3, Files changed: 8, Commits: 40
Description
- Add operation to summarize the context so far.
- The operation runs a compact task that summarizes the context.
- The operation clear the previous context to free the context window
- The operation didn't use
run_taskto avoid corrupting the session - Add /compact in the tui
https://github.com/user-attachments/assets/e06c24e5-dcfb-4806-934a-564d425a919c
Full Diff
diff --git a/SUMMARY.md b/SUMMARY.md
new file mode 100644
index 0000000000..5cdecdbf4c
--- /dev/null
+++ b/SUMMARY.md
@@ -0,0 +1,21 @@
+You are a summarization assistant. A conversation follows between a user and a coding-focused AI (Codex). Your task is to generate a clear summary capturing:
+
+• High-level objective or problem being solved
+• Key instructions or design decisions given by the user
+• Main code actions or behaviors from the AI
+• Important variables, functions, modules, or outputs discussed
+• Any unresolved questions or next steps
+
+Produce the summary in a structured format like:
+
+**Objective:** …
+
+**User instructions:** … (bulleted)
+
+**AI actions / code behavior:** … (bulleted)
+
+**Important entities:** … (e.g. function names, variables, files)
+
+**Open issues / next steps:** … (if any)
+
+**Summary (concise):** (one or two sentences)
diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 3dd1d513a2..5d48cd457f 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -442,6 +442,12 @@ impl Session {
let _ = self.tx_event.send(event).await;
}
+ /// Build the full turn input by concatenating the current conversation
+ /// history with additional items for this turn.
+ pub fn turn_input_with_history(&self, extra: Vec<ResponseItem>) -> Vec<ResponseItem> {
+ [self.state.lock().unwrap().history.contents(), extra].concat()
+ }
+
/// Returns the input if there was no task running to inject into
pub fn inject_input(&self, input: Vec<InputItem>) -> Result<(), Vec<InputItem>> {
let mut state = self.state.lock().unwrap();
@@ -564,6 +570,25 @@ impl AgentTask {
handle,
}
}
+ fn compact(
+ sess: Arc<Session>,
+ sub_id: String,
+ input: Vec<InputItem>,
+ compact_instructions: String,
+ ) -> Self {
+ let handle = tokio::spawn(run_compact_task(
+ Arc::clone(&sess),
+ sub_id.clone(),
+ input,
+ compact_instructions,
+ ))
+ .abort_handle();
+ Self {
+ sess,
+ sub_id,
+ handle,
+ }
+ }
fn abort(self) {
if !self.handle.is_finished() {
@@ -884,6 +909,31 @@ async fn submission_loop(
}
});
}
+ Op::Compact => {
+ let sess = match sess.as_ref() {
+ Some(sess) => sess,
+ None => {
+ send_no_session_event(sub.id).await;
+ continue;
+ }
+ };
+
+ // Create a summarization request as user input
+ const SUMMARIZATION_PROMPT: &str = include_str!("../../../SUMMARY.md");
+
+ // Attempt to inject input into current task
+ if let Err(items) = sess.inject_input(vec![InputItem::Text {
+ text: "Start Summarization".to_string(),
+ }]) {
+ let task = AgentTask::compact(
+ sess.clone(),
+ sub.id,
+ items,
+ SUMMARIZATION_PROMPT.to_string(),
+ );
+ sess.set_task(task);
+ }
+ }
Op::Shutdown => {
info!("Shutting down Codex instance");
@@ -945,7 +995,7 @@ async fn run_task(sess: Arc<Session>, sub_id: String, input: Vec<InputItem>) {
return;
}
- let initial_input_for_turn = ResponseInputItem::from(input);
+ let initial_input_for_turn: ResponseInputItem = ResponseInputItem::from(input);
sess.record_conversation_items(&[initial_input_for_turn.clone().into()])
.await;
@@ -966,8 +1016,7 @@ async fn run_task(sess: Arc<Session>, sub_id: String, input: Vec<InputItem>) {
// conversation history on each turn. The rollout file, however, should
// only record the new items that originated in this turn so that it
// represents an append-only log without duplicates.
- let turn_input: Vec<ResponseItem> =
- [sess.state.lock().unwrap().history.contents(), pending_input].concat();
+ let turn_input: Vec<ResponseItem> = sess.turn_input_with_history(pending_input);
let turn_input_messages: Vec<String> = turn_input
.iter()
@@ -1293,6 +1342,88 @@ async fn try_run_turn(
}
}
+async fn run_compact_task(
+ sess: Arc<Session>,
+ sub_id: String,
+ input: Vec<InputItem>,
+ compact_instructions: String,
+) {
+ let start_event = Event {
+ id: sub_id.clone(),
+ msg: EventMsg::TaskStarted,
+ };
+ if sess.tx_event.send(start_event).await.is_err() {
+ return;
+ }
+
+ let initial_input_for_turn: ResponseInputItem = ResponseInputItem::from(input);
+ let turn_input: Vec<ResponseItem> =
+ sess.turn_input_with_history(vec![initial_input_for_turn.clone().into()]);
+
+ let prompt = Prompt {
+ input: turn_input,
+ user_instructions: None,
+ store: !sess.disable_response_storage,
+ extra_tools: HashMap::new(),
+ base_instructions_override: Some(compact_instructions.clone()),
+ };
+
+ let max_retries = sess.client.get_provider().stream_max_retries();
+ let mut retries = 0;
+
+ loop {
+ let attempt_result = drain_to_completed(&sess, &prompt).await;
+
+ match attempt_result {
+ Ok(()) => break,
+ Err(CodexErr::Interrupted) => return,
+ Err(e) => {
+ if retries < max_retries {
+ retries += 1;
+ let delay = backoff(retries);
+ sess.notify_background_event(
+ &sub_id,
+ format!(
+ "stream error: {e}; retrying {retries}/{max_retries} in {delay:?}…"
+ ),
+ )
+ .await;
+ tokio::time::sleep(delay).await;
+ continue;
+ } else {
+ let event = Event {
+ id: sub_id.clone(),
+ msg: EventMsg::Error(ErrorEvent {
+ message: e.to_string(),
+ }),
+ };
+ sess.send_event(event).await;
+ return;
+ }
+ }
+ }
+ }
+
+ sess.remove_task(&sub_id);
+ let event = Event {
+ id: sub_id.clone(),
+ msg: EventMsg::AgentMessage(AgentMessageEvent {
+ message: "Compact task completed".to_string(),
+ }),
+ };
+ sess.send_event(event).await;
+ let event = Event {
+ id: sub_id.clone(),
+ msg: EventMsg::TaskComplete(TaskCompleteEvent {
+ last_agent_message: None,
+ }),
+ };
+ sess.send_event(event).await;
+
+ let mut state = sess.state.lock().unwrap();
+ state.history.keep_last_messages(1);
+}
+
async fn handle_response_item(
sess: &Session,
sub_id: &str,
@@ -1858,3 +1989,20 @@ fn get_last_assistant_message_from_turn(responses: &[ResponseItem]) -> Option<St
}
})
}
+
+async fn drain_to_completed(sess: &Session, prompt: &Prompt) -> CodexResult<()> {
+ let mut stream = sess.client.clone().stream(prompt).await?;
+ loop {
+ let maybe_event = stream.next().await;
+ let Some(event) = maybe_event else {
+ return Err(CodexErr::Stream(
+ "stream closed before response.completed".into(),
+ ));
+ };
+ match event {
+ Ok(ResponseEvent::Completed { .. }) => return Ok(()),
+ Ok(_) => continue,
+ Err(e) => return Err(e),
+ }
+ }
+}
diff --git a/codex-rs/core/src/conversation_history.rs b/codex-rs/core/src/conversation_history.rs
index 4cd989cbd9..f5254f339e 100644
--- a/codex-rs/core/src/conversation_history.rs
+++ b/codex-rs/core/src/conversation_history.rs
@@ -30,6 +30,34 @@ impl ConversationHistory {
}
}
}
+
+ pub(crate) fn keep_last_messages(&mut self, n: usize) {
+ if n == 0 {
+ self.items.clear();
+ return;
+ }
+
+ // Collect the last N message items (assistant/user), newest to oldest.
+ let mut kept: Vec<ResponseItem> = Vec::with_capacity(n);
+ for item in self.items.iter().rev() {
+ if let ResponseItem::Message { role, content, .. } = item {
+ kept.push(ResponseItem::Message {
+ // we need to remove the id or the model will complain that messages are sent without
+ // their reasonings
+ id: None,
+ role: role.clone(),
+ content: content.clone(),
+ });
+ if kept.len() == n {
+ break;
+ }
+ }
+ }
+
+ // Preserve chronological order (oldest to newest) within the kept slice.
+ kept.reverse();
+ self.items = kept;
+ }
}
/// Anything that is not a system message or "reasoning" message is considered
diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/core/src/protocol.rs
index bc922eb0e2..1b4832a273 100644
--- a/codex-rs/core/src/protocol.rs
+++ b/codex-rs/core/src/protocol.rs
@@ -121,6 +121,10 @@ pub enum Op {
/// Request a single history entry identified by `log_id` + `offset`.
GetHistoryEntryRequest { offset: usize, log_id: u64 },
+ /// Request the agent to summarize the current conversation context.
+ /// The agent will use its existing context (either conversation history or previous response id)
+ /// to generate a summary which will be returned as an AgentMessage event.
+ Compact,
/// Request to shut down codex instance.
Shutdown,
}
diff --git a/codex-rs/core/tests/compact.rs b/codex-rs/core/tests/compact.rs
new file mode 100644
index 0000000000..8967c86a8f
--- /dev/null
+++ b/codex-rs/core/tests/compact.rs
@@ -0,0 +1,254 @@
+#![expect(clippy::unwrap_used)]
+
+use codex_core::Codex;
+use codex_core::CodexSpawnOk;
+use codex_core::ModelProviderInfo;
+use codex_core::built_in_model_providers;
+use codex_core::exec::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
+use codex_core::protocol::EventMsg;
+use codex_core::protocol::InputItem;
+use codex_core::protocol::Op;
+use codex_login::CodexAuth;
+use core_test_support::load_default_config_for_test;
+use core_test_support::wait_for_event;
+use serde_json::Value;
+use tempfile::TempDir;
+use wiremock::Mock;
+use wiremock::MockServer;
+use wiremock::ResponseTemplate;
+use wiremock::matchers::method;
+use wiremock::matchers::path;
+
+use pretty_assertions::assert_eq;
+
+// --- Test helpers -----------------------------------------------------------
+
+/// Build an SSE stream body from a list of JSON events.
+fn sse(events: Vec<Value>) -> String {
+ use std::fmt::Write as _;
+ let mut out = String::new();
+ for ev in events {
+ let kind = ev.get("type").and_then(|v| v.as_str()).unwrap();
+ writeln!(&mut out, "event: {kind}").unwrap();
+ if !ev.as_object().map(|o| o.len() == 1).unwrap_or(false) {
+ write!(&mut out, "data: {ev}\n\n").unwrap();
+ } else {
+ out.push('\n');
+ }
+ }
+ out
+}
+
+/// Convenience: SSE event for a completed response with a specific id.
+fn ev_completed(id: &str) -> Value {
+ serde_json::json!({
+ "type": "response.completed",
+ "response": {
+ "id": id,
+ "usage": {"input_tokens":0,"input_tokens_details":null,"output_tokens":0,"output_tokens_details":null,"total_tokens":0}
+ }
+ })
+}
+
+/// Convenience: SSE event for a single assistant message output item.
+fn ev_assistant_message(id: &str, text: &str) -> Value {
+ serde_json::json!({
+ "type": "response.output_item.done",
+ "item": {
+ "type": "message",
+ "role": "assistant",
+ "id": id,
+ "content": [{"type": "output_text", "text": text}]
+ }
+ })
+}
+
+fn sse_response(body: String) -> ResponseTemplate {
+ ResponseTemplate::new(200)
+ .insert_header("content-type", "text/event-stream")
+ .set_body_raw(body, "text/event-stream")
+}
+
+async fn mount_sse_once<M>(server: &MockServer, matcher: M, body: String)
+where
+ M: wiremock::Match + Send + Sync + 'static,
+{
+ Mock::given(method("POST"))
+ .and(path("/v1/responses"))
+ .and(matcher)
+ .respond_with(sse_response(body))
+ .expect(1)
+ .mount(server)
+ .await;
+}
+
+const FIRST_REPLY: &str = "FIRST_REPLY";
+const SUMMARY_TEXT: &str = "SUMMARY_ONLY_CONTEXT";
+const SUMMARIZE_TRIGGER: &str = "Start Summarization";
+const THIRD_USER_MSG: &str = "next turn";
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn summarize_context_three_requests_and_instructions() {
+ if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
+ println!(
+ "Skipping test because it cannot execute when network is disabled in a Codex sandbox."
+ );
+ return;
+ }
+
+ // Set up a mock server that we can inspect after the run.
+ let server = MockServer::start().await;
+
+ // SSE 1: assistant replies normally so it is recorded in history.
+ let sse1 = sse(vec![
+ ev_assistant_message("m1", FIRST_REPLY),
+ ev_completed("r1"),
+ ]);
+
+ // SSE 2: summarizer returns a summary message.
+ let sse2 = sse(vec![
+ ev_assistant_message("m2", SUMMARY_TEXT),
+ ev_completed("r2"),
+ ]);
+
+ // SSE 3: minimal completed; we only need to capture the request body.
+ let sse3 = sse(vec![ev_completed("r3")]);
+
+ // Mount three expectations, one per request, matched by body content.
+ let first_matcher = |req: &wiremock::Request| {
+ let body = std::str::from_utf8(&req.body).unwrap_or("");
+ body.contains("\"text\":\"hello world\"")
+ && !body.contains(&format!("\"text\":\"{SUMMARIZE_TRIGGER}\""))
+ };
+ mount_sse_once(&server, first_matcher, sse1).await;
+
+ let second_matcher = |req: &wiremock::Request| {
+ let body = std::str::from_utf8(&req.body).unwrap_or("");
+ body.contains(&format!("\"text\":\"{SUMMARIZE_TRIGGER}\""))
+ };
+ mount_sse_once(&server, second_matcher, sse2).await;
+
+ let third_matcher = |req: &wiremock::Request| {
+ let body = std::str::from_utf8(&req.body).unwrap_or("");
+ body.contains(&format!("\"text\":\"{THIRD_USER_MSG}\""))
+ };
+ mount_sse_once(&server, third_matcher, sse3).await;
+
+ // Build config pointing to the mock server and spawn Codex.
+ let model_provider = ModelProviderInfo {
+ base_url: Some(format!("{}/v1", server.uri())),
+ ..built_in_model_providers()["openai"].clone()
+ };
+ let home = TempDir::new().unwrap();
+ let mut config = load_default_config_for_test(&home);
+ config.model_provider = model_provider;
+ let ctrl_c = std::sync::Arc::new(tokio::sync::Notify::new());
+ let CodexSpawnOk { codex, .. } = Codex::spawn(
+ config,
+ Some(CodexAuth::from_api_key("dummy".to_string())),
+ ctrl_c.clone(),
+ )
+ .await
+ .unwrap();
+
+ // 1) Normal user input – should hit server once.
+ codex
+ .submit(Op::UserInput {
+ items: vec![InputItem::Text {
+ text: "hello world".into(),
+ }],
+ })
+ .await
+ .unwrap();
+ wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
+
+ // 2) Summarize – second hit with summarization instructions.
+ codex.submit(Op::Compact).await.unwrap();
+ wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
+
+ // 3) Next user input – third hit; history should include only the summary.
+ codex
+ .submit(Op::UserInput {
+ items: vec![InputItem::Text {
+ text: THIRD_USER_MSG.into(),
+ }],
+ })
+ .await
+ .unwrap();
+ wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
+
+ // Inspect the three captured requests.
+ let requests = server.received_requests().await.unwrap();
+ assert_eq!(requests.len(), 3, "expected exactly three requests");
+
+ let req1 = &requests[0];
+ let req2 = &requests[1];
+ let req3 = &requests[2];
+
+ let body1 = req1.body_json::<serde_json::Value>().unwrap();
+ let body2 = req2.body_json::<serde_json::Value>().unwrap();
+ let body3 = req3.body_json::<serde_json::Value>().unwrap();
+
+ // System instructions should change for the summarization turn.
+ let instr1 = body1.get("instructions").and_then(|v| v.as_str()).unwrap();
+ let instr2 = body2.get("instructions").and_then(|v| v.as_str()).unwrap();
+ assert_ne!(
+ instr1, instr2,
+ "summarization should override base instructions"
+ );
+ assert!(
+ instr2.contains("You are a summarization assistant"),
+ "summarization instructions not applied"
+ );
+
+ // The summarization request should include the injected user input marker.
+ let input2 = body2.get("input").and_then(|v| v.as_array()).unwrap();
+ // The last item is the user message created from the injected input.
+ let last2 = input2.last().unwrap();
+ assert_eq!(last2.get("type").unwrap().as_str().unwrap(), "message");
+ assert_eq!(last2.get("role").unwrap().as_str().unwrap(), "user");
+ let text2 = last2["content"][0]["text"].as_str().unwrap();
+ assert!(text2.contains(SUMMARIZE_TRIGGER));
+
+ // Third request must contain only the summary from step 2 as prior history plus new user msg.
+ let input3 = body3.get("input").and_then(|v| v.as_array()).unwrap();
+ println!("third request body: {body3}");
+ assert!(
+ input3.len() >= 2,
+ "expected summary + new user message in third request"
+ );
+
+ // Collect all (role, text) message tuples.
+ let mut messages: Vec<(String, String)> = Vec::new();
+ for item in input3 {
+ if item["type"].as_str() == Some("message") {
+ let role = item["role"].as_str().unwrap_or_default().to_string();
+ let text = item["content"][0]["text"]
+ .as_str()
+ .unwrap_or_default()
+ .to_string();
+ messages.push((role, text));
+ }
+ }
+
+ // Exactly one assistant message should remain after compaction and the new user message is present.
+ let assistant_count = messages.iter().filter(|(r, _)| r == "assistant").count();
+ assert_eq!(
+ assistant_count, 1,
+ "exactly one assistant message should remain after compaction"
+ );
+ assert!(
+ messages
+ .iter()
+ .any(|(r, t)| r == "user" && t == THIRD_USER_MSG),
+ "third request should include the new user message"
+ );
+ assert!(
+ !messages.iter().any(|(_, t)| t.contains("hello world")),
+ "third request should not include the original user input"
+ );
+ assert!(
+ !messages.iter().any(|(_, t)| t.contains(SUMMARIZE_TRIGGER)),
+ "third request should not include the summarize trigger"
+ );
+}
diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs
index cc3118e860..d4c2f3c12d 100644
--- a/codex-rs/tui/src/app.rs
+++ b/codex-rs/tui/src/app.rs
@@ -10,6 +10,8 @@ use crate::tui;
use codex_core::config::Config;
use codex_core::protocol::Event;
use codex_core::protocol::EventMsg;
+use codex_core::protocol::ExecApprovalRequestEvent;
+use codex_core::protocol::Op;
use color_eyre::eyre::Result;
use crossterm::SynchronizedUpdate;
use crossterm::event::KeyCode;
@@ -298,6 +300,12 @@ impl App<'_> {
self.app_state = AppState::Chat { widget: new_widget };
self.app_event_tx.send(AppEvent::RequestRedraw);
}
+ SlashCommand::Compact => {
+ if let AppState::Chat { widget } = &mut self.app_state {
+ widget.clear_token_usage();
+ self.app_event_tx.send(AppEvent::CodexOp(Op::Compact));
+ }
+ }
SlashCommand::Quit => {
break;
}
diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs
index 4c415777a9..c777af5264 100644
--- a/codex-rs/tui/src/chatwidget.rs
+++ b/codex-rs/tui/src/chatwidget.rs
@@ -502,6 +502,12 @@ impl ChatWidget<'_> {
pub(crate) fn token_usage(&self) -> &TokenUsage {
&self.token_usage
}
+
+ pub(crate) fn clear_token_usage(&mut self) {
+ self.token_usage = TokenUsage::default();
+ self.bottom_pane
+ .set_token_usage(self.token_usage.clone(), self.config.model_context_window);
+ }
}
impl WidgetRef for &ChatWidget<'_> {
diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs
index 7df1bcbdec..d82a16608f 100644
--- a/codex-rs/tui/src/slash_command.rs
+++ b/codex-rs/tui/src/slash_command.rs
@@ -13,6 +13,7 @@ pub enum SlashCommand {
// DO NOT ALPHA-SORT! Enum order is presentation order in the popup, so
// more frequently used commands should be listed first.
New,
+ Compact,
Diff,
Quit,
#[cfg(debug_assertions)]
@@ -24,6 +25,7 @@ impl SlashCommand {
pub fn description(self) -> &'static str {
match self {
SlashCommand::New => "Start a new chat.",
+ SlashCommand::Compact => "Compact the chat history.",
SlashCommand::Quit => "Exit the application.",
SlashCommand::Diff => {
"Show git diff of the working directory (including untracked files)"
Review Comments
codex-rs/core/src/codex.rs
- Created: 2025-07-11 22:01:09 UTC | Link: https://github.com/openai/codex/pull/1527#discussion_r2201954177
@@ -782,6 +782,36 @@ async fn submission_loop(
}
});
}
+
+ Op::SummarizeContext => {
+ let sess = match sess.as_ref() {
+ Some(sess) => sess,
+ None => {
+ send_no_session_event(sub.id).await;
+ continue;
+ }
+ };
+
+ // Create a summarization request as user input
+ const SUMMARIZATION_PROMPT: &str = concat!(
+ "Please provide a summary of our conversation so far, highlighting key points, ",
I would favor a raw string (
r#) overconcat!()here (unless you really want this to be one line of text?): https://doc.rust-lang.org/rust-by-example/std/str.html#literals-and-escapes
- Created: 2025-07-11 22:03:00 UTC | Link: https://github.com/openai/codex/pull/1527#discussion_r2201956329
@@ -782,6 +782,36 @@ async fn submission_loop(
}
});
}
+
+ Op::SummarizeContext => {
+ let sess = match sess.as_ref() {
+ Some(sess) => sess,
+ None => {
+ send_no_session_event(sub.id).await;
+ continue;
+ }
+ };
+
+ // Create a summarization request as user input
+ const SUMMARIZATION_PROMPT: &str = concat!(
+ "Please provide a summary of our conversation so far, highlighting key points, ",
+ "decisions made, and any important context that would be useful for future reference. ",
+ "This summary will be used to replace our conversation history with a more concise ",
+ "version so choose what details you will need to continue your work. ",
+ "Provide the summary directly without main title."
+ );
+
+ let summarization_prompt = vec![InputItem::Text {
+ text: SUMMARIZATION_PROMPT.to_string(),
+ }];
+
+ // Attempt to inject input into current task
+ if let Err(items) = sess.inject_input(summarization_prompt) {
+ // No current task, spawn a new one
+ let task = AgentTask::spawn(Arc::clone(sess), sub.id, items);
Does this work? I think this is the more canonical way to do it.
let task = AgentTask::spawn(sess.clone(), sub.id, items);
- Created: 2025-07-12 16:59:35 UTC | Link: https://github.com/openai/codex/pull/1527#discussion_r2202822840
@@ -782,6 +782,36 @@ async fn submission_loop(
}
});
}
+
+ Op::SummarizeContext => {
+ let sess = match sess.as_ref() {
+ Some(sess) => sess,
+ None => {
+ send_no_session_event(sub.id).await;
+ continue;
+ }
+ };
+
+ // Create a summarization request as user input
+ const SUMMARIZATION_PROMPT: &str = concat!(
+ "Please provide a summary of our conversation so far, highlighting key points, ",
+ "decisions made, and any important context that would be useful for future reference. ",
+ "This summary will be used to replace our conversation history with a more concise ",
+ "version so choose what details you will need to continue your work. ",
+ "Provide the summary directly without main title."
+ );
+
+ let summarization_prompt = vec![InputItem::Text {
+ text: SUMMARIZATION_PROMPT.to_string(),
+ }];
+
+ // Attempt to inject input into current task
+ if let Err(items) = sess.inject_input(summarization_prompt) {
+ // No current task, spawn a new one
+ let task = AgentTask::spawn(Arc::clone(sess), sub.id, items);
Does what I suggested not work?
- Created: 2025-07-30 23:44:15 UTC | Link: https://github.com/openai/codex/pull/1527#discussion_r2244065346
@@ -829,6 +829,79 @@ async fn submission_loop(
}
});
}
+ Op::SummarizeContext => {
+ let sess = match sess.as_ref() {
+ Some(sess) => sess,
+ None => {
+ send_no_session_event(sub.id).await;
+ continue;
+ }
+ };
+
+ // Create a summarization request as user input
+ const SUMMARIZATION_PROMPT: &str = r#"
Let's put this in an
.mdfile andinclude_str!()so it's easier to maintain.
codex-rs/core/tests/summarize_context.rs
- Created: 2025-07-11 22:16:33 UTC | Link: https://github.com/openai/codex/pull/1527#discussion_r2201970664
@@ -0,0 +1,109 @@
+#![expect(clippy::unwrap_used, clippy::expect_used)]
+
+//! Tests for the `Op::SummarizeContext` operation added to verify that
+//! summarization requests are properly handled and injected as user input.
+
+use std::time::Duration;
+
+use codex_core::Codex;
+use codex_core::protocol::EventMsg;
+use codex_core::protocol::InputItem;
+use codex_core::protocol::Op;
+mod test_support;
+use tempfile::TempDir;
+use test_support::load_default_config_for_test;
+use tokio::sync::Notify;
+use tokio::time::timeout;
+
+/// Helper function to set up a codex session and wait for it to be configured
+async fn setup_configured_codex_session() -> Codex {
+ let codex_home = TempDir::new().unwrap();
+ let config = load_default_config_for_test(&codex_home);
+ let (codex, _init_id) = Codex::spawn(config, std::sync::Arc::new(Notify::new()))
+ .await
+ .unwrap();
+
+ // Wait for session configured
+ loop {
+ let event = timeout(Duration::from_secs(5), codex.next_event())
Consider using
codex_wrapper.rsinstead (and you can remove theloop):
- Created: 2025-07-11 22:21:29 UTC | Link: https://github.com/openai/codex/pull/1527#discussion_r2201983371
@@ -0,0 +1,109 @@
+#![expect(clippy::unwrap_used, clippy::expect_used)]
+
+//! Tests for the `Op::SummarizeContext` operation added to verify that
+//! summarization requests are properly handled and injected as user input.
+
+use std::time::Duration;
+
+use codex_core::Codex;
+use codex_core::protocol::EventMsg;
+use codex_core::protocol::InputItem;
+use codex_core::protocol::Op;
+mod test_support;
+use tempfile::TempDir;
+use test_support::load_default_config_for_test;
+use tokio::sync::Notify;
+use tokio::time::timeout;
+
+/// Helper function to set up a codex session and wait for it to be configured
+async fn setup_configured_codex_session() -> Codex {
+ let codex_home = TempDir::new().unwrap();
+ let config = load_default_config_for_test(&codex_home);
+ let (codex, _init_id) = Codex::spawn(config, std::sync::Arc::new(Notify::new()))
+ .await
+ .unwrap();
+
+ // Wait for session configured
+ loop {
+ let event = timeout(Duration::from_secs(5), codex.next_event())
+ .await
+ .expect("timeout waiting for session configured")
+ .expect("codex closed");
+
+ if matches!(event.msg, EventMsg::SessionConfigured(_)) {
+ break;
+ }
+ }
+
+ codex
+}
+
+#[tokio::test]
+async fn test_summarize_context_spawns_new_agent_task() {
+ // Test the specific behavior: when there's no current task,
+ // SummarizeContext should spawn a new AgentTask with the summarization prompt
+ let codex = setup_configured_codex_session().await;
Please make the test description a doc comment and try to keep comments as short as possible, so:
///indicates a doc comment in Rust.- Remove "Test the specific behavior:"
/// When there's no current task, verify SummarizeContext spawns /// a new AgentTask with the summarization prompt. #[tokio::test] async fn test_summarize_context_spawns_new_agent_task() { let codex = setup_configured_codex_session().await;
- Created: 2025-07-11 22:22:23 UTC | Link: https://github.com/openai/codex/pull/1527#discussion_r2201986392
@@ -0,0 +1,109 @@
+#![expect(clippy::unwrap_used, clippy::expect_used)]
+
+//! Tests for the `Op::SummarizeContext` operation added to verify that
+//! summarization requests are properly handled and injected as user input.
+
+use std::time::Duration;
+
+use codex_core::Codex;
+use codex_core::protocol::EventMsg;
+use codex_core::protocol::InputItem;
+use codex_core::protocol::Op;
+mod test_support;
+use tempfile::TempDir;
+use test_support::load_default_config_for_test;
+use tokio::sync::Notify;
+use tokio::time::timeout;
+
+/// Helper function to set up a codex session and wait for it to be configured
+async fn setup_configured_codex_session() -> Codex {
+ let codex_home = TempDir::new().unwrap();
+ let config = load_default_config_for_test(&codex_home);
+ let (codex, _init_id) = Codex::spawn(config, std::sync::Arc::new(Notify::new()))
+ .await
+ .unwrap();
+
+ // Wait for session configured
+ loop {
+ let event = timeout(Duration::from_secs(5), codex.next_event())
+ .await
+ .expect("timeout waiting for session configured")
+ .expect("codex closed");
+
+ if matches!(event.msg, EventMsg::SessionConfigured(_)) {
+ break;
+ }
+ }
+
+ codex
+}
+
+#[tokio::test]
+async fn test_summarize_context_spawns_new_agent_task() {
+ // Test the specific behavior: when there's no current task,
+ // SummarizeContext should spawn a new AgentTask with the summarization prompt
+ let codex = setup_configured_codex_session().await;
+
+ // At this point, there should be no current task running
+ // Submit SummarizeContext operation - this should trigger:
+ // if let Err(items) = sess.inject_input(summarization_prompt) {
+ // let task = AgentTask::spawn(Arc::clone(sess), sub.id, items);
+ // sess.set_task(task);
+ // }
+ let _sub_id = codex.submit(Op::SummarizeContext).await.unwrap();
+
+ // Should receive a TaskStarted event indicating a new AgentTask was spawned
I think this comment is duplicative with the message to your
assert!(), so I would favor keeping the one inassert!()over this one.
- Created: 2025-07-11 22:22:39 UTC | Link: https://github.com/openai/codex/pull/1527#discussion_r2201987277
@@ -0,0 +1,109 @@
+#![expect(clippy::unwrap_used, clippy::expect_used)]
+
+//! Tests for the `Op::SummarizeContext` operation added to verify that
+//! summarization requests are properly handled and injected as user input.
+
+use std::time::Duration;
+
+use codex_core::Codex;
+use codex_core::protocol::EventMsg;
+use codex_core::protocol::InputItem;
+use codex_core::protocol::Op;
+mod test_support;
+use tempfile::TempDir;
+use test_support::load_default_config_for_test;
+use tokio::sync::Notify;
+use tokio::time::timeout;
+
+/// Helper function to set up a codex session and wait for it to be configured
+async fn setup_configured_codex_session() -> Codex {
+ let codex_home = TempDir::new().unwrap();
+ let config = load_default_config_for_test(&codex_home);
+ let (codex, _init_id) = Codex::spawn(config, std::sync::Arc::new(Notify::new()))
+ .await
+ .unwrap();
+
+ // Wait for session configured
+ loop {
+ let event = timeout(Duration::from_secs(5), codex.next_event())
+ .await
+ .expect("timeout waiting for session configured")
+ .expect("codex closed");
+
+ if matches!(event.msg, EventMsg::SessionConfigured(_)) {
+ break;
+ }
+ }
+
+ codex
+}
+
+#[tokio::test]
+async fn test_summarize_context_spawns_new_agent_task() {
+ // Test the specific behavior: when there's no current task,
+ // SummarizeContext should spawn a new AgentTask with the summarization prompt
+ let codex = setup_configured_codex_session().await;
+
+ // At this point, there should be no current task running
+ // Submit SummarizeContext operation - this should trigger:
+ // if let Err(items) = sess.inject_input(summarization_prompt) {
+ // let task = AgentTask::spawn(Arc::clone(sess), sub.id, items);
+ // sess.set_task(task);
+ // }
+ let _sub_id = codex.submit(Op::SummarizeContext).await.unwrap();
+
+ // Should receive a TaskStarted event indicating a new AgentTask was spawned
+ let event = timeout(Duration::from_secs(5), codex.next_event())
+ .await
+ .expect("timeout waiting for task started event")
+ .expect("codex closed");
+
+ assert!(
+ matches!(event.msg, EventMsg::TaskStarted),
+ "Expected TaskStarted when no current task exists - should spawn new AgentTask"
+ );
+}
+
+#[tokio::test]
+async fn test_summarize_context_injects_into_running_task() {
+ // Test that when a task IS running, SummarizeContext injects into the existing task
Again, prefer doc comment.
- Created: 2025-07-11 22:25:17 UTC | Link: https://github.com/openai/codex/pull/1527#discussion_r2201993660
@@ -0,0 +1,109 @@
+#![expect(clippy::unwrap_used, clippy::expect_used)]
+
+//! Tests for the `Op::SummarizeContext` operation added to verify that
+//! summarization requests are properly handled and injected as user input.
+
+use std::time::Duration;
+
+use codex_core::Codex;
+use codex_core::protocol::EventMsg;
+use codex_core::protocol::InputItem;
+use codex_core::protocol::Op;
+mod test_support;
+use tempfile::TempDir;
+use test_support::load_default_config_for_test;
+use tokio::sync::Notify;
+use tokio::time::timeout;
+
+/// Helper function to set up a codex session and wait for it to be configured
+async fn setup_configured_codex_session() -> Codex {
+ let codex_home = TempDir::new().unwrap();
+ let config = load_default_config_for_test(&codex_home);
+ let (codex, _init_id) = Codex::spawn(config, std::sync::Arc::new(Notify::new()))
+ .await
+ .unwrap();
+
+ // Wait for session configured
+ loop {
+ let event = timeout(Duration::from_secs(5), codex.next_event())
+ .await
+ .expect("timeout waiting for session configured")
+ .expect("codex closed");
+
+ if matches!(event.msg, EventMsg::SessionConfigured(_)) {
+ break;
+ }
+ }
+
+ codex
+}
+
+#[tokio::test]
+async fn test_summarize_context_spawns_new_agent_task() {
+ // Test the specific behavior: when there's no current task,
+ // SummarizeContext should spawn a new AgentTask with the summarization prompt
+ let codex = setup_configured_codex_session().await;
+
+ // At this point, there should be no current task running
+ // Submit SummarizeContext operation - this should trigger:
+ // if let Err(items) = sess.inject_input(summarization_prompt) {
+ // let task = AgentTask::spawn(Arc::clone(sess), sub.id, items);
+ // sess.set_task(task);
+ // }
+ let _sub_id = codex.submit(Op::SummarizeContext).await.unwrap();
+
+ // Should receive a TaskStarted event indicating a new AgentTask was spawned
+ let event = timeout(Duration::from_secs(5), codex.next_event())
+ .await
+ .expect("timeout waiting for task started event")
+ .expect("codex closed");
+
+ assert!(
+ matches!(event.msg, EventMsg::TaskStarted),
+ "Expected TaskStarted when no current task exists - should spawn new AgentTask"
+ );
+}
+
+#[tokio::test]
+async fn test_summarize_context_injects_into_running_task() {
+ // Test that when a task IS running, SummarizeContext injects into the existing task
+ let codex = setup_configured_codex_session().await;
+
+ // First, start a task by submitting user input
+ let _input_sub_id = codex
+ .submit(Op::UserInput {
+ items: vec![InputItem::Text {
+ text: "Hello, this should start a task".to_string(),
+ }],
+ })
+ .await
+ .unwrap();
+
+ // Wait for the task to start
+ let event = timeout(Duration::from_secs(5), codex.next_event())
+ .await
+ .expect("timeout waiting for task started")
+ .expect("codex closed");
+
+ assert!(
+ matches!(event.msg, EventMsg::TaskStarted),
+ "First task should start"
+ );
+
+ // Now submit SummarizeContext while a task is running
+ // This should test the inject_input SUCCESS path (not the spawn new task path)
+ let _summary_sub_id = codex.submit(Op::SummarizeContext).await.unwrap();
+
+ // The summarization prompt should be injected into the existing task
+ // rather than spawning a new one. We shouldn't get another TaskStarted event
+ let result = timeout(Duration::from_millis(500), codex.next_event()).await;
+
+ // If we get an event, it should NOT be TaskStarted (since we're injecting into existing task)
Hmm, this does not feel like a strong verification. Can we instead test for what we should get instead of one possible thing we shouldn't get?