Skip to main content

runifold_agent/
outcome.rs

1use runifold_core::Usage;
2use runifold_model::{Message, ModelResponse, StructuredOutputError};
3use serde::de::DeserializeOwned;
4use serde::{Deserialize, Serialize};
5
6/// Successful terminal state of an agent run.
7#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
8pub struct AgentOutcome {
9    /// Final model response.
10    pub response: ModelResponse,
11    /// Complete canonical transcript, including tool calls and results.
12    pub transcript: Vec<Message>,
13    /// Model turns performed by this agent.
14    pub turns: u32,
15    /// Tool calls attempted by this agent.
16    pub tool_calls: u32,
17    /// Successful direct child-agent delegations performed by this agent.
18    pub delegations: u32,
19    /// Shared run-tree usage snapshot at completion.
20    pub usage: Usage,
21}
22
23impl AgentOutcome {
24    /// Collects model-visible terminal text in canonical content order.
25    #[must_use]
26    pub fn text(&self) -> String {
27        self.response.text()
28    }
29
30    /// Consumes the outcome and returns only model-visible terminal text.
31    ///
32    /// Use this only when the transcript, usage, counters, warnings, and
33    /// provider-specific response data are no longer needed.
34    #[must_use]
35    pub fn into_text(self) -> String {
36        self.response.into_text()
37    }
38
39    /// Locally validates and decodes the final model response while preserving
40    /// the complete canonical outcome.
41    ///
42    /// # Errors
43    ///
44    /// Returns [`StructuredOutputError`] when the response is missing textual
45    /// output, contains a refusal, or does not deserialize as `T`.
46    pub fn into_structured<T>(self) -> Result<StructuredAgentOutcome<T>, StructuredOutputError>
47    where
48        T: DeserializeOwned,
49    {
50        let output = self.response.structured()?;
51        Ok(StructuredAgentOutcome {
52            output,
53            outcome: self,
54        })
55    }
56}
57
58/// A locally validated typed value and its complete Agent execution outcome.
59#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
60pub struct StructuredAgentOutcome<T> {
61    /// Deserialized final output.
62    pub output: T,
63    /// Canonical response, transcript, counters, and usage.
64    pub outcome: AgentOutcome,
65}
66
67#[cfg(test)]
68mod tests {
69    use std::collections::BTreeMap;
70
71    use runifold_core::Usage;
72    use runifold_model::{
73        ContentPart, FinishReason, ModelRef, ModelResponse, ModelUsage, StructuredOutputErrorKind,
74    };
75    use serde::Deserialize;
76
77    use super::AgentOutcome;
78
79    #[derive(Debug, Deserialize, Eq, PartialEq)]
80    struct Answer {
81        value: u32,
82    }
83
84    fn outcome(text: &str) -> AgentOutcome {
85        AgentOutcome {
86            response: ModelResponse {
87                id: Some("response".into()),
88                model: ModelRef::new("test", "model"),
89                content: vec![ContentPart::text(text)],
90                finish_reason: FinishReason::Stop,
91                usage: ModelUsage::default(),
92                warnings: Vec::new(),
93                provider_metadata: BTreeMap::new(),
94                provider_events: Vec::new(),
95            },
96            transcript: Vec::new(),
97            turns: 1,
98            tool_calls: 0,
99            delegations: 0,
100            usage: Usage::default(),
101        }
102    }
103
104    #[test]
105    fn typed_outcome_preserves_canonical_execution_metadata() {
106        let typed = outcome("{\"value\":42}")
107            .into_structured::<Answer>()
108            .unwrap();
109
110        assert_eq!(typed.output, Answer { value: 42 });
111        assert_eq!(typed.outcome.response.id.as_deref(), Some("response"));
112        assert_eq!(typed.outcome.turns, 1);
113    }
114
115    #[test]
116    fn typed_outcome_rejects_a_shape_mismatch() {
117        let error = outcome("{\"value\":\"wrong\"}")
118            .into_structured::<Answer>()
119            .unwrap_err();
120
121        assert_eq!(error.kind, StructuredOutputErrorKind::InvalidOutput);
122    }
123}