Record base instructions in incremental tool history (#51188)

## What changed

- Record nonempty base instructions as a developer message after tool declarations at the start of each incremental tool context window, and omit the request-level `instructions` field.
- Preserve this prefix when rebuilding history after local or remote compaction, while keeping the compaction summary last.
- Count recorded base instructions once when estimating context usage and trimming tool outputs.

## Testing

Add regression coverage for instruction placement, omission of request-level instructions, compaction ordering, and token accounting. Exercise remote compaction trimming with both request-level and recorded base instructions.

GitOrigin-RevId: 6a6fda92c5c6eb33852a77b38d34c028cf78b164
This commit is contained in:
pakrym-oai
2026-10-05 22:20:38 +00:00
committed by copyberry
parent 5cbcf810e5
commit 062439b2f8
21 changed files with 285 additions and 75 deletions
+46 -12
View File
@@ -6,10 +6,13 @@ use std::time::Instant;
use crate::Prompt;
use crate::client::ModelClientSession;
use crate::client_common::ResponseEvent;
use crate::context::BaseInstructionsFragment;
use crate::context::CompactionSummary;
use crate::context::ContextualUserFragment;
use crate::context::world_state::WorldState;
use crate::context::world_state::WorldStateSnapshot;
use crate::context::world_state::split_prefix_updates;
use crate::context_manager::updates::merge_world_state_updates;
use crate::hook_runtime::PostCompactHookOutcome;
use crate::hook_runtime::PreCompactHookOutcome;
use crate::hook_runtime::run_post_compact_hooks;
@@ -40,6 +43,7 @@ use codex_protocol::error::Result as CodexResult;
use codex_protocol::items::ContextCompactionItem;
use codex_protocol::items::TurnItem;
use codex_protocol::models::AgentMessageInputContent;
use codex_protocol::models::BaseInstructions;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ContentItemKind;
use codex_protocol::models::ResponseInputItem;
@@ -72,23 +76,40 @@ pub(crate) struct CompactedHistoryMetadata {
pub(crate) reviewer_compaction_hash: Option<String>,
}
/// Render replacement context and its comparison baseline from the same captured step.
/// Initial context is installed only after compaction returns, above the last real user message
/// (or the summary when no user message remains), keeping the compaction summary last.
pub(crate) async fn build_compaction_initial_context(
/// Renders the window prefix and ordinary context with their comparison baseline.
pub(crate) async fn build_compaction_replacement_history(
sess: &Session,
step_context: &StepContext,
world_state: &WorldState,
compacted_history: Vec<ResponseItemEnvelope>,
) -> (Vec<ResponseItemEnvelope>, WorldStateSnapshot) {
let (items, snapshot) = sess
let (updates, snapshot) = sess
.build_initial_context_with_world_state(step_context, world_state)
.await;
let (prefix, context) = split_prefix_updates(updates);
let context = merge_world_state_updates(context);
(
items.into_iter().map(ResponseItemEnvelope::new).collect(),
assemble_compaction_history(compacted_history, prefix, context),
snapshot,
)
}
fn assemble_compaction_history(
compacted_history: Vec<ResponseItemEnvelope>,
prefix: Vec<ResponseItem>,
context: Vec<ResponseItem>,
) -> Vec<ResponseItemEnvelope> {
let history = insert_initial_context_before_last_real_user_or_summary(
compacted_history,
context.into_iter().map(ResponseItemEnvelope::new).collect(),
);
prefix
.into_iter()
.map(ResponseItemEnvelope::new)
.chain(history)
.collect()
}
pub(crate) async fn run_inline_auto_compact_task(
sess: Arc<Session>,
turn_context: Arc<TurnContext>,
@@ -269,9 +290,20 @@ async fn run_compact_task_inner_impl(
.executed_tool_calls
.attach_to_compaction_prompt(&mut turn_input);
let turn_input_len = turn_input.len();
let base_instructions = if turn_input
.iter()
.any(BaseInstructionsFragment::matches_item)
{
BaseInstructions {
text: String::new(),
provenance: None,
}
} else {
sess.get_prompt_base_instructions().await
};
let prompt = Prompt {
input: turn_input,
base_instructions: sess.get_prompt_base_instructions().await,
base_instructions,
cyber_access_program: turn_context.cyber_access_program,
..Default::default()
};
@@ -359,11 +391,13 @@ async fn run_compact_task_inner_impl(
}
let (window_number, window_ids) = sess.advance_auto_compact_window().await;
let (initial_context, world_state_baseline) =
build_compaction_initial_context(sess.as_ref(), &replacement_step_context, &world_state)
.await;
new_history =
insert_initial_context_before_last_real_user_or_summary(new_history, initial_context);
let (new_history, world_state_baseline) = build_compaction_replacement_history(
sess.as_ref(),
&replacement_step_context,
&world_state,
new_history,
)
.await;
sess.replace_compacted_history(
new_history,
replacement_step_context.to_turn_context_item(),
+8 -7
View File
@@ -7,9 +7,8 @@ use crate::client_common::ResponseEvent;
use crate::compact::CompactedHistoryMetadata;
use crate::compact::CompactionAnalyticsAttempt;
use crate::compact::CompactionAnalyticsDetails;
use crate::compact::build_compaction_initial_context;
use crate::compact::build_compaction_replacement_history;
use crate::compact::compaction_status_from_result;
use crate::compact::insert_initial_context_before_last_real_user_or_summary;
use crate::compact_model_fallback::record_model_fallback;
use crate::compact_model_fallback::should_retry_with_current_model;
use crate::compact_remote_history::HistoryItemGroup;
@@ -313,11 +312,13 @@ async fn run_remote_compact_task_inner_impl(
);
analytics_details.retained_image_count = Some(retained_images);
let (new_window_number, new_window_ids) = sess.advance_auto_compact_window().await;
let (initial_context, world_state_baseline) =
build_compaction_initial_context(sess.as_ref(), replacement_step_context, &world_state)
.await;
let new_history =
insert_initial_context_before_last_real_user_or_summary(compacted_history, initial_context);
let (new_history, world_state_baseline) = build_compaction_replacement_history(
sess.as_ref(),
replacement_step_context,
&world_state,
compacted_history,
)
.await;
if let Some(trace_input_history) = trace_input_history.as_deref() {
let replacement_history = new_history
@@ -14,6 +14,7 @@ use crate::session::session::Session;
use crate::session::step_context::StepContext;
use codex_history::CodexHarnessMetadata;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::models::BaseInstructions;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::TokenUsage;
use codex_rollout_trace::CompactionTraceContext;
@@ -42,7 +43,14 @@ pub(super) async fn run_remote_compact_v2_attempt(
let turn_context = &step_context.turn;
let mut history = sess.clone_history().await;
let input_goal_ids = UserGoalUpdate::message_ids(history.raw_items());
let base_instructions = sess.get_prompt_base_instructions().await;
let base_instructions = if step_context.uses_incremental_tools() {
BaseInstructions {
text: String::new(),
provenance: None,
}
} else {
sess.get_prompt_base_instructions().await
};
let (rewritten_outputs, estimated_deleted_tokens) =
trim_function_call_history_to_fit_context_window(
&mut history,
+12 -6
View File
@@ -560,7 +560,10 @@ fn build_compacted_history_preserves_user_message_passthrough_metadata() {
}
#[test]
fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last() {
fn assemble_compaction_history_keeps_prefix_first_and_summary_last() {
let prefix = ContextualUserFragment::into(crate::context::BaseInstructionsFragment(
"base instructions".to_string(),
));
let agent_completion = ResponseItem::AgentMessage {
id: None,
author: "child".to_string(),
@@ -610,11 +613,13 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
internal_chat_message_metadata_passthrough: None,
}];
let refreshed = raw(insert_initial_context_before_last_real_user_or_summary(
let refreshed = raw(assemble_compaction_history(
annotated(compacted_history),
annotated(initial_context),
vec![prefix.clone()],
initial_context,
));
let expected = vec![
prefix,
ResponseItem::Message {
id: None,
role: "user".to_string(),
@@ -657,7 +662,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
}
#[test]
fn insert_initial_context_before_last_real_user_or_summary_keeps_compaction_last() {
fn assemble_compaction_history_keeps_compaction_last() {
let agent_task = ResponseItem::AgentMessage {
id: None,
author: "parent".to_string(),
@@ -683,9 +688,10 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_compaction_last
internal_chat_message_metadata_passthrough: None,
}];
let refreshed = raw(insert_initial_context_before_last_real_user_or_summary(
let refreshed = raw(assemble_compaction_history(
annotated(compacted_history),
annotated(initial_context),
Vec::new(),
initial_context,
));
let expected = vec![
ResponseItem::Message {
+20 -1
View File
@@ -1,12 +1,31 @@
use super::ContextualUserFragment;
use codex_protocol::models::ContentItemKind;
use codex_protocol::models::ResponseItem;
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct BaseInstructionsFragment(pub(crate) String);
impl BaseInstructionsFragment {
pub(crate) const KIND: &str = "model.base_instructions";
pub(crate) fn matches_item(item: &ResponseItem) -> bool {
let ResponseItem::Message {
internal_chat_message_metadata_passthrough: Some(metadata),
..
} = item
else {
return false;
};
metadata
.content_item_kinds
.as_ref()
.is_some_and(|kinds| kinds.iter().any(|kind| kind.0 == Self::KIND))
}
}
impl ContextualUserFragment for BaseInstructionsFragment {
fn content_kind(&self) -> ContentItemKind {
ContentItemKind("model.base_instructions".to_string())
ContentItemKind(Self::KIND.to_string())
}
fn role(&self) -> &'static str {
@@ -0,0 +1,28 @@
//! Records fixed base instructions alongside tool declarations at the start of each window.
use super::PreviousSectionState;
use super::SectionTransition;
use super::WorldStateHash;
use super::WorldStateSection;
use super::WorldStateUpdate;
use crate::context::BaseInstructionsFragment;
pub(crate) struct BaseInstructionsState(pub(crate) String);
impl WorldStateSection for BaseInstructionsState {
const ID: &'static str = "base_instructions";
type Snapshot = WorldStateHash;
fn render_diff(
&self,
previous: PreviousSectionState<'_, Self::Snapshot>,
) -> SectionTransition<Self::Snapshot> {
let fragment = BaseInstructionsFragment(self.0.clone());
let hash = WorldStateHash::from_fragment(&fragment);
let updates = WorldStateUpdate::optional_prefix_boxed_fragment(
(matches!(previous, PreviousSectionState::Absent) && !self.0.is_empty())
.then(|| Box::new(fragment) as _),
);
(Some(hash), updates)
}
}
@@ -1,5 +1,6 @@
mod agents_md;
mod apps_instructions;
mod base_instructions;
mod collaboration_mode;
mod compact_permissions;
mod context_window_guidance;
@@ -39,6 +40,7 @@ use std::fmt;
pub(crate) use agents_md::AgentsMdState;
pub(crate) use apps_instructions::AppsInstructionsState;
pub(crate) use base_instructions::BaseInstructionsState;
pub(crate) use collaboration_mode::CollaborationModeState;
pub(crate) use compact_permissions::CompactPermissionsState;
pub(crate) use context_window_guidance::ContextWindowGuidanceState;
@@ -105,6 +107,18 @@ impl WorldStateUpdate {
.collect()
}
pub(crate) fn optional_prefix_boxed_fragment(
fragment: Option<Box<dyn ContextualUserFragment>>,
) -> Vec<Self> {
fragment
.into_iter()
.map(|fragment| Self {
placement: Placement::Prefix,
content: WorldStateUpdateContent::Fragment(fragment),
})
.collect()
}
pub(crate) fn standalone(mut self) -> Self {
self.placement = Placement::Standalone;
self
+9 -2
View File
@@ -12,6 +12,7 @@
#[path = "history_user_authorization.rs"]
mod user_authorization;
use crate::context::BaseInstructionsFragment;
use crate::context::ContextualUserFragment;
use crate::context::ModelSwitchInstructions;
use crate::context::is_guardian_context_message;
@@ -653,8 +654,14 @@ impl ContextManager {
&self,
base_instructions: &BaseInstructions,
) -> Option<i64> {
let base_tokens =
i64::try_from(approx_token_count(&base_instructions.text)).unwrap_or(i64::MAX);
// Incremental windows already account for their recorded base instructions below.
let has_recorded_instructions =
self.raw_items().any(BaseInstructionsFragment::matches_item);
let base_tokens = if has_recorded_instructions {
0
} else {
i64::try_from(approx_token_count(&base_instructions.text)).unwrap_or(i64::MAX)
};
let items_tokens = self
.items
@@ -1498,6 +1498,24 @@ fn estimate_token_count_with_base_instructions_uses_provided_text() {
assert_eq!(long_estimate - short_estimate, expected_delta);
}
#[test]
fn estimate_token_count_counts_recorded_base_instructions_once() {
use crate::context::ContextualUserFragment;
let base = BaseInstructions {
text: "base instructions ".repeat(100),
provenance: None,
};
let item =
ContextualUserFragment::into(crate::context::BaseInstructionsFragment(base.text.clone()));
let expected = estimate_item_token_count(&item);
let history = create_history_with_items(vec![item]);
assert_eq!(
history.estimate_token_count_with_base_instructions(&base),
Some(expected)
);
}
#[test]
fn remove_first_item_removes_matching_output_for_function_call() {
let items = vec![
+26 -14
View File
@@ -33,6 +33,7 @@ use crate::context::RecommendedPluginsInstructions;
use crate::context::world_state::Placement;
use crate::context::world_state::WorldState;
use crate::context::world_state::WorldStateSnapshot;
use crate::context::world_state::WorldStateUpdate;
use crate::context::world_state::WorldStateUpdateContent;
use crate::context::world_state::split_prefix_updates;
use crate::current_time::TimeProvider;
@@ -4182,13 +4183,12 @@ impl Session {
}
/// `step_context` and `world_state` must come from the same captured step.
/// If more callers need this pair, bundle them into a captured-context struct
/// so callers cannot mix settings and WorldState from different steps.
/// Returns rendered items with placement preserved for the caller's history layout.
pub(crate) async fn build_initial_context_with_world_state(
&self,
step_context: &StepContext,
world_state: &WorldState,
) -> (Vec<ResponseItem>, WorldStateSnapshot) {
) -> (Vec<WorldStateUpdate>, WorldStateSnapshot) {
let turn_context = step_context.turn.as_ref();
let mut developer_sections = Vec::<RenderedFragment>::with_capacity(8);
let mut contextual_user_sections = Vec::<RenderedFragment>::with_capacity(2);
@@ -4331,8 +4331,9 @@ impl Session {
.render_fragment(),
);
}
let (world_state_snapshot, updates) = world_state.render_full();
let (mut items, context) = split_prefix_updates(updates);
let (snapshot, updates) = world_state.render_full();
let (mut prefix, context) = split_prefix_updates(updates);
let mut items = Vec::with_capacity(4);
let mut context = context.into_iter();
// Keep the initial-context grouping within each fragment run, but never across an item.
loop {
@@ -4445,10 +4446,18 @@ impl Session {
}
}
// New context windows and compaction install these items directly into replacement history.
for item in &mut items {
for item in prefix.iter_mut().chain(&mut items) {
item.set_turn_id_if_missing(&turn_context.sub_id);
}
(items, world_state_snapshot)
let updates = prefix
.into_iter()
.map(WorldStateUpdate::prefix_item)
.chain(items.into_iter().map(|item| WorldStateUpdate {
placement: Placement::Standalone,
content: WorldStateUpdateContent::Item(Box::new(item)),
}))
.collect();
(updates, snapshot)
}
#[tracing::instrument(level = "trace", skip_all, fields(item_count = items.len()))]
@@ -4543,14 +4552,15 @@ impl Session {
state.start_new_context_window()
};
let (window_number, window_ids) = window;
let (context_items, world_state_snapshot) = self
let (context_updates, world_state_snapshot) = self
.build_initial_context_with_world_state(step_context, world_state.as_ref())
.await;
let context_items = context_items
.into_iter()
.map(ResponseItemEnvelope::new)
.chain(retained_client_developer_messages)
.collect();
let context_items =
crate::context_manager::updates::merge_world_state_updates(context_updates)
.into_iter()
.map(ResponseItemEnvelope::new)
.chain(retained_client_developer_messages)
.collect();
let turn_context_item = step_context.to_turn_context_item();
self.replace_compacted_history(
context_items,
@@ -4605,9 +4615,11 @@ impl Session {
let world_state = Arc::new(self.build_world_state_for_step(step_context).await?);
// Full initial context resets the baseline; later turns persist only its changes.
let (mut context_items, world_state_item) = if should_inject_full_context {
let (context_items, snapshot) = self
let (context_updates, snapshot) = self
.build_initial_context_with_world_state(step_context, world_state.as_ref())
.await;
let context_items =
crate::context_manager::updates::merge_world_state_updates(context_updates);
self.state
.lock()
.await
+21 -8
View File
@@ -6654,10 +6654,10 @@ async fn build_initial_context(
.build_world_state_for_step(&step_context)
.await
.expect("world state should build");
session
let (updates, _) = session
.build_initial_context_with_world_state(&step_context, &world_state)
.await
.0
.await;
crate::context_manager::updates::merge_world_state_updates(updates)
}
pub(crate) async fn build_world_state_from_turn_context(
@@ -10568,6 +10568,8 @@ async fn build_initial_context_reuses_in_flight_recommendation_prewarm() {
assert!(futures::poll!(initial_context.as_mut()).is_pending());
let (_, (initial_context, _)) = tokio::join!(prewarm, initial_context);
let initial_context =
crate::context_manager::updates::merge_world_state_updates(initial_context);
assert_eq!(
developer_input_texts(&initial_context)
.into_iter()
@@ -11298,8 +11300,13 @@ async fn build_initial_context_uses_retained_step_after_model_change() {
.await
.unwrap();
let world_a = Arc::new(session.build_world_state_for_step(&step_a).await.unwrap());
let (initial_a, _) =
crate::compact::build_compaction_initial_context(&session, &step_a, &world_a).await;
let (initial_a, _) = crate::compact::build_compaction_replacement_history(
&session,
&step_a,
&world_a,
Vec::new(),
)
.await;
let mut selected_b = step_a.settings.selected().clone();
selected_b.collaboration_mode.settings.model = "model-b".to_string();
@@ -11328,15 +11335,21 @@ async fn build_initial_context_uses_retained_step_after_model_change() {
.build_initial_context_with_world_state(&step_b, &world_b)
.await
.0;
let initial_b = crate::context_manager::updates::merge_world_state_updates(initial_b);
let turn_contributions_b = session.build_turn_context_contribution_items(&step_b).await;
let (restored_a, restored_snapshot) =
crate::compact::build_compaction_initial_context(&session, &step_a, &world_a).await;
let (restored_a, restored_snapshot) = crate::compact::build_compaction_replacement_history(
&session,
&step_a,
&world_a,
Vec::new(),
)
.await;
assert_eq!(restored_a, initial_a);
assert_eq!(restored_snapshot, world_a.render_full().0);
let initial_a = initial_a
.into_iter()
.map(ResponseItemEnvelope::into_item)
.map(|item| item.item)
.collect::<Vec<_>>();
let a_text = developer_input_texts(&initial_a).join("\n");
let b_text = developer_input_texts(&initial_b).join("\n");
@@ -1117,20 +1117,23 @@ async fn compaction_initial_context_preserves_separate_guardian_developer_messag
.await
.expect("world state should build"),
);
let (refreshed, _) =
crate::compact::build_compaction_initial_context(&session, &step_context, &world_state)
.await;
let (refreshed, _) = crate::compact::build_compaction_replacement_history(
&session,
&step_context,
&world_state,
Vec::new(),
)
.await;
let developer_messages = refreshed
.iter()
.filter_map(|envelope| match &envelope.item {
.map(|item| &item.item)
.filter_map(|item| match item {
ResponseItem::Message { role, content, .. } if role == "developer" => {
crate::content_items_to_text(content).map(|text| {
(
text,
envelope
.item
.executed_tool_call_metadata()
item.executed_tool_call_metadata()
.and_then(|metadata| metadata.content_item_kinds.clone()),
)
})
+8 -1
View File
@@ -1583,7 +1583,14 @@ pub(crate) fn build_prompt(
step_context.tool_router.model_visible_specs()
},
parallel_tool_calls: true,
base_instructions,
base_instructions: if step_context.uses_incremental_tools() {
BaseInstructions {
text: String::new(),
provenance: None,
}
} else {
base_instructions
},
output_schema: turn_context.final_output_json_schema.clone(),
output_schema_strict: !crate::guardian::is_basic_session_source(
&turn_context.session_source,
+2
View File
@@ -6,6 +6,7 @@ use crate::connectors;
use crate::context::TokenBudgetContext;
use crate::context::world_state::AgentsMdState;
use crate::context::world_state::AppsInstructionsState;
use crate::context::world_state::BaseInstructionsState;
use crate::context::world_state::CollaborationModeState;
use crate::context::world_state::CompactPermissionsState;
use crate::context::world_state::ContextWindowGuidanceState;
@@ -122,6 +123,7 @@ impl Session {
let specs = step_context.tool_router.model_visible_specs();
let definitions = codex_tools::create_tools_json_for_responses_lite(&specs)?;
world_state.add_section(TopLevelToolsState::new(definitions)?);
world_state.add_section(BaseInstructionsState(base_instructions));
}
world_state.add_section(ModelInstructionsState::new(
&model_info.slug,
@@ -13,6 +13,8 @@ use crate::context::world_state::WorldState;
use crate::context::world_state::WorldStateSection;
use crate::context::world_state::WorldStateUpdate;
use crate::context::world_state::WorldStateUpdateContent;
use crate::context::world_state::split_prefix_updates;
use crate::context_manager::updates::merge_world_state_updates;
use codex_protocol::models::ResponseItem;
use core_test_support::responses::strip_metadata_from_items;
use pretty_assertions::assert_eq;
@@ -85,14 +87,20 @@ async fn initial_context_preserves_world_state_items_and_snapshot() {
let step_context = StepContext::for_test(Arc::new(turn_context));
let mut world_state = WorldState::default();
world_state.add_section(ItemSection);
let (items, snapshot) = session
let (updates, snapshot) = session
.build_initial_context_with_world_state(&step_context, &world_state)
.await;
let (prefix, context) = split_prefix_updates(updates);
assert_eq!(
strip_metadata_from_items(&items),
strip_metadata_from_items(&prefix),
strip_metadata_from_items(&[
additional_tools("example"),
ContextualUserFragment::into(BaseInstructionsFragment("base instructions".to_string())),
]),
);
assert_eq!(
strip_metadata_from_items(&merge_world_state_updates(context)),
strip_metadata_from_items(&[
ContextualUserFragment::into(DeveloperInstructions::new("before item")),
ContextualUserFragment::into(DeveloperInstructions::new("separate before item")),
ContextualUserFragment::into(UserInstructions {
+4 -1
View File
@@ -839,10 +839,11 @@ async fn ignores_session_prefix_messages_when_truncating() {
let turn_context = Arc::new(turn_context);
let world_state = build_world_state_from_turn_context(&session, &turn_context).await;
let step_context = StepContext::for_test(turn_context);
let mut items = session
let updates = session
.build_initial_context_with_world_state(&step_context, &world_state)
.await
.0;
let mut items = crate::context_manager::updates::merge_world_state_updates(updates);
items.push(user_msg("feature request"));
items.push(assistant_msg("ack"));
items.push(user_msg("second question"));
@@ -1459,6 +1460,8 @@ async fn spawn_internal_session_preserves_parent_lineage_without_forking_history
.build_initial_context_with_world_state(&reviewer_step, &reviewer_world_state)
.await
.0;
let reviewer_context =
crate::context_manager::updates::merge_world_state_updates(reviewer_context);
assert!(
!serde_json::to_string(&reviewer_context)
.expect("reviewer context should serialize")
@@ -367,10 +367,11 @@ async fn ignores_session_prefix_messages_when_truncating_rollout_from_start() {
let turn_context = Arc::new(turn_context);
let world_state = build_world_state_from_turn_context(&session, &turn_context).await;
let step_context = StepContext::for_test(turn_context);
let mut items = session
let updates = session
.build_initial_context_with_world_state(&step_context, &world_state)
.await
.0;
let mut items = crate::context_manager::updates::merge_world_state_updates(updates);
items.push(user_msg("feature request"));
items.push(assistant_msg("ack"));
items.push(user_msg("second question"));
@@ -3,6 +3,7 @@
use super::*;
use core_test_support::apps_test_server::configure_search_capable_model;
use pretty_assertions::assert_eq;
use test_case::test_case;
fn compact_response() -> String {
sse(vec![
@@ -141,15 +142,21 @@ async fn remote_compact_v2_trims_tool_search_output_to_empty_tools_array() -> Re
Ok(())
}
#[test_case(false; "request_instructions")]
#[test_case(true; "recorded_instructions")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn remote_compact_v2_trim_estimate_uses_session_base_instructions() -> Result<()> {
async fn remote_compact_v2_trim_estimate_uses_session_base_instructions(
incremental_tools: bool,
) -> Result<()> {
skip_if_no_network!(Ok(()));
let short_instructions = "session base instructions";
let long_instructions = format!("{short_instructions} {}", "x".repeat(24_000));
let medium_instructions = format!("{short_instructions} {}", "x".repeat(16_000));
let long_instructions = format!("{short_instructions} {}", "x".repeat(40_000));
let trailing_output = "x".repeat(12_000);
for (instructions, expected_output) in [
(short_instructions, trailing_output.as_str()),
(medium_instructions.as_str(), trailing_output.as_str()),
(
long_instructions.as_str(),
CONTEXT_WINDOW_TRUNCATED_OUTPUT_MESSAGE,
@@ -158,11 +165,20 @@ async fn remote_compact_v2_trim_estimate_uses_session_base_instructions() -> Res
let harness = TestCodexHarness::with_builder(
test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_model_info_override("gpt-5.4", move |model| {
model.use_responses_lite = incremental_tools;
})
.with_config({
let instructions = instructions.to_string();
move |config| {
config.model_context_window = Some(8_000);
config.model_context_window = Some(12_000);
config.base_instructions = Some(instructions);
if incremental_tools {
config
.features
.enable(Feature::IncrementalTools)
.expect("enable incremental tools");
}
}
}),
)
@@ -126,6 +126,16 @@ async fn incremental_tools_append_changed_catalog_without_rewriting_history(
.iter()
.all(|request| request.body_json().get("tools").is_none())
);
for request in &requests {
let input = request.input();
assert_eq!(input[0]["type"], "additional_tools");
assert_eq!(input[1]["role"], "developer");
assert_eq!(
input[1]["content"][0]["text"],
"Use the available tools to help the user."
);
assert!(request.body_json().get("instructions").is_none());
}
let initial = requests[0].inputs_of_type("additional_tools");
assert_eq!(initial.len(), 1);
assert_eq!(
@@ -16,9 +16,7 @@ Settings:
text: {"verbosity":"low"}
tool_choice: "auto"
-- request 1 (turn) --
00:message/developer:
<MODEL_INSTRUCTIONS>
01:additional_tools/developer (3; hash=7748a53142f449b7):
00:additional_tools/developer (3; hash=7748a53142f449b7):
- namespace/functions; hash=124fee69a69ed6b2
- function/exec_command
- function/write_stdin
@@ -37,6 +35,8 @@ Settings:
- namespace/mcp__echo_service: Echo service instructions.; hash=a5144e5d70d01799
- function/cwd
- function/echo
01:message/developer:
<MODEL_INSTRUCTIONS>
02:message/developer:
<PERMISSIONS_INSTRUCTIONS>
03:message/user:
@@ -16,9 +16,7 @@ Settings:
text: {"verbosity":"low"}
tool_choice: "auto"
-- request 1 (turn) --
00:message/developer:
<MODEL_INSTRUCTIONS>
01:additional_tools/developer (2; hash=49f981f03ae5c5ce):
00:additional_tools/developer (2; hash=49f981f03ae5c5ce):
- namespace/functions; hash=cc6e8cf0e5405f3a
- custom/exec
- function/wait
@@ -28,6 +26,8 @@ Settings:
- custom/apply_patch
- function/view_image
- tool_search: # Tool discovery Searches over deferred tool metadata with BM25 and exposes matching tools for t...; args=[limit, query]; hash=400cd6ac61d524a9
01:message/developer:
<MODEL_INSTRUCTIONS>
02:message/developer:
<PERMISSIONS_INSTRUCTIONS>
03:message/user:
@@ -42,9 +42,7 @@ Settings:
Settings: relative to window 1
model: "updated-tools-model"
-- request 3 (turn) --
00:message/developer:
<MODEL_INSTRUCTIONS>
01:additional_tools/developer (2; hash=49f981f03ae5c5ce):
00:additional_tools/developer (2; hash=49f981f03ae5c5ce):
- namespace/functions; hash=cc6e8cf0e5405f3a
- custom/exec
- function/wait
@@ -54,6 +52,8 @@ Settings: relative to window 1
- custom/apply_patch
- function/view_image
- tool_search: # Tool discovery Searches over deferred tool metadata with BM25 and exposes matching tools for t...; args=[limit, query]; hash=400cd6ac61d524a9
01:message/developer:
<MODEL_INSTRUCTIONS>
02:message/developer:
<PERMISSIONS_INSTRUCTIONS>
03:message/user: