use serde::{Deserialize, Serialize};
use super::budget::{SearchLimits, SearchUsage};
use super::domain_types::{BranchMeasurement, SearchStrategy};
use crate::model::ModelId;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WitnessRef {
pub accepted_root: String,
pub chain: Vec<String>,
}
impl WitnessRef {
pub fn root(accepted_root: impl Into<String>) -> Self {
let accepted_root = accepted_root.into();
Self {
chain: vec![accepted_root.clone()],
accepted_root,
}
}
pub fn reaches(&self, accepted_root: &str) -> bool {
self.accepted_root == accepted_root
&& self.chain.last().map(String::as_str) == Some(accepted_root)
}
pub fn extend(&self, child_root: impl Into<String>) -> Self {
let mut chain = vec![child_root.into()];
chain.extend(self.chain.iter().cloned());
Self {
accepted_root: self.accepted_root.clone(),
chain,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ObligationRef(pub String);
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct PromptProgramDigest(pub String);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PartialCheckpointRef {
pub state_root: String,
pub accepted_ancestor: String,
pub parent_witness: WitnessRef,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub correction: Option<crate::residual::CorrectionPacketRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub remaining_obligations: Vec<ObligationRef>,
pub evidence_digest: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "state")]
pub enum SearchBranchState {
Ready,
Running,
PartialCheckpointed { checkpoint: PartialCheckpointRef },
CandidateMeasured { measurement: BranchMeasurement },
Selected,
Rejected { reason: String },
Abandoned { reason: String },
Contained { certificate_id: String },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchBranch {
pub branch_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_branch: Option<String>,
pub accepted_ancestor: String,
pub seed_checkpoint: String,
pub seed_witness: WitnessRef,
pub strategy: SearchStrategy,
pub route: ModelId,
pub prompt_program: PromptProgramDigest,
pub state: SearchBranchState,
pub usage: SearchUsage,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchForest {
pub forest_id: String,
pub task_id: String,
pub node_id: String,
pub generation: u32,
pub accepted_root: String,
pub branches: Vec<SearchBranch>,
pub limits: SearchLimits,
pub usage: SearchUsage,
}
impl SearchForest {
pub fn validate(&self) -> crate::error::Result<()> {
for branch in &self.branches {
if branch.accepted_ancestor != self.accepted_root {
return Err(crate::error::SdkError::Domain(format!(
"branch {} does not share the forest's accepted root",
branch.branch_id
)));
}
if !branch.seed_witness.reaches(&self.accepted_root) {
return Err(crate::error::SdkError::Domain(format!(
"branch {} seed witness does not reach the accepted root",
branch.branch_id
)));
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct SearchEvidenceDigest {
pub residual_clusters: Vec<String>,
pub no_good_refs: Vec<String>,
pub tried_strategies: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn witness_chains_reach_or_are_refused() {
let root = WitnessRef::root("root-a");
assert!(root.reaches("root-a"));
assert!(!root.reaches("root-b"));
let child = root.extend("partial-1");
assert!(child.reaches("root-a"));
assert_eq!(child.chain, vec!["partial-1", "root-a"]);
let broken = WitnessRef {
accepted_root: "root-a".into(),
chain: vec!["partial-1".into()],
};
assert!(!broken.reaches("root-a"), "chain must end at the root");
}
}