Use captured step model settings for extension context (#44202)

## Why

Model switches within a turn can leave extension context using stale model metadata. Skill catalogs, context windows, and metric attribution need to match the model captured for each sampling step.

## What changed

- Pass captured model metadata and step-specific telemetry to world-state contributors.
- Use that metadata for skill catalog budgets and usage instructions, preserving configured budget overrides.
- Supply the captured model's usable context window to turn-context contributors, including when rebuilding context.

## Testing

Add regression coverage for model switches during skill discovery, catalog budgets and metric attribution, and extension context windows after `new_context` and retained-step context rebuilds.

GitOrigin-RevId: c51b40b739a380b9767a54ea8c8ee6de45bf01c8
This commit is contained in:
Abhinav authored and copyberry committed 2026-09-09 16:48:49 +00:00
1 parent b4507997e0
commit 8ff4aa8ee4
9 files changed
+447 -99

No files matched your search

+2 -2
View File
@@ -3964,7 +3964,7 @@ impl Session {
session_store: &self.services.session_extension_data,
thread_store: &self.services.thread_extension_data,
turn_store: turn_context.extension_data.as_ref(),
model_context_window: turn_context.model_context_window(),
model_context_window: step_context.settings.model_info.usable_context_window(),
})
.await
{
@@ -4066,7 +4066,7 @@ impl Session {
session_store: &self.services.session_extension_data,
thread_store: &self.services.thread_extension_data,
turn_store: turn_context.extension_data.as_ref(),
model_context_window: turn_context.model_context_window(),
model_context_window: step_context.settings.model_info.usable_context_window(),
})
.await
{
+21 -1
View File
@@ -10810,7 +10810,7 @@ async fn record_context_updates_and_set_reference_context_item_persists_split_fi
#[tokio::test]
async fn build_initial_context_uses_retained_step_after_model_change() {
let (session, mut turn_context, _rx_event) =
let (mut session, mut turn_context, _rx_event) =
make_session_and_context_with_auth_and_config_and_rx(
CodexAuth::from_api_key("Test API Key"),
Vec::new(),
@@ -10821,6 +10821,17 @@ async fn build_initial_context_uses_retained_step_after_model_change() {
},
)
.await;
let mut builder = codex_extension_api::ExtensionRegistryBuilder::new();
builder.prompt_contributor(Arc::new(TurnContextExtensionTestContributor));
Arc::get_mut(&mut session)
.expect("unshared test session")
.services
.extensions = Arc::new(builder.build());
turn_context
.extension_data
.insert(TurnContextExtensionTestState {
expected_model_context_window: Some(64_000),
});
update_turn_settings_for_test(Arc::get_mut(&mut turn_context).unwrap(), |settings| {
let model_info = Arc::make_mut(&mut settings.model_info);
model_info.slug = "model-a".to_string();
@@ -10859,6 +10870,7 @@ async fn build_initial_context_uses_retained_step_after_model_change() {
let mut model_b = step_a.settings.model_info.as_ref().clone();
model_b.slug = "model-b".to_string();
model_b.context_window = Some(128_000);
model_b.effective_context_window_percent = 50;
model_b
.model_messages
.as_mut()
@@ -10879,6 +10891,7 @@ async fn build_initial_context_uses_retained_step_after_model_change() {
let initial_b = session
.build_initial_context_with_world_state(&step_b, &world_b)
.await;
let turn_contributions_b = session.build_turn_context_contribution_items(&step_b).await;
let (restored_a, restored_world) =
crate::compact::build_compaction_initial_context(&session, &retained).await;
@@ -10894,6 +10907,13 @@ async fn build_initial_context_uses_retained_step_after_model_change() {
assert!(!a_text.contains("<context_window>"));
assert!(b_text.contains("B instructions: pragmatic"));
assert!(!b_text.contains("A instructions:"));
assert!(!a_text.contains("turn context extension enabled"));
assert!(b_text.contains("turn context extension enabled"));
assert!(
developer_input_texts(&turn_contributions_b)
.join("\n")
.contains("turn context extension enabled")
);
assert!(
b_text.contains("<context_window>"),
"full-context metadata must use B's window even though the turn started without one"
+2 -1
View File
@@ -290,13 +290,14 @@ impl Session {
.map(|root| root.selected_root().clone())
.collect::<Vec<_>>();
let extension_metrics = super::extension_metrics::from_session_telemetry(
turn_context.session_telemetry.clone(),
step_context.session_telemetry.clone(),
);
for contributor in self.services.extensions.context_contributors() {
for section in contributor
.contribute_world_state(WorldStateContributionInput {
thread_id: self.thread_id(),
turn_id: turn_context.sub_id.as_str(),
model_info: &step_context.settings.model_info,
environments: &environments,
ready_selected_capability_roots: &ready_selected_capability_roots,
executor_capability_discovery: step_context