use crate::event::{EventKind, EventRow};
use std::collections::{HashMap, HashSet};
pub fn assign_steps(events: &mut [EventRow]) {
events.sort_by_key(|e| (e.ts_ms, e.id));
let mut present: HashSet<i64> = HashSet::with_capacity(events.len());
let mut is_call: HashSet<i64> = HashSet::new();
for e in events.iter() {
present.insert(e.id);
if e.kind == EventKind::ToolCall {
is_call.insert(e.id);
}
}
let mut call_step: HashMap<i64, i64> = HashMap::new();
let mut deferred: Vec<usize> = Vec::new();
let mut counter: i64 = 0;
for (i, e) in events.iter_mut().enumerate() {
if e.kind == EventKind::TerminalOutput {
e.step = None;
continue;
}
let defer = e.kind == EventKind::ToolResult
&& matches!(e.correlates, Some(cid) if present.contains(&cid) && is_call.contains(&cid));
if defer {
deferred.push(i);
continue;
}
counter += 1;
e.step = Some(counter);
if e.kind == EventKind::ToolCall {
call_step.insert(e.id, counter);
}
}
for &i in &deferred {
let e = &mut events[i];
let Some(cid) = e.correlates else {
counter += 1;
e.step = Some(counter);
continue;
};
if let Some(&step) = call_step.get(&cid) {
e.step = Some(step);
} else {
counter += 1;
e.step = Some(counter);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::{EventKind, EventRow};
fn row(id: i64, ts_ms: i64, kind: EventKind, correlates: Option<i64>) -> EventRow {
EventRow {
id,
session_id: "s".into(),
ts_ms,
kind,
step: None,
correlates,
}
}
fn steps(rows: &[EventRow]) -> Vec<Option<i64>> {
rows.iter().map(|r| r.step).collect()
}
#[test]
fn assign_steps_normal_call_result_share() {
let mut rows = vec![
row(1, 0, EventKind::ToolCall, None),
row(2, 5, EventKind::ToolResult, Some(1)),
];
assign_steps(&mut rows);
assert_eq!(steps(&rows), vec![Some(1), Some(1)]);
}
#[test]
fn assign_steps_result_before_call() {
let mut rows = vec![
row(2, 0, EventKind::ToolResult, Some(1)),
row(1, 10, EventKind::ToolCall, None),
];
assign_steps(&mut rows);
assert_eq!(steps(&rows), vec![Some(1), Some(1)]);
}
#[test]
fn assign_steps_orphan_result_own_step() {
let mut rows = vec![
row(1, 0, EventKind::UserMessage, None),
row(2, 5, EventKind::ToolResult, Some(99)),
];
assign_steps(&mut rows);
assert_eq!(steps(&rows), vec![Some(1), Some(2)]);
}
#[test]
fn assign_steps_none_correlates_own_step() {
let mut rows = vec![
row(1, 0, EventKind::ToolCall, None),
row(2, 5, EventKind::ToolResult, None),
];
assign_steps(&mut rows);
assert_eq!(steps(&rows), vec![Some(1), Some(2)]);
}
#[test]
fn assign_steps_terminal_output_null() {
let mut rows = vec![
row(1, 0, EventKind::UserMessage, None),
row(2, 1, EventKind::TerminalOutput, None),
row(3, 2, EventKind::AgentMessage, None),
];
assign_steps(&mut rows);
assert_eq!(steps(&rows), vec![Some(1), None, Some(2)]);
}
#[test]
fn assign_steps_multiple_pairs() {
let mut rows = vec![
row(1, 0, EventKind::ToolCall, None),
row(2, 1, EventKind::ToolResult, Some(1)),
row(3, 10, EventKind::ToolCall, None),
row(4, 11, EventKind::ToolResult, Some(3)),
];
assign_steps(&mut rows);
assert_eq!(steps(&rows), vec![Some(1), Some(1), Some(2), Some(2)]);
}
#[test]
fn assign_steps_thinking_own_step() {
let mut rows = vec![
row(1, 0, EventKind::Thinking, None),
row(2, 5, EventKind::AgentMessage, None),
];
assign_steps(&mut rows);
assert_eq!(steps(&rows), vec![Some(1), Some(2)]);
}
#[test]
fn assign_steps_idempotent() {
let mut rows = vec![
row(1, 0, EventKind::ToolCall, None),
row(2, 5, EventKind::ToolResult, Some(1)),
row(3, 6, EventKind::AgentMessage, None),
];
assign_steps(&mut rows);
let first = steps(&rows);
assign_steps(&mut rows);
assert_eq!(steps(&rows), first, "re-running must be idempotent");
assert_eq!(first, vec![Some(1), Some(1), Some(2)]);
}
#[test]
fn assign_steps_result_pointing_at_non_call_present_id_gets_own_step() {
let mut rows = vec![
row(1, 0, EventKind::UserMessage, None),
row(2, 5, EventKind::ToolResult, Some(1)),
];
assign_steps(&mut rows);
assert_eq!(steps(&rows), vec![Some(1), Some(2)]);
}
}