1use serde::{Deserialize, Serialize};
11
12use super::budget::{SearchLimits, SearchUsage};
13use super::domain_types::{BranchMeasurement, SearchStrategy};
14use crate::model::ModelId;
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct WitnessRef {
20 pub accepted_root: String,
22 pub chain: Vec<String>,
24}
25
26impl WitnessRef {
27 pub fn root(accepted_root: impl Into<String>) -> Self {
29 let accepted_root = accepted_root.into();
30 Self {
31 chain: vec![accepted_root.clone()],
32 accepted_root,
33 }
34 }
35
36 pub fn reaches(&self, accepted_root: &str) -> bool {
38 self.accepted_root == accepted_root
39 && self.chain.last().map(String::as_str) == Some(accepted_root)
40 }
41
42 pub fn extend(&self, child_root: impl Into<String>) -> Self {
44 let mut chain = vec![child_root.into()];
45 chain.extend(self.chain.iter().cloned());
46 Self {
47 accepted_root: self.accepted_root.clone(),
48 chain,
49 }
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub struct ObligationRef(pub String);
56
57#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
59pub struct PromptProgramDigest(pub String);
60
61#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
65pub struct PartialCheckpointRef {
66 pub state_root: String,
67 pub accepted_ancestor: String,
68 pub parent_witness: WitnessRef,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub correction: Option<crate::residual::CorrectionPacketRef>,
71 #[serde(default, skip_serializing_if = "Vec::is_empty")]
72 pub remaining_obligations: Vec<ObligationRef>,
73 pub evidence_digest: String,
74}
75
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78#[serde(rename_all = "snake_case", tag = "state")]
79pub enum SearchBranchState {
80 Ready,
81 Running,
82 PartialCheckpointed { checkpoint: PartialCheckpointRef },
83 CandidateMeasured { measurement: BranchMeasurement },
84 Selected,
85 Rejected { reason: String },
86 Abandoned { reason: String },
87 Contained { certificate_id: String },
88}
89
90#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92pub struct SearchBranch {
93 pub branch_id: String,
94 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub parent_branch: Option<String>,
96 pub accepted_ancestor: String,
98 pub seed_checkpoint: String,
100 pub seed_witness: WitnessRef,
102 pub strategy: SearchStrategy,
103 pub route: ModelId,
104 pub prompt_program: PromptProgramDigest,
105 pub state: SearchBranchState,
106 pub usage: SearchUsage,
107}
108
109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
111pub struct SearchForest {
112 pub forest_id: String,
113 pub task_id: String,
114 pub node_id: String,
115 pub generation: u32,
116 pub accepted_root: String,
118 pub branches: Vec<SearchBranch>,
119 pub limits: SearchLimits,
120 pub usage: SearchUsage,
121}
122
123impl SearchForest {
124 pub fn validate(&self) -> crate::error::Result<()> {
127 for branch in &self.branches {
128 if branch.accepted_ancestor != self.accepted_root {
129 return Err(crate::error::SdkError::Domain(format!(
130 "branch {} does not share the forest's accepted root",
131 branch.branch_id
132 )));
133 }
134 if !branch.seed_witness.reaches(&self.accepted_root) {
135 return Err(crate::error::SdkError::Domain(format!(
136 "branch {} seed witness does not reach the accepted root",
137 branch.branch_id
138 )));
139 }
140 }
141 Ok(())
142 }
143}
144
145#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
148pub struct SearchEvidenceDigest {
149 pub residual_clusters: Vec<String>,
150 pub no_good_refs: Vec<String>,
151 pub tried_strategies: Vec<String>,
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn witness_chains_reach_or_are_refused() {
160 let root = WitnessRef::root("root-a");
161 assert!(root.reaches("root-a"));
162 assert!(!root.reaches("root-b"));
163 let child = root.extend("partial-1");
164 assert!(child.reaches("root-a"));
165 assert_eq!(child.chain, vec!["partial-1", "root-a"]);
166 let broken = WitnessRef {
167 accepted_root: "root-a".into(),
168 chain: vec!["partial-1".into()],
169 };
170 assert!(!broken.reaches("root-a"), "chain must end at the root");
171 }
172}