1use time::OffsetDateTime;
2
3use crate::events::{EventEnvelope, RoderEvent, ThreadId};
4
5use super::{
6 ThreadItem, ThreadItemDelta, ThreadItemEvent, ThreadItemEventKind, ThreadItemStatus,
7 ThreadItemTurnRecord, TurnRecord,
8};
9
10pub fn project_turns_from_events(
15 thread_id: &ThreadId,
16 events: &[EventEnvelope],
17) -> Vec<TurnRecord> {
18 let mut turns = Vec::new();
19 for envelope in events {
20 match &envelope.event {
21 RoderEvent::TurnStarted(event) => {
22 ensure_turn_record(&mut turns, thread_id, &event.turn_id, event.timestamp);
23 }
24 RoderEvent::TranscriptItemAppended(event) => {
25 let turn =
26 ensure_turn_record(&mut turns, thread_id, &event.turn_id, event.timestamp);
27 if let Some(item) = &event.item {
28 turn.items.push(item.clone());
29 }
30 }
31 RoderEvent::TurnCompleted(event) => {
32 let turn =
33 ensure_turn_record(&mut turns, thread_id, &event.turn_id, event.timestamp);
34 turn.completed_at = Some(event.timestamp);
35 turn.usage = event.usage.clone();
36 turn.finish_reason = event.finish_reason.clone();
37 }
38 RoderEvent::TurnFailed(event) => {
39 let turn =
40 ensure_turn_record(&mut turns, thread_id, &event.turn_id, event.timestamp);
41 turn.completed_at = Some(event.timestamp);
42 turn.usage = event.usage.clone();
43 }
44 RoderEvent::TurnInterrupted(event) => {
45 let turn =
46 ensure_turn_record(&mut turns, thread_id, &event.turn_id, event.timestamp);
47 turn.completed_at = Some(event.timestamp);
48 turn.usage = event.usage.clone();
49 }
50 _ => continue,
51 }
52 }
53 turns
54}
55
56fn ensure_turn_record<'a>(
57 turns: &'a mut Vec<TurnRecord>,
58 thread_id: &ThreadId,
59 turn_id: &str,
60 created_at: OffsetDateTime,
61) -> &'a mut TurnRecord {
62 if let Some(index) = turns.iter().position(|turn| turn.turn_id == turn_id) {
63 return &mut turns[index];
64 }
65 turns.push(TurnRecord {
66 thread_id: thread_id.clone(),
67 turn_id: turn_id.to_string(),
68 items: Vec::new(),
69 created_at,
70 completed_at: None,
71 usage: None,
72 finish_reason: None,
73 });
74 turns.last_mut().expect("turn was just pushed")
75}
76
77pub fn project_thread_item_events(events: &[ThreadItemEvent]) -> Vec<ThreadItemTurnRecord> {
78 let mut turns = Vec::<ThreadItemTurnRecord>::new();
79 for event in events {
80 let turn_index = turns
81 .iter()
82 .position(|turn| turn.turn_id == event.turn_id)
83 .unwrap_or_else(|| {
84 turns.push(ThreadItemTurnRecord {
85 thread_id: event.thread_id.clone(),
86 turn_id: event.turn_id.clone(),
87 created_at: event.timestamp,
88 items: Vec::new(),
89 });
90 turns.len() - 1
91 });
92 apply_thread_item_event(&mut turns[turn_index].items, &event.event);
93 }
94 turns
95}
96
97fn apply_thread_item_event(items: &mut Vec<ThreadItem>, event: &ThreadItemEventKind) {
98 match event {
99 ThreadItemEventKind::ItemStarted { item } => {
100 if !upsert_item_if_missing(items, item.clone()) {
101 merge_started_item(items, item);
102 }
103 }
104 ThreadItemEventKind::ItemDelta { item_id, delta } => {
105 let index = item_index(items, item_id).unwrap_or_else(|| {
106 items.push(item_for_delta(item_id, delta));
107 items.len() - 1
108 });
109 apply_thread_item_delta(&mut items[index], delta);
110 }
111 ThreadItemEventKind::ItemCompleted { item } => {
112 if let Some(index) = item_index(items, item.id()) {
113 merge_completed_item(&mut items[index], item.clone());
114 } else {
115 items.push(item.clone());
116 }
117 }
118 }
119}
120
121fn upsert_item_if_missing(items: &mut Vec<ThreadItem>, item: ThreadItem) -> bool {
122 if item_index(items, item.id()).is_some() {
123 return false;
124 }
125 items.push(item);
126 true
127}
128
129fn merge_started_item(items: &mut [ThreadItem], incoming: &ThreadItem) {
130 if let Some(index) = item_index(items, incoming.id()) {
131 merge_completed_item(&mut items[index], incoming.clone());
132 }
133}
134
135fn item_index(items: &[ThreadItem], item_id: &str) -> Option<usize> {
136 items.iter().position(|item| item.id() == item_id)
137}
138
139fn item_for_delta(item_id: &str, delta: &ThreadItemDelta) -> ThreadItem {
140 match delta {
141 ThreadItemDelta::AgentMessageText { phase, .. } => ThreadItem::AgentMessage {
142 id: item_id.to_string(),
143 text: String::new(),
144 phase: phase.clone(),
145 status: Some(ThreadItemStatus::InProgress),
146 },
147 ThreadItemDelta::ReasoningText { content_index, .. } => ThreadItem::Reasoning {
148 id: item_id.to_string(),
149 summary: Vec::new(),
150 content: vec![String::new(); content_index + 1],
151 status: Some(ThreadItemStatus::InProgress),
152 },
153 ThreadItemDelta::ReasoningSummaryPartAdded { summary_index }
154 | ThreadItemDelta::ReasoningSummaryText { summary_index, .. } => ThreadItem::Reasoning {
155 id: item_id.to_string(),
156 summary: vec![String::new(); summary_index + 1],
157 content: Vec::new(),
158 status: Some(ThreadItemStatus::InProgress),
159 },
160 }
161}
162
163fn apply_thread_item_delta(item: &mut ThreadItem, delta: &ThreadItemDelta) {
164 match (item, delta) {
165 (
166 ThreadItem::AgentMessage {
167 text,
168 phase,
169 status,
170 ..
171 },
172 ThreadItemDelta::AgentMessageText {
173 delta,
174 phase: delta_phase,
175 },
176 ) => {
177 text.push_str(delta);
178 if phase.is_none() {
179 *phase = delta_phase.clone();
180 }
181 *status = Some(ThreadItemStatus::InProgress);
182 }
183 (
184 ThreadItem::Reasoning {
185 content, status, ..
186 },
187 ThreadItemDelta::ReasoningText {
188 delta,
189 content_index,
190 },
191 ) => {
192 ensure_vec_slot(content, *content_index);
193 content[*content_index].push_str(delta);
194 *status = Some(ThreadItemStatus::InProgress);
195 }
196 (
197 ThreadItem::Reasoning {
198 summary, status, ..
199 },
200 ThreadItemDelta::ReasoningSummaryPartAdded { summary_index },
201 ) => {
202 ensure_vec_slot(summary, *summary_index);
203 *status = Some(ThreadItemStatus::InProgress);
204 }
205 (
206 ThreadItem::Reasoning {
207 summary, status, ..
208 },
209 ThreadItemDelta::ReasoningSummaryText {
210 delta,
211 summary_index,
212 },
213 ) => {
214 ensure_vec_slot(summary, *summary_index);
215 summary[*summary_index].push_str(delta);
216 *status = Some(ThreadItemStatus::InProgress);
217 }
218 (item, delta) => {
219 *item = item_for_delta(item.id(), delta);
220 apply_thread_item_delta(item, delta);
221 }
222 }
223}
224
225fn ensure_vec_slot(values: &mut Vec<String>, index: usize) {
226 while values.len() <= index {
227 values.push(String::new());
228 }
229}
230
231fn merge_completed_item(existing: &mut ThreadItem, incoming: ThreadItem) {
232 match (&mut *existing, incoming) {
233 (
234 ThreadItem::Reasoning {
235 summary,
236 content,
237 status,
238 ..
239 },
240 ThreadItem::Reasoning {
241 summary: incoming_summary,
242 content: incoming_content,
243 status: incoming_status,
244 ..
245 },
246 ) => {
247 if !incoming_summary.is_empty() {
248 *summary = incoming_summary;
249 }
250 if !incoming_content.is_empty() {
251 *content = incoming_content;
252 }
253 *status = incoming_status.or(Some(ThreadItemStatus::Completed));
254 }
255 (
256 ThreadItem::ToolExecution {
257 status,
258 input,
259 output,
260 error,
261 ..
262 },
263 ThreadItem::ToolExecution {
264 status: incoming_status,
265 input: incoming_input,
266 output: incoming_output,
267 error: incoming_error,
268 ..
269 },
270 ) => {
271 *status = incoming_status;
272 if incoming_input.is_some() {
273 *input = incoming_input;
274 }
275 if incoming_output.is_some() {
276 *output = incoming_output;
277 }
278 if incoming_error.is_some() {
279 *error = incoming_error;
280 }
281 }
282 (slot, incoming) => *slot = incoming,
283 }
284}