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 pub fn into_structured<T>(self) -> Result<StructuredAgentOutcome<T>, StructuredOutputError>
61 where
62 T: DeserializeOwned,
63 {
64 let output = self.response.structured()?;
65 Ok(StructuredAgentOutcome {
66 output,
67 outcome: self,
68 })
69 }
70}
71
72#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
74pub struct StructuredAgentOutcome<T> {
75 pub output: T,
77 pub outcome: AgentOutcome,
79}
80
81#[cfg(test)]
82mod tests {
83 use std::collections::BTreeMap;
84
85 use runifold_core::Usage;
86 use runifold_model::{
87 ContentPart, FinishReason, ModelRef, ModelResponse, ModelUsage, StructuredOutputErrorKind,
88 };
89 use serde::Deserialize;
90
91 use super::AgentOutcome;
92
93 #[derive(Debug, Deserialize, Eq, PartialEq)]
94 struct Answer {
95 value: u32,
96 }
97
98 fn outcome(text: &str) -> AgentOutcome {
99 AgentOutcome {
100 response: ModelResponse {
101 id: Some("response".into()),
102 model: ModelRef::new("test", "model"),
103 content: vec![ContentPart::text(text)],
104 finish_reason: FinishReason::Stop,
105 usage: ModelUsage::default(),
106 warnings: Vec::new(),
107 provider_metadata: BTreeMap::new(),
108 provider_events: Vec::new(),
109 },
110 transcript: Vec::new(),
111 turns: 1,
112 tool_calls: 0,
113 delegations: 0,
114 usage: Usage::default(),
115 }
116 }
117
118 #[test]
119 fn typed_outcome_preserves_canonical_execution_metadata() {
120 let typed = outcome("{\"value\":42}")
121 .into_structured::<Answer>()
122 .unwrap();
123
124 assert_eq!(typed.output, Answer { value: 42 });
125 assert_eq!(typed.outcome.response.id.as_deref(), Some("response"));
126 assert_eq!(typed.outcome.turns, 1);
127 }
128
129 #[test]
130 fn typed_outcome_rejects_a_shape_mismatch() {
131 let error = outcome("{\"value\":\"wrong\"}")
132 .into_structured::<Answer>()
133 .unwrap_err();
134
135 assert_eq!(error.kind, StructuredOutputErrorKind::InvalidOutput);
136 }
137}