mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
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:
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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![
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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!(
|
||||
|
||||
+3
-3
@@ -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:
|
||||
|
||||
+6
-6
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user