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 ConsultationAsked {
105 node: String,
106 index: usize,
109 question: String,
110 #[serde(default, skip_serializing_if = "Vec::is_empty")]
111 choices: Vec<String>,
112 },
113 ConsultationAnswered {
114 node: String,
115 index: usize,
116 answer: String,
117 },
118 StateWritten {
119 node: String,
120 field: String,
121 },
122 Verified {
123 node: String,
124 verifier: String,
125 outcome: VerifyOutcome,
126 },
127 Checkpoint {
128 node: String,
129 label: String,
130 },
131 BranchTaken {
132 node: String,
133 arm: String,
135 },
136 LoopIteration {
137 node: String,
138 iteration: u32,
139 },
140 MapIteration {
141 node: String,
142 index: usize,
143 total: usize,
144 },
145 Emitted {
146 node: String,
147 output: String,
148 },
149 RunFinished {
150 steps: u32,
151 usage: Usage,
152 },
153 RunFailed {
154 reason: String,
155 },
156 RunStopped {
164 node: String,
165 label: String,
166 },
167}
168
169impl RunEvent {
170 pub fn to_line(&self) -> String {
172 match self {
173 RunEvent::RunStarted { agent, provider } => {
174 format!("run {agent} (provider: {provider})")
175 }
176 RunEvent::NodeStarted { node, kind } => format!(" {node} {kind}"),
177 RunEvent::ModelCall {
178 model,
179 response_type,
180 usage,
181 ..
182 } => format!(
183 " model {model} -> {response_type} ({} in, {} out)",
184 usage.input_tokens, usage.output_tokens
185 ),
186 RunEvent::ToolCall { tool, effects, .. } => {
187 format!(" tool {tool} [{}]", effects.join(", "))
188 }
189 RunEvent::AgentCall { agent, .. } => format!(" agent {agent}"),
190 RunEvent::ApprovalRequested {
191 effects, reason, ..
192 } => {
193 format!(
194 " approval needed for [{}]: {reason}",
195 effects.join(", ")
196 )
197 }
198 RunEvent::ConsultationAsked {
199 question, choices, ..
200 } => {
201 if choices.is_empty() {
202 format!(" asking a person: {question}")
203 } else {
204 format!(
205 " asking a person: {question} [{}]",
206 choices.join(" | ")
207 )
208 }
209 }
210 RunEvent::ConsultationAnswered { answer, .. } => {
211 format!(" a person answered: {answer}")
212 }
213 RunEvent::ApprovalDecided { allowed, .. } => {
214 format!(
215 " approval {}",
216 if *allowed { "granted" } else { "denied" }
217 )
218 }
219 RunEvent::StateWritten { field, .. } => format!(" state.{field} written"),
220 RunEvent::Verified {
221 verifier, outcome, ..
222 } => format!(" verify {verifier}: {}", outcome.describe()),
223 RunEvent::Checkpoint { label, .. } => format!(" checkpoint \"{label}\""),
224 RunEvent::BranchTaken { arm, .. } => format!(" branch: {arm}"),
225 RunEvent::LoopIteration { iteration, .. } => format!(" iteration {iteration}"),
226 RunEvent::MapIteration { index, total, .. } => {
227 format!(" element {}/{total}", index + 1)
228 }
229 RunEvent::Emitted { output, .. } => format!(" emit {output}"),
230 RunEvent::RunFinished { steps, usage } => {
231 format!("done: {steps} step(s), {} token(s)", usage.total())
232 }
233 RunEvent::RunFailed { reason } => format!("failed: {reason}"),
234 RunEvent::RunStopped { label, .. } => {
235 format!("stopped at \"{label}\"; resume to continue")
236 }
237 }
238 }
239
240 pub fn to_json_line(&self) -> String {
242 serde_json::to_string(self).expect("events are always serializable")
243 }
244}
245
246pub trait EventSink {
261 fn emit(&mut self, event: RunEvent);
262
263 fn delta(&mut self, node: &str, text: &str) {
270 let _ = (node, text);
271 }
272
273 fn settled(&mut self, node: &str, kept: bool) {
280 let _ = (node, kept);
281 }
282}
283
284#[derive(Debug, Default)]
286pub struct CollectingSink {
287 pub events: Vec<RunEvent>,
288}
289
290impl EventSink for CollectingSink {
291 fn emit(&mut self, event: RunEvent) {
292 self.events.push(event);
293 }
294}
295
296pub struct NullSink;
298
299impl EventSink for NullSink {
300 fn emit(&mut self, _event: RunEvent) {}
301}
302
303pub struct TeeSink<F: FnMut(&RunEvent)> {
310 pub events: Vec<RunEvent>,
311 callback: F,
312}
313
314impl<F: FnMut(&RunEvent)> TeeSink<F> {
315 pub fn new(callback: F) -> TeeSink<F> {
316 TeeSink {
317 events: Vec::new(),
318 callback,
319 }
320 }
321}
322
323impl<F: FnMut(&RunEvent)> EventSink for TeeSink<F> {
324 fn emit(&mut self, event: RunEvent) {
325 (self.callback)(&event);
326 self.events.push(event);
327 }
328}
329
330#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
332#[serde(rename_all = "camelCase")]
333pub struct Artifact {
334 pub name: String,
335 pub content_type: String,
337 pub value: Value,
338}
339
340impl Artifact {
341 pub fn to_bytes(&self) -> Vec<u8> {
346 match (&self.value, self.content_type.as_str()) {
347 (Value::String(text), "markdown" | "text") => text.clone().into_bytes(),
348 (value, _) => {
349 let mut json = serde_json::to_string_pretty(value)
350 .expect("artifact values are always serializable");
351 json.push('\n');
352 json.into_bytes()
353 }
354 }
355 }
356
357 pub fn extension(&self) -> &'static str {
359 match self.content_type.as_str() {
360 "markdown" => "md",
361 "text" => "txt",
362 _ => "json",
363 }
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370 use serde_json::json;
371
372 #[test]
373 fn events_round_trip_through_json() {
374 let event = RunEvent::ModelCall {
375 node: "n0".into(),
376 model: "test".into(),
377 response_type: "markdown".into(),
378 usage: Usage {
379 input_tokens: 1,
380 output_tokens: 2,
381 cache_read_tokens: 0,
382 },
383 };
384 let parsed: RunEvent = serde_json::from_str(&event.to_json_line()).unwrap();
385 assert_eq!(parsed, event);
386 }
387
388 #[test]
389 fn markdown_artifacts_are_written_as_prose() {
390 let artifact = Artifact {
391 name: "report".into(),
392 content_type: "markdown".into(),
393 value: json!("# Title\n\nBody"),
394 };
395 assert_eq!(artifact.to_bytes(), b"# Title\n\nBody");
396 assert_eq!(artifact.extension(), "md");
397 }
398
399 #[test]
400 fn structured_artifacts_are_written_as_json() {
401 let artifact = Artifact {
402 name: "data".into(),
403 content_type: "json".into(),
404 value: json!({"a": 1}),
405 };
406 let text = String::from_utf8(artifact.to_bytes()).unwrap();
407 assert!(text.starts_with('{'));
408 assert!(text.ends_with("}\n"));
409 assert_eq!(artifact.extension(), "json");
410 }
411
412 #[test]
413 fn the_collecting_sink_preserves_order() {
414 let mut sink = CollectingSink::default();
415 sink.emit(RunEvent::RunStarted {
416 agent: "a".into(),
417 provider: "p".into(),
418 });
419 sink.emit(RunEvent::RunFinished {
420 steps: 1,
421 usage: Usage::default(),
422 });
423 assert_eq!(sink.events.len(), 2);
424 assert!(matches!(sink.events[0], RunEvent::RunStarted { .. }));
425 }
426
427 #[test]
428 fn a_delta_is_not_an_event() {
429 let mut sink = CollectingSink::default();
432 sink.delta("n0", "half an ans");
433 sink.delta("n0", "wer");
434 sink.settled("n0", true);
435 assert!(
436 sink.events.is_empty(),
437 "deltas leaked into the event stream: {:?}",
438 sink.events
439 );
440 }
441}