1use serde::{Deserialize, Serialize};
2
3use crate::{Cursor, RequestSize, StepSource, Verdict};
4
5#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
21#[non_exhaustive]
22pub struct StepRow {
23 pub cursor: Cursor,
25 pub harness: String,
28 pub source: StepSource,
29 pub step_ref: String,
32 pub session: String,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub run_id: Option<String>,
36 pub occurred_at_ms: u64,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub provider: Option<String>,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub model: Option<String>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub input: Option<u64>,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub cache_read: Option<u64>,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub cache_write: Option<u64>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub cache_write_5m: Option<u64>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub cache_write_1h: Option<u64>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub output: Option<u64>,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub reasoning: Option<u64>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub finish: Option<String>,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub context_limit: Option<u64>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub request_body_len: Option<u64>,
72 pub prompt: u64,
74 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub retention: Option<f64>,
80 pub verdict: Verdict,
81 pub prompt_dropped: bool,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub request_size: Option<RequestSize>,
85}
86
87#[derive(Clone, Debug, PartialEq, Eq)]
90#[non_exhaustive]
91pub enum BadCounts {
92 ReasoningExceedsOutput {
94 harness: String,
95 step_ref: String,
96 reasoning: u64,
97 output: u64,
98 },
99}
100
101impl std::fmt::Display for BadCounts {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 match self {
104 Self::ReasoningExceedsOutput {
105 harness,
106 step_ref,
107 reasoning,
108 output,
109 } => write!(
110 f,
111 "row {harness} {step_ref}: reasoning {reasoning} exceeds output {output}"
112 ),
113 }
114 }
115}
116
117impl std::error::Error for BadCounts {}
118
119pub fn live_row_identity(
145 runner: &str,
146 harness: &str,
147 harness_step_ref: Option<&str>,
148 attempt_id: Option<&str>,
149) -> Option<(String, String)> {
150 match (harness_step_ref, attempt_id) {
151 (Some(host_record), _) => Some((harness.to_owned(), host_record.to_owned())),
152 (None, Some(attempt_id)) => Some((runner.to_owned(), attempt_id.to_owned())),
153 (None, None) => None,
154 }
155}
156
157#[derive(Clone, Debug, PartialEq, Eq)]
159#[non_exhaustive]
160pub enum NotClosed {
161 NotAnObject { object: &'static str },
163 UnknownMember {
165 object: &'static str,
166 member: String,
167 },
168}
169
170impl std::fmt::Display for NotClosed {
171 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 match self {
173 Self::NotAnObject { object } => write!(f, "{object} is not a JSON object"),
174 Self::UnknownMember { object, member } => {
175 write!(f, "{object} carries unknown member `{member}`")
176 }
177 }
178 }
179}
180
181impl std::error::Error for NotClosed {}
182
183pub(crate) fn check_members(
184 object: &'static str,
185 value: &serde_json::Value,
186 allowed: &[&str],
187) -> Result<(), NotClosed> {
188 let members = value.as_object().ok_or(NotClosed::NotAnObject { object })?;
189 match members.keys().find(|key| !allowed.contains(&key.as_str())) {
190 Some(member) => Err(NotClosed::UnknownMember {
191 object,
192 member: member.clone(),
193 }),
194 None => Ok(()),
195 }
196}
197
198impl StepRow {
199 pub const REQUIRED_MEMBERS: &'static [&'static str] = &[
201 "cursor",
202 "harness",
203 "source",
204 "step_ref",
205 "session",
206 "occurred_at_ms",
207 "prompt",
208 "verdict",
209 "prompt_dropped",
210 ];
211
212 pub const OPTIONAL_MEMBERS: &'static [&'static str] = &[
214 "run_id",
215 "provider",
216 "model",
217 "input",
218 "cache_read",
219 "cache_write",
220 "cache_write_5m",
221 "cache_write_1h",
222 "output",
223 "reasoning",
224 "finish",
225 "context_limit",
226 "request_body_len",
227 "retention",
228 "request_size",
229 ];
230
231 #[allow(clippy::too_many_arguments)]
233 pub fn new(
234 cursor: impl Into<Cursor>,
235 harness: impl Into<String>,
236 source: StepSource,
237 step_ref: impl Into<String>,
238 session: impl Into<String>,
239 occurred_at_ms: u64,
240 prompt: u64,
241 verdict: Verdict,
242 prompt_dropped: bool,
243 ) -> Self {
244 Self {
245 cursor: cursor.into(),
246 harness: harness.into(),
247 source,
248 step_ref: step_ref.into(),
249 session: session.into(),
250 run_id: None,
251 occurred_at_ms,
252 provider: None,
253 model: None,
254 input: None,
255 cache_read: None,
256 cache_write: None,
257 cache_write_5m: None,
258 cache_write_1h: None,
259 output: None,
260 reasoning: None,
261 finish: None,
262 context_limit: None,
263 request_body_len: None,
264 prompt,
265 retention: None,
266 verdict,
267 prompt_dropped,
268 request_size: None,
269 }
270 }
271
272 pub fn identity(&self) -> (&str, &str) {
274 (&self.harness, &self.step_ref)
275 }
276
277 pub fn check_closed(value: &serde_json::Value) -> Result<(), NotClosed> {
282 let allowed: Vec<&str> = Self::REQUIRED_MEMBERS
283 .iter()
284 .chain(Self::OPTIONAL_MEMBERS)
285 .copied()
286 .collect();
287 check_members("row", value, &allowed)
288 }
289
290 pub fn check_counts(&self) -> Result<(), BadCounts> {
295 match (self.reasoning, self.output) {
296 (Some(reasoning), Some(output)) if reasoning > output => {
297 Err(BadCounts::ReasoningExceedsOutput {
298 harness: self.harness.clone(),
299 step_ref: self.step_ref.clone(),
300 reasoning,
301 output,
302 })
303 }
304 _ => Ok(()),
305 }
306 }
307
308 pub fn with_run_id(mut self, run_id: impl Into<String>) -> Self {
309 self.run_id = Some(run_id.into());
310 self
311 }
312 pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
313 self.provider = Some(provider.into());
314 self
315 }
316 pub fn with_model(mut self, model: impl Into<String>) -> Self {
317 self.model = Some(model.into());
318 self
319 }
320 pub fn with_input(mut self, input: u64) -> Self {
321 self.input = Some(input);
322 self
323 }
324 pub fn with_cache_read(mut self, cache_read: u64) -> Self {
325 self.cache_read = Some(cache_read);
326 self
327 }
328 pub fn with_cache_write(mut self, cache_write: u64) -> Self {
329 self.cache_write = Some(cache_write);
330 self
331 }
332 pub fn with_cache_write_5m(mut self, cache_write_5m: u64) -> Self {
333 self.cache_write_5m = Some(cache_write_5m);
334 self
335 }
336 pub fn with_cache_write_1h(mut self, cache_write_1h: u64) -> Self {
337 self.cache_write_1h = Some(cache_write_1h);
338 self
339 }
340 pub fn with_output(mut self, output: u64) -> Self {
341 self.output = Some(output);
342 self
343 }
344 pub fn with_reasoning(mut self, reasoning: u64) -> Self {
345 self.reasoning = Some(reasoning);
346 self
347 }
348 pub fn with_finish(mut self, finish: impl Into<String>) -> Self {
349 self.finish = Some(finish.into());
350 self
351 }
352 pub fn with_context_limit(mut self, context_limit: u64) -> Self {
353 self.context_limit = Some(context_limit);
354 self
355 }
356 pub fn with_request_body_len(mut self, request_body_len: u64) -> Self {
357 self.request_body_len = Some(request_body_len);
358 self
359 }
360 pub fn with_retention(mut self, retention: f64) -> Self {
361 self.retention = Some(retention);
362 self
363 }
364 pub fn with_request_size(mut self, request_size: RequestSize) -> Self {
365 self.request_size = Some(request_size);
366 self
367 }
368}