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 pub guard_reason: Option<String>,
59 pub provider: String,
60 pub model: String,
61 pub input_prompt_shown: bool,
63 pub model_selection_pending: bool,
65 pub targeted_research: Option<String>,
67}
68
69impl PlanningRecoverySnapshot {
70 pub const MAX_REQUESTS: u8 = 3;
72
73 pub fn admit_request(&mut self) -> bool {
75 if matches!(self.outcome, PlanningOutcome::AwaitingInput | PlanningOutcome::AwaitingApproval)
76 || self.requests_used >= Self::MAX_REQUESTS
77 {
78 return false;
79 }
80 self.outcome = if self.requests_used == 0 {
81 PlanningOutcome::Synthesizing
82 } else {
83 PlanningOutcome::Repairing
84 };
85 self.requests_used += 1;
86 true
87 }
88
89 pub fn begin_explicit_cycle(&mut self) {
91 self.requests_used = 0;
92 self.cycle = self.cycle.saturating_add(1);
93 self.input_prompt_shown = false;
94 self.targeted_research = None;
95 self.outcome = PlanningOutcome::Synthesizing;
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn request_admission_remains_bounded_after_serialized_resume_and_explicit_retry() {
105 let mut snapshot = PlanningRecoverySnapshot {
106 research_closed: true,
107 best_draft: Some("public draft".into()),
108 ..Default::default()
109 };
110 assert!(snapshot.admit_request());
111 assert_eq!(snapshot.outcome, PlanningOutcome::Synthesizing);
112 let mut resumed: PlanningRecoverySnapshot =
113 serde_json::from_str(&serde_json::to_string(&snapshot).unwrap()).unwrap();
114 assert!(resumed.admit_request());
115 assert!(resumed.admit_request());
116 assert_eq!(resumed.requests_used, 3);
117 assert!(!resumed.admit_request());
118 resumed.outcome = PlanningOutcome::AwaitingInput;
119 assert!(!resumed.admit_request());
120 resumed.begin_explicit_cycle();
121 assert_eq!(resumed.cycle, 1);
122 assert!(resumed.research_closed);
123 assert_eq!(resumed.best_draft.as_deref(), Some("public draft"));
124 assert!(resumed.admit_request());
125 }
126}