mirror of
https://github.com/openai/codex.git
synced 2026-11-09 00:39:32 +00:00
Use the originating model when recording conversation history (#44243)
## Why History recording used the model captured at turn start, so switching models within a turn could apply stale image preparation and tool-output truncation rules. ## What changed Pass explicit model metadata through conversation recording. Record model responses and tool results using their originating step's settings, and capture the current model for pending input recorded during startup interruption or task completion. ## Testing Add regression coverage for tool results across a model switch, explicit model selection for plain and annotated history, and image input retained during interruption and completion. Extend model-switching tests to verify that prepared media and item identity survive switching back to a multimodal model in live, resumed, and forked threads. GitOrigin-RevId: 5bc750c30c334acb9b802cb80fdea01daffa84a1
This commit is contained in:
1 parent
205f3671e1
commit
b64de2f3ad
28 files changed
+721
-123
No files matched your search
@@ -5,6 +5,7 @@ use codex_features::Feature;
|
||||
use codex_history::CodexHarnessMetadata;
|
||||
use codex_history::ResponseItemEnvelope;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
|
||||
impl Session {
|
||||
/// Returns the input if there is no active turn to inject into.
|
||||
@@ -92,7 +93,7 @@ impl Session {
|
||||
return;
|
||||
}
|
||||
drop(active);
|
||||
self.record_annotated_conversation_items(turn_context, items)
|
||||
self.record_annotated_conversation_items(turn_context, turn_context.model_info(), items)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -110,6 +111,7 @@ impl Session {
|
||||
pub(crate) async fn record_annotated_conversation_items(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
model_info: &ModelInfo,
|
||||
items: Vec<ResponseItemEnvelope>,
|
||||
) {
|
||||
if items.iter().all(|item| item.metadata.is_none()) {
|
||||
@@ -117,7 +119,8 @@ impl Session {
|
||||
.into_iter()
|
||||
.map(ResponseItemEnvelope::into_item)
|
||||
.collect::<Vec<_>>();
|
||||
self.record_conversation_items(turn_context, &items).await;
|
||||
self.record_conversation_items(turn_context, model_info, &items)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -126,6 +129,7 @@ impl Session {
|
||||
for envelope in items {
|
||||
let (prepared_items, prepared_images) = self.prepare_conversation_items_for_history(
|
||||
turn_context,
|
||||
model_info,
|
||||
std::slice::from_ref(&envelope.item),
|
||||
);
|
||||
image_preparations.extend(prepared_images);
|
||||
@@ -138,8 +142,13 @@ impl Session {
|
||||
}
|
||||
}));
|
||||
}
|
||||
self.record_prepared_conversation_items(turn_context, annotated_items, image_preparations)
|
||||
.await;
|
||||
self.record_prepared_conversation_items(
|
||||
turn_context,
|
||||
model_info,
|
||||
annotated_items,
|
||||
image_preparations,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Injects items into active work, or records them without starting a turn.
|
||||
@@ -159,7 +168,8 @@ impl Session {
|
||||
default_turn_context.as_ref()
|
||||
}
|
||||
};
|
||||
self.record_conversation_items(turn_context, &items).await;
|
||||
self.record_conversation_items(turn_context, turn_context.model_info(), &items)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,11 @@ async fn harness_authored_configuration_updates_preserve_metadata_and_resume() {
|
||||
}),
|
||||
};
|
||||
session
|
||||
.record_annotated_conversation_items(&turn_context, vec![expected.clone()])
|
||||
.record_annotated_conversation_items(
|
||||
&turn_context,
|
||||
turn_context.model_info(),
|
||||
vec![expected.clone()],
|
||||
)
|
||||
.await;
|
||||
|
||||
let recorded = session.clone_history().await.into_annotated_items();
|
||||
|
||||
@@ -3293,22 +3293,20 @@ impl Session {
|
||||
item.set_create_time_if_missing(Self::response_item_create_time());
|
||||
}
|
||||
|
||||
/// Records conversation items: append to history, persist to rollout, and
|
||||
/// notify clients observing raw response items.
|
||||
/// Prepares media using the originating model and preserves existing item identity.
|
||||
pub(crate) fn prepare_conversation_items_for_history<'a>(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
model_info: &ModelInfo,
|
||||
items: &'a [ResponseItem],
|
||||
) -> (Cow<'a, [ResponseItem]>, Vec<ImagePreparationMetadata>) {
|
||||
let mut items = items.to_vec();
|
||||
let image_preparation_mode = if unified_image_budget_enabled(
|
||||
&turn_context.config.features,
|
||||
turn_context.model_info(),
|
||||
) {
|
||||
ImagePreparationMode::UnifiedBudget
|
||||
} else {
|
||||
ImagePreparationMode::DetailBased
|
||||
};
|
||||
let image_preparation_mode =
|
||||
if unified_image_budget_enabled(&turn_context.config.features, model_info) {
|
||||
ImagePreparationMode::UnifiedBudget
|
||||
} else {
|
||||
ImagePreparationMode::DetailBased
|
||||
};
|
||||
let image_resize_notice_mode = if turn_context
|
||||
.config
|
||||
.features
|
||||
@@ -3402,26 +3400,35 @@ impl Session {
|
||||
item
|
||||
}
|
||||
|
||||
/// Appends to history, persists the prepared items, then notifies raw-item observers.
|
||||
/// Execution callers supply their captured model; standalone callers need no tool runtime.
|
||||
#[tracing::instrument(level = "trace", skip_all, fields(item_count = items.len()))]
|
||||
pub(crate) async fn record_conversation_items(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
model_info: &ModelInfo,
|
||||
items: &[ResponseItem],
|
||||
) {
|
||||
let (items, image_preparations) =
|
||||
self.prepare_conversation_items_for_history(turn_context, items);
|
||||
self.prepare_conversation_items_for_history(turn_context, model_info, items);
|
||||
let items = items
|
||||
.into_owned()
|
||||
.into_iter()
|
||||
.map(ResponseItemEnvelope::new)
|
||||
.collect();
|
||||
self.record_prepared_conversation_items(turn_context, items, image_preparations)
|
||||
.await;
|
||||
self.record_prepared_conversation_items(
|
||||
turn_context,
|
||||
model_info,
|
||||
items,
|
||||
image_preparations,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn record_prepared_conversation_items(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
model_info: &ModelInfo,
|
||||
items: Vec<ResponseItemEnvelope>,
|
||||
image_preparations: Vec<ImagePreparationMetadata>,
|
||||
) {
|
||||
@@ -3436,7 +3443,7 @@ impl Session {
|
||||
.note_recorded_items(&response_items);
|
||||
state
|
||||
.history
|
||||
.record_annotated_items(&items, turn_context.model_info().truncation_policy.into());
|
||||
.record_annotated_items(&items, model_info.truncation_policy.into());
|
||||
}
|
||||
for image in image_preparations {
|
||||
self.services
|
||||
@@ -3478,7 +3485,8 @@ impl Session {
|
||||
world_state.render_diff(&previous_snapshot),
|
||||
);
|
||||
if !items.is_empty() {
|
||||
self.record_conversation_items(turn_context, &items).await;
|
||||
self.record_conversation_items(turn_context, &step_context.settings.model_info, &items)
|
||||
.await;
|
||||
}
|
||||
|
||||
// ContextManager remembers this for later turns; run_turn owns the live value.
|
||||
@@ -3713,11 +3721,13 @@ impl Session {
|
||||
pub(crate) async fn record_inter_agent_communication(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
model_info: &ModelInfo,
|
||||
communication: InterAgentCommunication,
|
||||
) {
|
||||
let response_item = communication.to_model_input_item();
|
||||
let (items, _) = self.prepare_conversation_items_for_history(
|
||||
turn_context,
|
||||
model_info,
|
||||
std::slice::from_ref(&response_item),
|
||||
);
|
||||
let items = items.as_ref();
|
||||
@@ -3725,10 +3735,7 @@ impl Session {
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
state.current_time_reminder.note_recorded_items(items);
|
||||
state.record_items(
|
||||
items.iter(),
|
||||
turn_context.model_info().truncation_policy.into(),
|
||||
);
|
||||
state.record_items(items.iter(), model_info.truncation_policy.into());
|
||||
}
|
||||
self.persist_rollout_items(&[
|
||||
RolloutItem::InterAgentCommunicationMetadata {
|
||||
@@ -4390,8 +4397,12 @@ impl Session {
|
||||
return Ok(world_state);
|
||||
}
|
||||
if !context_items.is_empty() {
|
||||
self.record_conversation_items(turn_context, &context_items)
|
||||
.await;
|
||||
self.record_conversation_items(
|
||||
turn_context,
|
||||
&step_context.settings.model_info,
|
||||
&context_items,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
// Persist state only after any model-visible context generated from it.
|
||||
if let Some(world_state_item) = world_state_item {
|
||||
@@ -4590,11 +4601,16 @@ impl Session {
|
||||
pub(crate) async fn record_response_item_and_emit_turn_item(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
model_info: &ModelInfo,
|
||||
response_item: ResponseItem,
|
||||
) {
|
||||
// Add to conversation history and persist response item to rollout.
|
||||
self.record_conversation_items(turn_context, std::slice::from_ref(&response_item))
|
||||
.await;
|
||||
self.record_conversation_items(
|
||||
turn_context,
|
||||
model_info,
|
||||
std::slice::from_ref(&response_item),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Derive a turn item and emit lifecycle events if applicable.
|
||||
if let Some(item) = parse_turn_item(&response_item) {
|
||||
@@ -4606,6 +4622,7 @@ impl Session {
|
||||
pub(crate) async fn record_user_prompt_and_emit_turn_item(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
model_info: &ModelInfo,
|
||||
input: &[UserInput],
|
||||
client_id: Option<String>,
|
||||
acceptance_order: Option<u64>,
|
||||
@@ -4617,6 +4634,7 @@ impl Session {
|
||||
let response_item = self.response_item_from_user_input(input.to_vec());
|
||||
self.record_annotated_conversation_items(
|
||||
turn_context,
|
||||
model_info,
|
||||
vec![ResponseItemEnvelope {
|
||||
item: response_item,
|
||||
metadata: acceptance_order.map(|order| CodexHarnessMetadata {
|
||||
|
||||
@@ -69,6 +69,7 @@ impl Session {
|
||||
|
||||
self.record_annotated_conversation_items(
|
||||
step_context.turn.as_ref(),
|
||||
&step_context.settings.model_info,
|
||||
vec![ResponseItemEnvelope {
|
||||
item: ResponseItem::ConfigurationUpdate {
|
||||
reasoning: ConfigurationReasoning { effort },
|
||||
|
||||
@@ -17,8 +17,12 @@ pub(super) async fn maybe_record_reminder(
|
||||
let response_item = ContextualUserFragment::into(crate::context::RolloutBudgetContext {
|
||||
remaining_tokens: reminder.remaining_tokens,
|
||||
});
|
||||
sess.record_conversation_items(turn_context, std::slice::from_ref(&response_item))
|
||||
.await;
|
||||
sess.record_conversation_items(
|
||||
turn_context,
|
||||
turn_context.model_info(),
|
||||
std::slice::from_ref(&response_item),
|
||||
)
|
||||
.await;
|
||||
budget.mark_reminder_delivered(sess.thread_id(), window_id, reminder);
|
||||
}
|
||||
|
||||
|
||||
@@ -355,6 +355,7 @@ async fn default_turn_context_assigns_missing_response_item_ids() {
|
||||
|
||||
let (items, _) = session.prepare_conversation_items_for_history(
|
||||
&turn_context,
|
||||
turn_context.model_info(),
|
||||
std::slice::from_ref(&response_item),
|
||||
);
|
||||
|
||||
@@ -2275,7 +2276,11 @@ async fn record_conversation_items_stamps_missing_turn_id_and_preserves_existing
|
||||
existing_item.set_turn_id_if_missing("older-turn");
|
||||
|
||||
session
|
||||
.record_conversation_items(&turn_context, &[fresh_item.clone(), existing_item.clone()])
|
||||
.record_conversation_items(
|
||||
&turn_context,
|
||||
turn_context.model_info(),
|
||||
&[fresh_item.clone(), existing_item.clone()],
|
||||
)
|
||||
.await;
|
||||
|
||||
let history = session.clone_history().await;
|
||||
@@ -2308,7 +2313,11 @@ async fn record_response_item_and_emit_turn_item_emits_hook_prompt_lifecycle() {
|
||||
let response_item_id = response_item.id().expect("hook prompt id").to_string();
|
||||
|
||||
session
|
||||
.record_response_item_and_emit_turn_item(&turn_context, response_item)
|
||||
.record_response_item_and_emit_turn_item(
|
||||
&turn_context,
|
||||
turn_context.model_info(),
|
||||
response_item,
|
||||
)
|
||||
.await;
|
||||
|
||||
let raw_response = rx.recv().await.expect("raw response item event");
|
||||
@@ -2391,7 +2400,7 @@ async fn record_inter_agent_communication_sets_turn_id_in_rollout_and_resume() {
|
||||
expected_item.set_turn_id_if_missing(&turn_context.sub_id);
|
||||
|
||||
session
|
||||
.record_inter_agent_communication(&turn_context, communication)
|
||||
.record_inter_agent_communication(&turn_context, turn_context.model_info(), communication)
|
||||
.await;
|
||||
|
||||
let recorded_history = session.clone_history().await;
|
||||
@@ -2468,7 +2477,7 @@ async fn record_inter_agent_communication_preserves_item_id_in_rollout_and_resum
|
||||
);
|
||||
|
||||
session
|
||||
.record_inter_agent_communication(&turn_context, communication)
|
||||
.record_inter_agent_communication(&turn_context, turn_context.model_info(), communication)
|
||||
.await;
|
||||
|
||||
let live_history = session.clone_history().await;
|
||||
@@ -2523,6 +2532,58 @@ async fn record_inter_agent_communication_preserves_item_id_in_rollout_and_resum
|
||||
);
|
||||
}
|
||||
|
||||
#[test_case::test_case(false; "plain items")]
|
||||
#[test_case::test_case(true; "annotated items")]
|
||||
#[tokio::test]
|
||||
async fn annotated_history_uses_explicit_model_without_a_step(retain_metadata: bool) {
|
||||
let (session, turn_context, _rx) = make_session_and_context_with_auth_and_config_and_rx(
|
||||
CodexAuth::from_api_key("Test API Key"),
|
||||
Vec::new(),
|
||||
|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::RetainClientDeveloperMessages)
|
||||
.unwrap();
|
||||
},
|
||||
)
|
||||
.await;
|
||||
let mut model_info = turn_context.model_info().as_ref().clone();
|
||||
model_info.truncation_policy =
|
||||
codex_protocol::openai_models::TruncationPolicyConfig::tokens(/*limit*/ 4);
|
||||
let text = "diagnostic line\n".repeat(50);
|
||||
let mut item = ResponseItem::FunctionCallOutput {
|
||||
id: Some(ResponseItemId::with_suffix("fco", "existing")),
|
||||
call_id: Some("call-1".to_string()),
|
||||
name: None,
|
||||
namespace: None,
|
||||
output: FunctionCallOutputPayload {
|
||||
body: FunctionCallOutputBody::Text(text.clone()),
|
||||
success: Some(true),
|
||||
},
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
};
|
||||
item.set_turn_id_if_missing("existing-turn");
|
||||
item.set_create_time_if_missing(123.into());
|
||||
let mut expected = ResponseItemEnvelope {
|
||||
item,
|
||||
metadata: retain_metadata.then_some(CodexHarnessMetadata {
|
||||
client_authored: true,
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
session
|
||||
.record_annotated_conversation_items(&turn_context, &model_info, vec![expected.clone()])
|
||||
.await;
|
||||
let ResponseItem::FunctionCallOutput { output, .. } = &mut expected.item else {
|
||||
unreachable!("fixture is a tool output");
|
||||
};
|
||||
output.body = FunctionCallOutputBody::Text(codex_utils_output_truncation::truncate_text(
|
||||
&text,
|
||||
codex_utils_output_truncation::TruncationPolicy::Tokens(4) * 1.2,
|
||||
));
|
||||
assert_eq!(session.clone_history().await.annotated_items(), &[expected]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn prepares_image_failures_before_history_insertion() {
|
||||
let (session, turn_context, _rx) = make_session_and_context_with_auth_and_config_and_rx(
|
||||
@@ -2556,7 +2617,11 @@ async fn prepares_image_failures_before_history_insertion() {
|
||||
};
|
||||
|
||||
session
|
||||
.record_conversation_items(turn_context.as_ref(), std::slice::from_ref(&item))
|
||||
.record_conversation_items(
|
||||
turn_context.as_ref(),
|
||||
turn_context.model_info(),
|
||||
std::slice::from_ref(&item),
|
||||
)
|
||||
.await;
|
||||
|
||||
let history = session.state.lock().await.clone_history();
|
||||
@@ -2948,7 +3013,11 @@ async fn recompute_token_usage_uses_session_base_instructions() {
|
||||
|
||||
let item = user_message("hello");
|
||||
session
|
||||
.record_conversation_items(&turn_context, std::slice::from_ref(&item))
|
||||
.record_conversation_items(
|
||||
&turn_context,
|
||||
turn_context.model_info(),
|
||||
std::slice::from_ref(&item),
|
||||
)
|
||||
.await;
|
||||
|
||||
let history = session.clone_history().await;
|
||||
@@ -3956,7 +4025,7 @@ async fn thread_rollback_fails_without_persisted_thread_history() {
|
||||
let (sess, tc, rx) = make_session_and_context_with_rx().await;
|
||||
|
||||
let initial_context = build_initial_context(&sess, &tc).await;
|
||||
sess.record_conversation_items(tc.as_ref(), &initial_context)
|
||||
sess.record_conversation_items(tc.as_ref(), tc.model_info(), &initial_context)
|
||||
.await;
|
||||
let history_before_rollback = sess.clone_history().await;
|
||||
|
||||
@@ -4400,7 +4469,7 @@ async fn thread_rollback_fails_when_turn_in_progress() {
|
||||
let (sess, tc, rx) = make_session_and_context_with_rx().await;
|
||||
|
||||
let initial_context = build_initial_context(&sess, &tc).await;
|
||||
sess.record_conversation_items(tc.as_ref(), &initial_context)
|
||||
sess.record_conversation_items(tc.as_ref(), tc.model_info(), &initial_context)
|
||||
.await;
|
||||
let history_before_rollback = sess.clone_history().await;
|
||||
|
||||
@@ -4425,7 +4494,7 @@ async fn thread_rollback_fails_when_num_turns_is_zero() {
|
||||
let (sess, tc, rx) = make_session_and_context_with_rx().await;
|
||||
|
||||
let initial_context = build_initial_context(&sess, &tc).await;
|
||||
sess.record_conversation_items(tc.as_ref(), &initial_context)
|
||||
sess.record_conversation_items(tc.as_ref(), tc.model_info(), &initial_context)
|
||||
.await;
|
||||
let history_before_rollback = sess.clone_history().await;
|
||||
|
||||
@@ -10658,7 +10727,11 @@ async fn record_context_updates_and_set_reference_context_item_reinjects_full_co
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
};
|
||||
session
|
||||
.record_conversation_items(&turn_context, std::slice::from_ref(&compacted_summary))
|
||||
.record_conversation_items(
|
||||
&turn_context,
|
||||
turn_context.model_info(),
|
||||
std::slice::from_ref(&compacted_summary),
|
||||
)
|
||||
.await;
|
||||
session
|
||||
.record_context_updates_and_set_reference_context_item(&step_context)
|
||||
@@ -11414,7 +11487,11 @@ async fn legacy_compaction_retains_only_the_selected_step(first_attempt: FirstAt
|
||||
let server = responses::start_mock_server().await;
|
||||
let (session, turn, events) = make_remote_compaction_session(&server.uri()).await;
|
||||
session
|
||||
.record_conversation_items(&turn, &[user_message("before compaction")])
|
||||
.record_conversation_items(
|
||||
&turn,
|
||||
turn.model_info(),
|
||||
&[user_message("before compaction")],
|
||||
)
|
||||
.await;
|
||||
session
|
||||
.spawn_task(
|
||||
@@ -11535,7 +11612,11 @@ async fn interrupting_compaction_fallback_retains_last_known_step_context() {
|
||||
}))
|
||||
.await;
|
||||
session
|
||||
.record_conversation_items(&turn, &[user_message("before compaction")])
|
||||
.record_conversation_items(
|
||||
&turn,
|
||||
turn.model_info(),
|
||||
&[user_message("before compaction")],
|
||||
)
|
||||
.await;
|
||||
session
|
||||
.spawn_task(turn, Vec::new(), crate::tasks::RegularTask::new())
|
||||
@@ -11870,7 +11951,22 @@ async fn submit_steer_only(
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input() {
|
||||
let (sess, tc, rx) = make_session_and_context_with_rx().await;
|
||||
let (sess, mut tc, rx) = make_session_and_context_with_auth_and_config_and_rx(
|
||||
CodexAuth::from_api_key("Test API Key"),
|
||||
Vec::new(),
|
||||
|config| {
|
||||
config.features.enable(Feature::UnifiedImageBudget).unwrap();
|
||||
},
|
||||
)
|
||||
.await;
|
||||
update_turn_settings_for_test(
|
||||
Arc::get_mut(&mut tc).expect("unshared context"),
|
||||
|settings| {
|
||||
let model = Arc::make_mut(&mut settings.model_info);
|
||||
model.use_responses_lite = false;
|
||||
model.supports_image_detail_original = false;
|
||||
},
|
||||
);
|
||||
let input = vec![TurnInput::UserInput {
|
||||
acceptance_order: None,
|
||||
content: vec![UserInput::Text {
|
||||
@@ -11891,17 +11987,32 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input()
|
||||
|
||||
while rx.try_recv().is_ok() {}
|
||||
|
||||
let mut encoded = std::io::Cursor::new(Vec::new());
|
||||
image::DynamicImage::new_rgba8(/*w*/ 8, /*h*/ 8)
|
||||
.write_to(&mut encoded, image::ImageFormat::Png)
|
||||
.expect("encode image");
|
||||
let image_url = codex_utils_image::data_url_from_bytes("image/png", &encoded.into_inner());
|
||||
let text_element = codex_protocol::user_input::TextElement::new(
|
||||
codex_protocol::user_input::ByteRange { start: 5, end: 12 },
|
||||
Some("pending marker".to_string()),
|
||||
);
|
||||
let pending_user_input = vec![UserInput::Text {
|
||||
text: "late pending input".to_string(),
|
||||
text_elements: vec![text_element.clone()],
|
||||
}];
|
||||
let pending_user_input = vec![
|
||||
UserInput::Text {
|
||||
text: "late pending input".to_string(),
|
||||
text_elements: vec![text_element.clone()],
|
||||
},
|
||||
UserInput::Image {
|
||||
image_url: image_url.clone(),
|
||||
detail: Some(ImageDetail::High),
|
||||
},
|
||||
];
|
||||
let submission = submit_steer_only(&sess, pending_user_input.clone(), &tc.sub_id).await;
|
||||
assert!(matches!(submission, TurnInputSubmission::Steered { .. }));
|
||||
|
||||
let mut current = tc.initial_settings.as_ref().clone();
|
||||
Arc::make_mut(&mut current.model_info).supports_image_detail_original = true;
|
||||
tc.current_settings.store(Arc::new(current));
|
||||
|
||||
sess.on_task_finished(Arc::clone(&tc), /*task_result*/ Ok(None))
|
||||
.await;
|
||||
|
||||
@@ -11909,16 +12020,22 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input()
|
||||
let expected = ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "late pending input".to_string(),
|
||||
}],
|
||||
content: vec![
|
||||
ContentItem::InputText {
|
||||
text: "late pending input".to_string(),
|
||||
},
|
||||
ContentItem::InputImage {
|
||||
image_url: image_url.clone(),
|
||||
detail: Some(ImageDetail::Original),
|
||||
},
|
||||
],
|
||||
phase: None,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
};
|
||||
assert!(
|
||||
strip_response_item_ids(&strip_metadata_from_items(&raw_history_items(&history)))
|
||||
.contains(&expected),
|
||||
"expected pending input to be persisted into history on turn completion"
|
||||
"expected pending input to use the current model's image rules on turn completion"
|
||||
);
|
||||
|
||||
let first = tokio::time::timeout(std::time::Duration::from_secs(2), rx.recv())
|
||||
@@ -11965,7 +12082,7 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input()
|
||||
local_images,
|
||||
..
|
||||
}) if message == "late pending input"
|
||||
&& images == Some(Vec::new())
|
||||
&& images == Some(vec![image_url])
|
||||
&& text_elements == vec![text_element]
|
||||
&& local_images.is_empty()
|
||||
));
|
||||
@@ -12427,7 +12544,7 @@ async fn tool_calls_reopen_mailbox_delivery_for_current_turn() {
|
||||
};
|
||||
let mut ctx = HandleOutputCtx {
|
||||
sess: Arc::clone(&sess),
|
||||
turn_context: Arc::clone(&tc),
|
||||
step_context: StepContext::for_test(Arc::clone(&tc)),
|
||||
turn_store: Arc::new(codex_extension_api::ExtensionData::new(tc.sub_id.clone())),
|
||||
tool_runtime: test_tool_runtime(Arc::clone(&sess), Arc::clone(&tc)),
|
||||
cancellation_token: CancellationToken::new(),
|
||||
|
||||
@@ -132,8 +132,12 @@ pub(super) async fn maybe_record_current_time_reminder(
|
||||
|
||||
let response_item =
|
||||
ContextualUserFragment::into(crate::context::CurrentTimeReminder::new(current_time));
|
||||
sess.record_conversation_items(turn_context, std::slice::from_ref(&response_item))
|
||||
.await;
|
||||
sess.record_conversation_items(
|
||||
turn_context,
|
||||
turn_context.model_info(),
|
||||
std::slice::from_ref(&response_item),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -189,8 +189,12 @@ pub(super) async fn maybe_record(
|
||||
&config.reminder_message_template,
|
||||
base_window_tokens_remaining,
|
||||
));
|
||||
sess.record_conversation_items(turn_context, std::slice::from_ref(&response_item))
|
||||
.await;
|
||||
sess.record_conversation_items(
|
||||
turn_context,
|
||||
turn_context.model_info(),
|
||||
std::slice::from_ref(&response_item),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,8 +215,12 @@ pub(super) async fn maybe_record(
|
||||
|
||||
let response_item =
|
||||
ContextualUserFragment::into(crate::context::AutoCompactFallbackPrompt::new(prompt));
|
||||
sess.record_conversation_items(turn_context, std::slice::from_ref(&response_item))
|
||||
.await;
|
||||
sess.record_conversation_items(
|
||||
turn_context,
|
||||
turn_context.model_info(),
|
||||
std::slice::from_ref(&response_item),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -93,6 +93,7 @@ use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::MessagePhase;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ModelMessages;
|
||||
use codex_protocol::protocol::AgentMessageContentDeltaEvent;
|
||||
use codex_protocol::protocol::AgentReasoningSectionBreakEvent;
|
||||
@@ -186,8 +187,14 @@ pub(crate) async fn run_turn(
|
||||
.await
|
||||
{
|
||||
if matches!(err.details(), CodexErrorDetails::TurnAborted) {
|
||||
run_hooks_and_record_inputs(&sess, &turn_context, &input, PersistContext::Standard)
|
||||
.await;
|
||||
run_hooks_and_record_inputs(
|
||||
&sess,
|
||||
&turn_context,
|
||||
&turn_context.capture_current_model_info(),
|
||||
&input,
|
||||
PersistContext::Standard,
|
||||
)
|
||||
.await;
|
||||
return Err(err);
|
||||
}
|
||||
if matches!(err.details(), CodexErrorDetails::ToolCollision(_)) {
|
||||
@@ -217,8 +224,14 @@ pub(crate) async fn run_turn(
|
||||
{
|
||||
Ok(requirements) => requirements,
|
||||
Err(err) => {
|
||||
run_hooks_and_record_inputs(&sess, &turn_context, &input, PersistContext::Standard)
|
||||
.await;
|
||||
run_hooks_and_record_inputs(
|
||||
&sess,
|
||||
&turn_context,
|
||||
&turn_context.capture_current_model_info(),
|
||||
&input,
|
||||
PersistContext::Standard,
|
||||
)
|
||||
.await;
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
@@ -239,8 +252,14 @@ pub(crate) async fn run_turn(
|
||||
{
|
||||
Ok(step_context) => step_context,
|
||||
Err(err) if matches!(err.details(), CodexErrorDetails::TurnAborted) => {
|
||||
run_hooks_and_record_inputs(&sess, &turn_context, &input, PersistContext::Standard)
|
||||
.await;
|
||||
run_hooks_and_record_inputs(
|
||||
&sess,
|
||||
&turn_context,
|
||||
&turn_context.capture_current_model_info(),
|
||||
&input,
|
||||
PersistContext::Standard,
|
||||
)
|
||||
.await;
|
||||
return Err(err);
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
@@ -288,7 +307,15 @@ pub(crate) async fn run_turn(
|
||||
return Ok(None);
|
||||
}
|
||||
let mut can_drain_pending_input = input.is_empty();
|
||||
if run_hooks_and_record_inputs(&sess, &turn_context, &input, PersistContext::TurnStart).await {
|
||||
if run_hooks_and_record_inputs(
|
||||
&sess,
|
||||
&turn_context,
|
||||
&first_step_context.settings.model_info,
|
||||
&input,
|
||||
PersistContext::TurnStart,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -310,8 +337,12 @@ pub(crate) async fn run_turn(
|
||||
}))
|
||||
.await;
|
||||
for response_item in injection_items {
|
||||
sess.record_conversation_items(&turn_context, std::slice::from_ref(&response_item))
|
||||
.await;
|
||||
sess.record_conversation_items(
|
||||
&turn_context,
|
||||
&first_step_context.settings.model_info,
|
||||
std::slice::from_ref(&response_item),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
track_turn_resolved_config_analytics(&sess, &turn_context, &input).await;
|
||||
@@ -348,6 +379,7 @@ pub(crate) async fn run_turn(
|
||||
if run_hooks_and_record_inputs(
|
||||
&sess,
|
||||
&turn_context,
|
||||
&turn_context.capture_current_model_info(),
|
||||
&pending_input,
|
||||
PersistContext::Standard,
|
||||
)
|
||||
@@ -574,6 +606,7 @@ pub(crate) async fn run_turn(
|
||||
{
|
||||
sess.record_response_item_and_emit_turn_item(
|
||||
&turn_context,
|
||||
&step_context.settings.model_info,
|
||||
hook_prompt_message,
|
||||
)
|
||||
.await;
|
||||
@@ -684,6 +717,7 @@ async fn turn_diff_display_roots(step_context: &StepContext) -> Vec<(String, Pat
|
||||
pub(crate) async fn run_hooks_and_record_inputs(
|
||||
sess: &Arc<Session>,
|
||||
turn_context: &Arc<TurnContext>,
|
||||
model_info: &ModelInfo,
|
||||
input: &[TurnInput],
|
||||
persist_context: PersistContext,
|
||||
) -> bool {
|
||||
@@ -701,6 +735,7 @@ pub(crate) async fn run_hooks_and_record_inputs(
|
||||
record_pending_input(
|
||||
sess,
|
||||
turn_context,
|
||||
model_info,
|
||||
input_item.clone(),
|
||||
hook_outcome.additional_contexts,
|
||||
persist_context,
|
||||
@@ -2175,13 +2210,14 @@ async fn emit_turn_item_in_plan_mode(
|
||||
/// Handle a completed assistant response item in plan mode, returning true if handled.
|
||||
async fn handle_assistant_item_done_in_plan_mode(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
step_context: &StepContext,
|
||||
turn_store: &codex_extension_api::ExtensionData,
|
||||
item: &ResponseItem,
|
||||
state: &mut PlanModeStreamState,
|
||||
previously_active_item: Option<&TurnItem>,
|
||||
last_agent_message: &mut Option<String>,
|
||||
) -> bool {
|
||||
let turn_context = &step_context.turn;
|
||||
if let ResponseItem::Message { role, .. } = item
|
||||
&& role == "assistant"
|
||||
{
|
||||
@@ -2212,7 +2248,7 @@ async fn handle_assistant_item_done_in_plan_mode(
|
||||
|
||||
record_completed_response_item_with_finalized_facts(
|
||||
sess,
|
||||
turn_context,
|
||||
step_context,
|
||||
item,
|
||||
finalized_facts.as_ref(),
|
||||
)
|
||||
@@ -2229,8 +2265,9 @@ async fn handle_assistant_item_done_in_plan_mode(
|
||||
async fn drain_in_flight(
|
||||
in_flight: &mut FuturesOrdered<InFlightFuture<'static>>,
|
||||
sess: Arc<Session>,
|
||||
turn_context: Arc<TurnContext>,
|
||||
step_context: &StepContext,
|
||||
) -> CodexResult<()> {
|
||||
let turn_context = &step_context.turn;
|
||||
while let Some(res) = in_flight.next().await {
|
||||
match res {
|
||||
Ok(envelope) => {
|
||||
@@ -2240,8 +2277,12 @@ async fn drain_in_flight(
|
||||
&envelope.item,
|
||||
)
|
||||
.await;
|
||||
sess.record_annotated_conversation_items(&turn_context, vec![envelope])
|
||||
.await;
|
||||
sess.record_annotated_conversation_items(
|
||||
turn_context,
|
||||
&step_context.settings.model_info,
|
||||
vec![envelope],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
error_or_panic(format!("in-flight tool future failed during drain: {err}"));
|
||||
@@ -2449,7 +2490,7 @@ async fn try_run_sampling_request(
|
||||
if let Some(state) = plan_mode_state.as_mut()
|
||||
&& handle_assistant_item_done_in_plan_mode(
|
||||
&sess,
|
||||
&turn_context,
|
||||
&step_context,
|
||||
turn_store.as_ref(),
|
||||
&item,
|
||||
state,
|
||||
@@ -2463,7 +2504,7 @@ async fn try_run_sampling_request(
|
||||
|
||||
let mut ctx = HandleOutputCtx {
|
||||
sess: sess.clone(),
|
||||
turn_context: turn_context.clone(),
|
||||
step_context: Arc::clone(&step_context),
|
||||
turn_store: Arc::clone(&turn_store),
|
||||
tool_runtime: tool_runtime.clone(),
|
||||
cancellation_token: cancellation_token.child_token(),
|
||||
@@ -2858,7 +2899,7 @@ async fn try_run_sampling_request(
|
||||
} else {
|
||||
Some(turn_context.turn_timing_state.begin_tool_blocking())
|
||||
};
|
||||
drain_in_flight(&mut in_flight, sess.clone(), turn_context.clone()).await?;
|
||||
drain_in_flight(&mut in_flight, sess.clone(), &step_context).await?;
|
||||
drop(tool_blocking_timing_guard);
|
||||
|
||||
if should_emit_token_count {
|
||||
|
||||
@@ -339,6 +339,11 @@ enum TurnMultiAgentRuntime {
|
||||
}
|
||||
|
||||
impl TurnContext {
|
||||
/// Captures current model metadata without preparing a step.
|
||||
pub(crate) fn capture_current_model_info(&self) -> Arc<ModelInfo> {
|
||||
Arc::clone(&self.current_settings.load().model_info)
|
||||
}
|
||||
|
||||
/// Legacy: returns the frozen initial-turn model metadata.
|
||||
/// Step-scoped consumers should use their captured `StepContext::settings`.
|
||||
pub(crate) fn model_info(&self) -> &Arc<ModelInfo> {
|
||||
|
||||
@@ -69,9 +69,10 @@ async fn plan_mode_uses_contributed_turn_item_for_last_agent_message() {
|
||||
let mut last_agent_message = None;
|
||||
let item = assistant_output_text("original assistant text");
|
||||
|
||||
let step_context = StepContext::for_test(Arc::new(turn_context));
|
||||
let handled = handle_assistant_item_done_in_plan_mode(
|
||||
&session,
|
||||
&turn_context,
|
||||
&step_context,
|
||||
&turn_store,
|
||||
&item,
|
||||
&mut state,
|
||||
|
||||
Reference in new issue
Block a user