strands_agents/agent/
result.rs

1//! Agent result types.
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7use crate::telemetry::EventLoopMetrics;
8use crate::tools::InvocationState;
9use crate::types::content::Message;
10use crate::types::interrupt::Interrupt;
11use crate::types::streaming::{StopReason, Usage};
12
13/// Result of an agent invocation.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct AgentResult {
16    /// The reason why the agent's processing stopped.
17    pub stop_reason: StopReason,
18    /// The last message generated by the agent.
19    pub message: Message,
20    /// Token usage statistics.
21    pub usage: Usage,
22    /// Performance metrics collected during processing.
23    pub metrics: EventLoopMetrics,
24    /// Additional state information from the event loop.
25    pub state: InvocationState,
26    /// List of interrupts if raised by user.
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub interrupts: Option<Vec<Interrupt>>,
29    /// Parsed structured output when structured_output_model was specified.
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub structured_output: Option<serde_json::Value>,
32}
33
34impl AgentResult {
35    /// Returns the text content of the result.
36    ///
37    /// Extracts and concatenates all text content from the final message,
38    /// ignoring any non-text content like images or structured data.
39    /// If there's no text content but structured output is present,
40    /// it serializes the structured output instead.
41    pub fn text(&self) -> String {
42        let text = self.message.text_content();
43        if text.is_empty() {
44            if let Some(ref output) = self.structured_output {
45                return serde_json::to_string(output).unwrap_or_default();
46            }
47        }
48        text
49    }
50
51    /// Returns true if the agent completed successfully.
52    pub fn is_success(&self) -> bool {
53        matches!(self.stop_reason, StopReason::EndTurn | StopReason::StopSequence)
54    }
55
56    /// Returns true if the agent was interrupted.
57    pub fn is_interrupted(&self) -> bool {
58        matches!(self.stop_reason, StopReason::Interrupt)
59    }
60
61    /// Returns true if there are pending interrupts.
62    pub fn has_interrupts(&self) -> bool {
63        self.interrupts.as_ref().map(|i| !i.is_empty()).unwrap_or(false)
64    }
65
66    /// Rehydrate an AgentResult from persisted JSON.
67    pub fn from_dict(data: serde_json::Value) -> Result<Self, String> {
68        let type_field = data.get("type").and_then(|v| v.as_str());
69        if type_field != Some("agent_result") {
70            return Err(format!(
71                "AgentResult.from_dict: unexpected type {:?}",
72                type_field
73            ));
74        }
75
76        let message: Message = serde_json::from_value(
77            data.get("message").cloned().unwrap_or_default()
78        ).map_err(|e| e.to_string())?;
79
80        let stop_reason: StopReason = serde_json::from_value(
81            data.get("stop_reason").cloned().unwrap_or_default()
82        ).map_err(|e| e.to_string())?;
83
84        Ok(Self {
85            message,
86            stop_reason,
87            usage: Usage::default(),
88            metrics: EventLoopMetrics::default(),
89            state: InvocationState::new(),
90            interrupts: None,
91            structured_output: None,
92        })
93    }
94
95    /// Convert this AgentResult to JSON-serializable dictionary.
96    pub fn to_dict(&self) -> serde_json::Value {
97        serde_json::json!({
98            "type": "agent_result",
99            "message": self.message,
100            "stop_reason": self.stop_reason,
101        })
102    }
103}
104
105impl fmt::Display for AgentResult {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        write!(f, "{}", self.text())
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use crate::types::content::{ContentBlock, Role};
115
116    fn create_test_result(text: &str, stop_reason: StopReason) -> AgentResult {
117        AgentResult {
118            stop_reason,
119            message: Message {
120                role: Role::Assistant,
121                content: vec![ContentBlock::text(text)],
122            },
123            usage: Usage::default(),
124            metrics: EventLoopMetrics::default(),
125            state: InvocationState::new(),
126            interrupts: None,
127            structured_output: None,
128        }
129    }
130
131    #[test]
132    fn test_result_text() {
133        let result = create_test_result("Hello, world!", StopReason::EndTurn);
134        assert_eq!(result.text(), "Hello, world!");
135        assert!(result.is_success());
136    }
137
138    #[test]
139    fn test_result_display() {
140        let result = create_test_result("Test", StopReason::EndTurn);
141        assert_eq!(format!("{}", result), "Test");
142    }
143
144    #[test]
145    fn test_result_with_structured_output() {
146        let mut result = create_test_result("", StopReason::EndTurn);
147        result.structured_output = Some(serde_json::json!({"key": "value"}));
148        assert!(result.text().contains("key"));
149    }
150
151    #[test]
152    fn test_result_interrupts() {
153        let mut result = create_test_result("Test", StopReason::Interrupt);
154        assert!(!result.has_interrupts());
155
156        result.interrupts = Some(vec![Interrupt::new("int-1", "test")]);
157        assert!(result.has_interrupts());
158        assert!(result.is_interrupted());
159    }
160
161    #[test]
162    fn test_result_serialization() {
163        let result = create_test_result("Hello", StopReason::EndTurn);
164        let dict = result.to_dict();
165        assert_eq!(dict["type"], "agent_result");
166
167        let restored = AgentResult::from_dict(dict).unwrap();
168        assert_eq!(restored.text(), "Hello");
169    }
170}