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 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#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
45pub struct StructuredAgentOutcome<T> {
46 pub output: T,
48 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}