vtcode_core/utils/session_archive/
metadata.rs1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
5pub struct SessionArchiveMetadata {
6 pub workspace_label: String,
7 pub workspace_path: String,
8 pub model: String,
9 pub provider: String,
10 pub theme: String,
11 pub reasoning_effort: String,
12 #[serde(default, skip_serializing_if = "Option::is_none")]
13 pub debug_log_path: Option<String>,
14 #[serde(default)]
16 pub loaded_skills: Vec<String>,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
18 pub prompt_cache_lineage_id: Option<String>,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
20 pub external_thread_id: Option<String>,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub parent_session_id: Option<String>,
23 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub primary_agent: Option<String>,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub fork_mode: Option<SessionForkMode>,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub continuation_metadata: Option<SessionContinuationMetadata>,
29}
30
31#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
32#[serde(rename_all = "snake_case")]
33pub enum SessionForkMode {
34 FullCopy,
35 Summarized,
36}
37
38#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
39#[serde(rename_all = "snake_case")]
40pub enum SessionContinuationExhaustionReason {
41 MaxBudgetUsd,
42}
43
44#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
45#[serde(rename_all = "snake_case")]
46pub enum SessionContinuationRecommendedAction {
47 ContinueFromSummary,
48 ContinueFullHistory,
49 StartFresh,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
53pub struct SessionContinuationMetadata {
54 pub exhaustion_reason: SessionContinuationExhaustionReason,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub max_budget_usd_micros: Option<u64>,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub actual_cost_usd_micros: Option<u64>,
59 #[serde(default)]
60 pub summary_available: bool,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub recommended_action: Option<SessionContinuationRecommendedAction>,
63}
64
65impl SessionContinuationMetadata {
66 pub fn budget_limit(max_budget_usd: f64, actual_cost_usd: f64, summary_available: bool) -> Self {
67 Self {
68 exhaustion_reason: SessionContinuationExhaustionReason::MaxBudgetUsd,
69 max_budget_usd_micros: usd_to_micros(max_budget_usd),
70 actual_cost_usd_micros: usd_to_micros(actual_cost_usd),
71 summary_available,
72 recommended_action: Some(SessionContinuationRecommendedAction::ContinueFromSummary),
73 }
74 }
75
76 pub fn max_budget_usd(&self) -> Option<f64> {
77 self.max_budget_usd_micros.map(micros_to_usd)
78 }
79
80 pub fn actual_cost_usd(&self) -> Option<f64> {
81 self.actual_cost_usd_micros.map(micros_to_usd)
82 }
83
84 pub fn is_budget_limit(&self) -> bool {
85 matches!(self.exhaustion_reason, SessionContinuationExhaustionReason::MaxBudgetUsd)
86 }
87}
88
89fn usd_to_micros(value: f64) -> Option<u64> {
90 if !value.is_finite() || value.is_sign_negative() {
91 return None;
92 }
93
94 #[allow(
95 clippy::cast_sign_loss,
96 reason = "Intentional compatibility, platform, or test-only suppression."
97 )]
98 Some((value * 1_000_000.0).round() as u64)
99}
100
101fn micros_to_usd(value: u64) -> f64 {
102 value as f64 / 1_000_000.0
103}
104
105impl SessionArchiveMetadata {
106 pub fn new(
107 workspace_label: impl Into<String>,
108 workspace_path: impl Into<String>,
109 model: impl Into<String>,
110 provider: impl Into<String>,
111 theme: impl Into<String>,
112 reasoning_effort: impl Into<String>,
113 ) -> Self {
114 Self {
115 workspace_label: workspace_label.into(),
116 workspace_path: workspace_path.into(),
117 model: model.into(),
118 provider: provider.into(),
119 theme: theme.into(),
120 reasoning_effort: reasoning_effort.into(),
121 debug_log_path: None,
122 loaded_skills: Vec::new(),
123 prompt_cache_lineage_id: None,
124 external_thread_id: None,
125 parent_session_id: None,
126 primary_agent: None,
127 fork_mode: None,
128 continuation_metadata: None,
129 }
130 }
131
132 pub fn with_loaded_skills(mut self, skills: Vec<String>) -> Self {
134 self.loaded_skills = skills;
135 self
136 }
137
138 pub fn with_debug_log_path(mut self, path: Option<String>) -> Self {
140 self.debug_log_path = path;
141 self
142 }
143
144 pub fn with_prompt_cache_lineage_id(mut self, lineage_id: impl Into<String>) -> Self {
145 self.prompt_cache_lineage_id = Some(lineage_id.into());
146 self
147 }
148
149 pub fn with_external_thread_id(mut self, thread_id: impl Into<String>) -> Self {
150 self.external_thread_id = Some(thread_id.into());
151 self
152 }
153
154 pub fn ensure_prompt_cache_lineage_id(mut self) -> Self {
155 if self.prompt_cache_lineage_id.is_none() {
156 self.prompt_cache_lineage_id = Some(format!("lineage-{}", Uuid::new_v4()));
157 }
158 self
159 }
160
161 pub fn with_parent_session_id(mut self, session_id: impl Into<String>) -> Self {
162 self.parent_session_id = Some(session_id.into());
163 self
164 }
165
166 pub fn with_primary_agent(mut self, primary_agent: impl Into<String>) -> Self {
167 self.primary_agent = Some(primary_agent.into());
168 self
169 }
170
171 pub fn with_fork_mode(mut self, fork_mode: SessionForkMode) -> Self {
172 self.fork_mode = Some(fork_mode);
173 self
174 }
175
176 pub fn with_continuation_metadata(mut self, continuation_metadata: Option<SessionContinuationMetadata>) -> Self {
177 self.continuation_metadata = continuation_metadata;
178 self
179 }
180
181 pub fn budget_limit_continuation(&self) -> Option<&SessionContinuationMetadata> {
182 self.continuation_metadata
183 .as_ref()
184 .filter(|continuation| continuation.is_budget_limit())
185 }
186
187 pub(crate) fn fork_seed(&self) -> Self {
188 Self {
189 workspace_label: self.workspace_label.clone(),
190 workspace_path: self.workspace_path.clone(),
191 model: self.model.clone(),
192 provider: self.provider.clone(),
193 theme: self.theme.clone(),
194 reasoning_effort: self.reasoning_effort.clone(),
195 debug_log_path: self.debug_log_path.clone(),
196 loaded_skills: self.loaded_skills.clone(),
197 prompt_cache_lineage_id: self.prompt_cache_lineage_id.clone(),
198 external_thread_id: self.external_thread_id.clone(),
199 parent_session_id: None,
200 primary_agent: self.primary_agent.clone(),
201 fork_mode: None,
202 continuation_metadata: None,
203 }
204 }
205}