use super::events::WorkflowEvent;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowRunOutput {
pub workflow_id: String,
pub entry_node: String,
pub trace: Vec<String>,
pub outputs: BTreeMap<String, Value>,
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub globals: BTreeMap<String, Value>,
pub terminal_node: String,
pub terminal_output: Option<Value>,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub human_request: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<RunMetadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub events: Option<Vec<WorkflowEvent>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunMetadata {
pub total_elapsed_ms: u128,
#[serde(skip_serializing_if = "Option::is_none")]
pub ttft_ms: Option<u128>,
pub total_input_tokens: u64,
pub total_output_tokens: u64,
pub total_tokens: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_reasoning_tokens: Option<u64>,
pub tokens_per_second: f64,
pub step_details: Vec<StepTiming>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trace_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StepTiming {
pub node_id: String,
pub node_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
pub elapsed_ms: u128,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_tokens: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_tokens: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_tokens: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_tokens: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ttft_ms: Option<u128>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TokenTotals {
pub input_tokens: u64,
pub output_tokens: u64,
pub total_tokens: u64,
pub reasoning_tokens: Option<u64>,
}
impl TokenTotals {
pub fn add(&mut self, input: u64, output: u64, total: u64, reasoning: Option<u64>) {
self.input_tokens += input;
self.output_tokens += output;
self.total_tokens += total;
if let Some(r) = reasoning {
*self.reasoning_tokens.get_or_insert(0) += r;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_token_totals_add() {
let mut t = TokenTotals::default();
t.add(10, 20, 30, Some(5));
t.add(5, 10, 15, None);
assert_eq!(t.input_tokens, 15);
assert_eq!(t.output_tokens, 30);
assert_eq!(t.total_tokens, 45);
assert_eq!(t.reasoning_tokens, Some(5));
}
#[test]
fn test_output_serialization() {
let output = WorkflowRunOutput {
workflow_id: "test".into(),
entry_node: "start".into(),
trace: vec!["start".into()],
outputs: BTreeMap::new(),
globals: BTreeMap::new(),
terminal_node: "start".into(),
terminal_output: Some(serde_json::json!("done")),
status: "completed".into(),
human_request: None,
metadata: None,
events: None,
};
let json = serde_json::to_string(&output).unwrap();
assert!(json.contains("test"));
}
}