strands-agents 0.1.0

A Rust implementation of the Strands AI Agents SDK
Documentation
//! Agent result types.

use std::fmt;

use serde::{Deserialize, Serialize};

use crate::telemetry::EventLoopMetrics;
use crate::tools::InvocationState;
use crate::types::content::Message;
use crate::types::interrupt::Interrupt;
use crate::types::streaming::{StopReason, Usage};

/// Result of an agent invocation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentResult {
    /// The reason why the agent's processing stopped.
    pub stop_reason: StopReason,
    /// The last message generated by the agent.
    pub message: Message,
    /// Token usage statistics.
    pub usage: Usage,
    /// Performance metrics collected during processing.
    pub metrics: EventLoopMetrics,
    /// Additional state information from the event loop.
    pub state: InvocationState,
    /// List of interrupts if raised by user.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub interrupts: Option<Vec<Interrupt>>,
    /// Parsed structured output when structured_output_model was specified.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub structured_output: Option<serde_json::Value>,
}

impl AgentResult {
    /// Returns the text content of the result.
    ///
    /// Extracts and concatenates all text content from the final message,
    /// ignoring any non-text content like images or structured data.
    /// If there's no text content but structured output is present,
    /// it serializes the structured output instead.
    pub fn text(&self) -> String {
        let text = self.message.text_content();
        if text.is_empty() {
            if let Some(ref output) = self.structured_output {
                return serde_json::to_string(output).unwrap_or_default();
            }
        }
        text
    }

    /// Returns true if the agent completed successfully.
    pub fn is_success(&self) -> bool {
        matches!(self.stop_reason, StopReason::EndTurn | StopReason::StopSequence)
    }

    /// Returns true if the agent was interrupted.
    pub fn is_interrupted(&self) -> bool {
        matches!(self.stop_reason, StopReason::Interrupt)
    }

    /// Returns true if there are pending interrupts.
    pub fn has_interrupts(&self) -> bool {
        self.interrupts.as_ref().map(|i| !i.is_empty()).unwrap_or(false)
    }

    /// Rehydrate an AgentResult from persisted JSON.
    pub fn from_dict(data: serde_json::Value) -> Result<Self, String> {
        let type_field = data.get("type").and_then(|v| v.as_str());
        if type_field != Some("agent_result") {
            return Err(format!(
                "AgentResult.from_dict: unexpected type {:?}",
                type_field
            ));
        }

        let message: Message = serde_json::from_value(
            data.get("message").cloned().unwrap_or_default()
        ).map_err(|e| e.to_string())?;

        let stop_reason: StopReason = serde_json::from_value(
            data.get("stop_reason").cloned().unwrap_or_default()
        ).map_err(|e| e.to_string())?;

        Ok(Self {
            message,
            stop_reason,
            usage: Usage::default(),
            metrics: EventLoopMetrics::default(),
            state: InvocationState::new(),
            interrupts: None,
            structured_output: None,
        })
    }

    /// Convert this AgentResult to JSON-serializable dictionary.
    pub fn to_dict(&self) -> serde_json::Value {
        serde_json::json!({
            "type": "agent_result",
            "message": self.message,
            "stop_reason": self.stop_reason,
        })
    }
}

impl fmt::Display for AgentResult {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.text())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::content::{ContentBlock, Role};

    fn create_test_result(text: &str, stop_reason: StopReason) -> AgentResult {
        AgentResult {
            stop_reason,
            message: Message {
                role: Role::Assistant,
                content: vec![ContentBlock::text(text)],
            },
            usage: Usage::default(),
            metrics: EventLoopMetrics::default(),
            state: InvocationState::new(),
            interrupts: None,
            structured_output: None,
        }
    }

    #[test]
    fn test_result_text() {
        let result = create_test_result("Hello, world!", StopReason::EndTurn);
        assert_eq!(result.text(), "Hello, world!");
        assert!(result.is_success());
    }

    #[test]
    fn test_result_display() {
        let result = create_test_result("Test", StopReason::EndTurn);
        assert_eq!(format!("{}", result), "Test");
    }

    #[test]
    fn test_result_with_structured_output() {
        let mut result = create_test_result("", StopReason::EndTurn);
        result.structured_output = Some(serde_json::json!({"key": "value"}));
        assert!(result.text().contains("key"));
    }

    #[test]
    fn test_result_interrupts() {
        let mut result = create_test_result("Test", StopReason::Interrupt);
        assert!(!result.has_interrupts());

        result.interrupts = Some(vec![Interrupt::new("int-1", "test")]);
        assert!(result.has_interrupts());
        assert!(result.is_interrupted());
    }

    #[test]
    fn test_result_serialization() {
        let result = create_test_result("Hello", StopReason::EndTurn);
        let dict = result.to_dict();
        assert_eq!(dict["type"], "agent_result");

        let restored = AgentResult::from_dict(dict).unwrap();
        assert_eq!(restored.text(), "Hello");
    }
}