reasonkit/thinktool/
step.rs

1//! Step execution types and result handling
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// Result of executing a single protocol step
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct StepResult {
9    /// Step identifier
10    pub step_id: String,
11
12    /// Whether step succeeded
13    pub success: bool,
14
15    /// Output data (format depends on step type)
16    pub output: StepOutput,
17
18    /// Confidence score (0.0 - 1.0)
19    pub confidence: f64,
20
21    /// Execution time in milliseconds
22    pub duration_ms: u64,
23
24    /// Token usage
25    pub tokens: TokenUsage,
26
27    /// Error message if failed
28    pub error: Option<String>,
29}
30
31/// Step output variants
32#[derive(Debug, Clone, Serialize, Deserialize)]
33#[serde(tag = "type", rename_all = "snake_case")]
34#[derive(Default)]
35pub enum StepOutput {
36    /// Free-form text output
37    Text {
38        /// The text content
39        content: String,
40    },
41
42    /// List of items
43    List {
44        /// The list items
45        items: Vec<ListItem>,
46    },
47
48    /// Structured key-value output
49    Structured {
50        /// Key-value data map
51        data: HashMap<String, serde_json::Value>,
52    },
53
54    /// Numeric score
55    Score {
56        /// The score value
57        value: f64,
58    },
59
60    /// Boolean result
61    Boolean {
62        /// The boolean value
63        value: bool,
64        /// Optional reason for the boolean
65        reason: Option<String>,
66    },
67
68    /// Empty (no output yet)
69    #[default]
70    Empty,
71}
72
73/// A single item in a list output
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct ListItem {
76    /// Item content
77    pub content: String,
78
79    /// Item-level confidence
80    #[serde(default)]
81    pub confidence: Option<f64>,
82
83    /// Optional metadata
84    #[serde(default)]
85    pub metadata: HashMap<String, serde_json::Value>,
86}
87
88impl ListItem {
89    /// Create a simple list item
90    pub fn new(content: impl Into<String>) -> Self {
91        Self {
92            content: content.into(),
93            confidence: None,
94            metadata: HashMap::new(),
95        }
96    }
97
98    /// Create a list item with confidence
99    pub fn with_confidence(content: impl Into<String>, confidence: f64) -> Self {
100        Self {
101            content: content.into(),
102            confidence: Some(confidence),
103            metadata: HashMap::new(),
104        }
105    }
106}
107
108/// Token usage tracking
109#[derive(Debug, Clone, Default, Serialize, Deserialize)]
110pub struct TokenUsage {
111    /// Input/prompt tokens
112    pub input_tokens: u32,
113
114    /// Output/completion tokens
115    pub output_tokens: u32,
116
117    /// Total tokens
118    pub total_tokens: u32,
119
120    /// Estimated cost in USD
121    pub cost_usd: f64,
122}
123
124impl TokenUsage {
125    /// Create new token usage
126    pub fn new(input: u32, output: u32, cost: f64) -> Self {
127        Self {
128            input_tokens: input,
129            output_tokens: output,
130            total_tokens: input + output,
131            cost_usd: cost,
132        }
133    }
134
135    /// Add another token usage
136    pub fn add(&mut self, other: &TokenUsage) {
137        self.input_tokens += other.input_tokens;
138        self.output_tokens += other.output_tokens;
139        self.total_tokens += other.total_tokens;
140        self.cost_usd += other.cost_usd;
141    }
142}
143
144/// Output format hint for parsing
145#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
146pub enum OutputFormat {
147    /// Raw text, no parsing
148    #[default]
149    Raw,
150    /// Parse as JSON
151    Json,
152    /// Parse as numbered/bulleted list
153    List,
154    /// Parse as key: value pairs
155    KeyValue,
156    /// Parse as single numeric value
157    Numeric,
158}
159
160impl StepResult {
161    /// Create a successful step result
162    pub fn success(step_id: impl Into<String>, output: StepOutput, confidence: f64) -> Self {
163        Self {
164            step_id: step_id.into(),
165            success: true,
166            output,
167            confidence,
168            duration_ms: 0,
169            tokens: TokenUsage::default(),
170            error: None,
171        }
172    }
173
174    /// Create a failed step result
175    pub fn failure(step_id: impl Into<String>, error: impl Into<String>) -> Self {
176        Self {
177            step_id: step_id.into(),
178            success: false,
179            output: StepOutput::Empty,
180            confidence: 0.0,
181            duration_ms: 0,
182            tokens: TokenUsage::default(),
183            error: Some(error.into()),
184        }
185    }
186
187    /// Set duration
188    pub fn with_duration(mut self, ms: u64) -> Self {
189        self.duration_ms = ms;
190        self
191    }
192
193    /// Set token usage
194    pub fn with_tokens(mut self, tokens: TokenUsage) -> Self {
195        self.tokens = tokens;
196        self
197    }
198
199    /// Check if confidence meets threshold
200    pub fn meets_threshold(&self, threshold: f64) -> bool {
201        self.success && self.confidence >= threshold
202    }
203
204    /// Extract text content if available
205    pub fn as_text(&self) -> Option<&str> {
206        match &self.output {
207            StepOutput::Text { content } => Some(content),
208            _ => None,
209        }
210    }
211
212    /// Extract list items if available
213    pub fn as_list(&self) -> Option<&[ListItem]> {
214        match &self.output {
215            StepOutput::List { items } => Some(items),
216            _ => None,
217        }
218    }
219
220    /// Extract score if available
221    pub fn as_score(&self) -> Option<f64> {
222        match &self.output {
223            StepOutput::Score { value } => Some(*value),
224            _ => None,
225        }
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[test]
234    fn test_step_result_success() {
235        let result = StepResult::success(
236            "test_step",
237            StepOutput::Text {
238                content: "Hello world".to_string(),
239            },
240            0.85,
241        );
242
243        assert!(result.success);
244        assert_eq!(result.confidence, 0.85);
245        assert_eq!(result.as_text(), Some("Hello world"));
246    }
247
248    #[test]
249    fn test_step_result_failure() {
250        let result = StepResult::failure("test_step", "Something went wrong");
251
252        assert!(!result.success);
253        assert_eq!(result.confidence, 0.0);
254        assert_eq!(result.error, Some("Something went wrong".to_string()));
255    }
256
257    #[test]
258    fn test_token_usage_add() {
259        let mut usage1 = TokenUsage::new(100, 50, 0.001);
260        let usage2 = TokenUsage::new(200, 100, 0.002);
261
262        usage1.add(&usage2);
263
264        assert_eq!(usage1.input_tokens, 300);
265        assert_eq!(usage1.output_tokens, 150);
266        assert_eq!(usage1.total_tokens, 450);
267    }
268
269    #[test]
270    fn test_list_item() {
271        let item = ListItem::with_confidence("Test item", 0.9);
272        assert_eq!(item.content, "Test item");
273        assert_eq!(item.confidence, Some(0.9));
274    }
275}