返回 CodeWhale
session.rs
根目录 / crates / tui / src / core / session.rs
1 //! Session state management for the core engine.
2 //!
3 //! Tracks conversation history, token usage, and session metadata.
4
5 use crate::models::{Message, SystemPrompt, Usage};
6 use crate::prefix_cache::PrefixStabilityManager;
7 use crate::project_context::{ProjectContext, load_project_context_with_parents};
8 use crate::prompt_zones::{AppendLog, FrozenPrefix};
9 use crate::tui::approval::ApprovalMode;
10 use crate::working_set::WorkingSet;
11 use std::path::PathBuf;
12
13 /// Session state for the engine.
14 #[derive(Debug, Clone)]
15 pub struct Session {
16 /// Model being used
17 pub model: String,
18
19 /// Reasoning-effort tier for DeepSeek thinking mode:
20 /// `"off" | "low" | "medium" | "high" | "max"`. `None` lets the provider
21 /// apply its own defaults.
22 pub reasoning_effort: Option<String>,
23 /// Whether the user selected automatic reasoning effort.
24 pub reasoning_effort_auto: bool,
25
26 /// Whether the user selected automatic model routing.
27 pub auto_model: bool,
28
29 /// Workspace directory
30 pub workspace: PathBuf,
31
32 /// System prompt (optional)
33 pub system_prompt: Option<SystemPrompt>,
34 /// True when `system_prompt` is a persisted/runtime-supplied prefix that
35 /// should not be replaced by mode/context refreshes.
36 pub system_prompt_override: bool,
37 /// Hash of the last assembled stable system prompt. Used to avoid
38 /// replacing `system_prompt` when unchanged.
39 pub last_system_prompt_hash: Option<u64>,
40 /// Persisted summary blocks generated by context compaction.
41 pub compaction_summary_prompt: Option<SystemPrompt>,
42
43 /// Conversation history (API format), backed by AppendLog (#2264).
44 pub messages: AppendLog,
45
46 /// Total tokens used in this session
47 pub total_usage: SessionUsage,
48
49 /// Whether shell execution is allowed
50 pub allow_shell: bool,
51
52 /// Whether to trust paths outside workspace
53 pub trust_mode: bool,
54
55 /// Whether the current session should auto-approve tool safety checks.
56 pub auto_approve: bool,
57
58 /// Live UI approval policy used to steer the system prompt.
59 pub approval_mode: ApprovalMode,
60
61 /// Notes file path
62 pub notes_path: PathBuf,
63
64 /// MCP config path
65 pub mcp_config_path: PathBuf,
66
67 /// Session ID (for tracking)
68 pub id: String,
69
70 /// Project context loaded from AGENTS.md, etc.
71 pub project_context: Option<ProjectContext>,
72
73 /// Repo-aware working set for context management.
74 pub working_set: WorkingSet,
75
76 /// Prefix-cache stability monitor (inspired by Reasonix's Pillar 1).
77 /// Tracks the immutable prefix fingerprint and detects drift across turns.
78 /// Set during engine construction; None until the first system prompt assembly.
79 pub prefix_stability: Option<PrefixStabilityManager>,
80
81 /// Three-zone immutable prefix baseline (#2264). Frozen on the first
82 /// request of the session; verified against the current system+tool
83 /// state before every subsequent request. None until the first turn.
84 pub frozen_prefix: Option<FrozenPrefix>,
85
86 /// Monotonic counter bumped on every direct mutation of `messages`.
87 /// Consumed by the engine token-estimate cache
88 /// to memoize the per-turn token estimate without re-walking the message
89 /// list. Defaults to 0; bumped in [`Session::add_message`],
90 /// [`Session::replace_messages`], and at other mutation sites in
91 /// `core/engine.rs`.
92 pub messages_revision: u64,
93 }
94
95 /// Cumulative usage statistics for a session.
96 #[derive(Debug, Clone, Default)]
97 #[allow(clippy::struct_field_names)]
98 pub struct SessionUsage {
99 pub input_tokens: u64,
100 pub output_tokens: u64,
101 /// Cache creation (write) tokens. `None` when never observed by the API —
102 /// do NOT display as 0, which would be indistinguishable from "no writes".
103 pub cache_creation_input_tokens: Option<u64>,
104 /// Cache read (hit) tokens. `None` when never observed by the API —
105 /// do NOT display as 0, which would be indistinguishable from "no hits".
106 pub cache_read_input_tokens: Option<u64>,
107 }
108
109 impl SessionUsage {
110 /// Add usage from a turn
111 pub fn add(&mut self, usage: &Usage) {
112 self.input_tokens += u64::from(usage.input_tokens);
113 self.output_tokens += u64::from(usage.output_tokens);
114 if let Some(tokens) = usage.prompt_cache_write_tokens {
115 self.cache_creation_input_tokens =
116 Some(self.cache_creation_input_tokens.unwrap_or(0) + u64::from(tokens));
117 }
118 if let Some(tokens) = usage.prompt_cache_hit_tokens {
119 self.cache_read_input_tokens =
120 Some(self.cache_read_input_tokens.unwrap_or(0) + u64::from(tokens));
121 }
122 }
123 }
124
125 impl Session {
126 /// Create a new session
127 pub fn new(
128 model: String,
129 workspace: PathBuf,
130 allow_shell: bool,
131 trust_mode: bool,
132 notes_path: PathBuf,
133 mcp_config_path: PathBuf,
134 ) -> Self {
135 // Load project context from AGENTS.md, CLAUDE.md, etc.
136 let project_context = load_project_context_with_parents(&workspace);
137 let has_context = project_context.has_instructions();
138
139 Self {
140 model,
141 reasoning_effort: None,
142 reasoning_effort_auto: false,
143 auto_model: false,
144 workspace,
145 system_prompt: None,
146 system_prompt_override: false,
147 compaction_summary_prompt: None,
148 messages: AppendLog::new(),
149 total_usage: SessionUsage::default(),
150 allow_shell,
151 trust_mode,
152 auto_approve: false,
153 approval_mode: ApprovalMode::Suggest,
154 notes_path,
155 mcp_config_path,
156 id: uuid::Uuid::new_v4().to_string(),
157 project_context: if has_context {
158 Some(project_context)
159 } else {
160 None
161 },
162 last_system_prompt_hash: None,
163 working_set: WorkingSet::default(),
164 prefix_stability: None,
165 frozen_prefix: None,
166 messages_revision: 0,
167 }
168 }
169
170 /// Add a message to the conversation
171 pub fn add_message(&mut self, message: Message) {
172 self.messages.push(message);
173 self.messages_revision = self.messages_revision.saturating_add(1);
174 }
175
176 /// Replace the entire message history. Used by session resume and
177 /// compaction. Bumps `messages_revision` exactly once even when the new
178 /// history has a different length, so downstream caches invalidate
179 /// atomically.
180 #[allow(dead_code)]
181 pub fn replace_messages(&mut self, messages: Vec<Message>) {
182 self.messages = messages.into();
183 self.messages_revision = self.messages_revision.saturating_add(1);
184 }
185
186 /// Bump `messages_revision` without otherwise mutating the message list.
187 /// Reserved for sites that mutate the message list in place (e.g. an
188 /// in-place rewrite of a content block). Most call sites do not need
189 /// this — prefer [`add_message`](Self::add_message) and
190 /// [`replace_messages`](Self::replace_messages).
191 pub fn bump_messages_revision(&mut self) {
192 self.messages_revision = self.messages_revision.saturating_add(1);
193 }
194
195 /// Rebuild the working set from current messages (best effort).
196 pub fn rebuild_working_set(&mut self) {
197 self.working_set
198 .rebuild_from_messages(&self.messages, &self.workspace);
199 }
200 }
201
202 #[cfg(test)]
203 mod tests {
204 use super::*;
205
206 #[test]
207 fn session_usage_cache_starts_none() {
208 let usage = SessionUsage::default();
209 assert!(usage.cache_creation_input_tokens.is_none());
210 assert!(usage.cache_read_input_tokens.is_none());
211 }
212
213 #[test]
214 fn session_usage_cache_remains_none_when_api_omits_cache() {
215 let mut usage = SessionUsage::default();
216 let api_usage = Usage {
217 input_tokens: 100,
218 output_tokens: 50,
219 prompt_cache_hit_tokens: None,
220 prompt_cache_miss_tokens: None,
221 prompt_cache_write_tokens: None,
222 reasoning_tokens: None,
223 reasoning_replay_tokens: None,
224 server_tool_use: None,
225 };
226 usage.add(&api_usage);
227 assert!(usage.cache_creation_input_tokens.is_none());
228 assert!(usage.cache_read_input_tokens.is_none());
229 }
230
231 #[test]
232 fn session_usage_cache_accumulates_when_reported() {
233 let mut usage = SessionUsage::default();
234 let api_usage = Usage {
235 input_tokens: 100,
236 output_tokens: 50,
237 prompt_cache_hit_tokens: Some(30),
238 prompt_cache_miss_tokens: Some(50),
239 prompt_cache_write_tokens: Some(20),
240 reasoning_tokens: None,
241 reasoning_replay_tokens: None,
242 server_tool_use: None,
243 };
244 usage.add(&api_usage);
245 assert_eq!(usage.cache_read_input_tokens, Some(30));
246 assert_eq!(usage.cache_creation_input_tokens, Some(20));
247 usage.add(&api_usage);
248 assert_eq!(usage.cache_read_input_tokens, Some(60));
249 assert_eq!(usage.cache_creation_input_tokens, Some(40));
250 }
251
252 #[test]
253 fn session_usage_cache_preserves_explicit_zero() {
254 let mut usage = SessionUsage::default();
255 let api_usage = Usage {
256 input_tokens: 100,
257 output_tokens: 50,
258 prompt_cache_hit_tokens: Some(0), // explicit zero from provider
259 prompt_cache_miss_tokens: Some(50),
260 prompt_cache_write_tokens: Some(1234),
261 reasoning_tokens: None,
262 reasoning_replay_tokens: None,
263 server_tool_use: None,
264 };
265 usage.add(&api_usage);
266 // 0 is a valid observed value, must NOT be converted to None
267 assert_eq!(usage.cache_read_input_tokens, Some(0));
268 assert_eq!(usage.cache_creation_input_tokens, Some(1234));
269 }
270 }
271
271 lines RUST