vtcode_core/core/agent/
hypothesis.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum MismatchKind {
25 NonZeroExit,
27 EmptySearch,
29 PatchMismatch,
31 VerifierFailed,
33 RepeatedEvidence,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
42pub struct MismatchEvidence {
43 pub exit_code: Option<i64>,
45 pub empty_search_no_match: bool,
47 pub patch_mismatch: bool,
49 pub verifier_failed: bool,
51 pub repeated_evidence: bool,
53}
54
55#[must_use]
60pub fn classify_mismatch(evidence: &MismatchEvidence) -> Option<MismatchKind> {
61 if evidence.patch_mismatch {
62 return Some(MismatchKind::PatchMismatch);
63 }
64 if evidence.verifier_failed {
65 return Some(MismatchKind::VerifierFailed);
66 }
67 if evidence.empty_search_no_match {
68 return Some(MismatchKind::EmptySearch);
69 }
70 if evidence.exit_code.is_some_and(|code| code != 0) {
71 return Some(MismatchKind::NonZeroExit);
72 }
73 if evidence.repeated_evidence {
74 return Some(MismatchKind::RepeatedEvidence);
75 }
76 None
77}
78
79#[must_use]
85pub fn revision_guidance(kind: Option<MismatchKind>) -> &'static str {
86 match kind {
87 Some(MismatchKind::EmptySearch) => {
88 "Treat this as evidence of absence in the queried scope; revise the hypothesis to a new scope or question."
89 }
90 Some(MismatchKind::PatchMismatch) => {
91 "Re-read the exact current lines, revise the patch hypothesis, then retry once with exact context."
92 }
93 Some(MismatchKind::VerifierFailed) => {
94 "Inspect the verifier output, revise the fix hypothesis, then re-verify standalone."
95 }
96 Some(MismatchKind::RepeatedEvidence) => {
97 "Stop repeating the same query; revise the hypothesis and change scope or approach."
98 }
99 Some(MismatchKind::NonZeroExit) => {
100 "Inspect the bounded evidence, revise the hypothesis, then retry with corrected arguments."
101 }
102 None => "Revise the hypothesis from the bounded evidence before retrying.",
103 }
104}
105
106#[must_use]
111pub fn append_revision_guidance(base: &str, kind: Option<MismatchKind>) -> String {
112 let base = base.trim_end();
113 if base.is_empty() {
114 return revision_guidance(kind).to_string();
115 }
116 format!("{base} {}", revision_guidance(kind))
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn exit_code_alone_distinguishes_zero_from_nonzero() {
125 let nonzero = MismatchEvidence { exit_code: Some(1), ..Default::default() };
126 assert_eq!(classify_mismatch(&nonzero), Some(MismatchKind::NonZeroExit));
127 let zero = MismatchEvidence { exit_code: Some(0), ..Default::default() };
128 assert_eq!(classify_mismatch(&zero), None);
129 assert_eq!(classify_mismatch(&MismatchEvidence::default()), None);
130 }
131
132 #[test]
133 fn empty_search_outranks_exit_code() {
134 let both = MismatchEvidence {
135 exit_code: Some(1),
136 empty_search_no_match: true,
137 ..Default::default()
138 };
139 assert_eq!(classify_mismatch(&both), Some(MismatchKind::EmptySearch));
140 let exit_only = MismatchEvidence { exit_code: Some(1), ..Default::default() };
141 assert_eq!(classify_mismatch(&exit_only), Some(MismatchKind::NonZeroExit));
142 }
143
144 #[test]
145 fn patch_mismatch_is_top_priority() {
146 let all = MismatchEvidence {
147 exit_code: Some(1),
148 empty_search_no_match: true,
149 patch_mismatch: true,
150 verifier_failed: true,
151 repeated_evidence: true,
152 };
153 assert_eq!(classify_mismatch(&all), Some(MismatchKind::PatchMismatch));
154 let verifier_and_exit = MismatchEvidence {
155 exit_code: Some(2),
156 verifier_failed: true,
157 repeated_evidence: true,
158 ..Default::default()
159 };
160 assert_eq!(classify_mismatch(&verifier_and_exit), Some(MismatchKind::VerifierFailed));
161 }
162
163 #[test]
164 fn repeated_evidence_is_lowest_priority() {
165 let repeated_only = MismatchEvidence { repeated_evidence: true, ..Default::default() };
166 assert_eq!(classify_mismatch(&repeated_only), Some(MismatchKind::RepeatedEvidence));
167 let repeated_and_exit = MismatchEvidence {
168 exit_code: Some(1),
169 repeated_evidence: true,
170 ..Default::default()
171 };
172 assert_eq!(classify_mismatch(&repeated_and_exit), Some(MismatchKind::NonZeroExit));
173 }
174
175 #[test]
176 fn guidance_names_revision_and_scopes_empty_search() {
177 for kind in [
178 Some(MismatchKind::NonZeroExit),
179 Some(MismatchKind::EmptySearch),
180 Some(MismatchKind::PatchMismatch),
181 Some(MismatchKind::VerifierFailed),
182 Some(MismatchKind::RepeatedEvidence),
183 None,
184 ] {
185 let guidance = revision_guidance(kind);
186 assert!(!guidance.is_empty(), "{kind:?} guidance must not be empty");
187 assert!(guidance.contains("hypothesis"), "{kind:?} guidance must name the hypothesis: {guidance}");
188 assert!(
189 guidance.to_ascii_lowercase().contains("revis"),
190 "{kind:?} guidance must name revision: {guidance}"
191 );
192 }
193 let empty = revision_guidance(Some(MismatchKind::EmptySearch));
194 assert!(empty.contains("new scope"), "empty search must redirect scope: {empty}");
195 let generic = revision_guidance(None);
196 assert_ne!(generic, revision_guidance(Some(MismatchKind::NonZeroExit)));
197 for shout in ["MUST", "NEVER", "ALWAYS", "CRITICAL", "IMPORTANT"] {
198 assert!(!generic.contains(shout), "unexpected shouting: {shout}");
199 }
200 }
201
202 #[test]
203 fn append_keeps_base_and_handles_empty_base() {
204 let appended = append_revision_guidance("Retry once.", Some(MismatchKind::NonZeroExit));
205 assert!(appended.starts_with("Retry once."));
206 assert!(appended.contains("hypothesis"));
207 assert!(appended.to_ascii_lowercase().contains("revis"));
208 let bare = append_revision_guidance(" ", None);
209 assert_eq!(bare, revision_guidance(None));
210 }
211}