use std::collections::BTreeMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::primitives::{ActionName, AssertId, FlowId, Hash, StepId};
use crate::run_path::RunPath;
use crate::runtime::{EvidenceRef, StepVerdict, Viewport};
use crate::vocab::{
ActChannel, AlignmentClass, Channel, CoordinateFallbackReason, ErrorClass, ExecutionMode,
HumanMode, HumanPurpose, ScreenshotOmissionReason, StepState, UiSnapshotOmissionReason,
VerdictStatus,
};
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "camelCase")]
pub enum ActionOutcomeKind {
Succeeded,
Failed,
Cancelled,
TimedOut,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AttemptRecord {
pub call_id: String,
pub outcome: ActionOutcomeKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_class: Option<ErrorClass>,
#[serde(skip_serializing_if = "Option::is_none")]
pub execution_mode: Option<ExecutionMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fallback_reason: Option<CoordinateFallbackReason>,
#[serde(skip_serializing_if = "Option::is_none")]
pub chain_index: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<ActChannel>,
#[serde(skip_serializing_if = "Option::is_none")]
pub action_name: Option<ActionName>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ProviderStateSummary {
pub session_lineage: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub event_cursor: Option<EventCursor>,
pub attestation: AttestationSnapshot,
pub health: SessionHealthSnapshot,
pub device_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub platform: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AttestationSnapshot {
pub lockfile_digest: Hash,
pub attested_at: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SessionHealthSnapshot {
pub ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub degraded: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ObservationRecord {
pub observation_id: String,
pub captured_at_ms: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub viewport: Option<Viewport>,
#[serde(skip_serializing_if = "Option::is_none")]
pub screenshot: Option<EvidenceRef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub screenshot_omission: Option<ScreenshotOmissionReason>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ui_snapshot: Option<EvidenceRef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ui_snapshot_omission: Option<UiSnapshotOmissionReason>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct VisionJudgeRecord {
pub provider: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub observations: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AssertionOutcomeRecord {
pub assert_id: AssertId,
pub result: VerdictStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<Channel>,
pub reason: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub vision_judge: Option<VisionJudgeRecord>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct StepRecord {
pub run_path: RunPath,
pub step_id: StepId,
pub effect_hash: Hash,
pub judge_hash: Hash,
pub attempts: Vec<AttemptRecord>,
pub resolved_inputs: Value,
#[serde(
default,
deserialize_with = "crate::run_log::some_even_if_null",
skip_serializing_if = "Option::is_none"
)]
pub output: Option<Value>,
pub observations: Vec<ObservationRecord>,
pub evidence: Vec<EvidenceRef>,
pub assertion_outcomes: Vec<AssertionOutcomeRecord>,
#[serde(skip_serializing_if = "Option::is_none")]
pub verdict: Option<StepVerdict>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct IterState {
#[serde(rename = "as")]
pub var: String,
pub index: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CallFrame {
pub flow_id: FlowId,
pub ir_hash: Hash,
#[serde(skip_serializing_if = "Option::is_none")]
pub call_step_id: Option<StepId>,
pub inputs_snapshot: Value,
pub vars: BTreeMap<String, Value>,
pub iter_stack: Vec<IterState>,
pub next_index: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PendingIntent {
pub call_id: String,
pub args_snapshot: Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Frontier {
pub run_path: RunPath,
pub state: StepState,
#[serde(skip_serializing_if = "Option::is_none")]
pub pending_intent: Option<PendingIntent>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct HumanPending {
pub run_path: RunPath,
pub request_id: String,
pub purpose: HumanPurpose,
#[serde(skip_serializing_if = "Option::is_none")]
pub mode: Option<HumanMode>,
pub prompt: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub deadline_at_ms: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct EventCursor {
pub session_id: String,
pub last_sequence: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct BindingState {
pub device_id: String,
pub session_lineage: Vec<String>,
pub event_cursor: EventCursor,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CheckpointView {
pub run_id: String,
pub ir_hash: Hash,
pub lockfile_digest: Hash,
pub params_snapshot: Value,
pub binding: BindingState,
pub completed: Vec<StepRecord>,
pub frames: Vec<CallFrame>,
pub frontier: Frontier,
#[serde(skip_serializing_if = "Option::is_none")]
pub human_pending: Option<HumanPending>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AlignmentEntry {
pub run_path: RunPath,
pub step_id: StepId,
pub class: AlignmentClass,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RequiresConfirmation {
pub run_path: RunPath,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub step_id: Option<StepId>,
pub cause: String,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AlignmentReport {
pub entries: Vec<AlignmentEntry>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resume_point: Option<RunPath>,
pub requires_confirmation: Vec<RequiresConfirmation>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn iter_state_uses_the_as_keyword_on_the_wire() {
let iter = IterState {
var: "item".to_owned(),
index: 2,
key: None,
};
let wire = serde_json::to_value(&iter).expect("serialize");
assert_eq!(wire, json!({"as": "item", "index": 2}));
}
#[test]
fn checkpoint_view_round_trips_with_absent_optionals() {
let view = CheckpointView {
run_id: "run-1".to_owned(),
ir_hash: serde_json::from_value(json!(format!("sha256:{}", "a".repeat(64))))
.expect("hash"),
lockfile_digest: serde_json::from_value(json!(format!("sha256:{}", "b".repeat(64))))
.expect("hash"),
params_snapshot: json!({"user": "alice"}),
binding: BindingState {
device_id: "dev-1".to_owned(),
session_lineage: vec!["s-1".to_owned()],
event_cursor: EventCursor {
session_id: "s-1".to_owned(),
last_sequence: 42,
},
},
completed: vec![],
frames: vec![CallFrame {
flow_id: serde_json::from_value(json!("checkout")).expect("flow id"),
ir_hash: serde_json::from_value(json!(format!("sha256:{}", "a".repeat(64))))
.expect("hash"),
call_step_id: None,
inputs_snapshot: json!({}),
vars: BTreeMap::new(),
iter_stack: vec![],
next_index: 3,
}],
frontier: Frontier {
run_path: vec![],
state: StepState::Acting,
pending_intent: Some(PendingIntent {
call_id: "c-1".to_owned(),
args_snapshot: json!({"x": 1}),
}),
},
human_pending: None,
};
let wire = serde_json::to_value(&view).expect("serialize");
assert_eq!(wire["frontier"]["state"], "acting");
assert_eq!(wire["binding"]["eventCursor"]["lastSequence"], 42);
assert!(wire.get("humanPending").is_none());
assert!(wire["frames"][0].get("callStepId").is_none());
let back: CheckpointView = serde_json::from_value(wire).expect("deserialize");
assert_eq!(back, view);
}
#[test]
fn human_pending_purpose_discriminates_supervision() {
let pending = HumanPending {
run_path: vec![],
request_id: "req-1".to_owned(),
purpose: HumanPurpose::Supervision,
mode: None,
prompt: "Approve mutating dispatch of tapPay".to_owned(),
deadline_at_ms: None,
};
let wire = serde_json::to_value(&pending).expect("serialize");
assert_eq!(wire["purpose"], "supervision");
let object = wire.as_object().expect("object");
assert!(!object.contains_key("mode"));
assert!(!object.contains_key("deadlineAtMs"));
let back: HumanPending = serde_json::from_value(wire).expect("deserialize");
assert_eq!(back, pending);
}
#[test]
fn human_pending_step_purpose_carries_mode_and_deadline() {
let pending = HumanPending {
run_path: vec![],
request_id: "req-2".to_owned(),
purpose: HumanPurpose::Step,
mode: Some(crate::vocab::HumanMode::Confirm),
prompt: "Confirm the transfer".to_owned(),
deadline_at_ms: Some(1_700_000_600_000),
};
let wire = serde_json::to_value(&pending).expect("serialize");
assert_eq!(wire["mode"], "confirm");
assert_eq!(
wire["deadlineAtMs"],
serde_json::json!(1_700_000_600_000_u64)
);
let back: HumanPending = serde_json::from_value(wire).expect("deserialize");
assert_eq!(back, pending);
}
}