1#![cfg(feature = "advanced-primitives")]
2
3use serde::{Deserialize, Serialize};
6use std::future::Future;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct VerificationLoopConfig {
11 pub enabled: bool,
13 pub max_automatic_retries: u8,
15}
16
17impl Default for VerificationLoopConfig {
18 fn default() -> Self {
19 Self {
20 enabled: true,
21 max_automatic_retries: 2,
22 }
23 }
24}
25
26#[derive(Debug, Clone, Default, Serialize, Deserialize)]
28pub struct PromptVerificationTargets {
29 pub required_headings: Vec<String>,
31 pub required_phrases: Vec<String>,
33 pub require_ordered_headings: bool,
35 pub require_verification_gate: bool,
37}
38
39#[derive(Debug, Clone, Default, Serialize, Deserialize)]
41pub struct VerificationCheck {
42 pub accepted: bool,
44 pub missing_requirements: Vec<String>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct VerificationAttempt {
51 pub attempt: u8,
53 pub candidate: String,
55 pub check: VerificationCheck,
57}
58
59#[derive(Debug, Clone, Default, Serialize, Deserialize)]
61pub struct VerificationReport {
62 pub passed: bool,
64 pub automatic_retries_used: u8,
66 pub attempts: Vec<VerificationAttempt>,
68 pub final_missing_requirements: Vec<String>,
70}
71
72#[derive(Debug, Clone)]
74pub struct VerificationLoop {
75 config: VerificationLoopConfig,
76}
77
78impl VerificationLoop {
79 pub fn new(config: VerificationLoopConfig) -> Self {
81 Self { config }
82 }
83
84 pub async fn run<Produce, ProduceFuture, Verify, VerifyFuture>(
86 &self,
87 mut produce: Produce,
88 mut verify: Verify,
89 ) -> VerificationReport
90 where
91 Produce: FnMut(u8, Option<&VerificationCheck>) -> ProduceFuture,
92 ProduceFuture: Future<Output = String>,
93 Verify: FnMut(u8, &str) -> VerifyFuture,
94 VerifyFuture: Future<Output = VerificationCheck>,
95 {
96 if !self.config.enabled {
97 let candidate = produce(0, None).await;
98 return VerificationReport {
99 passed: true,
100 automatic_retries_used: 0,
101 attempts: vec![VerificationAttempt {
102 attempt: 0,
103 candidate,
104 check: VerificationCheck {
105 accepted: true,
106 missing_requirements: Vec::new(),
107 },
108 }],
109 final_missing_requirements: Vec::new(),
110 };
111 }
112
113 let mut attempts = Vec::new();
114 let mut previous_check: Option<VerificationCheck> = None;
115
116 for attempt in 0..=self.config.max_automatic_retries {
117 let candidate = produce(attempt, previous_check.as_ref()).await;
118 let check = verify(attempt, &candidate).await;
119 let accepted = check.accepted;
120 attempts.push(VerificationAttempt {
121 attempt,
122 candidate,
123 check: check.clone(),
124 });
125
126 if accepted {
127 return VerificationReport {
128 passed: true,
129 automatic_retries_used: attempt,
130 attempts,
131 final_missing_requirements: Vec::new(),
132 };
133 }
134
135 previous_check = Some(check);
136 }
137
138 let final_missing_requirements = previous_check
139 .map(|check| check.missing_requirements)
140 .unwrap_or_default();
141 VerificationReport {
142 passed: false,
143 automatic_retries_used: self.config.max_automatic_retries,
144 attempts,
145 final_missing_requirements,
146 }
147 }
148}
149
150pub fn verify_prompt(candidate: &str, targets: &PromptVerificationTargets) -> VerificationCheck {
152 let lowered = candidate.to_ascii_lowercase();
153 let mut missing_requirements = Vec::new();
154 let mut heading_positions = Vec::new();
155
156 for heading in &targets.required_headings {
157 let needle = heading.to_ascii_lowercase();
158 match lowered.find(&needle) {
159 Some(position) => heading_positions.push(position),
160 None => missing_requirements.push(format!("missing heading `{heading}`")),
161 }
162 }
163
164 if targets.require_ordered_headings
165 && heading_positions
166 .windows(2)
167 .any(|window| window[0] > window[1])
168 {
169 missing_requirements.push("required headings are out of order".to_string());
170 }
171
172 for phrase in &targets.required_phrases {
173 if !lowered.contains(&phrase.to_ascii_lowercase()) {
174 missing_requirements.push(format!("missing phrase `{phrase}`"));
175 }
176 }
177
178 if targets.require_verification_gate && !lowered.contains("verification gate:") {
179 missing_requirements.push("missing explicit verification gate".to_string());
180 }
181
182 VerificationCheck {
183 accepted: missing_requirements.is_empty(),
184 missing_requirements,
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[tokio::test]
193 async fn verification_loop_repairs_until_prompt_passes() {
194 let loop_runner = VerificationLoop::new(VerificationLoopConfig::default());
195 let report = loop_runner
196 .run(
197 |attempt, _| async move {
198 if attempt == 0 {
199 "Intent anchor:\nCompletion guardrails:".to_string()
200 } else {
201 "Intent anchor:\nOrdered execution phases:\nVerification gate:\nCompletion guardrails:\nkeep subtasks ordered and deduplicated\ndo not mark implementation complete before verification evidence exists".to_string()
202 }
203 },
204 |_, candidate| {
205 let candidate = candidate.to_string();
206 async move {
207 verify_prompt(
208 &candidate,
209 &PromptVerificationTargets {
210 required_headings: vec![
211 "Intent anchor:".to_string(),
212 "Ordered execution phases:".to_string(),
213 "Verification gate:".to_string(),
214 "Completion guardrails:".to_string(),
215 ],
216 required_phrases: vec![
217 "keep subtasks ordered and deduplicated".to_string(),
218 "do not mark implementation complete before verification evidence exists"
219 .to_string(),
220 ],
221 require_ordered_headings: true,
222 require_verification_gate: true,
223 },
224 )
225 }
226 },
227 )
228 .await;
229
230 assert!(report.passed);
231 assert_eq!(report.automatic_retries_used, 1);
232 assert_eq!(report.attempts.len(), 2);
233 }
234
235 #[test]
236 fn verify_prompt_reports_missing_sections() {
237 let check = verify_prompt(
238 "Intent anchor:\nCompletion guardrails:",
239 &PromptVerificationTargets {
240 required_headings: vec![
241 "Intent anchor:".to_string(),
242 "Ordered execution phases:".to_string(),
243 "Verification gate:".to_string(),
244 "Completion guardrails:".to_string(),
245 ],
246 required_phrases: vec!["keep subtasks ordered and deduplicated".to_string()],
247 require_ordered_headings: true,
248 require_verification_gate: true,
249 },
250 );
251
252 assert!(!check.accepted);
253 assert!(
254 check
255 .missing_requirements
256 .iter()
257 .any(|entry| entry.contains("Ordered execution phases"))
258 );
259 }
260}