Skip to main content

oharness_core/
outcome.rs

1//! `RunOutcome` and related types (§4.4) plus `AgentError` (§16.2).
2
3use crate::completion::Usage;
4use crate::ids::{ModelId, RunId};
5use crate::trajectory::TrajectoryHandle;
6use crate::MetadataMap;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::time::Duration;
10use time::OffsetDateTime;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct RunOutcome {
14    pub run_id: RunId,
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    pub task_id: Option<String>,
17
18    pub termination: Termination,
19
20    /// Final conversation state as the LLM saw it.
21    pub final_messages: Vec<crate::message::Message>,
22
23    /// Lazy reference to the event stream. Never inlined on serialization.
24    pub trajectory: TrajectoryHandle,
25
26    pub usage: ResourceUsage,
27    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
28    pub per_model_usage: HashMap<ModelId, ResourceUsage>,
29
30    #[serde(with = "time::serde::rfc3339")]
31    pub started_at: OffsetDateTime,
32    #[serde(with = "time::serde::rfc3339")]
33    pub finished_at: OffsetDateTime,
34
35    /// Agent-specific opaque state (e.g. planner scratch, memory summaries).
36    #[serde(default, skip_serializing_if = "MetadataMap::is_empty")]
37    pub agent_state: MetadataMap,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41#[serde(tag = "kind", rename_all = "snake_case")]
42pub enum Termination {
43    Completed { reason: CompletionReason },
44    Truncated { limit: TruncationLimit },
45    Failed { error: RunError, at_turn: u32 },
46    Interrupted { reason: InterruptionReason },
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub enum CompletionReason {
52    EndTurn,
53    StopSequence(String),
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
57#[serde(rename_all = "snake_case")]
58pub enum TruncationLimit {
59    MaxTurns(u32),
60    Budget(String),
61    Timeout,
62    MaxTokens,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(rename_all = "snake_case")]
67pub enum InterruptionReason {
68    User,
69    ApprovalDenied(String),
70    Cancellation,
71}
72
73/// In-run failure captured on the outcome (distinct from `AgentError`, which is
74/// returned when the run couldn't even start).
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct RunError {
77    pub category: RunErrorCategory,
78    pub message: String,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(rename_all = "snake_case")]
83pub enum RunErrorCategory {
84    Llm,
85    Tool,
86    Memory,
87    Budget,
88    Critic,
89    UserSimulator,
90    Other,
91}
92
93#[derive(Debug, Clone, Default, Serialize, Deserialize)]
94pub struct ResourceUsage {
95    pub tokens_input: u64,
96    pub tokens_output: u64,
97    pub tokens_cache_read: u64,
98    pub tokens_cache_create: u64,
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub cost_usd: Option<f64>,
101    #[serde(with = "duration_seconds")]
102    pub wall_clock: Duration,
103    pub turns: u32,
104    pub tool_calls: u32,
105}
106
107impl ResourceUsage {
108    pub fn add_usage(&mut self, u: &Usage) {
109        self.tokens_input += u.tokens_input;
110        self.tokens_output += u.tokens_output;
111        self.tokens_cache_read += u.tokens_cache_read;
112        self.tokens_cache_create += u.tokens_cache_create;
113    }
114}
115
116mod duration_seconds {
117    use serde::{Deserialize, Deserializer, Serializer};
118    use std::time::Duration;
119
120    pub fn serialize<S: Serializer>(d: &Duration, s: S) -> Result<S::Ok, S::Error> {
121        s.serialize_f64(d.as_secs_f64())
122    }
123
124    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Duration, D::Error> {
125        let secs = f64::deserialize(d)?;
126        Ok(Duration::from_secs_f64(secs))
127    }
128}
129
130/// Top-level error for "couldn't start the run." Mid-run failures are reported via
131/// `Termination::Failed` on a successful `Ok(RunOutcome)`.
132#[derive(Debug, thiserror::Error)]
133pub enum AgentError {
134    #[error("llm error: {0}")]
135    Llm(String),
136    #[error("tool error: {0}")]
137    Tool(String),
138    #[error("memory error: {0}")]
139    Memory(String),
140    #[error("budget exceeded: {0}")]
141    Budget(String),
142    #[error("configuration error: {0}")]
143    Configuration(String),
144    #[error("cancelled")]
145    Cancelled,
146    #[error(transparent)]
147    Io(#[from] std::io::Error),
148    #[error("{0}")]
149    Other(String),
150}