codex_rollout/
model_context.rs1use codex_protocol::items::TurnItem;
2use codex_protocol::models::ResponseItem;
3use codex_protocol::protocol::EventMsg;
4use codex_protocol::protocol::InterAgentCommunication;
5use codex_protocol::protocol::RolloutItem;
6use codex_protocol::protocol::SessionMetaLine;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum ModelContextScanProgress {
11 Continue,
13 Complete,
15}
16
17#[derive(Debug, Default)]
48pub struct ModelContextScan {
49 items_newest_first: Vec<RolloutItem>,
50 saw_compaction: bool,
51 saw_completed_turn_context: bool,
52 must_scan_to_start: bool,
53 active_segment: ActiveTurnSegment,
54}
55
56impl ModelContextScan {
57 pub fn push(&mut self, item: RolloutItem) -> ModelContextScanProgress {
59 let progress = self.observe(&item);
60 self.items_newest_first.push(item);
61 progress
62 }
63
64 pub fn finish(mut self, session_meta: SessionMetaLine) -> Vec<RolloutItem> {
69 self.items_newest_first.reverse();
70 if self.has_bounded_cutoff() {
71 self.items_newest_first
75 .insert(0, RolloutItem::SessionMeta(session_meta));
76 }
77 self.items_newest_first
78 }
79
80 fn observe(&mut self, item: &RolloutItem) -> ModelContextScanProgress {
81 if self.must_scan_to_start {
82 return ModelContextScanProgress::Continue;
83 }
84
85 match item {
86 RolloutItem::Compacted(compacted)
87 if compacted.replacement_history.is_none() || compacted.window_number.is_none() =>
88 {
89 self.must_scan_to_start = true;
90 }
91 RolloutItem::Compacted(_) => {
92 self.saw_compaction = true;
93 }
94 RolloutItem::EventMsg(EventMsg::ThreadRolledBack(_)) => {
95 self.must_scan_to_start = true;
98 }
99 RolloutItem::EventMsg(EventMsg::ItemCompleted(event)) => {
100 if self.active_segment.turn_id.is_none() {
101 self.active_segment.turn_id = Some(event.turn_id.clone());
102 }
103 if turn_ids_are_compatible(
104 self.active_segment.turn_id.as_deref(),
105 Some(event.turn_id.as_str()),
106 ) {
107 self.active_segment.has_user_turn |=
108 matches!(&event.item, TurnItem::UserMessage(_));
109 }
110 }
111 RolloutItem::EventMsg(EventMsg::TurnComplete(event)) => {
112 self.active_segment
113 .turn_id
114 .get_or_insert_with(|| event.turn_id.clone());
115 }
116 RolloutItem::EventMsg(EventMsg::TurnAborted(event)) => {
117 if let Some(turn_id) = &event.turn_id {
118 self.active_segment
119 .turn_id
120 .get_or_insert_with(|| turn_id.clone());
121 }
122 }
123 RolloutItem::EventMsg(EventMsg::TurnStarted(event)) => {
124 if turn_ids_are_compatible(
125 self.active_segment.turn_id.as_deref(),
126 Some(event.turn_id.as_str()),
127 ) {
128 self.finalize_active_segment();
129 }
130 }
131 RolloutItem::TurnContext(context) => {
132 if self.active_segment.turn_id.is_none() {
133 self.active_segment.turn_id = context.turn_id.clone();
134 }
135 if turn_ids_are_compatible(
136 self.active_segment.turn_id.as_deref(),
137 context.turn_id.as_deref(),
138 ) {
139 self.active_segment.has_turn_context = true;
140 }
141 }
142 RolloutItem::ResponseItem(response_item) => {
143 self.active_segment.has_user_turn |=
144 response_item_counts_as_user_turn(response_item);
145 }
146 RolloutItem::InterAgentCommunication(_) => {
147 self.active_segment.has_user_turn = true;
148 }
149 RolloutItem::EventMsg(EventMsg::UserMessage(_)) => {
150 self.active_segment.has_user_turn = true;
151 }
152 RolloutItem::EventMsg(_)
153 | RolloutItem::SessionMeta(_)
154 | RolloutItem::InterAgentCommunicationMetadata { .. }
155 | RolloutItem::WorldState(_) => {}
156 }
157
158 if self.has_bounded_cutoff() {
159 ModelContextScanProgress::Complete
160 } else {
161 ModelContextScanProgress::Continue
162 }
163 }
164
165 fn finalize_active_segment(&mut self) {
166 if self.active_segment.has_user_turn && self.active_segment.has_turn_context {
167 self.saw_completed_turn_context = true;
168 }
169 self.active_segment = ActiveTurnSegment::default();
170 }
171
172 fn has_bounded_cutoff(&self) -> bool {
173 !self.must_scan_to_start && self.saw_compaction && self.saw_completed_turn_context
174 }
175}
176
177#[derive(Debug, Default)]
178struct ActiveTurnSegment {
179 turn_id: Option<String>,
180 has_user_turn: bool,
181 has_turn_context: bool,
182}
183
184fn turn_ids_are_compatible(active_turn_id: Option<&str>, item_turn_id: Option<&str>) -> bool {
185 active_turn_id
186 .is_none_or(|turn_id| item_turn_id.is_none_or(|item_turn_id| item_turn_id == turn_id))
187}
188
189fn response_item_counts_as_user_turn(response_item: &ResponseItem) -> bool {
190 match response_item {
191 ResponseItem::AgentMessage { .. } => true,
192 ResponseItem::Message { role, content, .. } => {
193 role == "assistant" && InterAgentCommunication::is_message_content(content)
194 }
195 _ => false,
196 }
197}