1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10fn default_outcome_timestamp() -> DateTime<Utc> {
15 Utc::now()
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct AgentOutcome {
27 pub status: OutcomeStatus,
29 #[serde(default)]
31 pub summary: String,
32 #[serde(default)]
34 pub evidence: Vec<Evidence>,
35 #[serde(default)]
37 pub metrics: OutcomeMetrics,
38 #[serde(default = "default_outcome_timestamp")]
40 pub timestamp: DateTime<Utc>,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum OutcomeStatus {
47 Success,
49 PartialSuccess,
51 GiveUp,
53 Timeout,
55 Failure,
57 Done,
59}
60
61impl OutcomeStatus {
62 pub fn is_completed(&self) -> bool {
64 matches!(self, Self::Success | Self::PartialSuccess | Self::Done)
65 }
66
67 pub fn is_terminal(&self) -> bool {
69 true }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct Evidence {
76 pub kind: EvidenceKind,
78 pub description: String,
80 pub data: Option<Value>,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(rename_all = "snake_case")]
87pub enum EvidenceKind {
88 SelfAssessment,
90 ToolResult,
92 StateChange,
94 ExternalVerification,
96 StopReason,
98 Evaluator,
100}
101
102#[derive(Debug, Clone, Default, Serialize, Deserialize)]
108pub struct OutcomeMetrics {
109 #[serde(default)]
111 pub turns: u32,
112 #[serde(default)]
114 pub tool_calls: u32,
115 #[serde(default)]
117 pub duration_ms: f64,
118 #[serde(default)]
120 pub retries: u32,
121 #[serde(default)]
123 pub actions_succeeded: u32,
124 #[serde(default)]
126 pub actions_failed: u32,
127}
128
129impl AgentOutcome {
130 pub fn success(summary: &str) -> Self {
132 Self {
133 status: OutcomeStatus::Success,
134 summary: summary.to_string(),
135 evidence: Vec::new(),
136 metrics: OutcomeMetrics::default(),
137 timestamp: Utc::now(),
138 }
139 }
140
141 pub fn failure(summary: &str) -> Self {
143 Self {
144 status: OutcomeStatus::Failure,
145 summary: summary.to_string(),
146 evidence: Vec::new(),
147 metrics: OutcomeMetrics::default(),
148 timestamp: Utc::now(),
149 }
150 }
151
152 pub fn timeout(summary: &str, turns: u32, max_turns: u32) -> Self {
154 Self {
155 status: OutcomeStatus::Timeout,
156 summary: summary.to_string(),
157 evidence: vec![Evidence {
158 kind: EvidenceKind::StopReason,
159 description: format!("Reached {} of {} max turns", turns, max_turns),
160 data: Some(serde_json::json!({
161 "turns": turns,
162 "max_turns": max_turns,
163 })),
164 }],
165 metrics: OutcomeMetrics {
166 turns,
167 ..Default::default()
168 },
169 timestamp: Utc::now(),
170 }
171 }
172
173 pub fn give_up(reason: &str) -> Self {
175 Self {
176 status: OutcomeStatus::GiveUp,
177 summary: reason.to_string(),
178 evidence: vec![Evidence {
179 kind: EvidenceKind::SelfAssessment,
180 description: reason.to_string(),
181 data: None,
182 }],
183 metrics: OutcomeMetrics::default(),
184 timestamp: Utc::now(),
185 }
186 }
187
188 pub fn with_evidence(mut self, evidence: Evidence) -> Self {
190 self.evidence.push(evidence);
191 self
192 }
193
194 pub fn with_metrics(mut self, metrics: OutcomeMetrics) -> Self {
196 self.metrics = metrics;
197 self
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn test_outcome_status_classification() {
207 assert!(OutcomeStatus::Success.is_completed());
208 assert!(OutcomeStatus::PartialSuccess.is_completed());
209 assert!(OutcomeStatus::Done.is_completed());
210 assert!(!OutcomeStatus::Failure.is_completed());
211 assert!(!OutcomeStatus::Timeout.is_completed());
212 assert!(!OutcomeStatus::GiveUp.is_completed());
213 }
214
215 #[test]
216 fn test_all_statuses_are_terminal() {
217 assert!(OutcomeStatus::Success.is_terminal());
218 assert!(OutcomeStatus::PartialSuccess.is_terminal());
219 assert!(OutcomeStatus::Done.is_terminal());
220 assert!(OutcomeStatus::Failure.is_terminal());
221 assert!(OutcomeStatus::Timeout.is_terminal());
222 assert!(OutcomeStatus::GiveUp.is_terminal());
223 }
224
225 #[test]
226 fn test_timeout_outcome() {
227 let outcome = AgentOutcome::timeout("Exceeded step limit", 10, 10);
228 assert_eq!(outcome.status, OutcomeStatus::Timeout);
229 assert_eq!(outcome.metrics.turns, 10);
230 assert_eq!(outcome.evidence.len(), 1);
231 assert_eq!(outcome.evidence[0].kind, EvidenceKind::StopReason);
232 }
233
234 #[test]
235 fn test_outcome_with_evidence() {
236 let outcome = AgentOutcome::success("Task done")
237 .with_evidence(Evidence {
238 kind: EvidenceKind::ToolResult,
239 description: "File created".to_string(),
240 data: Some(serde_json::json!({"path": "/tmp/out.txt"})),
241 })
242 .with_evidence(Evidence {
243 kind: EvidenceKind::ExternalVerification,
244 description: "Tests passed".to_string(),
245 data: None,
246 });
247 assert_eq!(outcome.evidence.len(), 2);
248 }
249
250 #[test]
251 fn test_give_up_outcome() {
252 let outcome = AgentOutcome::give_up("Cannot access required API");
253 assert_eq!(outcome.status, OutcomeStatus::GiveUp);
254 assert_eq!(outcome.evidence.len(), 1);
255 assert_eq!(outcome.evidence[0].kind, EvidenceKind::SelfAssessment);
256 }
257
258 #[test]
259 fn test_failure_outcome() {
260 let outcome = AgentOutcome::failure("Connection refused");
261 assert_eq!(outcome.status, OutcomeStatus::Failure);
262 assert!(!outcome.status.is_completed());
263 }
264
265 #[test]
266 fn test_outcome_serde_roundtrip() {
267 let outcome = AgentOutcome::success("Done")
268 .with_evidence(Evidence {
269 kind: EvidenceKind::ToolResult,
270 description: "ok".to_string(),
271 data: Some(serde_json::json!(42)),
272 })
273 .with_metrics(OutcomeMetrics {
274 turns: 5,
275 tool_calls: 3,
276 duration_ms: 1234.5,
277 retries: 1,
278 actions_succeeded: 4,
279 actions_failed: 1,
280 });
281
282 let json = serde_json::to_string(&outcome).unwrap();
283 let roundtripped: AgentOutcome = serde_json::from_str(&json).unwrap();
284
285 assert_eq!(roundtripped.status, OutcomeStatus::Success);
286 assert_eq!(roundtripped.summary, "Done");
287 assert_eq!(roundtripped.evidence.len(), 1);
288 assert_eq!(roundtripped.metrics.turns, 5);
289 assert_eq!(roundtripped.metrics.tool_calls, 3);
290 }
291
292 #[test]
299 fn test_outcome_deserializes_harness_shape_without_timestamp() {
300 let harness = r#"{"status":"success","summary":"Created file","evidence":[],"metrics":{"turns":3,"tool_calls":3,"actions_succeeded":3,"actions_failed":0},"tools_called":["drive_cli","check_outcome","finish"]}"#;
301 let outcome: AgentOutcome =
302 serde_json::from_str(harness).expect("harness shape must deserialize");
303 assert_eq!(outcome.status, OutcomeStatus::Success);
304 assert_eq!(outcome.summary, "Created file");
305 assert!(outcome.evidence.is_empty());
306 assert_eq!(outcome.metrics.turns, 3);
307 assert_eq!(outcome.metrics.tool_calls, 3);
308 assert_eq!(outcome.metrics.actions_succeeded, 3);
309 assert_eq!(outcome.metrics.actions_failed, 0);
310 assert_eq!(outcome.metrics.duration_ms, 0.0);
312 assert_eq!(outcome.metrics.retries, 0);
313 let json = serde_json::to_string(&outcome).unwrap();
315 assert!(json.contains("\"timestamp\""));
316 }
317
318 #[test]
319 fn test_outcome_status_snake_case_serde() {
320 assert_eq!(
321 serde_json::to_string(&OutcomeStatus::PartialSuccess).unwrap(),
322 "\"partial_success\""
323 );
324 assert_eq!(
325 serde_json::to_string(&OutcomeStatus::GiveUp).unwrap(),
326 "\"give_up\""
327 );
328 }
329}