Skip to main content

agent_evaluate_contract/
lib.rs

1//! HTTP API contract for the Evaluate service.
2//!
3//! This service provides a programmatic interface for running agent evaluation
4//! cases, collecting metrics, and producing reports. The eval harness replays
5//! predefined conversation turns against an agent runtime instance and judges
6//! outcomes against oracles.
7//!
8//! # Layering
9//!
10//! ```text
11//! agent-evaluate-contract   paths + DTOs        (this crate)
12//! agent-evaluate-runtime    eval runner + judge
13//! agent-evaluate-server     router + auth + bin
14//! ```
15
16use serde::{Deserialize, Serialize};
17use serde_json::Value;
18
19pub const SERVICE_NAME: &str = "agent-evaluate";
20pub const DEFAULT_PORT: u16 = 5150;
21pub const AUDIENCE: &str = "agent-evaluate";
22pub const SERVICE_TOKEN_ENV: &str = "AGENT_EVALUATE_SERVICE_TOKEN";
23pub const MAX_RESOURCE_ID_CHARS: usize = 191;
24
25pub const HEALTH_PATH: &str = "/health";
26pub const RUN_EVAL_PATH: &str = "/internal/v1/evaluate/run";
27pub const LIST_CASES_PATH: &str = "/internal/v1/evaluate/cases";
28pub const GET_CASE_PATH: &str = "/internal/v1/evaluate/cases/get";
29pub const GET_REPORT_PATH: &str = "/internal/v1/evaluate/reports";
30pub const LOAD_CASES_PATH: &str = "/internal/v1/evaluate/cases/load";
31pub const SUITES_PATH: &str = "/internal/v1/evaluate/suites";
32pub const RUNS_PATH: &str = "/internal/v1/evaluate/runs";
33pub const OPERATIONS_PATH: &str = "/internal/v1/evaluate/operations";
34pub const RUN_CASES_PATH: &str = "/internal/v1/evaluate/runs/cases";
35pub const CANCEL_RUN_PATH: &str = "/internal/v1/evaluate/runs/cancel";
36pub const DURABLE_REPORT_PATH: &str = "/internal/v1/evaluate/reports/get";
37pub const COMPARE_REPORTS_PATH: &str = "/internal/v1/evaluate/reports/compare";
38pub const REVIEW_QUEUE_PATH: &str = "/internal/v1/evaluate/reviews";
39pub const CLAIM_REVIEW_PATH: &str = "/internal/v1/evaluate/reviews/claim";
40pub const COMPLETE_REVIEW_PATH: &str = "/internal/v1/evaluate/reviews/complete";
41
42// ---------------------------------------------------------------------------
43// Eval case types
44// ---------------------------------------------------------------------------
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47#[serde(rename_all = "camelCase")]
48pub struct EvalCaseFile {
49    pub schema_version: i32,
50    pub cases: Vec<EvalCase>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct EvalCase {
56    pub id: String,
57    pub category: String,
58    #[serde(default)]
59    pub notes: String,
60    pub turns: Vec<EvalTurn>,
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub workspace_files: Option<Value>,
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub oracle: Option<EvalOracle>,
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub verify_commands: Option<Vec<VerifyCommand>>,
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub trace_assertions: Option<Value>,
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub outcome_assertions: Option<Value>,
71    #[serde(default)]
72    pub expects_artifact: bool,
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub generated_workspace_files: Option<Value>,
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub workspace_copies: Option<Value>,
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub workspace_patches: Option<Value>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct EvalTurn {
84    pub user: String,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase")]
89pub struct EvalOracle {
90    pub kind: String,
91    #[serde(default)]
92    pub expected_values: Vec<Value>,
93    #[serde(default)]
94    pub notes: String,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct VerifyCommand {
100    pub name: String,
101    pub command: String,
102    #[serde(default)]
103    pub expected_exit_code: i32,
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub output_contains: Option<Vec<String>>,
106}
107
108// ---------------------------------------------------------------------------
109// API request/response types
110// ---------------------------------------------------------------------------
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
113#[serde(rename_all = "camelCase")]
114pub struct RunEvalRequest {
115    #[serde(default)]
116    pub case_ids: Vec<String>,
117    #[serde(default)]
118    pub categories: Vec<String>,
119    #[serde(default)]
120    pub runtime_types: Vec<String>,
121    #[serde(default)]
122    pub max_turns: usize,
123    #[serde(default)]
124    pub turn_timeout_ms: u64,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128#[serde(rename_all = "camelCase")]
129pub struct EvalReport {
130    pub id: String,
131    pub total_cases: usize,
132    pub passed_cases: usize,
133    pub failed_cases: usize,
134    pub infrastructure_failure_cases: usize,
135    /// Cases whose immutable schema/fixture/evaluator configuration could not
136    /// produce a meaningful quality verdict. Kept separate from provider or
137    /// runtime infrastructure failures.
138    #[serde(default)]
139    pub invalid_case_count: usize,
140    pub non_infrastructure_pass_rate: f64,
141    pub case_results: Vec<EvalCaseResult>,
142    pub created_at: String,
143    pub duration_ms: u64,
144    #[serde(default)]
145    pub total_cost_micros: u64,
146    #[serde(default)]
147    pub p95_case_duration_ms: u64,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
151#[serde(rename_all = "camelCase")]
152pub struct EvalCaseResult {
153    pub case_id: String,
154    pub category: String,
155    pub passed: bool,
156    pub infrastructure_failure: bool,
157    /// Structured terminal classification for non-quality failures. The
158    /// legacy `infrastructure_failure` boolean remains for wire compatibility.
159    #[serde(default, skip_serializing_if = "Option::is_none")]
160    pub failure_kind: Option<CaseFailureKind>,
161    pub turns_completed: usize,
162    pub total_turns: usize,
163    #[serde(default)]
164    pub oracle_results: Vec<OracleVerdict>,
165    #[serde(default)]
166    pub verify_results: Vec<VerifyCommandResult>,
167    #[serde(default)]
168    pub trace_assertion_results: Vec<AssertionVerdict>,
169    #[serde(default)]
170    pub outcome_assertion_results: Vec<AssertionVerdict>,
171    #[serde(default, skip_serializing_if = "Option::is_none")]
172    pub error: Option<String>,
173    pub duration_ms: u64,
174    #[serde(default)]
175    pub cost_micros: u64,
176    /// Durable experiment identity. Legacy synchronous runs leave these at
177    /// their defaults; durable reports retain them so repeated samples from
178    /// different variants are never conflated as flakiness.
179    #[serde(default)]
180    pub sample_index: u32,
181    #[serde(default, skip_serializing_if = "Option::is_none")]
182    pub variant_id: Option<String>,
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
186#[serde(rename_all = "snake_case")]
187pub enum CaseFailureKind {
188    InfraFailed,
189    InvalidCase,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
193#[serde(rename_all = "camelCase")]
194pub struct OracleVerdict {
195    pub expected: String,
196    pub actual: String,
197    pub matched: bool,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
201#[serde(rename_all = "camelCase")]
202pub struct VerifyCommandResult {
203    pub name: String,
204    pub exit_code: i32,
205    pub expected_exit_code: i32,
206    pub passed: bool,
207    #[serde(default, skip_serializing_if = "Option::is_none")]
208    pub output: Option<String>,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
212#[serde(rename_all = "camelCase")]
213pub struct AssertionVerdict {
214    pub rule: String,
215    pub passed: bool,
216    #[serde(default, skip_serializing_if = "Option::is_none")]
217    pub detail: Option<String>,
218}
219
220/// Strictly parsed compatibility schema for `outcomeAssertions`.
221///
222/// `EvalCase` retains the original JSON field so v1 wire JSON is unchanged;
223/// evaluators must deserialize it into this type and fail closed on malformed
224/// or unknown rules.
225#[derive(Debug, Clone, Default, Serialize, Deserialize)]
226#[serde(rename_all = "camelCase", deny_unknown_fields)]
227pub struct OutcomeAssertionSpec {
228    #[serde(default)]
229    pub turns: Vec<TurnAssertionSpec>,
230    #[serde(default)]
231    pub workspace_files: Vec<WorkspaceFileAssertionSpec>,
232    #[serde(default)]
233    pub required_changed_paths: Vec<String>,
234    #[serde(default)]
235    pub forbid_workspace_changes: bool,
236}
237
238#[derive(Debug, Clone, Default, Serialize, Deserialize)]
239#[serde(rename_all = "camelCase", deny_unknown_fields)]
240pub struct TurnAssertionSpec {
241    pub index: usize,
242    #[serde(default)]
243    pub contains: Vec<String>,
244    #[serde(default)]
245    pub contains_any: Vec<Vec<String>>,
246}
247
248#[derive(Debug, Clone, Default, Serialize, Deserialize)]
249#[serde(rename_all = "camelCase", deny_unknown_fields)]
250pub struct WorkspaceFileAssertionSpec {
251    pub path: String,
252    #[serde(default)]
253    pub contains: Vec<String>,
254    #[serde(default)]
255    pub contains_any: Vec<Vec<String>>,
256    #[serde(default, skip_serializing_if = "Option::is_none")]
257    pub max_chars: Option<usize>,
258    #[serde(default, skip_serializing_if = "Option::is_none")]
259    pub max_lines: Option<usize>,
260}
261
262/// Versioned typed assertion schema used by durable suites. The legacy
263/// `Value` fields on [`EvalCase`] remain only for v1 wire compatibility.
264#[derive(Debug, Clone, Serialize, Deserialize)]
265#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
266pub enum TypedAssertion {
267    OutputContains {
268        turn: usize,
269        values: Vec<String>,
270        #[serde(default)]
271        any: bool,
272    },
273    WorkspaceFile {
274        path: String,
275        #[serde(default)]
276        contains: Vec<String>,
277        #[serde(default, skip_serializing_if = "Option::is_none")]
278        max_bytes: Option<usize>,
279    },
280    CommandExit {
281        name: String,
282        command_ref: String,
283        expected_exit_code: i32,
284    },
285    TraceEvent {
286        event_type: String,
287        #[serde(default)]
288        minimum_count: usize,
289    },
290    ArtifactExists {
291        artifact_kind: String,
292    },
293    /// A custom evaluator that is executed by the isolated executor. The
294    /// control plane accepts only an attested result for the exact immutable
295    /// plugin reference; it never loads plugin code into the service process.
296    Plugin {
297        plugin_ref: String,
298        #[serde(default)]
299        config: Value,
300    },
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
304#[serde(rename_all = "camelCase", deny_unknown_fields)]
305pub struct VersionedEvalCase {
306    pub id: String,
307    pub category: String,
308    #[serde(default)]
309    pub turns: Vec<EvalTurn>,
310    #[serde(default)]
311    pub assertions: Vec<TypedAssertion>,
312    #[serde(default)]
313    pub verify_commands: Vec<VerifyCommand>,
314    #[serde(default, skip_serializing_if = "Option::is_none")]
315    pub fixture_ref: Option<String>,
316    #[serde(default)]
317    pub budget: EvalBudget,
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
321#[serde(rename_all = "camelCase", deny_unknown_fields)]
322pub struct EvalBudget {
323    pub timeout_ms: u64,
324    pub max_output_bytes: usize,
325    pub max_cost_micros: u64,
326}
327
328impl Default for EvalBudget {
329    fn default() -> Self {
330        Self {
331            timeout_ms: 300_000,
332            max_output_bytes: 8 * 1024 * 1024,
333            max_cost_micros: 0,
334        }
335    }
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
339#[serde(rename_all = "camelCase", deny_unknown_fields)]
340pub struct EvalSuiteVersion {
341    pub tenant_id: String,
342    pub project_id: String,
343    pub suite_id: String,
344    pub version: String,
345    pub manifest_digest: String,
346    pub schema_version: u32,
347    pub cases: Vec<VersionedEvalCase>,
348    pub created_at_ms: i64,
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize)]
352#[serde(rename_all = "camelCase", deny_unknown_fields)]
353pub struct RegisterSuiteRequest {
354    pub suite_id: String,
355    pub version: String,
356    pub manifest_digest: String,
357    pub schema_version: u32,
358    pub cases: Vec<VersionedEvalCase>,
359}
360
361#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
362#[serde(rename_all = "snake_case")]
363pub enum OperationState {
364    Queued,
365    Running,
366    Succeeded,
367    Failed,
368    Canceling,
369    Canceled,
370}
371
372#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
373#[serde(rename_all = "snake_case")]
374pub enum CaseRunState {
375    Queued,
376    Running,
377    Passed,
378    QualityFailed,
379    InfraFailed,
380    InvalidCase,
381    Canceled,
382}
383
384#[derive(Debug, Clone, Serialize, Deserialize)]
385#[serde(rename_all = "camelCase", deny_unknown_fields)]
386pub struct CreateEvalRunRequest {
387    pub suite_id: String,
388    pub suite_version: String,
389    pub subject_version: String,
390    pub executor_profile: String,
391    #[serde(default)]
392    pub case_ids: Vec<String>,
393    #[serde(default)]
394    pub seed: u64,
395    #[serde(default = "default_concurrency")]
396    pub concurrency: usize,
397    #[serde(default)]
398    pub idempotency_key: String,
399    /// Independent deterministic samples per `(case, variant)`.
400    #[serde(default = "default_samples")]
401    pub samples: u32,
402    /// Empty preserves the v1 single-subject run. A non-empty matrix expands
403    /// the run without requiring consumers to coordinate many operations.
404    #[serde(default)]
405    pub experiment_matrix: Vec<ExperimentVariant>,
406}
407
408fn default_concurrency() -> usize {
409    8
410}
411
412fn default_samples() -> u32 {
413    1
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize)]
417#[serde(rename_all = "camelCase", deny_unknown_fields)]
418pub struct ExperimentVariant {
419    pub id: String,
420    pub subject_version: String,
421    pub executor_profile: String,
422    #[serde(default)]
423    pub parameters: Value,
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize)]
427#[serde(rename_all = "camelCase")]
428pub struct EvalOperation {
429    pub id: String,
430    pub run_id: String,
431    pub state: OperationState,
432    pub phase: String,
433    pub attempt: u32,
434    pub completed_cases: usize,
435    pub total_cases: usize,
436    #[serde(default, skip_serializing_if = "Option::is_none")]
437    pub report_id: Option<String>,
438    pub created_at_ms: i64,
439    pub updated_at_ms: i64,
440    pub version: i64,
441    #[serde(default, skip_serializing_if = "Option::is_none")]
442    pub next_poll_after_ms: Option<u64>,
443    #[serde(default, skip_serializing_if = "Option::is_none")]
444    pub error_code: Option<String>,
445}
446
447#[derive(Debug, Clone, Serialize, Deserialize)]
448#[serde(rename_all = "camelCase", deny_unknown_fields)]
449pub struct GetOperationRequest {
450    pub operation_id: String,
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize)]
454#[serde(rename_all = "camelCase", deny_unknown_fields)]
455pub struct CancelRunRequest {
456    pub run_id: String,
457}
458
459#[derive(Debug, Clone, Serialize, Deserialize)]
460#[serde(rename_all = "camelCase", deny_unknown_fields)]
461pub struct ListCaseRunsRequest {
462    pub run_id: String,
463    #[serde(default, skip_serializing_if = "Option::is_none")]
464    pub cursor: Option<String>,
465    #[serde(default, skip_serializing_if = "Option::is_none")]
466    pub state: Option<CaseRunState>,
467    #[serde(default = "default_case_page")]
468    pub limit: usize,
469}
470
471fn default_case_page() -> usize {
472    50
473}
474
475#[derive(Debug, Clone, Serialize, Deserialize)]
476#[serde(rename_all = "camelCase")]
477pub struct CaseRunSummary {
478    pub id: String,
479    pub run_id: String,
480    pub case_id: String,
481    pub position: usize,
482    pub state: CaseRunState,
483    pub attempt: u32,
484    pub duration_ms: u64,
485    #[serde(default, skip_serializing_if = "Option::is_none")]
486    pub error: Option<String>,
487    #[serde(default)]
488    pub evidence_refs: Vec<EvidenceRef>,
489    #[serde(default)]
490    pub sample_index: u32,
491    #[serde(default, skip_serializing_if = "Option::is_none")]
492    pub variant_id: Option<String>,
493}
494
495#[derive(Debug, Clone, Serialize, Deserialize)]
496#[serde(rename_all = "camelCase")]
497pub struct CaseRunPage {
498    pub items: Vec<CaseRunSummary>,
499    #[serde(default, skip_serializing_if = "Option::is_none")]
500    pub next_cursor: Option<String>,
501}
502
503#[derive(Debug, Clone, Serialize, Deserialize)]
504#[serde(rename_all = "camelCase", deny_unknown_fields)]
505pub struct EvalAccessScope {
506    pub tenant_id: String,
507    pub project_id: String,
508    pub subject: String,
509}
510
511#[derive(Debug, Clone, Serialize, Deserialize)]
512#[serde(rename_all = "camelCase", deny_unknown_fields)]
513pub struct SandboxExecutionRequest {
514    /// Tenant/project ownership is passed explicitly so the isolated executor
515    /// can only finalize evidence in the namespace later validated by the
516    /// control plane.
517    pub scope: EvalAccessScope,
518    pub operation_id: String,
519    pub run_id: String,
520    pub case_run_id: String,
521    pub subject_version: String,
522    pub executor_profile: String,
523    pub seed: u64,
524    pub case: VersionedEvalCase,
525    pub fencing_token: i64,
526    #[serde(default)]
527    pub sample_index: u32,
528    #[serde(default, skip_serializing_if = "Option::is_none")]
529    pub variant: Option<ExperimentVariant>,
530}
531
532#[derive(Debug, Clone, Serialize, Deserialize)]
533#[serde(rename_all = "camelCase", deny_unknown_fields)]
534pub struct PluginEvaluationEvidence {
535    pub plugin_ref: String,
536    pub passed: bool,
537    #[serde(default)]
538    pub detail: String,
539    /// Attestation produced by the configured sandbox boundary. Production
540    /// evaluation fails closed when it is absent.
541    pub attestation: String,
542}
543
544/// Immutable evidence object owned by the scoped Evaluate namespace.
545#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
546#[serde(rename_all = "camelCase", deny_unknown_fields)]
547pub struct EvidenceRef {
548    pub reference: String,
549    pub tenant_id: String,
550    pub project_id: String,
551    pub run_id: String,
552    pub case_run_id: String,
553    /// Lower-case `sha256:<64 hex>` digest of the finalized bytes.
554    pub digest: String,
555    pub size_bytes: u64,
556    /// Attestation bound to the isolated executor response boundary.
557    pub attestation: String,
558}
559
560/// Bounded evidence returned by the isolated executor. Verdict calculation
561/// remains owned by Evaluate and is deterministic from this typed structure.
562#[derive(Debug, Clone, Default, Serialize, Deserialize)]
563#[serde(rename_all = "camelCase", deny_unknown_fields)]
564pub struct SandboxExecutionEvidence {
565    #[serde(default)]
566    pub responses: Vec<String>,
567    #[serde(default)]
568    pub workspace_files: std::collections::BTreeMap<String, String>,
569    #[serde(default)]
570    pub command_exit_codes: std::collections::BTreeMap<String, i32>,
571    #[serde(default)]
572    pub trace_event_counts: std::collections::BTreeMap<String, usize>,
573    #[serde(default)]
574    pub artifact_kinds: Vec<String>,
575    /// Immutable blob/object references owned by Evaluate's evidence namespace.
576    #[serde(default)]
577    pub evidence_refs: Vec<EvidenceRef>,
578    #[serde(default)]
579    pub plugin_evaluations: Vec<PluginEvaluationEvidence>,
580    #[serde(default, skip_serializing_if = "Option::is_none")]
581    pub infrastructure_error: Option<String>,
582    /// A sandbox may explicitly reject an immutable case as unsupported or
583    /// invalid. This is deliberately typed so provider text is never parsed as
584    /// a terminal-state code.
585    #[serde(default, skip_serializing_if = "Option::is_none")]
586    pub invalid_case_error: Option<String>,
587    #[serde(default)]
588    pub duration_ms: u64,
589    /// Executor-measured cost in millionths of the configured billing unit.
590    #[serde(default)]
591    pub cost_micros: u64,
592}
593
594#[derive(Debug, Clone, Serialize, Deserialize)]
595#[serde(rename_all = "camelCase")]
596pub struct ListCasesRequest {
597    #[serde(default, skip_serializing_if = "Option::is_none")]
598    pub category: Option<String>,
599}
600
601#[derive(Debug, Clone, Serialize, Deserialize)]
602#[serde(rename_all = "camelCase")]
603pub struct ListCasesResponse {
604    pub cases: Vec<EvalCaseSummary>,
605}
606
607#[derive(Debug, Clone, Serialize, Deserialize)]
608#[serde(rename_all = "camelCase")]
609pub struct EvalCaseSummary {
610    pub id: String,
611    pub category: String,
612    pub notes: String,
613    pub turns: usize,
614    #[serde(default)]
615    pub has_oracle: bool,
616    #[serde(default)]
617    pub has_verify_commands: bool,
618    #[serde(default)]
619    pub expects_artifact: bool,
620}
621
622#[derive(Debug, Clone, Serialize, Deserialize)]
623#[serde(rename_all = "camelCase")]
624pub struct GetCaseRequest {
625    pub case_id: String,
626}
627
628#[derive(Debug, Clone, Serialize, Deserialize)]
629#[serde(rename_all = "camelCase")]
630pub struct LoadCasesRequest {
631    pub file_path: String,
632}
633
634#[derive(Debug, Clone, Serialize, Deserialize)]
635#[serde(rename_all = "camelCase")]
636pub struct GetReportRequest {
637    pub report_id: String,
638}
639
640#[derive(Debug, Clone, Serialize, Deserialize)]
641#[serde(rename_all = "camelCase", deny_unknown_fields)]
642pub struct CompareReportsRequest {
643    pub report_id: String,
644    pub baseline_report_id: String,
645    #[serde(default)]
646    pub minimum_pass_rate: f64,
647    #[serde(default = "default_max_infra_rate")]
648    pub maximum_infra_failure_rate: f64,
649    #[serde(default = "default_significance")]
650    pub significance_level: f64,
651    #[serde(default)]
652    pub minimum_effect: f64,
653}
654
655fn default_max_infra_rate() -> f64 {
656    1.0
657}
658
659fn default_significance() -> f64 {
660    0.05
661}
662
663#[derive(Debug, Clone, Serialize, Deserialize)]
664#[serde(rename_all = "camelCase")]
665pub struct CaseOutcomeDiff {
666    pub case_id: String,
667    #[serde(default, skip_serializing_if = "Option::is_none")]
668    pub variant_id: Option<String>,
669    pub baseline_passed: bool,
670    pub current_passed: bool,
671    pub flaky_signal: bool,
672    #[serde(default)]
673    pub historical_flake_rate: f64,
674    #[serde(default)]
675    pub historical_samples: u64,
676}
677
678#[derive(Debug, Clone, Serialize, Deserialize)]
679#[serde(rename_all = "camelCase")]
680pub struct ReportComparison {
681    pub report_id: String,
682    pub baseline_report_id: String,
683    pub pass_rate_delta: f64,
684    pub infra_failure_rate_delta: f64,
685    pub pass_rate_confidence_low: f64,
686    pub pass_rate_confidence_high: f64,
687    pub effect_confidence_low: f64,
688    pub effect_confidence_high: f64,
689    pub p_value: f64,
690    pub statistically_significant: bool,
691    pub changed_cases: Vec<CaseOutcomeDiff>,
692    pub gate_passed: bool,
693}
694
695#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
696#[serde(rename_all = "snake_case")]
697pub enum ReviewState {
698    Queued,
699    Claimed,
700    Approved,
701    Rejected,
702}
703
704#[derive(Debug, Clone, Serialize, Deserialize)]
705#[serde(rename_all = "camelCase")]
706pub struct HumanReview {
707    pub id: String,
708    pub run_id: String,
709    pub case_run_id: String,
710    pub state: ReviewState,
711    pub reason: String,
712    #[serde(default, skip_serializing_if = "Option::is_none")]
713    pub reviewer: Option<String>,
714    #[serde(default, skip_serializing_if = "Option::is_none")]
715    pub decision_note: Option<String>,
716    pub fencing_token: i64,
717    pub created_at_ms: i64,
718    pub updated_at_ms: i64,
719}
720
721#[derive(Debug, Clone, Serialize, Deserialize)]
722#[serde(rename_all = "camelCase", deny_unknown_fields)]
723pub struct EnqueueReviewRequest {
724    pub run_id: String,
725    pub case_run_id: String,
726    pub reason: String,
727}
728
729#[derive(Debug, Clone, Serialize, Deserialize)]
730#[serde(rename_all = "camelCase", deny_unknown_fields)]
731pub struct ClaimReviewRequest {
732    #[serde(default = "default_review_lease")]
733    pub lease_ms: i64,
734}
735
736fn default_review_lease() -> i64 {
737    300_000
738}
739
740#[derive(Debug, Clone, Serialize, Deserialize)]
741#[serde(rename_all = "camelCase", deny_unknown_fields)]
742pub struct CompleteReviewRequest {
743    pub review_id: String,
744    pub fencing_token: i64,
745    pub approved: bool,
746    #[serde(default)]
747    pub note: String,
748}
749
750/// Error body shared by every non-2xx response.
751#[derive(Debug, Clone, Serialize, Deserialize)]
752pub struct ApiError {
753    pub status: u16,
754    pub message: String,
755}