mirror of
https://github.com/openai/codex.git
synced 2026-11-07 00:33:29 +00:00
core: add UUIDv7 context window IDs (#28953)
## Why The token-budget context currently identifies a context window by its thread-local sequence number. A UUIDv7 gives the model a stable opaque identity that remains fixed for a window and rotates when compaction or `new_context` starts the next one. ## What changed - Preserve the existing monotonic value as `window_number` and add a UUIDv7 `window_id` to `CompactedItem`. - Generate and rotate the UUID with auto-compaction window state, persist it alongside the number, and reconstruct it on resume and rollback. - Accept legacy compacted rollout records where the numeric `window_id` represented the window number. - Use the UUID only in token-budget context; existing request headers and metadata continue using `thread_id:window_number`. ## Testing - `just test -p codex-protocol compacted_item::tests` - `just test -p codex-core token_budget`
This commit is contained in:
1 parent
e83b7841b0
commit
5c12034e42
19 files changed
+319
-87
No files matched your search
@@ -3340,6 +3340,7 @@ mod tests {
|
|||||||
RolloutItem::Compacted(CompactedItem {
|
RolloutItem::Compacted(CompactedItem {
|
||||||
message: String::new(),
|
message: String::new(),
|
||||||
replacement_history: None,
|
replacement_history: None,
|
||||||
|
window_number: None,
|
||||||
window_id: None,
|
window_id: None,
|
||||||
}),
|
}),
|
||||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||||
|
|||||||
@@ -1105,6 +1105,7 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history() {
|
|||||||
RolloutItem::Compacted(CompactedItem {
|
RolloutItem::Compacted(CompactedItem {
|
||||||
message: String::new(),
|
message: String::new(),
|
||||||
replacement_history: Some(replacement_history),
|
replacement_history: Some(replacement_history),
|
||||||
|
window_number: None,
|
||||||
window_id: None,
|
window_id: None,
|
||||||
}),
|
}),
|
||||||
RolloutItem::TurnContext(turn_context.to_turn_context_item()),
|
RolloutItem::TurnContext(turn_context.to_turn_context_item()),
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ async fn run_compact_task_inner_impl(
|
|||||||
let user_messages = collect_user_messages(history_items);
|
let user_messages = collect_user_messages(history_items);
|
||||||
|
|
||||||
let mut new_history = build_compacted_history(Vec::new(), &user_messages, &summary_text);
|
let mut new_history = build_compacted_history(Vec::new(), &user_messages, &summary_text);
|
||||||
let window_id = sess.advance_auto_compact_window_id().await;
|
let (window_number, window_id) = sess.advance_auto_compact_window().await;
|
||||||
|
|
||||||
if matches!(
|
if matches!(
|
||||||
initial_context_injection,
|
initial_context_injection,
|
||||||
@@ -319,6 +319,7 @@ async fn run_compact_task_inner_impl(
|
|||||||
let compacted_item = CompactedItem {
|
let compacted_item = CompactedItem {
|
||||||
message: summary_text.clone(),
|
message: summary_text.clone(),
|
||||||
replacement_history: Some(new_history.clone()),
|
replacement_history: Some(new_history.clone()),
|
||||||
|
window_number: Some(window_number),
|
||||||
window_id: Some(window_id),
|
window_id: Some(window_id),
|
||||||
};
|
};
|
||||||
sess.replace_compacted_history(new_history, reference_context_item, compacted_item)
|
sess.replace_compacted_history(new_history, reference_context_item, compacted_item)
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ async fn run_remote_compact_task_inner_impl(
|
|||||||
&responses_metadata,
|
&responses_metadata,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let new_window_id = sess.advance_auto_compact_window_id().await;
|
let (new_window_number, new_window_id) = sess.advance_auto_compact_window().await;
|
||||||
new_history = process_compacted_history(
|
new_history = process_compacted_history(
|
||||||
sess.as_ref(),
|
sess.as_ref(),
|
||||||
turn_context.as_ref(),
|
turn_context.as_ref(),
|
||||||
@@ -274,6 +274,7 @@ async fn run_remote_compact_task_inner_impl(
|
|||||||
let compacted_item = CompactedItem {
|
let compacted_item = CompactedItem {
|
||||||
message: String::new(),
|
message: String::new(),
|
||||||
replacement_history: Some(new_history.clone()),
|
replacement_history: Some(new_history.clone()),
|
||||||
|
window_number: Some(new_window_number),
|
||||||
window_id: Some(new_window_id),
|
window_id: Some(new_window_id),
|
||||||
};
|
};
|
||||||
// Install is the semantic boundary where the compact endpoint's output becomes live
|
// Install is the semantic boundary where the compact endpoint's output becomes live
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ async fn run_remote_compact_task_inner_impl(
|
|||||||
let (compacted_history, retained_images) =
|
let (compacted_history, retained_images) =
|
||||||
build_v2_compacted_history(&prompt_input, compaction_output);
|
build_v2_compacted_history(&prompt_input, compaction_output);
|
||||||
analytics_details.retained_image_count = Some(retained_images);
|
analytics_details.retained_image_count = Some(retained_images);
|
||||||
let new_window_id = sess.advance_auto_compact_window_id().await;
|
let (new_window_number, new_window_id) = sess.advance_auto_compact_window().await;
|
||||||
let new_history = process_compacted_history(
|
let new_history = process_compacted_history(
|
||||||
sess.as_ref(),
|
sess.as_ref(),
|
||||||
turn_context.as_ref(),
|
turn_context.as_ref(),
|
||||||
@@ -308,6 +308,7 @@ async fn run_remote_compact_task_inner_impl(
|
|||||||
let compacted_item = CompactedItem {
|
let compacted_item = CompactedItem {
|
||||||
message: String::new(),
|
message: String::new(),
|
||||||
replacement_history: Some(new_history.clone()),
|
replacement_history: Some(new_history.clone()),
|
||||||
|
window_number: Some(new_window_number),
|
||||||
window_id: Some(new_window_id),
|
window_id: Some(new_window_id),
|
||||||
};
|
};
|
||||||
compaction_trace.record_installed(&CompactionCheckpointTracePayload {
|
compaction_trace.record_installed(&CompactionCheckpointTracePayload {
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
use super::ContextualUserFragment;
|
use super::ContextualUserFragment;
|
||||||
use codex_protocol::ThreadId;
|
use codex_protocol::ThreadId;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub(crate) struct TokenBudgetContext {
|
pub(crate) struct TokenBudgetContext {
|
||||||
thread_id: ThreadId,
|
thread_id: ThreadId,
|
||||||
window_id: u64,
|
window_id: Uuid,
|
||||||
tokens_left: i64,
|
tokens_left: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TokenBudgetContext {
|
impl TokenBudgetContext {
|
||||||
pub(crate) fn new(thread_id: ThreadId, window_id: u64, tokens_left: i64) -> Self {
|
pub(crate) fn new(thread_id: ThreadId, window_id: Uuid, tokens_left: i64) -> Self {
|
||||||
Self {
|
Self {
|
||||||
thread_id,
|
thread_id,
|
||||||
window_id,
|
window_id,
|
||||||
@@ -36,7 +37,7 @@ impl ContextualUserFragment for TokenBudgetContext {
|
|||||||
let window_id = self.window_id;
|
let window_id = self.window_id;
|
||||||
let tokens_left = self.tokens_left;
|
let tokens_left = self.tokens_left;
|
||||||
format!(
|
format!(
|
||||||
"Thread id {thread_id}.\nCurrent context window {window_id}.\nYou have {tokens_left} tokens left in this context window."
|
"Thread id {thread_id}.\nCurrent context window id {window_id}.\nYou have {tokens_left} tokens left in this context window."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1352,6 +1352,7 @@ impl Session {
|
|||||||
mut history,
|
mut history,
|
||||||
previous_turn_settings,
|
previous_turn_settings,
|
||||||
reference_context_item,
|
reference_context_item,
|
||||||
|
window_number,
|
||||||
window_id,
|
window_id,
|
||||||
} = self
|
} = self
|
||||||
.reconstruct_history_from_rollout(turn_context, rollout_items)
|
.reconstruct_history_from_rollout(turn_context, rollout_items)
|
||||||
@@ -1370,7 +1371,8 @@ impl Session {
|
|||||||
{
|
{
|
||||||
let mut state = self.state.lock().await;
|
let mut state = self.state.lock().await;
|
||||||
state.replace_history(history, reference_context_item);
|
state.replace_history(history, reference_context_item);
|
||||||
state.set_auto_compact_window_id(window_id);
|
let window_id = window_id.unwrap_or_else(|| state.auto_compact_window_id());
|
||||||
|
state.restore_auto_compact_window(window_number, window_id);
|
||||||
state.set_previous_turn_settings(previous_turn_settings.clone());
|
state.set_previous_turn_settings(previous_turn_settings.clone());
|
||||||
}
|
}
|
||||||
let prefix_tokens = if matches!(
|
let prefix_tokens = if matches!(
|
||||||
@@ -3231,13 +3233,14 @@ impl Session {
|
|||||||
pub(crate) async fn current_window_id(&self) -> String {
|
pub(crate) async fn current_window_id(&self) -> String {
|
||||||
let state = self.state.lock().await;
|
let state = self.state.lock().await;
|
||||||
let thread_id = self.thread_id;
|
let thread_id = self.thread_id;
|
||||||
let window_id = state.auto_compact_window_id();
|
let window_number = state.auto_compact_window_number();
|
||||||
format!("{thread_id}:{window_id}")
|
format!("{thread_id}:{window_number}")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn advance_auto_compact_window_id(&self) -> u64 {
|
pub(crate) async fn advance_auto_compact_window(&self) -> (u64, String) {
|
||||||
let mut state = self.state.lock().await;
|
let mut state = self.state.lock().await;
|
||||||
state.advance_auto_compact_window_id()
|
let (window_number, window_id) = state.advance_auto_compact_window();
|
||||||
|
(window_number, window_id.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn request_new_context_window(&self) {
|
pub(crate) async fn request_new_context_window(&self) {
|
||||||
@@ -3249,11 +3252,11 @@ impl Session {
|
|||||||
&self,
|
&self,
|
||||||
turn_context: &TurnContext,
|
turn_context: &TurnContext,
|
||||||
) -> Option<u64> {
|
) -> Option<u64> {
|
||||||
let window_id = {
|
let window = {
|
||||||
let mut state = self.state.lock().await;
|
let mut state = self.state.lock().await;
|
||||||
state.start_new_context_window_if_requested()
|
state.start_new_context_window_if_requested()
|
||||||
};
|
};
|
||||||
let window_id = window_id?;
|
let (window_number, window_id) = window?;
|
||||||
let context_items = self.build_initial_context(turn_context).await;
|
let context_items = self.build_initial_context(turn_context).await;
|
||||||
let turn_context_item = turn_context.to_turn_context_item();
|
let turn_context_item = turn_context.to_turn_context_item();
|
||||||
let replacement_history = context_items;
|
let replacement_history = context_items;
|
||||||
@@ -3265,7 +3268,8 @@ impl Session {
|
|||||||
RolloutItem::Compacted(CompactedItem {
|
RolloutItem::Compacted(CompactedItem {
|
||||||
message: String::new(),
|
message: String::new(),
|
||||||
replacement_history: Some(replacement_history),
|
replacement_history: Some(replacement_history),
|
||||||
window_id: Some(window_id),
|
window_number: Some(window_number),
|
||||||
|
window_id: Some(window_id.to_string()),
|
||||||
}),
|
}),
|
||||||
RolloutItem::TurnContext(turn_context_item),
|
RolloutItem::TurnContext(turn_context_item),
|
||||||
])
|
])
|
||||||
@@ -3275,7 +3279,7 @@ impl Session {
|
|||||||
state.queue_pending_session_start_source(codex_hooks::SessionStartSource::Compact);
|
state.queue_pending_session_start_source(codex_hooks::SessionStartSource::Compact);
|
||||||
}
|
}
|
||||||
self.recompute_token_usage(turn_context).await;
|
self.recompute_token_usage(turn_context).await;
|
||||||
Some(window_id)
|
Some(window_number)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn reference_context_item(&self) -> Option<TurnContextItem> {
|
pub(crate) async fn reference_context_item(&self) -> Option<TurnContextItem> {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::context_manager::is_user_turn_boundary;
|
use crate::context_manager::is_user_turn_boundary;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
// Return value of `Session::reconstruct_history_from_rollout`, bundling the rebuilt history with
|
// Return value of `Session::reconstruct_history_from_rollout`, bundling the rebuilt history with
|
||||||
// the resume/fork hydration metadata derived from the same replay.
|
// the resume/fork hydration metadata derived from the same replay.
|
||||||
@@ -8,7 +9,14 @@ pub(super) struct RolloutReconstruction {
|
|||||||
pub(super) history: Vec<ResponseItem>,
|
pub(super) history: Vec<ResponseItem>,
|
||||||
pub(super) previous_turn_settings: Option<PreviousTurnSettings>,
|
pub(super) previous_turn_settings: Option<PreviousTurnSettings>,
|
||||||
pub(super) reference_context_item: Option<TurnContextItem>,
|
pub(super) reference_context_item: Option<TurnContextItem>,
|
||||||
pub(super) window_id: u64,
|
pub(super) window_number: u64,
|
||||||
|
pub(super) window_id: Option<Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct ReconstructedWindow {
|
||||||
|
number: u64,
|
||||||
|
id: Option<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
@@ -34,7 +42,7 @@ struct ActiveReplaySegment<'a> {
|
|||||||
previous_turn_settings: Option<PreviousTurnSettings>,
|
previous_turn_settings: Option<PreviousTurnSettings>,
|
||||||
reference_context_item: TurnReferenceContextItem,
|
reference_context_item: TurnReferenceContextItem,
|
||||||
base_replacement_history: Option<&'a [ResponseItem]>,
|
base_replacement_history: Option<&'a [ResponseItem]>,
|
||||||
window_id: Option<u64>,
|
window: Option<ReconstructedWindow>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn turn_ids_are_compatible(active_turn_id: Option<&str>, item_turn_id: Option<&str>) -> bool {
|
fn turn_ids_are_compatible(active_turn_id: Option<&str>, item_turn_id: Option<&str>) -> bool {
|
||||||
@@ -47,7 +55,7 @@ fn finalize_active_segment<'a>(
|
|||||||
base_replacement_history: &mut Option<&'a [ResponseItem]>,
|
base_replacement_history: &mut Option<&'a [ResponseItem]>,
|
||||||
previous_turn_settings: &mut Option<PreviousTurnSettings>,
|
previous_turn_settings: &mut Option<PreviousTurnSettings>,
|
||||||
reference_context_item: &mut TurnReferenceContextItem,
|
reference_context_item: &mut TurnReferenceContextItem,
|
||||||
window_id: &mut Option<u64>,
|
window: &mut Option<ReconstructedWindow>,
|
||||||
pending_rollback_turns: &mut usize,
|
pending_rollback_turns: &mut usize,
|
||||||
) {
|
) {
|
||||||
// Thread rollback drops the newest surviving real user-message boundaries. In replay, that
|
// Thread rollback drops the newest surviving real user-message boundaries. In replay, that
|
||||||
@@ -68,8 +76,8 @@ fn finalize_active_segment<'a>(
|
|||||||
*base_replacement_history = Some(segment_base_replacement_history);
|
*base_replacement_history = Some(segment_base_replacement_history);
|
||||||
}
|
}
|
||||||
|
|
||||||
if window_id.is_none() {
|
if window.is_none() {
|
||||||
*window_id = active_segment.window_id;
|
*window = active_segment.window;
|
||||||
}
|
}
|
||||||
|
|
||||||
// `previous_turn_settings` come from the newest surviving user turn that established them.
|
// `previous_turn_settings` come from the newest surviving user turn that established them.
|
||||||
@@ -104,7 +112,7 @@ impl Session {
|
|||||||
let mut base_replacement_history: Option<&[ResponseItem]> = None;
|
let mut base_replacement_history: Option<&[ResponseItem]> = None;
|
||||||
let mut previous_turn_settings = None;
|
let mut previous_turn_settings = None;
|
||||||
let mut reference_context_item = TurnReferenceContextItem::NeverSet;
|
let mut reference_context_item = TurnReferenceContextItem::NeverSet;
|
||||||
let mut window_id = None;
|
let mut window = None;
|
||||||
// Rollback is "drop the newest N user turns". While scanning in reverse, that becomes
|
// Rollback is "drop the newest N user turns". While scanning in reverse, that becomes
|
||||||
// "skip the next N user-turn segments we finalize".
|
// "skip the next N user-turn segments we finalize".
|
||||||
let mut pending_rollback_turns = 0usize;
|
let mut pending_rollback_turns = 0usize;
|
||||||
@@ -120,8 +128,13 @@ impl Session {
|
|||||||
RolloutItem::Compacted(compacted) => {
|
RolloutItem::Compacted(compacted) => {
|
||||||
let active_segment =
|
let active_segment =
|
||||||
active_segment.get_or_insert_with(ActiveReplaySegment::default);
|
active_segment.get_or_insert_with(ActiveReplaySegment::default);
|
||||||
if active_segment.window_id.is_none() {
|
if active_segment.window.is_none()
|
||||||
active_segment.window_id = compacted.window_id;
|
&& let Some(window_number) = compacted.window_number
|
||||||
|
{
|
||||||
|
active_segment.window = Some(ReconstructedWindow {
|
||||||
|
number: window_number,
|
||||||
|
id: compacted.window_id.as_deref().and_then(parse_uuid_v7),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// Looking backward, compaction clears any older baseline unless a newer
|
// Looking backward, compaction clears any older baseline unless a newer
|
||||||
// `TurnContextItem` in this same segment has already re-established it.
|
// `TurnContextItem` in this same segment has already re-established it.
|
||||||
@@ -210,7 +223,7 @@ impl Session {
|
|||||||
&mut base_replacement_history,
|
&mut base_replacement_history,
|
||||||
&mut previous_turn_settings,
|
&mut previous_turn_settings,
|
||||||
&mut reference_context_item,
|
&mut reference_context_item,
|
||||||
&mut window_id,
|
&mut window,
|
||||||
&mut pending_rollback_turns,
|
&mut pending_rollback_turns,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -245,12 +258,12 @@ impl Session {
|
|||||||
&mut base_replacement_history,
|
&mut base_replacement_history,
|
||||||
&mut previous_turn_settings,
|
&mut previous_turn_settings,
|
||||||
&mut reference_context_item,
|
&mut reference_context_item,
|
||||||
&mut window_id,
|
&mut window,
|
||||||
&mut pending_rollback_turns,
|
&mut pending_rollback_turns,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let fallback_window_id = u64::try_from(
|
let fallback_window_number = u64::try_from(
|
||||||
rollout_items
|
rollout_items
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|item| matches!(item, RolloutItem::Compacted(_)))
|
.filter(|item| matches!(item, RolloutItem::Compacted(_)))
|
||||||
@@ -326,11 +339,22 @@ impl Session {
|
|||||||
reference_context_item
|
reference_context_item
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let window = window.unwrap_or(ReconstructedWindow {
|
||||||
|
number: fallback_window_number,
|
||||||
|
id: None,
|
||||||
|
});
|
||||||
RolloutReconstruction {
|
RolloutReconstruction {
|
||||||
history: history.raw_items().to_vec(),
|
history: history.raw_items().to_vec(),
|
||||||
previous_turn_settings,
|
previous_turn_settings,
|
||||||
reference_context_item,
|
reference_context_item,
|
||||||
window_id: window_id.unwrap_or(fallback_window_id),
|
window_number: window.number,
|
||||||
|
window_id: window.id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_uuid_v7(value: &str) -> Option<Uuid> {
|
||||||
|
Uuid::parse_str(value)
|
||||||
|
.ok()
|
||||||
|
.filter(|uuid| uuid.get_version_num() == 7)
|
||||||
|
}
|
||||||
@@ -826,6 +826,7 @@ async fn record_initial_history_resumed_rollback_drops_incomplete_user_turn_comp
|
|||||||
RolloutItem::Compacted(CompactedItem {
|
RolloutItem::Compacted(CompactedItem {
|
||||||
message: String::new(),
|
message: String::new(),
|
||||||
replacement_history: Some(Vec::new()),
|
replacement_history: Some(Vec::new()),
|
||||||
|
window_number: None,
|
||||||
window_id: None,
|
window_id: None,
|
||||||
}),
|
}),
|
||||||
RolloutItem::EventMsg(EventMsg::ThreadRolledBack(
|
RolloutItem::EventMsg(EventMsg::ThreadRolledBack(
|
||||||
@@ -883,6 +884,7 @@ async fn record_initial_history_resumed_does_not_seed_reference_context_item_aft
|
|||||||
RolloutItem::Compacted(CompactedItem {
|
RolloutItem::Compacted(CompactedItem {
|
||||||
message: String::new(),
|
message: String::new(),
|
||||||
replacement_history: Some(Vec::new()),
|
replacement_history: Some(Vec::new()),
|
||||||
|
window_number: None,
|
||||||
window_id: None,
|
window_id: None,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
@@ -909,6 +911,7 @@ async fn reconstruct_history_legacy_compaction_without_replacement_history_does_
|
|||||||
RolloutItem::Compacted(CompactedItem {
|
RolloutItem::Compacted(CompactedItem {
|
||||||
message: "legacy summary".to_string(),
|
message: "legacy summary".to_string(),
|
||||||
replacement_history: None,
|
replacement_history: None,
|
||||||
|
window_number: None,
|
||||||
window_id: None,
|
window_id: None,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
@@ -941,6 +944,7 @@ async fn reconstruct_history_legacy_compaction_without_replacement_history_clear
|
|||||||
RolloutItem::Compacted(CompactedItem {
|
RolloutItem::Compacted(CompactedItem {
|
||||||
message: "legacy summary".to_string(),
|
message: "legacy summary".to_string(),
|
||||||
replacement_history: None,
|
replacement_history: None,
|
||||||
|
window_number: None,
|
||||||
window_id: None,
|
window_id: None,
|
||||||
}),
|
}),
|
||||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||||
@@ -1035,6 +1039,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
|
|||||||
RolloutItem::Compacted(CompactedItem {
|
RolloutItem::Compacted(CompactedItem {
|
||||||
message: String::new(),
|
message: String::new(),
|
||||||
replacement_history: Some(Vec::new()),
|
replacement_history: Some(Vec::new()),
|
||||||
|
window_number: None,
|
||||||
window_id: None,
|
window_id: None,
|
||||||
}),
|
}),
|
||||||
RolloutItem::TurnContext(previous_context_item),
|
RolloutItem::TurnContext(previous_context_item),
|
||||||
@@ -1185,6 +1190,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
|
|||||||
RolloutItem::Compacted(CompactedItem {
|
RolloutItem::Compacted(CompactedItem {
|
||||||
message: String::new(),
|
message: String::new(),
|
||||||
replacement_history: Some(Vec::new()),
|
replacement_history: Some(Vec::new()),
|
||||||
|
window_number: None,
|
||||||
window_id: None,
|
window_id: None,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
@@ -1419,6 +1425,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
|
|||||||
RolloutItem::Compacted(CompactedItem {
|
RolloutItem::Compacted(CompactedItem {
|
||||||
message: String::new(),
|
message: String::new(),
|
||||||
replacement_history: Some(Vec::new()),
|
replacement_history: Some(Vec::new()),
|
||||||
|
window_number: None,
|
||||||
window_id: None,
|
window_id: None,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
@@ -1583,6 +1590,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
|
|||||||
RolloutItem::Compacted(CompactedItem {
|
RolloutItem::Compacted(CompactedItem {
|
||||||
message: String::new(),
|
message: String::new(),
|
||||||
replacement_history: Some(Vec::new()),
|
replacement_history: Some(Vec::new()),
|
||||||
|
window_number: None,
|
||||||
window_id: None,
|
window_id: None,
|
||||||
}),
|
}),
|
||||||
// A newer TurnStarted replaces the incomplete compacted turn without a matching
|
// A newer TurnStarted replaces the incomplete compacted turn without a matching
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ use tokio::sync::Semaphore;
|
|||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
use tracing_opentelemetry::OpenTelemetrySpanExt;
|
use tracing_opentelemetry::OpenTelemetrySpanExt;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use codex_protocol::mcp::CallToolResult as McpCallToolResult;
|
use codex_protocol::mcp::CallToolResult as McpCallToolResult;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
@@ -1592,7 +1593,13 @@ async fn reconstruct_history_matches_live_compactions() {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert_eq!(expected, reconstructed.history);
|
assert_eq!(expected, reconstructed.history);
|
||||||
assert_eq!(2, reconstructed.window_id);
|
assert_eq!(2, reconstructed.window_number);
|
||||||
|
assert_eq!(
|
||||||
|
reconstructed
|
||||||
|
.window_id
|
||||||
|
.map(|window_id| window_id.get_version_num()),
|
||||||
|
Some(7)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -1622,10 +1629,12 @@ async fn reconstruct_history_uses_replacement_history_verbatim() {
|
|||||||
metadata: None,
|
metadata: None,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
let window_id = Uuid::now_v7();
|
||||||
let rollout_items = vec![RolloutItem::Compacted(CompactedItem {
|
let rollout_items = vec![RolloutItem::Compacted(CompactedItem {
|
||||||
message: String::new(),
|
message: String::new(),
|
||||||
replacement_history: Some(replacement_history.clone()),
|
replacement_history: Some(replacement_history.clone()),
|
||||||
window_id: Some(42),
|
window_number: Some(42),
|
||||||
|
window_id: Some(window_id.to_string()),
|
||||||
})];
|
})];
|
||||||
|
|
||||||
let reconstructed = session
|
let reconstructed = session
|
||||||
@@ -1633,7 +1642,8 @@ async fn reconstruct_history_uses_replacement_history_verbatim() {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert_eq!(reconstructed.history, replacement_history);
|
assert_eq!(reconstructed.history, replacement_history);
|
||||||
assert_eq!(42, reconstructed.window_id);
|
assert_eq!(42, reconstructed.window_number);
|
||||||
|
assert_eq!(Some(window_id), reconstructed.window_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -3014,6 +3024,7 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
|
|||||||
user_message("turn 1 user"),
|
user_message("turn 1 user"),
|
||||||
user_message("summary after compaction"),
|
user_message("summary after compaction"),
|
||||||
];
|
];
|
||||||
|
let compacted_window_id = Uuid::now_v7();
|
||||||
|
|
||||||
sess.persist_rollout_items(&[
|
sess.persist_rollout_items(&[
|
||||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||||
@@ -3055,7 +3066,8 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
|
|||||||
RolloutItem::Compacted(CompactedItem {
|
RolloutItem::Compacted(CompactedItem {
|
||||||
message: "summary after compaction".to_string(),
|
message: "summary after compaction".to_string(),
|
||||||
replacement_history: Some(compacted_history.clone()),
|
replacement_history: Some(compacted_history.clone()),
|
||||||
window_id: Some(7),
|
window_number: Some(7),
|
||||||
|
window_id: Some(compacted_window_id.to_string()),
|
||||||
}),
|
}),
|
||||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||||
turn_id: compact_turn_id,
|
turn_id: compact_turn_id,
|
||||||
@@ -3105,7 +3117,7 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
|
|||||||
.await;
|
.await;
|
||||||
{
|
{
|
||||||
let mut state = sess.state.lock().await;
|
let mut state = sess.state.lock().await;
|
||||||
state.set_auto_compact_window_id(/*window_id*/ 99);
|
state.restore_auto_compact_window(/*window_number*/ 99, Uuid::now_v7());
|
||||||
}
|
}
|
||||||
|
|
||||||
handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 1).await;
|
handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 1).await;
|
||||||
@@ -3114,6 +3126,10 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
|
|||||||
|
|
||||||
assert_eq!(sess.clone_history().await.raw_items(), compacted_history);
|
assert_eq!(sess.clone_history().await.raw_items(), compacted_history);
|
||||||
assert!(sess.reference_context_item().await.is_none());
|
assert!(sess.reference_context_item().await.is_none());
|
||||||
|
assert_eq!(
|
||||||
|
sess.state.lock().await.auto_compact_window_id(),
|
||||||
|
compacted_window_id
|
||||||
|
);
|
||||||
assert!(sess.current_window_id().await.ends_with(":7"));
|
assert!(sess.current_window_id().await.ends_with(":7"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9846,10 +9862,12 @@ async fn sample_rollout(
|
|||||||
let user_messages1 = collect_user_messages(&snapshot1);
|
let user_messages1 = collect_user_messages(&snapshot1);
|
||||||
let rebuilt1 = compact::build_compacted_history(Vec::new(), &user_messages1, summary1);
|
let rebuilt1 = compact::build_compacted_history(Vec::new(), &user_messages1, summary1);
|
||||||
live_history.replace(rebuilt1);
|
live_history.replace(rebuilt1);
|
||||||
|
let (window_number, window_id) = session.advance_auto_compact_window().await;
|
||||||
rollout_items.push(RolloutItem::Compacted(CompactedItem {
|
rollout_items.push(RolloutItem::Compacted(CompactedItem {
|
||||||
message: summary1.to_string(),
|
message: summary1.to_string(),
|
||||||
replacement_history: None,
|
replacement_history: None,
|
||||||
window_id: None,
|
window_number: Some(window_number),
|
||||||
|
window_id: Some(window_id),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let user2 = ResponseItem::Message {
|
let user2 = ResponseItem::Message {
|
||||||
@@ -9889,10 +9907,12 @@ async fn sample_rollout(
|
|||||||
let user_messages2 = collect_user_messages(&snapshot2);
|
let user_messages2 = collect_user_messages(&snapshot2);
|
||||||
let rebuilt2 = compact::build_compacted_history(Vec::new(), &user_messages2, summary2);
|
let rebuilt2 = compact::build_compacted_history(Vec::new(), &user_messages2, summary2);
|
||||||
live_history.replace(rebuilt2);
|
live_history.replace(rebuilt2);
|
||||||
|
let (window_number, window_id) = session.advance_auto_compact_window().await;
|
||||||
rollout_items.push(RolloutItem::Compacted(CompactedItem {
|
rollout_items.push(RolloutItem::Compacted(CompactedItem {
|
||||||
message: summary2.to_string(),
|
message: summary2.to_string(),
|
||||||
replacement_history: None,
|
replacement_history: None,
|
||||||
window_id: None,
|
window_number: Some(window_number),
|
||||||
|
window_id: Some(window_id),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let user3 = ResponseItem::Message {
|
let user3 = ResponseItem::Message {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use codex_protocol::protocol::TokenUsage;
|
use codex_protocol::protocol::TokenUsage;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub(crate) struct AutoCompactWindowSnapshot {
|
pub(crate) struct AutoCompactWindowSnapshot {
|
||||||
@@ -13,7 +14,8 @@ enum AutoCompactWindowPrefill {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub(super) struct AutoCompactWindow {
|
pub(super) struct AutoCompactWindow {
|
||||||
window_id: u64,
|
window_number: u64,
|
||||||
|
window_id: Uuid,
|
||||||
new_context_window_requested: bool,
|
new_context_window_requested: bool,
|
||||||
/// Absolute input-token baseline for the current compaction window.
|
/// Absolute input-token baseline for the current compaction window.
|
||||||
///
|
///
|
||||||
@@ -26,7 +28,8 @@ pub(super) struct AutoCompactWindow {
|
|||||||
impl AutoCompactWindow {
|
impl AutoCompactWindow {
|
||||||
pub(super) fn new() -> Self {
|
pub(super) fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
window_id: 0,
|
window_number: 0,
|
||||||
|
window_id: Uuid::now_v7(),
|
||||||
new_context_window_requested: false,
|
new_context_window_requested: false,
|
||||||
prefill_input_tokens: None,
|
prefill_input_tokens: None,
|
||||||
}
|
}
|
||||||
@@ -36,18 +39,24 @@ impl AutoCompactWindow {
|
|||||||
self.prefill_input_tokens = None;
|
self.prefill_input_tokens = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn window_id(&self) -> u64 {
|
pub(super) fn window_number(&self) -> u64 {
|
||||||
|
self.window_number
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn window_id(&self) -> Uuid {
|
||||||
self.window_id
|
self.window_id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn set_window_id(&mut self, window_id: u64) {
|
pub(super) fn restore(&mut self, window_number: u64, window_id: Uuid) {
|
||||||
|
self.window_number = window_number;
|
||||||
self.window_id = window_id;
|
self.window_id = window_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn advance_window_id(&mut self) -> u64 {
|
pub(super) fn advance(&mut self) -> (u64, Uuid) {
|
||||||
self.window_id = self.window_id.saturating_add(1);
|
self.window_number = self.window_number.saturating_add(1);
|
||||||
|
self.window_id = Uuid::now_v7();
|
||||||
self.new_context_window_requested = false;
|
self.new_context_window_requested = false;
|
||||||
self.window_id
|
(self.window_number, self.window_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn request_new_context_window(&mut self) {
|
pub(super) fn request_new_context_window(&mut self) {
|
||||||
@@ -108,15 +117,22 @@ mod tests {
|
|||||||
fn tracks_prefill_and_window_boundaries() {
|
fn tracks_prefill_and_window_boundaries() {
|
||||||
let mut window = AutoCompactWindow::new();
|
let mut window = AutoCompactWindow::new();
|
||||||
|
|
||||||
assert_eq!(window.window_id(), 0);
|
assert_eq!(window.window_number(), 0);
|
||||||
window.set_window_id(/*window_id*/ 3);
|
assert_eq!(window.window_id().get_version_num(), 7);
|
||||||
assert_eq!(window.window_id(), 3);
|
let restored_window_id = Uuid::now_v7();
|
||||||
|
window.restore(/*window_number*/ 3, restored_window_id);
|
||||||
|
assert_eq!(window.window_number(), 3);
|
||||||
|
assert_eq!(window.window_id(), restored_window_id);
|
||||||
window.request_new_context_window();
|
window.request_new_context_window();
|
||||||
assert!(window.take_new_context_window_request());
|
assert!(window.take_new_context_window_request());
|
||||||
assert!(!window.take_new_context_window_request());
|
assert!(!window.take_new_context_window_request());
|
||||||
window.request_new_context_window();
|
window.request_new_context_window();
|
||||||
assert_eq!(window.advance_window_id(), 4);
|
let (window_number, window_id) = window.advance();
|
||||||
assert_eq!(window.window_id(), 4);
|
assert_eq!(window_number, 4);
|
||||||
|
assert_eq!(window.window_number(), 4);
|
||||||
|
assert_eq!(window.window_id(), window_id);
|
||||||
|
assert_eq!(window_id.get_version_num(), 7);
|
||||||
|
assert_ne!(window_id, restored_window_id);
|
||||||
assert!(!window.take_new_context_window_request());
|
assert!(!window.take_new_context_window_request());
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use codex_sandboxing::policy_transforms::merge_permission_profiles;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::AdditionalContextStore;
|
use super::AdditionalContextStore;
|
||||||
use super::auto_compact_window::AutoCompactWindow;
|
use super::auto_compact_window::AutoCompactWindow;
|
||||||
@@ -147,30 +148,34 @@ impl SessionState {
|
|||||||
self.auto_compact_window.snapshot()
|
self.auto_compact_window.snapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn auto_compact_window_id(&self) -> u64 {
|
pub(crate) fn auto_compact_window_number(&self) -> u64 {
|
||||||
|
self.auto_compact_window.window_number()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn auto_compact_window_id(&self) -> Uuid {
|
||||||
self.auto_compact_window.window_id()
|
self.auto_compact_window.window_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_auto_compact_window_id(&mut self, window_id: u64) {
|
pub(crate) fn restore_auto_compact_window(&mut self, window_number: u64, window_id: Uuid) {
|
||||||
self.auto_compact_window.set_window_id(window_id);
|
self.auto_compact_window.restore(window_number, window_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn advance_auto_compact_window_id(&mut self) -> u64 {
|
pub(crate) fn advance_auto_compact_window(&mut self) -> (u64, Uuid) {
|
||||||
self.auto_compact_window.advance_window_id()
|
self.auto_compact_window.advance()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn request_new_context_window(&mut self) {
|
pub(crate) fn request_new_context_window(&mut self) {
|
||||||
self.auto_compact_window.request_new_context_window();
|
self.auto_compact_window.request_new_context_window();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn start_new_context_window_if_requested(&mut self) -> Option<u64> {
|
pub(crate) fn start_new_context_window_if_requested(&mut self) -> Option<(u64, Uuid)> {
|
||||||
if !self.auto_compact_window.take_new_context_window_request() {
|
if !self.auto_compact_window.take_new_context_window_request() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let window_id = self.auto_compact_window.advance_window_id();
|
let window = self.auto_compact_window.advance();
|
||||||
self.auto_compact_window.clear_prefill();
|
self.auto_compact_window.clear_prefill();
|
||||||
Some(window_id)
|
Some(window)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn token_info(&self) -> Option<TokenUsageInfo> {
|
pub(crate) fn token_info(&self) -> Option<TokenUsageInfo> {
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ Scenario: New context window tool installs fresh full context before the next fo
|
|||||||
00:message/developer[3]:
|
00:message/developer[3]:
|
||||||
[01] <PERMISSIONS_INSTRUCTIONS>
|
[01] <PERMISSIONS_INSTRUCTIONS>
|
||||||
[02] <SKILLS_INSTRUCTIONS>
|
[02] <SKILLS_INSTRUCTIONS>
|
||||||
[03] <token_budget>\nThread id <THREAD_ID>.\nCurrent context window 1.\nYou have 121600 tokens left in this context window.\n</token_budget>
|
[03] <token_budget>\nThread id <THREAD_ID>.\nCurrent context window id <UUID>.\nYou have 121600 tokens left in this context window.\n</token_budget>
|
||||||
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
|
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
|
||||||
02:function_call/update_plan
|
02:function_call/update_plan
|
||||||
03:function_call_output:Plan updated
|
03:function_call_output:Plan updated
|
||||||
@@ -4,6 +4,7 @@ use codex_model_provider_info::built_in_model_providers;
|
|||||||
use codex_protocol::protocol::EventMsg;
|
use codex_protocol::protocol::EventMsg;
|
||||||
use codex_protocol::protocol::Op;
|
use codex_protocol::protocol::Op;
|
||||||
use core_test_support::PathBufExt;
|
use core_test_support::PathBufExt;
|
||||||
|
use core_test_support::assert_regex_match;
|
||||||
use core_test_support::context_snapshot;
|
use core_test_support::context_snapshot;
|
||||||
use core_test_support::context_snapshot::ContextSnapshotOptions;
|
use core_test_support::context_snapshot::ContextSnapshotOptions;
|
||||||
use core_test_support::responses::ResponsesRequest;
|
use core_test_support::responses::ResponsesRequest;
|
||||||
@@ -80,17 +81,17 @@ async fn token_budget_context_is_only_emitted_with_full_context() -> Result<()>
|
|||||||
assert_eq!(requests.len(), 2);
|
assert_eq!(requests.len(), 2);
|
||||||
|
|
||||||
let thread_id = test.session_configured.thread_id;
|
let thread_id = test.session_configured.thread_id;
|
||||||
let expected = vec![format!(
|
let initial_token_budget = token_budget_texts(&requests[0]);
|
||||||
"<token_budget>\nThread id {thread_id}.\nCurrent context window 0.\nYou have {EFFECTIVE_CONTEXT_WINDOW} tokens left in this context window.\n</token_budget>"
|
assert_eq!(initial_token_budget.len(), 1);
|
||||||
)];
|
assert_regex_match(
|
||||||
assert_eq!(
|
&format!(
|
||||||
token_budget_texts(&requests[0]),
|
r"^<token_budget>\nThread id {thread_id}\.\nCurrent context window id [0-9a-f-]{{36}}\.\nYou have {EFFECTIVE_CONTEXT_WINDOW} tokens left in this context window\.\n</token_budget>$"
|
||||||
expected,
|
),
|
||||||
"initial full context should report context window 0"
|
&initial_token_budget[0],
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
token_budget_texts(&requests[1]),
|
token_budget_texts(&requests[1]),
|
||||||
expected,
|
initial_token_budget,
|
||||||
"steady-state context update should not advance the context window"
|
"steady-state context update should not advance the context window"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -144,9 +145,15 @@ async fn token_budget_remaining_context_emits_on_first_threshold_crossing() -> R
|
|||||||
assert_eq!(requests.len(), 5);
|
assert_eq!(requests.len(), 5);
|
||||||
|
|
||||||
let thread_id = test.session_configured.thread_id;
|
let thread_id = test.session_configured.thread_id;
|
||||||
let full_context = format!(
|
let full_context = token_budget_texts(&requests[0]);
|
||||||
"<token_budget>\nThread id {thread_id}.\nCurrent context window 0.\nYou have 9500 tokens left in this context window.\n</token_budget>"
|
assert_eq!(full_context.len(), 1);
|
||||||
|
assert_regex_match(
|
||||||
|
&format!(
|
||||||
|
r"^<token_budget>\nThread id {thread_id}\.\nCurrent context window id [0-9a-f-]{{36}}\.\nYou have 9500 tokens left in this context window\.\n</token_budget>$"
|
||||||
|
),
|
||||||
|
&full_context[0],
|
||||||
);
|
);
|
||||||
|
let full_context = full_context[0].clone();
|
||||||
let threshold_25 =
|
let threshold_25 =
|
||||||
"<token_budget>\nYou have 7000 tokens left in this context window.\n</token_budget>"
|
"<token_budget>\nYou have 7000 tokens left in this context window.\n</token_budget>"
|
||||||
.to_string();
|
.to_string();
|
||||||
@@ -233,16 +240,18 @@ async fn get_context_remaining_returns_token_budget_remaining_fragment() -> Resu
|
|||||||
);
|
);
|
||||||
|
|
||||||
let thread_id = test.session_configured.thread_id;
|
let thread_id = test.session_configured.thread_id;
|
||||||
let full_context = format!(
|
|
||||||
"<token_budget>\nThread id {thread_id}.\nCurrent context window 0.\nYou have 9500 tokens left in this context window.\n</token_budget>"
|
|
||||||
);
|
|
||||||
let remaining_context =
|
let remaining_context =
|
||||||
"<token_budget>\nYou have 7000 tokens left in this context window.\n</token_budget>"
|
"<token_budget>\nYou have 7000 tokens left in this context window.\n</token_budget>"
|
||||||
.to_string();
|
.to_string();
|
||||||
assert_eq!(
|
let token_budgets = token_budget_texts(&requests[1]);
|
||||||
token_budget_texts(&requests[1]),
|
assert_eq!(token_budgets.len(), 2);
|
||||||
vec![full_context, remaining_context.clone()]
|
assert_regex_match(
|
||||||
|
&format!(
|
||||||
|
r"^<token_budget>\nThread id {thread_id}\.\nCurrent context window id [0-9a-f-]{{36}}\.\nYou have 9500 tokens left in this context window\.\n</token_budget>$"
|
||||||
|
),
|
||||||
|
&token_budgets[0],
|
||||||
);
|
);
|
||||||
|
assert_eq!(token_budgets[1], remaining_context);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
requests[2].function_call_output_content_and_success(call_id),
|
requests[2].function_call_output_content_and_success(call_id),
|
||||||
Some((Some(remaining_context), None))
|
Some((Some(remaining_context), None))
|
||||||
@@ -362,13 +371,31 @@ async fn token_budget_context_uses_new_window_after_compaction() -> Result<()> {
|
|||||||
assert_eq!(requests.len(), 3);
|
assert_eq!(requests.len(), 3);
|
||||||
|
|
||||||
let thread_id = test.session_configured.thread_id;
|
let thread_id = test.session_configured.thread_id;
|
||||||
assert_eq!(
|
let initial_token_budget = token_budget_texts(&requests[0]);
|
||||||
token_budget_texts(&requests[2]),
|
assert_eq!(initial_token_budget.len(), 1);
|
||||||
vec![format!(
|
let initial_window_id = assert_regex_match(
|
||||||
"<token_budget>\nThread id {thread_id}.\nCurrent context window 1.\nYou have {EFFECTIVE_CONTEXT_WINDOW} tokens left in this context window.\n</token_budget>"
|
&format!(
|
||||||
)],
|
r"^<token_budget>\nThread id {thread_id}\.\nCurrent context window id ([0-9a-f-]{{36}})\.\nYou have {EFFECTIVE_CONTEXT_WINDOW} tokens left in this context window\.\n</token_budget>$"
|
||||||
"post-compaction full context should report context window 1"
|
),
|
||||||
);
|
&initial_token_budget[0],
|
||||||
|
)
|
||||||
|
.get(1)
|
||||||
|
.expect("window id capture")
|
||||||
|
.as_str()
|
||||||
|
.to_string();
|
||||||
|
let post_compaction_token_budget = token_budget_texts(&requests[2]);
|
||||||
|
assert_eq!(post_compaction_token_budget.len(), 1);
|
||||||
|
let post_compaction_window_id = assert_regex_match(
|
||||||
|
&format!(
|
||||||
|
r"^<token_budget>\nThread id {thread_id}\.\nCurrent context window id ([0-9a-f-]{{36}})\.\nYou have {EFFECTIVE_CONTEXT_WINDOW} tokens left in this context window\.\n</token_budget>$"
|
||||||
|
),
|
||||||
|
&post_compaction_token_budget[0],
|
||||||
|
)
|
||||||
|
.get(1)
|
||||||
|
.expect("window id capture")
|
||||||
|
.as_str()
|
||||||
|
.to_string();
|
||||||
|
assert_ne!(post_compaction_window_id, initial_window_id);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -429,12 +456,31 @@ async fn new_context_tool_starts_new_window_before_follow_up() -> Result<()> {
|
|||||||
"new_context should be exposed when token budget is enabled"
|
"new_context should be exposed when token budget is enabled"
|
||||||
);
|
);
|
||||||
let thread_id = test.session_configured.thread_id;
|
let thread_id = test.session_configured.thread_id;
|
||||||
assert_eq!(
|
let initial_token_budget = token_budget_texts(&requests[0]);
|
||||||
token_budget_texts(&requests[2]),
|
assert_eq!(initial_token_budget.len(), 1);
|
||||||
vec![format!(
|
let initial_window_id = assert_regex_match(
|
||||||
"<token_budget>\nThread id {thread_id}.\nCurrent context window 1.\nYou have {EFFECTIVE_CONTEXT_WINDOW} tokens left in this context window.\n</token_budget>"
|
&format!(
|
||||||
)]
|
r"^<token_budget>\nThread id {thread_id}\.\nCurrent context window id ([0-9a-f-]{{36}})\.\nYou have {EFFECTIVE_CONTEXT_WINDOW} tokens left in this context window\.\n</token_budget>$"
|
||||||
);
|
),
|
||||||
|
&initial_token_budget[0],
|
||||||
|
)
|
||||||
|
.get(1)
|
||||||
|
.expect("window id capture")
|
||||||
|
.as_str()
|
||||||
|
.to_string();
|
||||||
|
let new_window_token_budget = token_budget_texts(&requests[2]);
|
||||||
|
assert_eq!(new_window_token_budget.len(), 1);
|
||||||
|
let window_id = assert_regex_match(
|
||||||
|
&format!(
|
||||||
|
r"^<token_budget>\nThread id {thread_id}\.\nCurrent context window id ([0-9a-f-]{{36}})\.\nYou have {EFFECTIVE_CONTEXT_WINDOW} tokens left in this context window\.\n</token_budget>$"
|
||||||
|
),
|
||||||
|
&new_window_token_budget[0],
|
||||||
|
)
|
||||||
|
.get(1)
|
||||||
|
.expect("window id capture")
|
||||||
|
.as_str()
|
||||||
|
.to_string();
|
||||||
|
assert_ne!(window_id, initial_window_id);
|
||||||
assert!(
|
assert!(
|
||||||
!requests[2].body_contains_text("request new context window"),
|
!requests[2].body_contains_text("request new context window"),
|
||||||
"new_context should drop the prior window history before continuing the turn"
|
"new_context should drop the prior window history before continuing the turn"
|
||||||
@@ -448,7 +494,9 @@ async fn new_context_tool_starts_new_window_before_follow_up() -> Result<()> {
|
|||||||
&[("Final Follow-Up Request", &requests[2])],
|
&[("Final Follow-Up Request", &requests[2])],
|
||||||
&ContextSnapshotOptions::default(),
|
&ContextSnapshotOptions::default(),
|
||||||
);
|
);
|
||||||
let snapshot = snapshot.replace(&thread_id.to_string(), "<THREAD_ID>");
|
let snapshot = snapshot
|
||||||
|
.replace(&thread_id.to_string(), "<THREAD_ID>")
|
||||||
|
.replace(&window_id, "<UUID>");
|
||||||
insta::assert_snapshot!(
|
insta::assert_snapshot!(
|
||||||
"token_budget_new_context_window_tool_full_context",
|
"token_budget_new_context_window_tool_full_context",
|
||||||
snapshot
|
snapshot
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
use crate::models::ResponseItem;
|
||||||
|
use crate::protocol::CompactedItem;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
// Before `window_number` was introduced, the numeric window number was serialized as
|
||||||
|
// `window_id`. Accept that shape so existing rollouts remain resumable.
|
||||||
|
impl<'de> Deserialize<'de> for CompactedItem {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let serialized = SerializedCompactedItem::deserialize(deserializer)?;
|
||||||
|
let mut window_number = serialized.window_number;
|
||||||
|
let window_id = match serialized.window_id {
|
||||||
|
Some(SerializedWindowId::Id(window_id)) => Some(window_id),
|
||||||
|
Some(SerializedWindowId::LegacyWindowNumber(legacy_window_number)) => {
|
||||||
|
window_number.get_or_insert(legacy_window_number);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
Ok(Self {
|
||||||
|
message: serialized.message,
|
||||||
|
replacement_history: serialized.replacement_history,
|
||||||
|
window_number,
|
||||||
|
window_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct SerializedCompactedItem {
|
||||||
|
message: String,
|
||||||
|
#[serde(default)]
|
||||||
|
replacement_history: Option<Vec<ResponseItem>>,
|
||||||
|
#[serde(default)]
|
||||||
|
window_number: Option<u64>,
|
||||||
|
#[serde(default)]
|
||||||
|
window_id: Option<SerializedWindowId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum SerializedWindowId {
|
||||||
|
Id(String),
|
||||||
|
LegacyWindowNumber(u64),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use anyhow::Result;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serializes_window_number_and_id() -> Result<()> {
|
||||||
|
let item = CompactedItem {
|
||||||
|
message: "summary".to_string(),
|
||||||
|
replacement_history: None,
|
||||||
|
window_number: Some(3),
|
||||||
|
window_id: Some("019b3f6e-7a10-7cc3-8b6e-1d09e2f7a001".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_value(item)?,
|
||||||
|
json!({
|
||||||
|
"message": "summary",
|
||||||
|
"window_number": 3,
|
||||||
|
"window_id": "019b3f6e-7a10-7cc3-8b6e-1d09e2f7a001",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn migrates_legacy_numeric_window_id() -> Result<()> {
|
||||||
|
let item = serde_json::from_value::<CompactedItem>(json!({
|
||||||
|
"message": "summary",
|
||||||
|
"window_id": 3,
|
||||||
|
}))?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
item,
|
||||||
|
CompactedItem {
|
||||||
|
message: "summary".to_string(),
|
||||||
|
replacement_history: None,
|
||||||
|
window_number: Some(3),
|
||||||
|
window_id: None,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ pub use thread_id::ThreadId;
|
|||||||
pub use tool_name::ToolName;
|
pub use tool_name::ToolName;
|
||||||
pub mod approvals;
|
pub mod approvals;
|
||||||
pub mod capabilities;
|
pub mod capabilities;
|
||||||
|
mod compacted_item;
|
||||||
pub mod config_types;
|
pub mod config_types;
|
||||||
pub mod dynamic_tools;
|
pub mod dynamic_tools;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|||||||
@@ -2971,13 +2971,17 @@ pub enum RolloutItem {
|
|||||||
EventMsg(EventMsg),
|
EventMsg(EventMsg),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
|
#[derive(Serialize, Clone, Debug, PartialEq, JsonSchema, TS)]
|
||||||
pub struct CompactedItem {
|
pub struct CompactedItem {
|
||||||
pub message: String,
|
pub message: String,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub replacement_history: Option<Vec<ResponseItem>>,
|
pub replacement_history: Option<Vec<ResponseItem>>,
|
||||||
|
/// Monotonic position of this context window within the thread.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub window_id: Option<u64>,
|
pub window_number: Option<u64>,
|
||||||
|
/// UUIDv7 identity of this context window.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub window_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<CompactedItem> for ResponseItem {
|
impl From<CompactedItem> for ResponseItem {
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ fn builder_from_items_falls_back_to_filename() {
|
|||||||
let items = vec![RolloutItem::Compacted(CompactedItem {
|
let items = vec![RolloutItem::Compacted(CompactedItem {
|
||||||
message: "noop".to_string(),
|
message: "noop".to_string(),
|
||||||
replacement_history: None,
|
replacement_history: None,
|
||||||
|
window_number: None,
|
||||||
window_id: None,
|
window_id: None,
|
||||||
})];
|
})];
|
||||||
|
|
||||||
|
|||||||
@@ -483,6 +483,7 @@ mod tests {
|
|||||||
let item = RolloutItem::Compacted(CompactedItem {
|
let item = RolloutItem::Compacted(CompactedItem {
|
||||||
message: "compacted".to_string(),
|
message: "compacted".to_string(),
|
||||||
replacement_history: None,
|
replacement_history: None,
|
||||||
|
window_number: None,
|
||||||
window_id: None,
|
window_id: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user