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#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct RunResult {
14 pub output: String,
16 #[serde(skip_serializing_if = "Option::is_none")]
18 pub structured: Option<Value>,
19 #[serde(skip)]
21 pub schema: Option<SchemaHandle>,
22 pub steps: Vec<AgentStep>,
24 pub tokens: Option<TokenUsage>,
26 pub duration: Duration,
28 pub iterations: usize,
30}
31
32#[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 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 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 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 pub fn structured(&self) -> Option<&Value> {
164 self.structured.as_ref()
165 }
166
167 pub fn schema(&self) -> Option<&SchemaHandle> {
169 self.schema.as_ref()
170 }
171
172 pub fn has_structured(&self) -> bool {
174 self.structured.is_some()
175 }
176
177 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 pub fn action_count(&self) -> usize {
197 self.steps
198 .iter()
199 .filter(|s| matches!(s, AgentStep::Action { .. }))
200 .count()
201 }
202
203 pub fn observation_count(&self) -> usize {
205 self.steps
206 .iter()
207 .filter(|s| matches!(s, AgentStep::Observation { .. }))
208 .count()
209 }
210
211 pub fn is_success(&self) -> bool {
213 self.steps
214 .iter()
215 .any(|s| matches!(s, AgentStep::FinalAnswer { .. }))
216 }
217
218 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}