use crate::ids::{AttemptId, CommandId, CostMicros, EngineId, LeaseId, Seq, TaskId, Timestamp};
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)]
#[serde(deny_unknown_fields)]
pub struct OpenAttemptRef {
pub attempt_id: AttemptId,
pub task_id: TaskId,
pub state: String,
pub engine: EngineId,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub worktree_lease_id: Option<LeaseId>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)]
#[serde(deny_unknown_fields)]
pub struct LeaseSnapshot {
pub lease_id: LeaseId,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub scope: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub holder: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub expires_at: Option<Timestamp>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)]
#[serde(deny_unknown_fields)]
pub struct PendingApproval {
pub kind: String,
pub question: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub subject_ref: Option<String>,
pub raised_at: Timestamp,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)]
#[serde(deny_unknown_fields)]
pub struct BudgetCursor {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub spent_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub spent_tool_calls: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub spent_cost_micros: Option<CostMicros>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub window_started_at: Option<Timestamp>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)]
#[serde(deny_unknown_fields)]
pub struct OrchestratorCheckpoint {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub orchestrator_id: Option<String>,
pub seq: Seq,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub native_session_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub active_goal: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub active_step_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub latest_command_ref: Option<CommandId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub open_attempts: Option<Vec<OpenAttemptRef>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub leases: Option<Vec<LeaseSnapshot>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub pending_approvals: Option<Vec<PendingApproval>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(optional)]
pub budget_cursor: Option<BudgetCursor>,
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
serde::Serialize,
serde::Deserialize,
specta::Type,
)]
#[serde(rename_all = "snake_case")]
pub enum FindingAction {
AutoFix,
AskUser,
NoOp,
}
impl FindingAction {
pub const ALL: &'static [Self] = &[Self::AutoFix, Self::AskUser, Self::NoOp];
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)]
#[serde(deny_unknown_fields)]
pub struct RoundFindingSummary {
pub total: u32,
pub auto_fix: u32,
pub ask_user: u32,
pub no_op: u32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn checkpoint_tri_state_absent_fields_are_omitted() {
let cp = OrchestratorCheckpoint {
orchestrator_id: None,
seq: Seq::new(7),
native_session_ref: None,
active_goal: Some("ship the contract".into()),
active_step_ref: None,
latest_command_ref: None,
open_attempts: Some(vec![]),
leases: None,
pending_approvals: None,
budget_cursor: None,
};
let json = serde_json::to_string(&cp).expect("serialize");
assert!(!json.contains("orchestrator_id"));
assert!(!json.contains("leases"));
assert!(json.contains("\"open_attempts\":[]"));
assert!(json.contains("\"seq\":\"7\""));
let back: OrchestratorCheckpoint = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, cp);
}
#[test]
fn finding_actions_are_the_three_buckets_the_summary_counts() {
let wire: Vec<serde_json::Value> = FindingAction::ALL
.iter()
.map(|a| serde_json::to_value(a).expect("serialize"))
.collect();
assert_eq!(
wire,
[
serde_json::json!("auto_fix"),
serde_json::json!("ask_user"),
serde_json::json!("no_op")
]
);
}
}