vtcode_exec_events/
planning.rs1use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
6#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
7#[serde(rename_all = "snake_case")]
8pub enum PlanningOutcome {
9 #[default]
10 Researching,
11 Synthesizing,
12 Repairing,
13 AwaitingInput,
14 AwaitingApproval,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
20#[serde(rename_all = "snake_case")]
21pub enum PlanningResponseClass {
22 EmptyContent,
23 ReasoningOnly,
24 NativeToolCalls,
25 MalformedPlan,
26 Truncation,
27 ProviderFailure,
28 ValidPlan,
29 ClarifyingQuestion,
30}
31
32#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
34#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
35#[serde(default)]
36pub struct PlanningRecoverySnapshot {
37 pub outcome: PlanningOutcome,
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub request_id: Option<String>,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub request_fingerprint: Option<u64>,
44 pub research_closed: bool,
46 pub requests_used: u8,
48 pub cycle: u32,
50 pub latest_request: String,
51 pub evidence: Vec<String>,
53 pub best_draft: Option<String>,
54 pub best_draft_defects: Option<usize>,
55 pub validation_report: Option<String>,
56 pub response_class: Option<PlanningResponseClass>,
57 #[serde(skip_serializing_if = "Option::is_none")]
63 pub previous_response_class: Option<PlanningResponseClass>,
64 pub guard_reason: Option<String>,
66 pub provider: String,
67 pub model: String,
68 pub input_prompt_shown: bool,
70 pub model_selection_pending: bool,
72 pub targeted_research: Option<String>,
74}
75
76impl PlanningRecoverySnapshot {
77 pub const MAX_REQUESTS: u8 = 3;
79
80 pub fn admit_request(&mut self) -> bool {
82 if matches!(self.outcome, PlanningOutcome::AwaitingInput | PlanningOutcome::AwaitingApproval)
83 || self.requests_used >= Self::MAX_REQUESTS
84 {
85 return false;
86 }
87 self.outcome = if self.requests_used == 0 {
88 PlanningOutcome::Synthesizing
89 } else {
90 PlanningOutcome::Repairing
91 };
92 self.requests_used += 1;
93 true
94 }
95
96 pub fn record_response_class(&mut self, class: PlanningResponseClass) {
101 self.previous_response_class = self.response_class;
102 self.response_class = Some(class);
103 }
104
105 pub fn begin_explicit_cycle(&mut self) {
107 self.requests_used = 0;
108 self.cycle = self.cycle.saturating_add(1);
109 self.input_prompt_shown = false;
110 self.targeted_research = None;
111 self.outcome = PlanningOutcome::Synthesizing;
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn request_admission_remains_bounded_after_serialized_resume_and_explicit_retry() {
121 let mut snapshot = PlanningRecoverySnapshot {
122 research_closed: true,
123 best_draft: Some("public draft".into()),
124 ..Default::default()
125 };
126 assert!(snapshot.admit_request());
127 assert_eq!(snapshot.outcome, PlanningOutcome::Synthesizing);
128 let mut resumed: PlanningRecoverySnapshot =
129 serde_json::from_str(&serde_json::to_string(&snapshot).unwrap()).unwrap();
130 assert!(resumed.admit_request());
131 assert!(resumed.admit_request());
132 assert_eq!(resumed.requests_used, 3);
133 assert!(!resumed.admit_request());
134 resumed.outcome = PlanningOutcome::AwaitingInput;
135 assert!(!resumed.admit_request());
136 resumed.begin_explicit_cycle();
137 assert_eq!(resumed.cycle, 1);
138 assert!(resumed.research_closed);
139 assert_eq!(resumed.best_draft.as_deref(), Some("public draft"));
140 assert!(resumed.admit_request());
141 }
142}