1use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11use crate::provider::Usage;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "camelCase")]
24pub enum VerifyOutcome {
25 NotPerformed,
28 Passed,
29 Failed,
30}
31
32impl VerifyOutcome {
33 pub fn describe(self) -> &'static str {
34 match self {
35 VerifyOutcome::NotPerformed => "not performed",
36 VerifyOutcome::Passed => "passed",
37 VerifyOutcome::Failed => "FAILED",
38 }
39 }
40
41 pub fn is_failure(self) -> bool {
47 matches!(self, VerifyOutcome::Failed)
48 }
49}
50
51#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
60#[serde(
61 tag = "event",
62 rename_all = "camelCase",
63 rename_all_fields = "camelCase"
64)]
65pub enum RunEvent {
66 RunStarted {
67 agent: String,
68 provider: String,
69 },
70 NodeStarted {
71 node: String,
72 kind: String,
73 },
74 ModelCall {
75 node: String,
76 model: String,
77 response_type: String,
78 usage: Usage,
79 },
80 ToolCall {
81 node: String,
82 tool: String,
83 effects: Vec<String>,
84 },
85 AgentCall {
86 node: String,
87 agent: String,
88 },
89 ApprovalRequested {
91 node: String,
92 effects: Vec<String>,
93 reason: String,
94 },
95 ApprovalDecided {
96 node: String,
97 allowed: bool,
98 },
99 StateWritten {
100 node: String,
101 field: String,
102 },
103 Verified {
104 node: String,
105 verifier: String,
106 outcome: VerifyOutcome,
107 },
108 Checkpoint {
109 node: String,
110 label: String,
111 },
112 BranchTaken {
113 node: String,
114 arm: String,
116 },
117 LoopIteration {
118 node: String,
119 iteration: u32,
120 },
121 MapIteration {
122 node: String,
123 index: usize,
124 total: usize,
125 },
126 Emitted {
127 node: String,
128 output: String,
129 },
130 RunFinished {
131 steps: u32,
132 usage: Usage,
133 },
134 RunFailed {
135 reason: String,
136 },
137 RunStopped {
145 node: String,
146 label: String,
147 },
148}
149
150impl RunEvent {
151 pub fn to_line(&self) -> String {
153 match self {
154 RunEvent::RunStarted { agent, provider } => {
155 format!("run {agent} (provider: {provider})")
156 }
157 RunEvent::NodeStarted { node, kind } => format!(" {node} {kind}"),
158 RunEvent::ModelCall {
159 model,
160 response_type,
161 usage,
162 ..
163 } => format!(
164 " model {model} -> {response_type} ({} in, {} out)",
165 usage.input_tokens, usage.output_tokens
166 ),
167 RunEvent::ToolCall { tool, effects, .. } => {
168 format!(" tool {tool} [{}]", effects.join(", "))
169 }
170 RunEvent::AgentCall { agent, .. } => format!(" agent {agent}"),
171 RunEvent::ApprovalRequested {
172 effects, reason, ..
173 } => {
174 format!(
175 " approval needed for [{}]: {reason}",
176 effects.join(", ")
177 )
178 }
179 RunEvent::ApprovalDecided { allowed, .. } => {
180 format!(
181 " approval {}",
182 if *allowed { "granted" } else { "denied" }
183 )
184 }
185 RunEvent::StateWritten { field, .. } => format!(" state.{field} written"),
186 RunEvent::Verified {
187 verifier, outcome, ..
188 } => format!(" verify {verifier}: {}", outcome.describe()),
189 RunEvent::Checkpoint { label, .. } => format!(" checkpoint \"{label}\""),
190 RunEvent::BranchTaken { arm, .. } => format!(" branch: {arm}"),
191 RunEvent::LoopIteration { iteration, .. } => format!(" iteration {iteration}"),
192 RunEvent::MapIteration { index, total, .. } => {
193 format!(" element {}/{total}", index + 1)
194 }
195 RunEvent::Emitted { output, .. } => format!(" emit {output}"),
196 RunEvent::RunFinished { steps, usage } => {
197 format!("done: {steps} step(s), {} token(s)", usage.total())
198 }
199 RunEvent::RunFailed { reason } => format!("failed: {reason}"),
200 RunEvent::RunStopped { label, .. } => {
201 format!("stopped at \"{label}\"; resume to continue")
202 }
203 }
204 }
205
206 pub fn to_json_line(&self) -> String {
208 serde_json::to_string(self).expect("events are always serializable")
209 }
210}
211
212pub trait EventSink {
227 fn emit(&mut self, event: RunEvent);
228
229 fn delta(&mut self, node: &str, text: &str) {
236 let _ = (node, text);
237 }
238
239 fn settled(&mut self, node: &str, kept: bool) {
246 let _ = (node, kept);
247 }
248}
249
250#[derive(Debug, Default)]
252pub struct CollectingSink {
253 pub events: Vec<RunEvent>,
254}
255
256impl EventSink for CollectingSink {
257 fn emit(&mut self, event: RunEvent) {
258 self.events.push(event);
259 }
260}
261
262pub struct NullSink;
264
265impl EventSink for NullSink {
266 fn emit(&mut self, _event: RunEvent) {}
267}
268
269pub struct TeeSink<F: FnMut(&RunEvent)> {
276 pub events: Vec<RunEvent>,
277 callback: F,
278}
279
280impl<F: FnMut(&RunEvent)> TeeSink<F> {
281 pub fn new(callback: F) -> TeeSink<F> {
282 TeeSink {
283 events: Vec::new(),
284 callback,
285 }
286 }
287}
288
289impl<F: FnMut(&RunEvent)> EventSink for TeeSink<F> {
290 fn emit(&mut self, event: RunEvent) {
291 (self.callback)(&event);
292 self.events.push(event);
293 }
294}
295
296#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
298#[serde(rename_all = "camelCase")]
299pub struct Artifact {
300 pub name: String,
301 pub content_type: String,
303 pub value: Value,
304}
305
306impl Artifact {
307 pub fn to_bytes(&self) -> Vec<u8> {
312 match (&self.value, self.content_type.as_str()) {
313 (Value::String(text), "markdown" | "text") => text.clone().into_bytes(),
314 (value, _) => {
315 let mut json = serde_json::to_string_pretty(value)
316 .expect("artifact values are always serializable");
317 json.push('\n');
318 json.into_bytes()
319 }
320 }
321 }
322
323 pub fn extension(&self) -> &'static str {
325 match self.content_type.as_str() {
326 "markdown" => "md",
327 "text" => "txt",
328 _ => "json",
329 }
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336 use serde_json::json;
337
338 #[test]
339 fn events_round_trip_through_json() {
340 let event = RunEvent::ModelCall {
341 node: "n0".into(),
342 model: "test".into(),
343 response_type: "markdown".into(),
344 usage: Usage {
345 input_tokens: 1,
346 output_tokens: 2,
347 cache_read_tokens: 0,
348 },
349 };
350 let parsed: RunEvent = serde_json::from_str(&event.to_json_line()).unwrap();
351 assert_eq!(parsed, event);
352 }
353
354 #[test]
355 fn markdown_artifacts_are_written_as_prose() {
356 let artifact = Artifact {
357 name: "report".into(),
358 content_type: "markdown".into(),
359 value: json!("# Title\n\nBody"),
360 };
361 assert_eq!(artifact.to_bytes(), b"# Title\n\nBody");
362 assert_eq!(artifact.extension(), "md");
363 }
364
365 #[test]
366 fn structured_artifacts_are_written_as_json() {
367 let artifact = Artifact {
368 name: "data".into(),
369 content_type: "json".into(),
370 value: json!({"a": 1}),
371 };
372 let text = String::from_utf8(artifact.to_bytes()).unwrap();
373 assert!(text.starts_with('{'));
374 assert!(text.ends_with("}\n"));
375 assert_eq!(artifact.extension(), "json");
376 }
377
378 #[test]
379 fn the_collecting_sink_preserves_order() {
380 let mut sink = CollectingSink::default();
381 sink.emit(RunEvent::RunStarted {
382 agent: "a".into(),
383 provider: "p".into(),
384 });
385 sink.emit(RunEvent::RunFinished {
386 steps: 1,
387 usage: Usage::default(),
388 });
389 assert_eq!(sink.events.len(), 2);
390 assert!(matches!(sink.events[0], RunEvent::RunStarted { .. }));
391 }
392
393 #[test]
394 fn a_delta_is_not_an_event() {
395 let mut sink = CollectingSink::default();
398 sink.delta("n0", "half an ans");
399 sink.delta("n0", "wer");
400 sink.settled("n0", true);
401 assert!(
402 sink.events.is_empty(),
403 "deltas leaked into the event stream: {:?}",
404 sink.events
405 );
406 }
407}