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 #[must_use]
41 pub fn terminal_repairs(&self) -> u32 {
42 self.transcript
43 .iter()
44 .filter(|message| {
45 message.metadata.get("runifold.terminal_repair")
46 == Some(&serde_json::Value::Bool(true))
47 })
48 .count()
49 .try_into()
50 .unwrap_or(u32::MAX)
51 }
52
53 #[must_use]
55 pub fn turn_review_repairs(&self) -> u32 {
56 self.transcript
57 .iter()
58 .filter(|message| {
59 message.metadata.get("runifold.turn_review_repair")
60 == Some(&serde_json::Value::Bool(true))
61 })
62 .count()
63 .try_into()
64 .unwrap_or(u32::MAX)
65 }
66
67 #[must_use]
69 pub fn terminal_review_repairs(&self) -> u32 {
70 self.transcript
71 .iter()
72 .filter(|message| {
73 message.metadata.get("runifold.terminal_review_repair")
74 == Some(&serde_json::Value::Bool(true))
75 })
76 .count()
77 .try_into()
78 .unwrap_or(u32::MAX)
79 }
80
81 pub fn into_structured<T>(self) -> Result<StructuredAgentOutcome<T>, StructuredOutputError>
89 where
90 T: DeserializeOwned,
91 {
92 let output = self.response.structured()?;
93 Ok(StructuredAgentOutcome {
94 output,
95 outcome: self,
96 })
97 }
98}
99
100#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
102pub struct StructuredAgentOutcome<T> {
103 pub output: T,
105 pub outcome: AgentOutcome,
107}
108
109#[cfg(test)]
110mod tests {
111 use std::collections::BTreeMap;
112
113 use runifold_core::Usage;
114 use runifold_model::{
115 ContentPart, FinishReason, ModelRef, ModelResponse, ModelUsage, StructuredOutputErrorKind,
116 };
117 use serde::Deserialize;
118
119 use super::AgentOutcome;
120
121 #[derive(Debug, Deserialize, Eq, PartialEq)]
122 struct Answer {
123 value: u32,
124 }
125
126 fn outcome(text: &str) -> AgentOutcome {
127 AgentOutcome {
128 response: ModelResponse {
129 id: Some("response".into()),
130 model: ModelRef::new("test", "model"),
131 content: vec![ContentPart::text(text)],
132 finish_reason: FinishReason::Stop,
133 usage: ModelUsage::default(),
134 warnings: Vec::new(),
135 provider_metadata: BTreeMap::new(),
136 provider_events: Vec::new(),
137 },
138 transcript: Vec::new(),
139 turns: 1,
140 tool_calls: 0,
141 delegations: 0,
142 usage: Usage::default(),
143 }
144 }
145
146 #[test]
147 fn typed_outcome_preserves_canonical_execution_metadata() {
148 let typed = outcome("{\"value\":42}")
149 .into_structured::<Answer>()
150 .unwrap();
151
152 assert_eq!(typed.output, Answer { value: 42 });
153 assert_eq!(typed.outcome.response.id.as_deref(), Some("response"));
154 assert_eq!(typed.outcome.turns, 1);
155 }
156
157 #[test]
158 fn typed_outcome_rejects_a_shape_mismatch() {
159 let error = outcome("{\"value\":\"wrong\"}")
160 .into_structured::<Answer>()
161 .unwrap_err();
162
163 assert_eq!(error.kind, StructuredOutputErrorKind::InvalidOutput);
164 }
165}