runifold_agent/
outcome.rs1use runifold_core::Usage;
2use runifold_model::{Message, ModelResponse, StructuredOutputError};
3use serde::de::DeserializeOwned;
4use serde::{Deserialize, Serialize};
5
6#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
8pub struct AgentOutcome {
9 pub response: ModelResponse,
11 pub transcript: Vec<Message>,
13 pub turns: u32,
15 pub tool_calls: u32,
17 pub delegations: u32,
19 pub usage: Usage,
21}
22
23impl AgentOutcome {
24 #[must_use]
26 pub fn text(&self) -> String {
27 self.response.text()
28 }
29
30 #[must_use]
35 pub fn into_text(self) -> String {
36 self.response.into_text()
37 }
38
39 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#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
60pub struct StructuredAgentOutcome<T> {
61 pub output: T,
63 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}