1use super::runner::WorkspaceRetention;
2use crate::evals::{RetainedWorkspaceInfo, TaskRun};
3use schemars::{JsonSchema, Schema, schema_for};
4use serde::Serialize;
5use serde_json::Value;
6
7#[derive(Debug, Clone, Serialize)]
9#[serde(rename_all = "camelCase")]
10pub struct EvalFilesReport {
11 pub evals: Vec<EvalOutcome>,
12}
13
14#[derive(Debug, Clone, Serialize, JsonSchema)]
15#[serde(rename_all = "camelCase", deny_unknown_fields)]
16pub struct EvalOutcome {
17 pub name: String,
18 pub passed: bool,
19 pub failures: Vec<String>,
20 pub tool_calls: Vec<EvalToolCall>,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub judge: Option<JudgeSummary>,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub failure_context: Option<String>,
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub retained_workspace: Option<RetainedWorkspaceInfo>,
28}
29
30#[derive(Debug, Clone, Serialize, JsonSchema)]
31#[serde(rename_all = "camelCase", deny_unknown_fields)]
32pub struct EvalToolCall {
33 pub name: String,
34 pub arguments: Option<Value>,
35 pub raw_arguments: String,
36}
37
38impl EvalOutcome {
39 pub fn schema() -> Schema {
41 schema_for!(Self)
42 }
43
44 pub(crate) fn setup_failed(name: impl Into<String>, error: impl std::fmt::Display) -> Self {
45 Self::failed(name, vec![format!("workspace setup failed: {error}")])
46 }
47
48 pub(crate) fn failed(name: impl Into<String>, failures: Vec<String>) -> Self {
49 Self {
50 name: name.into(),
51 passed: false,
52 failures,
53 tool_calls: Vec::new(),
54 judge: None,
55 failure_context: None,
56 retained_workspace: None,
57 }
58 }
59
60 pub(crate) fn from_task_run(
61 name: impl Into<String>,
62 run: TaskRun,
63 failures: Vec<String>,
64 judge: Option<JudgeSummary>,
65 retention: WorkspaceRetention,
66 ) -> Self {
67 let passed = failures.is_empty();
68 let failure_context = (!passed).then(|| run.failure_context());
69 let tool_calls = EvalToolCall::from_task_run(&run);
70 let retained_workspace = match retention {
71 WorkspaceRetention::Retain => Some(run.into_workspace().persist()),
72 WorkspaceRetention::Discard => None,
73 };
74
75 Self { name: name.into(), passed, failures, tool_calls, judge, failure_context, retained_workspace }
76 }
77}
78
79impl EvalToolCall {
80 fn from_task_run(run: &TaskRun) -> Vec<Self> {
81 run.transcript()
82 .all_tool_calls()
83 .map(|call| Self {
84 name: call.name.to_string(),
85 arguments: call.arguments_json().ok(),
86 raw_arguments: call.arguments.to_string(),
87 })
88 .collect()
89 }
90}
91
92#[derive(Debug, Clone, Serialize, JsonSchema)]
93#[serde(rename_all = "camelCase", deny_unknown_fields)]
94pub struct JudgeSummary {
95 pub passed: bool,
96 pub score: f64,
97 pub reason: String,
98 pub criteria: Vec<JudgeCriterionSummary>,
99}
100
101#[derive(Debug, Clone, Serialize, JsonSchema)]
102#[serde(rename_all = "camelCase", deny_unknown_fields)]
103pub struct JudgeCriterionSummary {
104 pub id: String,
105 pub description: String,
106 pub blocking: bool,
107 pub weight: f64,
108 pub threshold: f64,
109 pub score: f64,
110 pub reason: String,
111}
112
113impl EvalFilesReport {
114 pub fn passed(&self) -> bool {
115 self.evals.iter().all(|eval| eval.passed)
116 }
117
118 pub fn passed_count(&self) -> usize {
119 self.evals.iter().filter(|eval| eval.passed).count()
120 }
121
122 pub fn failed_count(&self) -> usize {
123 self.evals.iter().filter(|eval| !eval.passed).count()
124 }
125}
126
127impl JudgeSummary {
128 pub fn blocking_failures(&self) -> impl Iterator<Item = String> + '_ {
130 self.criteria
131 .iter()
132 .filter(|criterion| criterion.blocking && !criterion.passed())
133 .map(|criterion| format!("judge criterion `{}`: {}", criterion.id, criterion.reason))
134 }
135}
136
137impl JudgeCriterionSummary {
138 pub fn passed(&self) -> bool {
139 self.score >= self.threshold
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn blocking_failures_report_only_blocking_criteria_below_threshold() {
149 let criterion = |id: &str, blocking, score| JudgeCriterionSummary {
150 id: id.to_string(),
151 description: "desc".to_string(),
152 blocking,
153 weight: 1.0,
154 threshold: 0.8,
155 score,
156 reason: format!("{id} reason"),
157 };
158 let summary = JudgeSummary {
159 passed: false,
160 score: 0.0,
161 reason: "r".to_string(),
162 criteria: vec![
163 criterion("met", true, 0.9),
164 criterion("failed", true, 0.5),
165 criterion("advisory", false, 0.0),
166 ],
167 };
168
169 let failures: Vec<String> = summary.blocking_failures().collect();
170
171 assert_eq!(failures, vec!["judge criterion `failed`: failed reason".to_string()]);
172 }
173}