Skip to main content

harness/
event.rs

1use serde::{Deserialize, Serialize};
2
3/// Returns the current epoch time in milliseconds.
4pub fn now_ms() -> u64 {
5    std::time::SystemTime::now()
6        .duration_since(std::time::UNIX_EPOCH)
7        .unwrap_or_default()
8        .as_millis() as u64
9}
10
11/// Token usage and cost data.
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
13pub struct UsageData {
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub input_tokens: Option<u64>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub output_tokens: Option<u64>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub cache_read_tokens: Option<u64>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub cache_creation_tokens: Option<u64>,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub cost_usd: Option<f64>,
24}
25
26/// Unified event stream — the common language spoken by all agent adapters.
27///
28/// Every adapter translates its native streaming output into this enum so that
29/// consumers only need to handle one set of types regardless of which backend
30/// is running.
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
32#[serde(tag = "type", rename_all = "snake_case")]
33pub enum Event {
34    /// The agent session has been initialized.
35    SessionStart(SessionStartEvent),
36
37    /// A chunk of assistant text (streaming delta).
38    TextDelta(TextDeltaEvent),
39
40    /// A complete assistant message.
41    Message(MessageEvent),
42
43    /// The agent is invoking a tool.
44    ToolStart(ToolStartEvent),
45
46    /// A tool invocation has completed.
47    ToolEnd(ToolEndEvent),
48
49    /// Incremental usage/cost update.
50    UsageDelta(UsageDeltaEvent),
51
52    /// The agent run has finished.
53    Result(ResultEvent),
54
55    /// An error occurred during the run.
56    Error(ErrorEvent),
57}
58
59impl Event {
60    /// Stamp the event with the current wall-clock time (epoch ms).
61    pub fn stamp(self) -> Self {
62        let ts = now_ms();
63        match self {
64            Event::SessionStart(mut e) => { e.timestamp_ms = ts; Event::SessionStart(e) }
65            Event::TextDelta(mut e) => { e.timestamp_ms = ts; Event::TextDelta(e) }
66            Event::Message(mut e) => { e.timestamp_ms = ts; Event::Message(e) }
67            Event::ToolStart(mut e) => { e.timestamp_ms = ts; Event::ToolStart(e) }
68            Event::ToolEnd(mut e) => { e.timestamp_ms = ts; Event::ToolEnd(e) }
69            Event::UsageDelta(mut e) => { e.timestamp_ms = ts; Event::UsageDelta(e) }
70            Event::Result(mut e) => { e.timestamp_ms = ts; Event::Result(e) }
71            Event::Error(mut e) => { e.timestamp_ms = ts; Event::Error(e) }
72        }
73    }
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
77pub struct SessionStartEvent {
78    pub session_id: String,
79    pub agent: String,
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub model: Option<String>,
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub cwd: Option<String>,
84    #[serde(default)]
85    pub timestamp_ms: u64,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
89pub struct TextDeltaEvent {
90    pub text: String,
91    #[serde(default)]
92    pub timestamp_ms: u64,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
96pub struct MessageEvent {
97    pub role: Role,
98    pub text: String,
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub usage: Option<UsageData>,
101    #[serde(default)]
102    pub timestamp_ms: u64,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
106#[serde(rename_all = "snake_case")]
107pub enum Role {
108    Assistant,
109    User,
110    System,
111}
112
113impl std::fmt::Display for Role {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        match self {
116            Role::Assistant => f.write_str("assistant"),
117            Role::User => f.write_str("user"),
118            Role::System => f.write_str("system"),
119        }
120    }
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
124pub struct ToolStartEvent {
125    pub call_id: String,
126    pub tool_name: String,
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub input: Option<serde_json::Value>,
129    #[serde(default)]
130    pub timestamp_ms: u64,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
134pub struct ToolEndEvent {
135    pub call_id: String,
136    pub tool_name: String,
137    pub success: bool,
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub output: Option<String>,
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub usage: Option<UsageData>,
142    #[serde(default)]
143    pub timestamp_ms: u64,
144}
145
146/// Incremental usage report emitted during streaming.
147#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
148pub struct UsageDeltaEvent {
149    pub usage: UsageData,
150    #[serde(default)]
151    pub timestamp_ms: u64,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
155pub struct ResultEvent {
156    pub success: bool,
157    pub text: String,
158    pub session_id: String,
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub duration_ms: Option<u64>,
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub total_cost_usd: Option<f64>,
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub usage: Option<UsageData>,
165    #[serde(default)]
166    pub timestamp_ms: u64,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
170pub struct ErrorEvent {
171    pub message: String,
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub code: Option<String>,
174    #[serde(default)]
175    pub timestamp_ms: u64,
176}
177
178// ─── Aggregation helpers ────────────────────────────────────────
179
180/// Sum all cost_usd values from UsageDelta and Result events.
181pub fn sum_costs(events: &[Event]) -> f64 {
182    let mut total = 0.0;
183    for event in events {
184        match event {
185            Event::UsageDelta(u) => {
186                if let Some(c) = u.usage.cost_usd {
187                    total += c;
188                }
189            }
190            Event::Result(r) => {
191                if let Some(c) = r.total_cost_usd {
192                    total += c;
193                }
194            }
195            _ => {}
196        }
197    }
198    total
199}
200
201/// Sum all input and output tokens from UsageDelta events.
202pub fn total_tokens(events: &[Event]) -> (u64, u64) {
203    let mut input = 0u64;
204    let mut output = 0u64;
205    for event in events {
206        if let Event::UsageDelta(u) = event {
207            if let Some(i) = u.usage.input_tokens {
208                input += i;
209            }
210            if let Some(o) = u.usage.output_tokens {
211                output += o;
212            }
213        }
214    }
215    (input, output)
216}
217
218/// Extract paired ToolStart/ToolEnd events by call_id.
219pub fn extract_tool_calls(events: &[Event]) -> Vec<(&ToolStartEvent, Option<&ToolEndEvent>)> {
220    let mut starts: Vec<(&ToolStartEvent, Option<&ToolEndEvent>)> = Vec::new();
221    for event in events {
222        if let Event::ToolStart(ts) = event {
223            starts.push((ts, None));
224        }
225    }
226    for event in events {
227        if let Event::ToolEnd(te) = event {
228            for (ts, end) in &mut starts {
229                if ts.call_id == te.call_id && end.is_none() {
230                    *end = Some(te);
231                    break;
232                }
233            }
234        }
235    }
236    starts
237}
238
239impl std::fmt::Display for Event {
240    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241        match self {
242            Event::SessionStart(e) => write!(f, "[session:{}] agent={}", e.session_id, e.agent),
243            Event::TextDelta(e) => write!(f, "{}", e.text),
244            Event::Message(e) => write!(f, "[{}] {}", e.role, e.text),
245            Event::ToolStart(e) => write!(f, "[tool:start] {}({})", e.tool_name, e.call_id),
246            Event::ToolEnd(e) => {
247                let status = if e.success { "ok" } else { "fail" };
248                write!(f, "[tool:{}] {}({})", status, e.tool_name, e.call_id)
249            }
250            Event::UsageDelta(e) => {
251                let input = e.usage.input_tokens.unwrap_or(0);
252                let output = e.usage.output_tokens.unwrap_or(0);
253                write!(f, "[usage] {input} in / {output} out")
254            }
255            Event::Result(e) => {
256                let status = if e.success { "success" } else { "error" };
257                write!(f, "[result:{}] {}", status, e.text)
258            }
259            Event::Error(e) => write!(f, "[error] {}", e.message),
260        }
261    }
262}