Skip to main content

simple_agents_workflow/yaml_runner/
output.rs

1use super::events::WorkflowEvent;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use std::collections::BTreeMap;
5
6/// The final output of a successful workflow run.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct WorkflowRunOutput {
9    /// Identifier for the workflow.
10    pub workflow_id: String,
11    /// The first node that executed.
12    pub entry_node: String,
13    /// Ordered list of node IDs that executed.
14    pub trace: Vec<String>,
15    /// Outputs collected from each node, keyed by node ID.
16    pub outputs: BTreeMap<String, Value>,
17    /// The last node in the trace.
18    pub terminal_node: String,
19    /// The output value of the terminal node.
20    pub terminal_output: Option<Value>,
21    /// Optional performance metadata.
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub metadata: Option<RunMetadata>,
24    /// Collected events if event recording was enabled.
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub events: Option<Vec<WorkflowEvent>>,
27}
28
29/// Performance and observability metadata for a workflow run.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct RunMetadata {
32    /// Total wall-clock time for the workflow.
33    pub total_elapsed_ms: u128,
34    /// Time to first LLM token across all nodes.
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub ttft_ms: Option<u128>,
37    /// Sum of input tokens across all LLM calls.
38    pub total_input_tokens: u64,
39    /// Sum of output tokens across all LLM calls.
40    pub total_output_tokens: u64,
41    /// Sum of all tokens across all LLM calls.
42    pub total_tokens: u64,
43    /// Sum of reasoning tokens (if applicable).
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub total_reasoning_tokens: Option<u64>,
46    /// Output tokens per second across the workflow.
47    pub tokens_per_second: f64,
48    /// Per-step timing details.
49    pub step_details: Vec<StepTiming>,
50    /// Trace ID if telemetry was enabled.
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub trace_id: Option<String>,
53}
54
55/// Timing and token details for a single workflow step.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct StepTiming {
58    /// Node ID.
59    pub node_id: String,
60    /// Node type (e.g. "llm_call", "switch").
61    pub node_type: String,
62    /// Model used (for LLM nodes).
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub model: Option<String>,
65    /// Wall-clock time for this step.
66    pub elapsed_ms: u128,
67    /// Input tokens for this step.
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub input_tokens: Option<u64>,
70    /// Output tokens for this step.
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub output_tokens: Option<u64>,
73    /// Total tokens for this step.
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub total_tokens: Option<u64>,
76    /// Reasoning tokens for this step.
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub reasoning_tokens: Option<u64>,
79    /// Time to first token for this step.
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub ttft_ms: Option<u128>,
82}
83
84/// Running token counter.
85#[derive(Debug, Clone, Default, Serialize, Deserialize)]
86pub struct TokenTotals {
87    /// Total input tokens so far.
88    pub input_tokens: u64,
89    /// Total output tokens so far.
90    pub output_tokens: u64,
91    /// Total tokens so far.
92    pub total_tokens: u64,
93    /// Total reasoning tokens so far.
94    pub reasoning_tokens: Option<u64>,
95}
96
97impl TokenTotals {
98    /// Add token counts from a step.
99    pub fn add(&mut self, input: u64, output: u64, total: u64, reasoning: Option<u64>) {
100        self.input_tokens += input;
101        self.output_tokens += output;
102        self.total_tokens += total;
103        if let Some(r) = reasoning {
104            *self.reasoning_tokens.get_or_insert(0) += r;
105        }
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn test_token_totals_add() {
115        let mut t = TokenTotals::default();
116        t.add(10, 20, 30, Some(5));
117        t.add(5, 10, 15, None);
118        assert_eq!(t.input_tokens, 15);
119        assert_eq!(t.output_tokens, 30);
120        assert_eq!(t.total_tokens, 45);
121        assert_eq!(t.reasoning_tokens, Some(5));
122    }
123
124    #[test]
125    fn test_output_serialization() {
126        let output = WorkflowRunOutput {
127            workflow_id: "test".into(),
128            entry_node: "start".into(),
129            trace: vec!["start".into()],
130            outputs: BTreeMap::new(),
131            terminal_node: "start".into(),
132            terminal_output: Some(serde_json::json!("done")),
133            metadata: None,
134            events: None,
135        };
136        let json = serde_json::to_string(&output).unwrap();
137        assert!(json.contains("test"));
138    }
139}