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