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    /// Locally validates and decodes the final model response while preserving
25    /// the complete canonical outcome.
26    ///
27    /// # Errors
28    ///
29    /// Returns [`StructuredOutputError`] when the response is missing textual
30    /// output, contains a refusal, or does not deserialize as `T`.
31    pub fn into_structured<T>(self) -> Result<StructuredAgentOutcome<T>, StructuredOutputError>
32    where
33        T: DeserializeOwned,
34    {
35        let output = self.response.structured()?;
36        Ok(StructuredAgentOutcome {
37            output,
38            outcome: self,
39        })
40    }
41}
42
43/// A locally validated typed value and its complete Agent execution outcome.
44#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
45pub struct StructuredAgentOutcome<T> {
46    /// Deserialized final output.
47    pub output: T,
48    /// Canonical response, transcript, counters, and usage.
49    pub outcome: AgentOutcome,
50}
51
52#[cfg(test)]
53mod tests {
54    use std::collections::BTreeMap;
55
56    use runifold_core::Usage;
57    use runifold_model::{
58        ContentPart, FinishReason, ModelRef, ModelResponse, ModelUsage, StructuredOutputErrorKind,
59    };
60    use serde::Deserialize;
61
62    use super::AgentOutcome;
63
64    #[derive(Debug, Deserialize, Eq, PartialEq)]
65    struct Answer {
66        value: u32,
67    }
68
69    fn outcome(text: &str) -> AgentOutcome {
70        AgentOutcome {
71            response: ModelResponse {
72                id: Some("response".into()),
73                model: ModelRef::new("test", "model"),
74                content: vec![ContentPart::text(text)],
75                finish_reason: FinishReason::Stop,
76                usage: ModelUsage::default(),
77                warnings: Vec::new(),
78                provider_metadata: BTreeMap::new(),
79                provider_events: Vec::new(),
80            },
81            transcript: Vec::new(),
82            turns: 1,
83            tool_calls: 0,
84            delegations: 0,
85            usage: Usage::default(),
86        }
87    }
88
89    #[test]
90    fn typed_outcome_preserves_canonical_execution_metadata() {
91        let typed = outcome("{\"value\":42}")
92            .into_structured::<Answer>()
93            .unwrap();
94
95        assert_eq!(typed.output, Answer { value: 42 });
96        assert_eq!(typed.outcome.response.id.as_deref(), Some("response"));
97        assert_eq!(typed.outcome.turns, 1);
98    }
99
100    #[test]
101    fn typed_outcome_rejects_a_shape_mismatch() {
102        let error = outcome("{\"value\":\"wrong\"}")
103            .into_structured::<Answer>()
104            .unwrap_err();
105
106        assert_eq!(error.kind, StructuredOutputErrorKind::InvalidOutput);
107    }
108}