use runifold_core::Usage;
use runifold_model::{Message, ModelResponse, StructuredOutputError};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct AgentOutcome {
pub response: ModelResponse,
pub transcript: Vec<Message>,
pub turns: u32,
pub tool_calls: u32,
pub delegations: u32,
pub usage: Usage,
}
impl AgentOutcome {
#[must_use]
pub fn text(&self) -> String {
self.response.text()
}
#[must_use]
pub fn into_text(self) -> String {
self.response.into_text()
}
pub fn into_structured<T>(self) -> Result<StructuredAgentOutcome<T>, StructuredOutputError>
where
T: DeserializeOwned,
{
let output = self.response.structured()?;
Ok(StructuredAgentOutcome {
output,
outcome: self,
})
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct StructuredAgentOutcome<T> {
pub output: T,
pub outcome: AgentOutcome,
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use runifold_core::Usage;
use runifold_model::{
ContentPart, FinishReason, ModelRef, ModelResponse, ModelUsage, StructuredOutputErrorKind,
};
use serde::Deserialize;
use super::AgentOutcome;
#[derive(Debug, Deserialize, Eq, PartialEq)]
struct Answer {
value: u32,
}
fn outcome(text: &str) -> AgentOutcome {
AgentOutcome {
response: ModelResponse {
id: Some("response".into()),
model: ModelRef::new("test", "model"),
content: vec![ContentPart::text(text)],
finish_reason: FinishReason::Stop,
usage: ModelUsage::default(),
warnings: Vec::new(),
provider_metadata: BTreeMap::new(),
provider_events: Vec::new(),
},
transcript: Vec::new(),
turns: 1,
tool_calls: 0,
delegations: 0,
usage: Usage::default(),
}
}
#[test]
fn typed_outcome_preserves_canonical_execution_metadata() {
let typed = outcome("{\"value\":42}")
.into_structured::<Answer>()
.unwrap();
assert_eq!(typed.output, Answer { value: 42 });
assert_eq!(typed.outcome.response.id.as_deref(), Some("response"));
assert_eq!(typed.outcome.turns, 1);
}
#[test]
fn typed_outcome_rejects_a_shape_mismatch() {
let error = outcome("{\"value\":\"wrong\"}")
.into_structured::<Answer>()
.unwrap_err();
assert_eq!(error.kind, StructuredOutputErrorKind::InvalidOutput);
}
}