Skip to main content

tiny_agent_rs/types/
result.rs

1use super::response::deserialize_structured_response;
2use crate::{
3    core::steps::AgentStep,
4    error::{AgentError, Result as AgentResult},
5    schemas::{CompletionSchema, SchemaHandle},
6};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::time::Duration;
10
11/// Result of an agent execution run
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct RunResult {
14    /// Final output from the agent
15    pub output: String,
16    /// Optional structured payload returned alongside the final answer
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub structured: Option<Value>,
19    /// Schema metadata associated with the structured payload
20    #[serde(skip)]
21    pub schema: Option<SchemaHandle>,
22    /// All reasoning steps taken during execution
23    pub steps: Vec<AgentStep>,
24    /// Total tokens used (if available from API)
25    pub tokens: Option<TokenUsage>,
26    /// Total execution duration
27    pub duration: Duration,
28    /// Number of iterations used
29    pub iterations: usize,
30}
31
32/// Token usage information from the API
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct TokenUsage {
35    pub prompt_tokens: u32,
36    pub completion_tokens: u32,
37    pub total_tokens: u32,
38}
39
40impl RunResult {
41    /// Create a new RunResult
42    pub fn new(
43        output: String,
44        structured: Option<Value>,
45        schema: Option<SchemaHandle>,
46        steps: Vec<AgentStep>,
47        tokens: Option<TokenUsage>,
48        duration: Duration,
49        iterations: usize,
50    ) -> Self {
51        Self {
52            output,
53            structured,
54            schema,
55            steps,
56            tokens,
57            duration,
58            iterations,
59        }
60    }
61
62    /// Generate a human-readable replay of the execution
63    pub fn replay(&self) -> String {
64        let mut lines = Vec::new();
65
66        lines.push("=== Agent Execution Trace ===".to_string());
67        lines.push(format!("Duration: {:.2}s", self.duration.as_secs_f64()));
68        lines.push(format!("Iterations: {}", self.iterations));
69
70        if let Some(tokens) = &self.tokens {
71            lines.push(format!(
72                "Tokens: {} prompt + {} completion = {} total",
73                tokens.prompt_tokens, tokens.completion_tokens, tokens.total_tokens
74            ));
75        }
76
77        lines.push(String::new());
78        lines.push("--- Steps ---".to_string());
79
80        for (idx, step) in self.steps.iter().enumerate() {
81            lines.push(format!("{}. {}", idx + 1, step.describe()));
82        }
83
84        lines.push(String::new());
85        lines.push("--- Final Output ---".to_string());
86        lines.push(self.output.clone());
87
88        if let Some(structured) = &self.structured {
89            lines.push(String::new());
90            lines.push("--- Structured Output ---".to_string());
91            lines.push(structured.to_string());
92        }
93
94        lines.join("\n")
95    }
96
97    /// Generate a detailed explanation with full step data
98    pub fn explain(&self) -> String {
99        let mut lines = Vec::new();
100
101        lines.push("=== Agent Execution Explanation ===".to_string());
102        lines.push(format!("Duration: {:.2}s", self.duration.as_secs_f64()));
103        lines.push(format!("Iterations: {}", self.iterations));
104
105        if let Some(tokens) = &self.tokens {
106            lines.push(format!(
107                "Tokens: {} prompt + {} completion = {} total",
108                tokens.prompt_tokens, tokens.completion_tokens, tokens.total_tokens
109            ));
110        }
111
112        lines.push(String::new());
113        lines.push("--- Detailed Steps ---".to_string());
114
115        for (idx, step) in self.steps.iter().enumerate() {
116            lines.push(format!("\n{}. {}", idx + 1, step.describe()));
117
118            match step {
119                AgentStep::Task { content } => {
120                    lines.push(format!("   Content: {}", content));
121                }
122                AgentStep::Planning { plan } => {
123                    lines.push(format!("   Plan: {}", plan));
124                }
125                AgentStep::Action {
126                    tool_name,
127                    tool_call_id,
128                    arguments,
129                } => {
130                    lines.push(format!("   Tool: {}", tool_name));
131                    lines.push(format!("   Call ID: {}", tool_call_id));
132                    lines.push(format!("   Arguments: {}", arguments));
133                }
134                AgentStep::Observation {
135                    tool_call_id,
136                    result,
137                    is_error,
138                } => {
139                    lines.push(format!("   Call ID: {}", tool_call_id));
140                    lines.push(format!("   Error: {}", is_error));
141                    lines.push(format!("   Result: {}", result));
142                }
143                AgentStep::FinalAnswer { answer, .. } => {
144                    lines.push(format!("   Answer: {}", answer));
145                }
146            }
147        }
148
149        lines.push(String::new());
150        lines.push("--- Final Output ---".to_string());
151        lines.push(self.output.clone());
152
153        if let Some(structured) = &self.structured {
154            lines.push(String::new());
155            lines.push("--- Structured Output ---".to_string());
156            lines.push(structured.to_string());
157        }
158
159        lines.join("\n")
160    }
161
162    /// Access the structured payload, if present.
163    pub fn structured(&self) -> Option<&Value> {
164        self.structured.as_ref()
165    }
166
167    /// Access the schema handle associated with the structured payload.
168    pub fn schema(&self) -> Option<&SchemaHandle> {
169        self.schema.as_ref()
170    }
171
172    /// Check whether a structured payload is available.
173    pub fn has_structured(&self) -> bool {
174        self.structured.is_some()
175    }
176
177    /// Deserialize the structured payload into the requested type using the stored schema metadata.
178    pub fn deserialize_structured<T>(&self) -> AgentResult<T>
179    where
180        T: CompletionSchema,
181    {
182        let payload = self.structured.as_ref().ok_or_else(|| {
183            AgentError::Validation("No structured response available on this run".to_string())
184        })?;
185
186        let schema = self.schema.as_ref().ok_or_else(|| {
187            AgentError::Validation(
188                "Missing completion schema metadata for structured response".to_string(),
189            )
190        })?;
191
192        deserialize_structured_response::<T>(payload, schema)
193    }
194
195    /// Get count of actions (tool calls) executed
196    pub fn action_count(&self) -> usize {
197        self.steps
198            .iter()
199            .filter(|s| matches!(s, AgentStep::Action { .. }))
200            .count()
201    }
202
203    /// Get count of observations (tool results)
204    pub fn observation_count(&self) -> usize {
205        self.steps
206            .iter()
207            .filter(|s| matches!(s, AgentStep::Observation { .. }))
208            .count()
209    }
210
211    /// Check if execution completed successfully (has final answer)
212    pub fn is_success(&self) -> bool {
213        self.steps
214            .iter()
215            .any(|s| matches!(s, AgentStep::FinalAnswer { .. }))
216    }
217
218    /// Get all error observations
219    pub fn errors(&self) -> Vec<&str> {
220        self.steps
221            .iter()
222            .filter_map(|s| match s {
223                AgentStep::Observation {
224                    result, is_error, ..
225                } if *is_error => Some(result.as_str()),
226                _ => None,
227            })
228            .collect()
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235    use crate::completion_schema;
236    use crate::schema::CompletionSchema;
237    use schemars::JsonSchema;
238    use serde::{Deserialize, Serialize};
239    use serde_json::json;
240
241    #[test]
242    fn test_run_result_creation() {
243        let steps = vec![
244            AgentStep::Task {
245                content: "Test task".to_string(),
246            },
247            AgentStep::FinalAnswer {
248                answer: "Test answer".to_string(),
249                structured: None,
250            },
251        ];
252
253        let result = RunResult::new(
254            "Test answer".to_string(),
255            None,
256            None,
257            steps,
258            None,
259            Duration::from_secs(1),
260            1,
261        );
262
263        assert_eq!(result.output, "Test answer");
264        assert_eq!(result.iterations, 1);
265        assert!(result.is_success());
266    }
267
268    #[test]
269    fn test_action_count() {
270        let steps = vec![
271            AgentStep::Action {
272                tool_name: "tool1".to_string(),
273                tool_call_id: "1".to_string(),
274                arguments: json!({}),
275            },
276            AgentStep::Action {
277                tool_name: "tool2".to_string(),
278                tool_call_id: "2".to_string(),
279                arguments: json!({}),
280            },
281        ];
282
283        let result = RunResult::new(
284            "output".to_string(),
285            None,
286            None,
287            steps,
288            None,
289            Duration::from_secs(1),
290            1,
291        );
292
293        assert_eq!(result.action_count(), 2);
294    }
295
296    #[test]
297    fn test_replay_format() {
298        let steps = vec![
299            AgentStep::Task {
300                content: "Test".to_string(),
301            },
302            AgentStep::FinalAnswer {
303                answer: "Done".to_string(),
304                structured: None,
305            },
306        ];
307
308        let result = RunResult::new(
309            "Done".to_string(),
310            None,
311            None,
312            steps,
313            Some(TokenUsage {
314                prompt_tokens: 100,
315                completion_tokens: 50,
316                total_tokens: 150,
317            }),
318            Duration::from_secs(2),
319            1,
320        );
321
322        let replay = result.replay();
323        assert!(replay.contains("Duration"));
324        assert!(replay.contains("Tokens"));
325        assert!(replay.contains("Task"));
326        assert!(replay.contains("Final Answer"));
327    }
328
329    #[test]
330    fn test_error_tracking() {
331        let steps = vec![
332            AgentStep::Observation {
333                tool_call_id: "1".to_string(),
334                result: "Error occurred".to_string(),
335                is_error: true,
336            },
337            AgentStep::Observation {
338                tool_call_id: "2".to_string(),
339                result: "Success".to_string(),
340                is_error: false,
341            },
342        ];
343
344        let result = RunResult::new(
345            "output".to_string(),
346            None,
347            None,
348            steps,
349            None,
350            Duration::from_secs(1),
351            1,
352        );
353
354        let errors = result.errors();
355        assert_eq!(errors.len(), 1);
356        assert_eq!(errors[0], "Error occurred");
357    }
358
359    #[test]
360    fn test_deserialize_structured_payload() {
361        #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
362        #[completion_schema]
363        struct SamplePlan {
364            title: String,
365        }
366
367        let structured = json!({ "title": "Sample" });
368        let schema = SamplePlan::schema().clone();
369
370        let steps = vec![AgentStep::FinalAnswer {
371            answer: "Sample".to_string(),
372            structured: Some(structured.clone()),
373        }];
374
375        let result = RunResult::new(
376            "Sample".to_string(),
377            Some(structured),
378            Some(schema),
379            steps,
380            None,
381            Duration::from_secs(1),
382            1,
383        );
384
385        let typed = result.deserialize_structured::<SamplePlan>().unwrap();
386        assert_eq!(typed.title, "Sample");
387    }
388}