Record reasoning effort changes in conversation history behind a flag (#43110)

## What changed

Add the disabled-by-default `reasoning_effort_override` feature for OpenAI models with `use_responses_lite` enabled. Append a trusted `configuration_update` after accepted input when no effort is established in surviving history or the resolved effort changes. Preserve the existing history prefix and continue sending the request-level reasoning effort.

Share effort normalization through `ModelInfo::resolve_reasoning_effort` so requests and history updates resolve `ultra` consistently and translate `persistent` to `disabled`. Exclude other custom effort values from history updates, and compare only against harness-authored configuration items.

## Testing

Add integration coverage for effort transitions, deduplication, history prefix and cache-key preservation, alias normalization, and feature/provider/model gating. Add unit coverage for model-specific `ultra` resolution, fallbacks, and `persistent` translation.

GitOrigin-RevId: c3fe7050058076454d8ea20054a618bca9d48c7c
This commit is contained in:
felixxia-oai authored and copyberry committed 2026-09-05 21:56:43 +00:00
1 parent e67a8ae6c9
commit 56a8470aa0
11 files changed
+523 -39

No files matched your search

+1
View File
@@ -230,6 +230,7 @@ mod mcp_refresh;
mod mcp_runtime;
pub(crate) mod multi_agents;
mod realtime_history;
mod reasoning_effort;
mod retained_context;
mod review;
mod rollout_budget;
@@ -0,0 +1,87 @@
//! Gated reasoning-effort updates appended after accepted input.
//!
//! Only trusted harness items establish an effort. Updates append to surviving
//! history without replacing the request-level reasoning effort.
use super::session::Session;
use super::step_context::StepContext;
use super::step_settings::ResolvedStepSettings;
use codex_features::Feature;
use codex_history::CodexHarnessMetadata;
use codex_history::ResponseItemEnvelope;
use codex_protocol::models::ConfigurationReasoning;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ReasoningEffort;
impl Session {
/// Establishes the selected effort in surviving history, independent of replayed settings.
pub(crate) async fn record_reasoning_effort_override(&self, step_context: &StepContext) {
let settings = &step_context.settings;
let Some(effort) = self.effort_for_configuration_update(settings).await else {
return;
};
let should_skip = {
let state = self.state.lock().await;
let established_effort =
state
.history
.annotated_items()
.iter()
.rev()
.find_map(|envelope| {
if !envelope
.metadata
.as_ref()
.is_some_and(|metadata| metadata.harness_authored_configuration)
{
return None;
}
match &envelope.item {
ResponseItem::ConfigurationUpdate { reasoning } => {
Some(&reasoning.effort)
}
_ => None,
}
});
established_effort == Some(&effort)
};
if should_skip {
return;
}
self.record_annotated_conversation_items(
step_context.turn.as_ref(),
vec![ResponseItemEnvelope {
item: ResponseItem::ConfigurationUpdate {
reasoning: ConfigurationReasoning { effort },
},
metadata: Some(CodexHarnessMetadata {
harness_authored_configuration: true,
..Default::default()
}),
}],
)
.await;
}
async fn effort_for_configuration_update(
&self,
settings: &ResolvedStepSettings,
) -> Option<ReasoningEffort> {
if !self.enabled(Feature::ReasoningEffortOverride)
|| !settings.model_info.use_responses_lite
|| !self.provider().await.is_openai()
{
return None;
}
let effort = settings
.model_info
.resolve_reasoning_effort(settings.effective_reasoning_effort()?);
// Persistent normalizes to "disabled". Keep unknown custom values out of
// durable updates so injected items stay bounded to known backend modes.
if matches!(&effort, ReasoningEffort::Custom(value) if value != "disabled") {
return None;
}
Some(effort)
}
}
+4
View File
@@ -413,6 +413,10 @@ pub(crate) async fn run_turn(
.record_step_world_state_if_changed(&world_state, step_context.as_ref())
.await?;
// Keep the override after accepted input so ordinary turn rollback removes it too.
sess.record_reasoning_effort_override(step_context.as_ref())
.await;
// Construct the input that we will send to the model.
let sampling_request_input: Vec<ResponseItem> = async {
sess.clone_history()