Add ephemeral turn context support

Introduce app-server ephemeralContext, render it as additional_context_for_this_turn fragments, and strip it correctly across contextual-user diffing and compaction flows.

Regenerate app-server schema fixtures and add coverage for model-visible layout, compaction, oversize validation, and resumed/forked context reconstruction.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Charles Cunningham
2026-03-13 11:41:35 -07:00
co-authored by Codex
parent 4bcfdafd01
commit 72d53a2c9b
54 changed files with 1422 additions and 26 deletions
+1
View File
@@ -305,6 +305,7 @@ impl AgentControl {
agent_id,
Op::UserInput {
items,
ephemeral_context: Vec::new(),
final_output_json_schema: None,
},
)
+3
View File
@@ -335,6 +335,7 @@ async fn send_input_submits_user_message() {
text: "hello from tests".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
},
);
@@ -366,6 +367,7 @@ async fn spawn_agent_creates_thread_and_sends_prompt() {
text: "spawned".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
},
);
@@ -442,6 +444,7 @@ async fn spawn_agent_can_fork_parent_thread_history() {
text: "child task".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
},
);
+19 -3
View File
@@ -323,6 +323,7 @@ use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::protocol::CodexErrorInfo;
use codex_protocol::protocol::InitialHistory;
use codex_protocol::user_input::EphemeralContext;
use codex_protocol::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_readiness::Readiness;
@@ -773,6 +774,7 @@ pub(crate) struct TurnContext {
pub(crate) sub_id: String,
pub(crate) trace_id: Option<String>,
pub(crate) realtime_active: bool,
pub(crate) ephemeral_context: Vec<EphemeralContext>,
pub(crate) config: Arc<Config>,
pub(crate) auth_manager: Option<Arc<AuthManager>>,
pub(crate) model_info: ModelInfo,
@@ -877,6 +879,7 @@ impl TurnContext {
sub_id: self.sub_id.clone(),
trace_id: self.trace_id.clone(),
realtime_active: self.realtime_active,
ephemeral_context: self.ephemeral_context.clone(),
config: Arc::new(config),
auth_manager: self.auth_manager.clone(),
model_info: model_info.clone(),
@@ -1308,6 +1311,7 @@ impl Session {
sub_id,
trace_id: current_span_trace_id(),
realtime_active: false,
ephemeral_context: Vec::new(),
config: per_turn_config.clone(),
auth_manager: auth_manager_for_context,
model_info: model_info.clone(),
@@ -1942,6 +1946,7 @@ impl Session {
text,
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
},
)
@@ -2214,6 +2219,7 @@ impl Session {
&self,
sub_id: String,
updates: SessionSettingsUpdate,
ephemeral_context: Vec<EphemeralContext>,
) -> ConstraintResult<Arc<TurnContext>> {
let (
session_configuration,
@@ -2274,6 +2280,7 @@ impl Session {
session_configuration,
updates.final_output_json_schema,
sandbox_policy_changed,
ephemeral_context,
)
.await)
}
@@ -2284,6 +2291,7 @@ impl Session {
session_configuration: SessionConfiguration,
final_output_json_schema: Option<Option<Value>>,
sandbox_policy_changed: bool,
ephemeral_context: Vec<EphemeralContext>,
) -> Arc<TurnContext> {
let per_turn_config = Self::build_per_turn_config(&session_configuration);
self.services
@@ -2342,6 +2350,7 @@ impl Session {
skills_outcome,
);
turn_context.realtime_active = self.conversation.running_state().await.is_some();
turn_context.ephemeral_context = ephemeral_context;
if let Some(final_schema) = final_output_json_schema {
turn_context.final_output_json_schema = final_schema;
@@ -2499,7 +2508,7 @@ impl Session {
let state = self.state.lock().await;
state.session_configuration.clone()
};
self.new_turn_from_configuration(sub_id, session_configuration, None, false)
self.new_turn_from_configuration(sub_id, session_configuration, None, false, Vec::new())
.await
}
@@ -4365,7 +4374,7 @@ mod handlers {
}
pub async fn user_input_or_turn(sess: &Arc<Session>, sub_id: String, op: Op) {
let (items, updates) = match op {
let (items, ephemeral_context, updates) = match op {
Op::UserTurn {
cwd,
approval_policy,
@@ -4391,6 +4400,7 @@ mod handlers {
});
(
items,
Vec::new(),
SessionSettingsUpdate {
cwd: Some(cwd),
approval_policy: Some(approval_policy),
@@ -4407,9 +4417,11 @@ mod handlers {
}
Op::UserInput {
items,
ephemeral_context,
final_output_json_schema,
} => (
items,
ephemeral_context,
SessionSettingsUpdate {
final_output_json_schema: Some(final_output_json_schema),
..Default::default()
@@ -4418,7 +4430,10 @@ mod handlers {
_ => unreachable!(),
};
let Ok(current_context) = sess.new_turn_with_sub_id(sub_id, updates).await else {
let Ok(current_context) = sess
.new_turn_with_sub_id(sub_id, updates, ephemeral_context)
.await
else {
// new_turn_with_sub_id already emits the error event.
return;
};
@@ -5222,6 +5237,7 @@ async fn spawn_review_thread(
sub_id: review_turn_id,
trace_id: current_span_trace_id(),
realtime_active: parent_turn_context.realtime_active,
ephemeral_context: Vec::new(),
config: per_turn_config,
auth_manager: auth_manager_for_context,
model_info: model_info.clone(),
+1
View File
@@ -148,6 +148,7 @@ pub(crate) async fn run_codex_thread_one_shot(
// Send the initial input to kick off the one-shot turn.
io.submit(Op::UserInput {
items: input,
ephemeral_context: Vec::new(),
final_output_json_schema,
})
.await?;
+14 -4
View File
@@ -761,7 +761,11 @@ async fn resumed_history_injects_initial_context_on_first_context_update_only()
session
.record_context_updates_and_set_reference_context_item(&turn_context)
.await;
expected.extend(session.build_initial_context(&turn_context).await);
expected.extend(
session
.build_initial_context_without_reference_context_item(&turn_context)
.await,
);
let history_after_seed = session.clone_history().await;
assert_eq!(expected, history_after_seed.raw_items());
@@ -926,7 +930,7 @@ async fn record_initial_history_reconstructs_forked_transcript() {
let reconstruction_turn = session.new_default_turn().await;
expected.extend(
session
.build_initial_context(reconstruction_turn.as_ref())
.build_initial_context_without_reference_context_item(reconstruction_turn.as_ref())
.await,
);
let history = session.state.lock().await.clone_history();
@@ -3409,7 +3413,9 @@ async fn record_context_updates_and_set_reference_context_item_injects_full_cont
.record_context_updates_and_set_reference_context_item(&turn_context)
.await;
let history = session.clone_history().await;
let initial_context = session.build_initial_context(&turn_context).await;
let initial_context = session
.build_initial_context_without_reference_context_item(&turn_context)
.await;
assert_eq!(history.raw_items().to_vec(), initial_context);
let current_context = session.reference_context_item().await;
@@ -3453,7 +3459,11 @@ async fn record_context_updates_and_set_reference_context_item_reinjects_full_co
let history = session.clone_history().await;
let mut expected_history = vec![compacted_summary];
expected_history.extend(session.build_initial_context(&turn_context).await);
expected_history.extend(
session
.build_initial_context_without_reference_context_item(&turn_context)
.await,
);
assert_eq!(history.raw_items().to_vec(), expected_history);
}
+29
View File
@@ -121,6 +121,35 @@ do things
assert_eq!(vec!["real user message".to_string()], collected);
}
#[test]
fn collect_user_messages_filters_ephemeral_context_entries() {
let items = vec![
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "<additional_context_for_this_turn>\n <title>Context from my editor</title>\n <content>\n## Active file: src/main.rs\n </content>\n</additional_context_for_this_turn>"
.to_string(),
}],
end_turn: None,
phase: None,
},
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "real user message".to_string(),
}],
end_turn: None,
phase: None,
},
];
let collected = collect_user_messages(&items);
assert_eq!(vec!["real user message".to_string()], collected);
}
#[test]
fn build_token_limited_compacted_history_truncates_overlong_user_messages() {
// Use a small truncation limit so the test remains fast while still validating
+12 -1
View File
@@ -19,13 +19,24 @@ use tracing::warn;
use uuid::Uuid;
use crate::model_visible_fragments::is_contextual_user_fragment;
use crate::model_visible_fragments::is_ephemeral_context_fragment;
use crate::web_search::web_search_action_detail;
fn strip_ephemeral_context_prefix(message: &[ContentItem]) -> &[ContentItem] {
let prefix_len = message
.iter()
.take_while(|content_item| is_ephemeral_context_fragment(content_item))
.count();
&message[prefix_len..]
}
pub(crate) fn is_contextual_user_message_content(message: &[ContentItem]) -> bool {
message.iter().any(is_contextual_user_fragment)
let stripped = strip_ephemeral_context_prefix(message);
stripped.is_empty() || stripped.iter().any(is_contextual_user_fragment)
}
fn parse_user_message(message: &[ContentItem]) -> Option<UserMessageItem> {
let message = strip_ephemeral_context_prefix(message);
if is_contextual_user_message_content(message) {
return None;
}
+45
View File
@@ -218,6 +218,51 @@ fn skips_user_instructions_and_env() {
}
}
#[test]
fn strips_ephemeral_context_prefix_from_user_message() {
let item = ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![
ContentItem::InputText {
text: "<additional_context_for_this_turn>\n <title>Context from my editor</title>\n <content>\n## Active file: src/main.rs\n </content>\n</additional_context_for_this_turn>".to_string(),
},
ContentItem::InputText {
text: "Explain this function.".to_string(),
},
],
end_turn: None,
phase: None,
};
let turn_item = parse_turn_item(&item).expect("expected user message turn item");
let TurnItem::UserMessage(user_message) = turn_item else {
panic!("expected TurnItem::UserMessage");
};
assert_eq!(
user_message.content,
vec![UserInput::Text {
text: "Explain this function.".to_string(),
text_elements: Vec::new(),
}]
);
}
#[test]
fn drops_user_message_when_only_ephemeral_context_prefix_is_present() {
let item = ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "<additional_context_for_this_turn>\n <title>Context from my editor</title>\n <content>\n## Active file: src/main.rs\n </content>\n</additional_context_for_this_turn>".to_string(),
}],
end_turn: None,
phase: None,
};
assert!(parse_turn_item(&item).is_none());
}
#[test]
fn parses_agent_message() {
let item = ResponseItem::Message {
+1
View File
@@ -628,6 +628,7 @@ async fn run_guardian_subagent(
codex
.submit(Op::UserInput {
items: prompt_items,
ephemeral_context: Vec::new(),
final_output_json_schema: Some(schema),
})
.await?;
+19 -1
View File
@@ -19,7 +19,9 @@
//! - `ContextualUserContextRole` for contextual user-role state that must be
//! parsed as context rather than literal user intent
//! - If the fragment is durable turn/session state that should rebuild across
//! resume, compaction, backtracking, or fork, implement `build(...)`.
//! resume, compaction, backtracking, or fork, implement `build(...)` for the
//! common zero-or-one case, or override `build_many(...)` when a fragment
//! source needs to emit multiple content items from one turn-state source.
//! `reference_context_item` is the baseline already represented in
//! model-visible history; compare against it to avoid duplicates, and use
//! `TurnContextDiffParams` for other runtime/session inputs such as
@@ -187,6 +189,22 @@ pub(crate) trait ModelVisibleContextFragment: Sized {
None
}
/// Build zero or more fragments from the current turn state.
///
/// Most fragments should implement `build(...)` and use this default,
/// which lifts the common zero-or-one case into a vector. Override this
/// only when one turn-state source intentionally renders multiple
/// model-visible content items.
fn build_many(
turn_context: &TurnContext,
reference_context_item: Option<&TurnContextItem>,
params: &TurnContextDiffParams<'_>,
) -> Vec<Self> {
Self::build(turn_context, reference_context_item, params)
.into_iter()
.collect()
}
/// Stable markers used to recognize contextual-user fragments in persisted
/// history. Developer fragments should keep the default `None`.
fn contextual_user_markers() -> Option<ContextualUserFragmentMarkers> {
+52 -13
View File
@@ -73,10 +73,13 @@ use codex_protocol::models::developer_realtime_start_text_with_instructions;
use codex_protocol::protocol::AgentStatus;
use codex_protocol::protocol::ENVIRONMENT_CONTEXT_CLOSE_TAG;
use codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG;
use codex_protocol::protocol::EPHEMERAL_CONTEXT_CLOSE_TAG;
use codex_protocol::protocol::EPHEMERAL_CONTEXT_OPEN_TAG;
use codex_protocol::protocol::TurnContextItem;
use codex_protocol::protocol::TurnContextNetworkItem;
use codex_protocol::protocol::USER_INSTRUCTIONS_CLOSE_TAG;
use codex_protocol::protocol::USER_INSTRUCTIONS_OPEN_TAG;
use codex_protocol::user_input::EphemeralContext;
use serde::Deserialize;
use serde::Serialize;
use std::path::PathBuf;
@@ -94,7 +97,7 @@ struct ModelVisibleFragmentRegistration {
Option<&TurnContextItem>,
&TurnContext,
&TurnContextDiffParams<'_>,
) -> Option<BuiltTurnStateFragment>,
) -> Vec<BuiltTurnStateFragment>,
}
impl ModelVisibleFragmentRegistration {
@@ -117,17 +120,19 @@ fn build_registered_turn_state_fragment<F: ModelVisibleContextFragment>(
reference_context_item: Option<&TurnContextItem>,
turn_context: &TurnContext,
params: &TurnContextDiffParams<'_>,
) -> Option<BuiltTurnStateFragment> {
let fragment = F::build(turn_context, reference_context_item, params)?;
match F::Role::MESSAGE_ROLE {
MessageRole::Developer => Some(BuiltTurnStateFragment::Developer(
DeveloperTextFragment::new(fragment.render_text()),
)),
MessageRole::User => Some(BuiltTurnStateFragment::ContextualUser(
ContextualUserTextFragment::new(fragment.render_text()),
)),
MessageRole::Assistant | MessageRole::System => None,
}
) -> Vec<BuiltTurnStateFragment> {
F::build_many(turn_context, reference_context_item, params)
.into_iter()
.filter_map(|fragment| match F::Role::MESSAGE_ROLE {
MessageRole::Developer => Some(BuiltTurnStateFragment::Developer(
DeveloperTextFragment::new(fragment.render_text()),
)),
MessageRole::User => Some(BuiltTurnStateFragment::ContextualUser(
ContextualUserTextFragment::new(fragment.render_text()),
)),
MessageRole::Assistant | MessageRole::System => None,
})
.collect()
}
/// Canonical ordered registry for all current model-visible fragments.
@@ -146,6 +151,7 @@ const REGISTERED_MODEL_VISIBLE_FRAGMENTS: &[ModelVisibleFragmentRegistration] =
ModelVisibleFragmentRegistration::of::<SkillsSectionFragment>(),
ModelVisibleFragmentRegistration::of::<ChildAgentsInstructionsFragment>(),
ModelVisibleFragmentRegistration::of::<EnvironmentContext>(),
ModelVisibleFragmentRegistration::of::<EphemeralContext>(),
ModelVisibleFragmentRegistration::of::<SkillInstructions>(),
ModelVisibleFragmentRegistration::of::<PluginInstructions>(),
ModelVisibleFragmentRegistration::of::<UserShellCommandFragment>(),
@@ -842,6 +848,32 @@ impl ModelVisibleContextFragment for EnvironmentContext {
}
}
impl ModelVisibleContextFragment for EphemeralContext {
type Role = ContextualUserContextRole;
fn render_text(&self) -> String {
Self::wrap_contextual_user_body(format!(
" <title>{}</title>\n <content>\n{}\n </content>",
self.title, self.text
))
}
fn build_many(
turn_context: &TurnContext,
_reference_context_item: Option<&TurnContextItem>,
_params: &TurnContextDiffParams<'_>,
) -> Vec<Self> {
turn_context.ephemeral_context.clone()
}
fn contextual_user_markers() -> Option<ContextualUserFragmentMarkers> {
Some(ContextualUserFragmentMarkers::new(
EPHEMERAL_CONTEXT_OPEN_TAG,
EPHEMERAL_CONTEXT_CLOSE_TAG,
))
}
}
// ---------------------------------------------------------------------------
// Contextual-user runtime fragments
// ---------------------------------------------------------------------------
@@ -1012,6 +1044,13 @@ pub(crate) fn is_contextual_user_fragment(content_item: &ContentItem) -> bool {
|| is_legacy_contextual_user_fragment(text)
}
pub(crate) fn is_ephemeral_context_fragment(content_item: &ContentItem) -> bool {
let ContentItem::InputText { text } = content_item else {
return false;
};
EphemeralContext::matches_contextual_user_text(text)
}
pub(crate) fn build_turn_state_fragments(
reference_context_item: Option<&TurnContextItem>,
turn_context: &TurnContext,
@@ -1019,7 +1058,7 @@ pub(crate) fn build_turn_state_fragments(
) -> Vec<BuiltTurnStateFragment> {
REGISTERED_MODEL_VISIBLE_FRAGMENTS
.iter()
.filter_map(|registration| {
.flat_map(|registration| {
(registration.build_turn_state)(reference_context_item, turn_context, params)
})
.collect()
@@ -535,6 +535,7 @@ async fn send_input_accepts_structured_items() {
text_elements: Vec::new(),
},
],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
};
let captured = manager
+5
View File
@@ -50,6 +50,7 @@ async fn interrupt_long_running_tool_emits_turn_aborted() {
text: "start sleep".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -104,6 +105,7 @@ async fn interrupt_tool_records_history_entries() {
text: "start history recording".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -122,6 +124,7 @@ async fn interrupt_tool_records_history_entries() {
text: "follow up".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -202,6 +205,7 @@ async fn interrupt_persists_turn_aborted_marker_in_next_request() {
text: "start interrupt marker".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -220,6 +224,7 @@ async fn interrupt_persists_turn_aborted_marker_in_next_request() {
text: "follow up".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
+29
View File
@@ -276,6 +276,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -634,6 +635,7 @@ async fn includes_conversation_id_and_model_headers_in_request() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -682,6 +684,7 @@ async fn includes_base_instructions_override_in_request() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -733,6 +736,7 @@ async fn chatgpt_auth_sends_correct_request() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -834,6 +838,7 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -870,6 +875,7 @@ async fn includes_user_instructions_message_in_request() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -958,6 +964,7 @@ async fn includes_apps_guidance_as_developer_message_for_chatgpt_auth() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1049,6 +1056,7 @@ async fn omits_apps_guidance_for_api_key_auth_even_when_feature_enabled() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1118,6 +1126,7 @@ async fn skills_append_to_instructions() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1175,6 +1184,7 @@ async fn includes_configured_effort_in_request() -> anyhow::Result<()> {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1217,6 +1227,7 @@ async fn includes_no_effort_in_request() -> anyhow::Result<()> {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1257,6 +1268,7 @@ async fn includes_default_reasoning_effort_in_request_when_defined_by_model_info
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1368,6 +1380,7 @@ async fn configured_reasoning_summary_is_sent() -> anyhow::Result<()> {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1482,6 +1495,7 @@ async fn reasoning_summary_is_omitted_when_disabled() -> anyhow::Result<()> {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1538,6 +1552,7 @@ async fn reasoning_summary_none_overrides_model_catalog_default() -> anyhow::Res
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1574,6 +1589,7 @@ async fn includes_default_verbosity_in_request() -> anyhow::Result<()> {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1619,6 +1635,7 @@ async fn configured_verbosity_not_sent_for_models_without_support() -> anyhow::R
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1663,6 +1680,7 @@ async fn configured_verbosity_is_sent() -> anyhow::Result<()> {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1712,6 +1730,7 @@ async fn includes_developer_instructions_message_in_request() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1993,6 +2012,7 @@ async fn token_count_includes_rate_limits_snapshot() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -2158,6 +2178,7 @@ async fn usage_limit_error_emits_rate_limit_event() -> anyhow::Result<()> {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -2232,6 +2253,7 @@ async fn context_window_error_sets_total_tokens_to_model_window() -> anyhow::Res
text: "seed turn".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -2244,6 +2266,7 @@ async fn context_window_error_sets_total_tokens_to_model_window() -> anyhow::Res
text: "trigger context window".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -2326,6 +2349,7 @@ async fn incomplete_response_emits_content_filter_error_message() -> anyhow::Res
text: "trigger incomplete".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -2423,6 +2447,7 @@ async fn azure_overrides_assign_properties_used_for_responses_url() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -2507,6 +2532,7 @@ async fn env_var_overrides_loaded_auth() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -2568,6 +2594,7 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
text: "U1".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -2581,6 +2608,7 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
text: "U2".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -2594,6 +2622,7 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
text: "U3".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -836,6 +836,7 @@ async fn responses_websocket_usage_limit_error_emits_rate_limit_event() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -921,6 +922,7 @@ async fn responses_websocket_invalid_request_error_with_status_is_forwarded() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -76,6 +76,7 @@ async fn no_collaboration_instructions_by_default() -> Result<()> {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -125,6 +126,7 @@ async fn user_input_includes_collaboration_instructions_after_override() -> Resu
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -221,6 +223,7 @@ async fn override_then_next_turn_uses_updated_collaboration_instructions() -> Re
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -341,6 +344,7 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()>
text: "hello 1".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -367,6 +371,7 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()>
text: "hello 2".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -422,6 +427,7 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> {
text: "hello 1".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -448,6 +454,7 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> {
text: "hello 2".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -505,6 +512,7 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang
text: "hello 1".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -534,6 +542,7 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang
text: "hello 2".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -592,6 +601,7 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged()
text: "hello 1".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -621,6 +631,7 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged()
text: "hello 2".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -683,6 +694,7 @@ async fn resume_replays_collaboration_instructions() -> Result<()> {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -696,6 +708,7 @@ async fn resume_replays_collaboration_instructions() -> Result<()> {
text: "after resume".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -751,6 +764,7 @@ async fn empty_collaboration_instructions_are_ignored() -> Result<()> {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
+272
View File
@@ -17,6 +17,7 @@ use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::RolloutLine;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::WarningEvent;
use codex_protocol::user_input::EphemeralContext;
use codex_protocol::user_input::UserInput;
use core_test_support::context_snapshot;
use core_test_support::context_snapshot::ContextSnapshotOptions;
@@ -81,6 +82,13 @@ fn set_test_compact_prompt(config: &mut Config) {
config.compact_prompt = Some(SUMMARIZATION_PROMPT.to_string());
}
fn editor_context(path: &str) -> Vec<EphemeralContext> {
vec![EphemeralContext {
title: "Context from my editor".to_string(),
text: format!("## Active file: {path}"),
}]
}
fn body_contains_text(body: &str, text: &str) -> bool {
body.contains(&json_fragment(text))
}
@@ -239,6 +247,7 @@ async fn summarize_context_three_requests_and_instructions() {
text: "hello world".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -261,6 +270,7 @@ async fn summarize_context_three_requests_and_instructions() {
text: THIRD_USER_MSG.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -437,6 +447,7 @@ async fn manual_compact_uses_custom_prompt() {
text: "USER_ONE".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -581,6 +592,7 @@ async fn manual_compact_emits_context_compaction_items() {
text: "manual compact".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -744,6 +756,7 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() {
text: user_message.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1248,6 +1261,7 @@ async fn auto_compact_runs_after_token_limit_hit() {
text: FIRST_AUTO_MSG.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1261,6 +1275,7 @@ async fn auto_compact_runs_after_token_limit_hit() {
text: SECOND_AUTO_MSG.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1274,6 +1289,7 @@ async fn auto_compact_runs_after_token_limit_hit() {
text: POST_AUTO_USER_MSG.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1442,6 +1458,7 @@ async fn auto_compact_emits_context_compaction_items() {
text: user.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1520,6 +1537,7 @@ async fn auto_compact_starts_after_turn_started() {
text: FIRST_AUTO_MSG.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1532,6 +1550,7 @@ async fn auto_compact_starts_after_turn_started() {
text: SECOND_AUTO_MSG.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1544,6 +1563,7 @@ async fn auto_compact_starts_after_turn_started() {
text: POST_AUTO_USER_MSG.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -2031,6 +2051,7 @@ async fn auto_compact_persists_rollout_entries() {
text: FIRST_AUTO_MSG.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -2043,6 +2064,7 @@ async fn auto_compact_persists_rollout_entries() {
text: SECOND_AUTO_MSG.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -2055,6 +2077,7 @@ async fn auto_compact_persists_rollout_entries() {
text: POST_AUTO_USER_MSG.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -2141,6 +2164,7 @@ async fn manual_compact_retries_after_context_window_error() {
text: "first turn".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -2252,6 +2276,7 @@ async fn manual_compact_non_context_failure_retries_then_emits_task_error() {
text: "first turn".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -2344,6 +2369,7 @@ async fn manual_compact_twice_preserves_latest_user_messages() {
text: first_user_message.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -2359,6 +2385,7 @@ async fn manual_compact_twice_preserves_latest_user_messages() {
text: second_user_message.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -2374,6 +2401,7 @@ async fn manual_compact_twice_preserves_latest_user_messages() {
text: final_user_message.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -2535,6 +2563,7 @@ async fn auto_compact_allows_multiple_attempts_when_interleaved_with_other_turn_
text: user.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -2637,6 +2666,7 @@ async fn snapshot_request_shape_mid_turn_continuation_compaction() {
text: FUNCTION_CALL_LIMIT_MSG.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -2696,6 +2726,125 @@ async fn snapshot_request_shape_mid_turn_continuation_compaction() {
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn snapshot_request_shape_mid_turn_compaction_replaces_ephemeral_context() {
skip_if_no_network!();
let server = start_mock_server().await;
let context_window = 100;
let limit = context_window * 90 / 100;
let over_limit_tokens = context_window * 95 / 100 + 1;
let request_log = mount_sse_sequence(
&server,
vec![
sse(vec![
ev_assistant_message("m1", FIRST_REPLY),
ev_completed_with_tokens("r1", 60),
]),
sse(vec![
ev_function_call(DUMMY_CALL_ID, DUMMY_FUNCTION_NAME, "{}"),
ev_completed_with_tokens("r2", over_limit_tokens),
]),
sse(vec![
ev_assistant_message("m3", &auto_summary(AUTO_SUMMARY_TEXT)),
ev_completed_with_tokens("r3", 10),
]),
sse(vec![
ev_assistant_message("m4", FINAL_REPLY),
ev_completed_with_tokens("r4", 10),
]),
],
)
.await;
let model_provider = non_openai_model_provider(&server);
let codex = test_codex()
.with_config(move |config| {
config.model_provider = model_provider;
set_test_compact_prompt(config);
config.model_context_window = Some(context_window);
config.model_auto_compact_token_limit = Some(limit);
})
.build(&server)
.await
.expect("build codex")
.codex;
codex
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "SETUP_USER".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: editor_context("src/old.rs"),
final_output_json_schema: None,
})
.await
.expect("submit setup user input");
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
codex
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: FUNCTION_CALL_LIMIT_MSG.to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: editor_context("src/new.rs"),
final_output_json_schema: None,
})
.await
.expect("submit active user input");
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
let requests = request_log.requests();
assert_eq!(
requests.len(),
4,
"expected setup turn, active turn, compact, and continuation requests"
);
insta::assert_snapshot!(
"mid_turn_compaction_replaces_ephemeral_context_shapes",
format_labeled_requests_snapshot(
"Mid-turn continuation compaction keeps both prior and active ephemeral_context in the compact request, then reinjects only the active turn ephemeral_context into the continuation request.",
&[
("Local Compaction Request", &requests[2]),
("Local Post-Compaction History Layout", &requests[3]),
]
)
);
let compact_user_texts = requests[2].message_input_texts("user");
assert!(
compact_user_texts
.iter()
.any(|text| text.contains("src/old.rs")),
"expected compact request to include earlier turn ephemeral context from history"
);
assert!(
compact_user_texts
.iter()
.any(|text| text.contains("src/new.rs")),
"expected compact request to include the active turn ephemeral context"
);
let follow_up_user_texts = requests[3].message_input_texts("user");
assert!(
follow_up_user_texts
.iter()
.any(|text| text.contains("src/new.rs")),
"expected continuation request to reinject the active turn ephemeral context"
);
assert!(
!follow_up_user_texts
.iter()
.any(|text| text.contains("src/old.rs")),
"did not expect continuation request to retain the compacted-away earlier ephemeral context"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn auto_compact_clamps_config_limit_to_context_window() {
skip_if_no_network!();
@@ -2831,6 +2980,7 @@ async fn auto_compact_counts_encrypted_reasoning_before_last_user() {
text: user.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -2948,6 +3098,7 @@ async fn auto_compact_runs_when_reasoning_header_clears_between_turns() {
text: user.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -3007,6 +3158,7 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess
text: user.to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -3041,6 +3193,7 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess
text_elements: Vec::new(),
},
],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -3081,6 +3234,122 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn snapshot_request_shape_pre_turn_compaction_replaces_ephemeral_context() {
skip_if_no_network!();
let server = start_mock_server().await;
let request_log = mount_sse_sequence(
&server,
vec![
sse(vec![
ev_assistant_message("m1", FIRST_REPLY),
ev_completed_with_tokens("r1", 60),
]),
sse(vec![
ev_assistant_message("m2", "SECOND_REPLY"),
ev_completed_with_tokens("r2", 500),
]),
sse(vec![
ev_assistant_message("m3", "PRE_TURN_SUMMARY"),
ev_completed_with_tokens("r3", 100),
]),
sse(vec![
ev_assistant_message("m4", FINAL_REPLY),
ev_completed_with_tokens("r4", 80),
]),
],
)
.await;
let model_provider = non_openai_model_provider(&server);
let codex = test_codex()
.with_config(move |config| {
config.model_provider = model_provider;
set_test_compact_prompt(config);
config.model_auto_compact_token_limit = Some(200);
})
.build(&server)
.await
.expect("build codex")
.codex;
codex
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "USER_ONE".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: editor_context("src/old.rs"),
final_output_json_schema: None,
})
.await
.expect("submit first user input");
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
codex
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "USER_TWO".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
.expect("submit second user input");
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
codex
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "USER_THREE".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: editor_context("src/new.rs"),
final_output_json_schema: None,
})
.await
.expect("submit third user input");
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
let requests = request_log.requests();
assert_eq!(requests.len(), 4, "expected user, user, compact, follow-up");
insta::assert_snapshot!(
"pre_turn_compaction_replaces_ephemeral_context_shapes",
format_labeled_requests_snapshot(
"Pre-turn auto-compaction keeps prior turn ephemeral_context in the compact request, but the follow-up request carries only the fresh turn ephemeral_context.",
&[
("Local Compaction Request", &requests[2]),
("Local Post-Compaction History Layout", &requests[3]),
]
)
);
let compact_user_texts = requests[2].message_input_texts("user");
assert!(
compact_user_texts
.iter()
.any(|text| text.contains("src/old.rs")),
"expected compact request to include the previous turn ephemeral context"
);
let follow_up_user_texts = requests[3].message_input_texts("user");
assert!(
follow_up_user_texts
.iter()
.any(|text| text.contains("src/new.rs")),
"expected follow-up request to include the incoming turn ephemeral context"
);
assert!(
!follow_up_user_texts
.iter()
.any(|text| text.contains("src/old.rs")),
"did not expect follow-up request to retain the compacted-away ephemeral context"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
// TODO(ccunningham): Update once pre-turn compaction context-overflow handling includes incoming
// user input and emits richer oversized-input messaging.
@@ -3251,6 +3520,7 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() {
text: "USER_ONE".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -3263,6 +3533,7 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() {
text: "USER_TWO".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -3333,6 +3604,7 @@ async fn snapshot_request_shape_manual_compact_without_previous_user_messages()
text: "AFTER_MANUAL_EMPTY_COMPACT".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
+165
View File
@@ -19,6 +19,7 @@ use codex_protocol::protocol::RealtimeConversationRealtimeEvent;
use codex_protocol::protocol::RealtimeEvent;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::RolloutLine;
use codex_protocol::user_input::EphemeralContext;
use codex_protocol::user_input::UserInput;
use core_test_support::context_snapshot;
use core_test_support::context_snapshot::ContextSnapshotOptions;
@@ -59,6 +60,13 @@ fn summary_with_prefix(summary: &str) -> String {
format!("{SUMMARY_PREFIX}\n{summary}")
}
fn editor_context(path: &str) -> Vec<EphemeralContext> {
vec![EphemeralContext {
title: "Context from my editor".to_string(),
text: format!("## Active file: {path}"),
}]
}
fn context_snapshot_options() -> ContextSnapshotOptions {
ContextSnapshotOptions::default()
.render_mode(ContextSnapshotRenderMode::KindWithTextPrefix { max_chars: 64 })
@@ -233,6 +241,7 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> {
text: "hello remote compact".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -247,6 +256,7 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> {
text: "after compact".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -377,6 +387,7 @@ async fn remote_compact_runs_automatically() -> Result<()> {
text: "hello remote compact".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -451,6 +462,7 @@ async fn remote_compact_trims_function_call_history_to_fit_context_window() -> R
text: first_user_message.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -462,6 +474,7 @@ async fn remote_compact_trims_function_call_history_to_fit_context_window() -> R
text: second_user_message.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -571,6 +584,7 @@ async fn auto_remote_compact_trims_function_call_history_to_fit_context_window()
text: first_user_message.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -582,6 +596,7 @@ async fn auto_remote_compact_trims_function_call_history_to_fit_context_window()
text: second_user_message.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -599,6 +614,7 @@ async fn auto_remote_compact_trims_function_call_history_to_fit_context_window()
text: "turn that triggers auto compact".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -696,6 +712,7 @@ async fn auto_remote_compact_failure_stops_agent_loop() -> Result<()> {
text: "turn that exceeds token threshold".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -707,6 +724,7 @@ async fn auto_remote_compact_failure_stops_agent_loop() -> Result<()> {
text: "turn that triggers auto compact".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -798,6 +816,7 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result
text: first_user_message.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -812,6 +831,7 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result
text: second_user_message.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -897,6 +917,7 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result
text: first_user_message.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -911,6 +932,7 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result
text: second_user_message.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -979,6 +1001,7 @@ async fn remote_manual_compact_emits_context_compaction_items() -> Result<()> {
text: "manual remote compact".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -1057,6 +1080,7 @@ async fn remote_manual_compact_failure_emits_task_error_event() -> Result<()> {
text: "manual remote compact".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -1139,6 +1163,7 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()>
text: "needs compaction".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -1280,6 +1305,7 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res
text: "start remote compact flow".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -1295,6 +1321,7 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res
text: "after compact in same session".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -1317,6 +1344,7 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res
text: "after resume".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -1411,6 +1439,7 @@ async fn remote_compact_refreshes_stale_developer_instructions_without_resume()
text: "start remote compact flow".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -1425,6 +1454,7 @@ async fn remote_compact_refreshes_stale_developer_instructions_without_resume()
text: "after compact in same session".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -1494,6 +1524,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_sta
text: "USER_ONE".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -1505,6 +1536,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_sta
text: "USER_TWO".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -1569,6 +1601,7 @@ async fn remote_request_uses_custom_experimental_realtime_start_instructions() -
text: "USER_ONE".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -1627,6 +1660,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_end
text: "USER_ONE".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -1640,6 +1674,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_end
text: "USER_TWO".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -1712,6 +1747,7 @@ async fn snapshot_request_shape_remote_manual_compact_restates_realtime_start()
text: "USER_ONE".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -1726,6 +1762,7 @@ async fn snapshot_request_shape_remote_manual_compact_restates_realtime_start()
text: "USER_TWO".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -1806,6 +1843,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_does_not_restate_real
text: "SETUP_USER".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -1819,6 +1857,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_does_not_restate_real
text: "USER_TWO".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -1907,6 +1946,7 @@ async fn snapshot_request_shape_remote_compact_resume_restates_realtime_end() ->
text: "USER_ONE".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -1934,6 +1974,7 @@ async fn snapshot_request_shape_remote_compact_resume_restates_realtime_end() ->
text: "USER_TWO".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -2026,6 +2067,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_including_incoming_us
text: user.to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -2110,6 +2152,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model
text: "BEFORE_SWITCH_USER".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -2135,6 +2178,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model
text: "AFTER_SWITCH_USER".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -2251,6 +2295,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_context_window_exceed
text: "USER_ONE".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -2262,6 +2307,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_context_window_exceed
text: "USER_TWO".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -2344,6 +2390,7 @@ async fn snapshot_request_shape_remote_mid_turn_continuation_compaction() -> Res
text: "USER_ONE".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -2372,6 +2419,120 @@ async fn snapshot_request_shape_remote_mid_turn_continuation_compaction() -> Res
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn snapshot_request_shape_remote_mid_turn_compaction_replaces_ephemeral_context() -> Result<()>
{
skip_if_no_network!(Ok(()));
let harness = TestCodexHarness::with_builder(
test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_config(|config| {
config.model_auto_compact_token_limit = Some(200);
}),
)
.await?;
let codex = harness.test().codex.clone();
let responses_mock = responses::mount_sse_sequence(
harness.server(),
vec![
responses::sse(vec![
responses::ev_assistant_message("m1", "REMOTE_SETUP_REPLY"),
responses::ev_completed_with_tokens("r1", 60),
]),
responses::sse(vec![
responses::ev_function_call("call-remote-mid-turn", DUMMY_FUNCTION_NAME, "{}"),
responses::ev_completed_with_tokens("r2", 500),
]),
responses::sse(vec![
responses::ev_assistant_message("m3", "REMOTE_MID_TURN_FINAL_REPLY"),
responses::ev_completed_with_tokens("r3", 80),
]),
],
)
.await;
let compact_mock = responses::mount_compact_user_history_with_summary_once(
harness.server(),
&summary_with_prefix("REMOTE_MID_TURN_SUMMARY"),
)
.await;
codex
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "SETUP_USER".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: editor_context("src/old.rs"),
final_output_json_schema: None,
})
.await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
codex
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "USER_TWO".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: editor_context("src/new.rs"),
final_output_json_schema: None,
})
.await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
assert_eq!(compact_mock.requests().len(), 1);
let requests = responses_mock.requests();
assert_eq!(
requests.len(),
3,
"expected setup turn, active turn, and continuation requests"
);
let compact_request = compact_mock.single_request();
let post_compact_request = &requests[2];
insta::assert_snapshot!(
"remote_mid_turn_compaction_replaces_ephemeral_context_shapes",
format_labeled_requests_snapshot(
"Remote mid-turn continuation compaction keeps both prior and active ephemeral_context in the compact request, then reinjects only the active turn ephemeral_context into the continuation request.",
&[
("Remote Compaction Request", &compact_request),
(
"Remote Post-Compaction History Layout",
post_compact_request
),
]
)
);
assert!(
compact_request.body_contains_text("src/old.rs"),
"expected remote compact request to include earlier turn ephemeral context from history"
);
assert!(
compact_request.body_contains_text("src/new.rs"),
"expected remote compact request to include the active turn ephemeral context"
);
let follow_up_user_texts = post_compact_request.message_input_texts("user");
assert!(
follow_up_user_texts
.iter()
.any(|text| text.contains("src/new.rs")),
"expected remote continuation request to reinject the active turn ephemeral context"
);
assert!(
!follow_up_user_texts
.iter()
.any(|text| text.contains("src/old.rs")),
"did not expect remote continuation request to retain the compacted-away earlier ephemeral context"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn snapshot_request_shape_remote_mid_turn_compaction_summary_only_reinjects_context()
-> Result<()> {
@@ -2419,6 +2580,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_summary_only_reinject
text: "USER_ONE".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -2502,6 +2664,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_multi_summary_reinjec
text: "USER_ONE".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -2516,6 +2679,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_multi_summary_reinjec
text: "USER_TWO".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -2595,6 +2759,7 @@ async fn snapshot_request_shape_remote_manual_compact_without_previous_user_mess
text: "USER_ONE".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -649,6 +649,7 @@ async fn user_turn(conversation: &Arc<CodexThread>, text: &str) {
text: text.into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
+1
View File
@@ -51,6 +51,7 @@ async fn fork_thread_twice_drops_to_first_message() {
text: text.to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
+9
View File
@@ -59,6 +59,7 @@ async fn user_message_item_is_emitted() -> anyhow::Result<()> {
codex
.submit(Op::UserInput {
items: vec![expected_input.clone()],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -115,6 +116,7 @@ async fn assistant_message_item_is_emitted() -> anyhow::Result<()> {
text: "please summarize results".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -173,6 +175,7 @@ async fn reasoning_item_is_emitted() -> anyhow::Result<()> {
text: "explain your reasoning".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -232,6 +235,7 @@ async fn web_search_item_is_emitted() -> anyhow::Result<()> {
text: "find the weather".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -287,6 +291,7 @@ async fn image_generation_call_event_is_emitted() -> anyhow::Result<()> {
text: "generate a tiny blue square".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -340,6 +345,7 @@ async fn image_generation_call_event_is_emitted_when_image_save_fails() -> anyho
text: "generate an image".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -393,6 +399,7 @@ async fn agent_message_content_delta_has_item_metadata() -> anyhow::Result<()> {
text: "please stream text".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -1035,6 +1042,7 @@ async fn reasoning_content_delta_has_item_metadata() -> anyhow::Result<()> {
text: "reason through it".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -1094,6 +1102,7 @@ async fn reasoning_raw_content_delta_respects_flag() -> anyhow::Result<()> {
text: "show raw reasoning".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -10,6 +10,7 @@ use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::user_input::EphemeralContext;
use codex_protocol::user_input::UserInput;
use core_test_support::context_snapshot;
use core_test_support::context_snapshot::ContextSnapshotOptions;
@@ -149,6 +150,85 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn snapshot_model_visible_layout_ephemeral_context() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let responses = mount_sse_sequence(
&server,
vec![
sse(vec![
ev_response_created("resp-1"),
ev_assistant_message("msg-1", "turn one complete"),
ev_completed("resp-1"),
]),
sse(vec![
ev_response_created("resp-2"),
ev_assistant_message("msg-2", "turn two complete"),
ev_completed("resp-2"),
]),
],
)
.await;
let test = test_codex()
.with_model("gpt-5.2-codex")
.build(&server)
.await?;
test.codex
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "first turn with editor context".into(),
text_elements: Vec::new(),
}],
ephemeral_context: vec![EphemeralContext {
title: "Context from my editor".to_string(),
text: "## Active file: src/one.rs".to_string(),
}],
final_output_json_schema: None,
})
.await?;
wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_))
})
.await;
test.codex
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "second turn with refreshed editor context".into(),
text_elements: Vec::new(),
}],
ephemeral_context: vec![EphemeralContext {
title: "Context from my editor".to_string(),
text: "## Active file: src/two.rs".to_string(),
}],
final_output_json_schema: None,
})
.await?;
wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_))
})
.await;
let requests = responses.requests();
assert_eq!(requests.len(), 2, "expected two requests");
insta::assert_snapshot!(
"model_visible_layout_ephemeral_context",
format_labeled_requests_snapshot(
"Turns resend fresh ephemeral editor context while keeping it outside durable turn-state diffs.",
&[
("First Request (With Editor Context)", &requests[0]),
("Second Request (Refreshed Editor Context)", &requests[1]),
]
)
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn snapshot_model_visible_layout_cwd_change_refreshes_agents() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -290,6 +370,7 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul
text: "seed resume history".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -388,6 +469,7 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() -
text: "seed resume history".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -432,6 +514,7 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() -
text: "first resumed turn after model override".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
+22
View File
@@ -106,6 +106,7 @@ async fn responses_api_emits_api_request_event() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -149,6 +150,7 @@ async fn process_sse_emits_tracing_for_output_item() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -192,6 +194,7 @@ async fn process_sse_emits_failed_event_on_parse_error() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -236,6 +239,7 @@ async fn process_sse_records_failed_event_when_stream_closes_without_completed()
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -300,6 +304,7 @@ async fn process_sse_failed_event_records_response_error_message() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -362,6 +367,7 @@ async fn process_sse_failed_event_logs_parse_error() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -411,6 +417,7 @@ async fn process_sse_failed_event_logs_missing_error() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -469,6 +476,7 @@ async fn process_sse_failed_event_logs_response_completed_parse_error() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -521,6 +529,7 @@ async fn process_sse_emits_completed_telemetry() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -593,6 +602,7 @@ async fn handle_responses_span_records_response_kind_and_tool_name() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -677,6 +687,7 @@ async fn record_responses_sets_span_fields_for_response_events() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -761,6 +772,7 @@ async fn handle_response_item_records_tool_result_for_custom_tool_call() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -834,6 +846,7 @@ async fn handle_response_item_records_tool_result_for_function_call() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -917,6 +930,7 @@ async fn handle_response_item_records_tool_result_for_local_shell_missing_ids()
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -984,6 +998,7 @@ async fn handle_response_item_records_tool_result_for_local_shell_call() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1092,6 +1107,7 @@ async fn handle_container_exec_autoapprove_from_config_records_tool_decision() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1143,6 +1159,7 @@ async fn handle_container_exec_user_approved_records_tool_decision() {
text: "approved".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1209,6 +1226,7 @@ async fn handle_container_exec_user_approved_for_session_records_tool_decision()
text: "persist".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1275,6 +1293,7 @@ async fn handle_sandbox_error_user_approves_retry_records_tool_decision() {
text: "retry".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1341,6 +1360,7 @@ async fn handle_container_exec_user_denies_records_tool_decision() {
text: "deny".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1407,6 +1427,7 @@ async fn handle_sandbox_error_user_approves_for_session_records_tool_decision()
text: "persist".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1474,6 +1495,7 @@ async fn handle_sandbox_error_user_denies_records_tool_decision() {
text: "deny".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -103,6 +103,7 @@ async fn injected_user_input_triggers_follow_up_request_with_deltas() {
text: "first prompt".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -119,6 +120,7 @@ async fn injected_user_input_triggers_follow_up_request_with_deltas() {
text: "second prompt".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -66,6 +66,7 @@ async fn permissions_message_sent_once_on_start() -> Result<()> {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -191,6 +192,7 @@ async fn permissions_message_added_on_override_change() -> Result<()> {
text: "hello 1".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -217,6 +219,7 @@ async fn permissions_message_added_on_override_change() -> Result<()> {
text: "hello 2".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -264,6 +267,7 @@ async fn permissions_message_not_added_when_no_change() -> Result<()> {
text: "hello 1".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -275,6 +279,7 @@ async fn permissions_message_not_added_when_no_change() -> Result<()> {
text: "hello 2".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -333,6 +338,7 @@ async fn resume_replays_permissions_messages() -> Result<()> {
text: "hello 1".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -361,6 +367,7 @@ async fn resume_replays_permissions_messages() -> Result<()> {
text: "hello 2".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -374,6 +381,7 @@ async fn resume_replays_permissions_messages() -> Result<()> {
text: "after resume".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -433,6 +441,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
text: "hello 1".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -461,6 +470,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
text: "hello 2".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -482,6 +492,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
text: "after resume".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -510,6 +521,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
text: "after fork".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -564,6 +576,7 @@ async fn permissions_message_includes_writable_roots() -> Result<()> {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
+2
View File
@@ -210,6 +210,7 @@ async fn plugin_instructions_are_split_from_agents_instructions() -> Result<()>
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -313,6 +314,7 @@ async fn explicit_plugin_mentions_inject_plugin_guidance() -> Result<()> {
name: "sample".into(),
path: format!("plugin://{SAMPLE_PLUGIN_CONFIG_NAME}"),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -160,6 +160,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
text: "hello 1".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -171,6 +172,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
text: "hello 2".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -251,6 +253,7 @@ async fn gpt_5_tools_without_apply_patch_append_apply_patch_instructions() -> an
text: "hello 1".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -262,6 +265,7 @@ async fn gpt_5_tools_without_apply_patch_append_apply_patch_instructions() -> an
text: "hello 2".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -324,6 +328,7 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests
text: "hello 1".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -335,6 +340,7 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests
text: "hello 2".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -416,6 +422,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an
text: "hello 1".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -451,6 +458,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an
text: "hello 2".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -532,6 +540,7 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul
text: "first message".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -683,6 +692,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
text: "hello 1".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -45,6 +45,7 @@ async fn quota_exceeded_emits_single_error_event() -> Result<()> {
text: "quota?".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -1938,6 +1938,7 @@ async fn inbound_handoff_request_steers_active_turn() -> Result<()> {
text: "first prompt".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -44,6 +44,7 @@ async fn request_body_is_zstd_compressed_for_codex_backend_when_enabled() -> any
text: "compress me".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -91,6 +92,7 @@ async fn request_body_is_not_compressed_for_api_key_auth_even_when_enabled() ->
text: "do not compress".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
+7
View File
@@ -90,6 +90,7 @@ async fn resume_includes_initial_messages_from_rollout_events() -> Result<()> {
text: "Record some messages".into(),
text_elements: text_elements.clone(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -175,6 +176,7 @@ async fn resume_includes_initial_messages_from_reasoning_events() -> Result<()>
text: "Record reasoning messages".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -264,6 +266,7 @@ async fn resume_switches_models_preserves_base_instructions() -> Result<()> {
text: "Record initial instructions".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -304,6 +307,7 @@ async fn resume_switches_models_preserves_base_instructions() -> Result<()> {
text: "Resume with different model".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -319,6 +323,7 @@ async fn resume_switches_models_preserves_base_instructions() -> Result<()> {
text: "Second turn after resume".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -389,6 +394,7 @@ async fn resume_model_switch_is_not_duplicated_after_pre_turn_override() -> Resu
text: "Record initial instructions".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -431,6 +437,7 @@ async fn resume_model_switch_is_not_duplicated_after_pre_turn_override() -> Resu
text: "first turn after override".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
+1
View File
@@ -716,6 +716,7 @@ async fn review_history_surfaces_in_parent_session() {
text: followup.clone(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
+1
View File
@@ -381,6 +381,7 @@ async fn tool_search_returns_deferred_tools_without_follow_up_tool_injection() -
text: "Find the calendar create tool".to_string(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;
@@ -0,0 +1,29 @@
---
source: core/tests/suite/compact.rs
expression: "format_labeled_requests_snapshot(\"Mid-turn continuation compaction keeps both prior and active ephemeral_context in the compact request, then reinjects only the active turn ephemeral_context into the continuation request.\",\n&[(\"Local Compaction Request\", &requests[2]),\n(\"Local Post-Compaction History Layout\", &requests[3]),])"
---
Scenario: Mid-turn continuation compaction keeps both prior and active ephemeral_context in the compact request, then reinjects only the active turn ephemeral_context into the continuation request.
## Local Compaction Request
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
01:message/user[2]:
[01] <AGENTS_MD>
[02] <ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:<additional_context_for_this_turn>\n <title>Context from my edi...
03:message/user:SETUP_USER
04:message/assistant:FIRST_REPLY
05:message/user:<additional_context_for_this_turn>\n <title>Context from my edi...
06:message/user:function call limit push
07:function_call/test_tool
08:function_call_output:unsupported call: test_tool
09:message/user:<SUMMARIZATION_PROMPT>
## Local Post-Compaction History Layout
00:message/user:SETUP_USER
01:message/developer:<PERMISSIONS_INSTRUCTIONS>
02:message/user[3]:
[01] <AGENTS_MD>
[02] <ENVIRONMENT_CONTEXT:cwd=<CWD>>
[03] <additional_context_for_this_turn>\n <title>Context from my edi...
03:message/user:function call limit push
04:message/user:<COMPACTION_SUMMARY>\nAUTO_SUMMARY
@@ -0,0 +1,28 @@
---
source: core/tests/suite/compact.rs
expression: "format_labeled_requests_snapshot(\"Pre-turn auto-compaction keeps prior turn ephemeral_context in the compact request, but the follow-up request carries only the fresh turn ephemeral_context.\",\n&[(\"Local Compaction Request\", &requests[2]),\n(\"Local Post-Compaction History Layout\", &requests[3]),])"
---
Scenario: Pre-turn auto-compaction keeps prior turn ephemeral_context in the compact request, but the follow-up request carries only the fresh turn ephemeral_context.
## Local Compaction Request
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
01:message/user[2]:
[01] <AGENTS_MD>
[02] <ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:<additional_context_for_this_turn>\n <title>Context from my edi...
03:message/user:USER_ONE
04:message/assistant:FIRST_REPLY
05:message/user:USER_TWO
06:message/assistant:SECOND_REPLY
07:message/user:<SUMMARIZATION_PROMPT>
## Local Post-Compaction History Layout
00:message/user:USER_ONE
01:message/user:USER_TWO
02:message/user:<COMPACTION_SUMMARY>\nPRE_TURN_SUMMARY
03:message/developer:<PERMISSIONS_INSTRUCTIONS>
04:message/user[3]:
[01] <AGENTS_MD>
[02] <ENVIRONMENT_CONTEXT:cwd=<CWD>>
[03] <additional_context_for_this_turn>\n <title>Context from my edi...
05:message/user:USER_THREE
@@ -0,0 +1,28 @@
---
source: core/tests/suite/compact_remote.rs
expression: "format_labeled_requests_snapshot(\"Remote mid-turn continuation compaction keeps both prior and active ephemeral_context in the compact request, then reinjects only the active turn ephemeral_context into the continuation request.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", post_compact_request),])"
---
Scenario: Remote mid-turn continuation compaction keeps both prior and active ephemeral_context in the compact request, then reinjects only the active turn ephemeral_context into the continuation request.
## Remote Compaction Request
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
01:message/user[2]:
[01] <AGENTS_MD>
[02] <ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:<additional_context_for_this_turn>\n <title>Context from my edi...
03:message/user:SETUP_USER
04:message/assistant:REMOTE_SETUP_REPLY
05:message/user:<additional_context_for_this_turn>\n <title>Context from my edi...
06:message/user:USER_TWO
07:function_call/test_tool
08:function_call_output:unsupported call: test_tool
## Remote Post-Compaction History Layout
00:message/user:SETUP_USER
01:message/developer:<PERMISSIONS_INSTRUCTIONS>
02:message/user[3]:
[01] <AGENTS_MD>
[02] <ENVIRONMENT_CONTEXT:cwd=<CWD>>
[03] <additional_context_for_this_turn>\n <title>Context from my edi...
03:message/user:USER_TWO
04:compaction:encrypted=true
@@ -0,0 +1,24 @@
---
source: core/tests/suite/model_visible_layout.rs
expression: "format_labeled_requests_snapshot(\"Turns resend fresh ephemeral editor context while keeping it outside durable turn-state diffs.\",\n&[(\"First Request (With Editor Context)\", &requests[0]),\n(\"Second Request (Refreshed Editor Context)\", &requests[1]),])"
---
Scenario: Turns resend fresh ephemeral editor context while keeping it outside durable turn-state diffs.
## First Request (With Editor Context)
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
01:message/user[2]:
[01] <AGENTS_MD>
[02] <ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:<additional_context_for_this_turn>\n <title>Context from my editor</title>\n <content>\n## Act...
03:message/user:first turn with editor context
## Second Request (Refreshed Editor Context)
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
01:message/user[2]:
[01] <AGENTS_MD>
[02] <ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:<additional_context_for_this_turn>\n <title>Context from my editor</title>\n <content>\n## Act...
03:message/user:first turn with editor context
04:message/assistant:turn one complete
05:message/user:<additional_context_for_this_turn>\n <title>Context from my editor</title>\n <content>\n## Act...
06:message/user:second turn with refreshed editor context
@@ -95,6 +95,7 @@ async fn continue_after_stream_error() {
text: "first message".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -114,6 +115,7 @@ async fn continue_after_stream_error() {
text: "follow up".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -79,6 +79,7 @@ async fn retries_on_early_close() {
text: "hello".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await
@@ -61,6 +61,7 @@ mv "${tmp_path}" "${payload_path}""#,
text: "hello world".into(),
text_elements: Vec::new(),
}],
ephemeral_context: Vec::new(),
final_output_json_schema: None,
})
.await?;