1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
//! Session state management for the core engine.
//!
//! Tracks conversation history, token usage, and session metadata.
use crate::cycle_manager::CycleBriefing;
use crate::models::{Message, SystemPrompt, Usage};
use crate::project_context::{ProjectContext, load_project_context_with_parents};
use crate::tui::approval::ApprovalMode;
use crate::working_set::WorkingSet;
use chrono::{DateTime, Utc};
use std::path::PathBuf;
/// Session state for the engine.
#[derive(Debug, Clone)]
pub struct Session {
/// Model being used
pub model: String,
/// Reasoning-effort tier for DeepSeek thinking mode:
/// `"off" | "low" | "medium" | "high" | "max"`. `None` lets the provider
/// apply its own defaults.
pub reasoning_effort: Option<String>,
/// Whether the user selected automatic reasoning effort.
pub reasoning_effort_auto: bool,
/// Whether the user selected automatic model routing.
pub auto_model: bool,
/// Workspace directory
pub workspace: PathBuf,
/// System prompt (optional)
pub system_prompt: Option<SystemPrompt>,
/// Hash of the last assembled stable system prompt. Used to avoid
/// replacing `system_prompt` when unchanged.
pub last_system_prompt_hash: Option<u64>,
/// Persisted summary blocks generated by context compaction.
pub compaction_summary_prompt: Option<SystemPrompt>,
/// Conversation history (API format)
pub messages: Vec<Message>,
/// Total tokens used in this session
pub total_usage: SessionUsage,
/// Whether shell execution is allowed
pub allow_shell: bool,
/// Whether to trust paths outside workspace
pub trust_mode: bool,
/// Whether the current session should auto-approve tool safety checks.
pub auto_approve: bool,
/// Live UI approval policy used to steer the system prompt.
pub approval_mode: ApprovalMode,
/// Notes file path
pub notes_path: PathBuf,
/// MCP config path
pub mcp_config_path: PathBuf,
/// Session ID (for tracking)
pub id: String,
/// Project context loaded from AGENTS.md, etc.
pub project_context: Option<ProjectContext>,
/// Repo-aware working set for context management.
pub working_set: WorkingSet,
/// Number of cycle boundaries crossed in this session (issue #124). The
/// active cycle index is `cycle_count + 1` (cycles are 1-based for users).
pub cycle_count: u32,
/// UTC start time of the *current* cycle. Updated when the engine resets
/// the conversation buffer. Used by archive headers and the `/cycles`
/// command's display.
pub current_cycle_started: DateTime<Utc>,
/// Briefings produced at past cycle boundaries, in chronological order.
/// Bounded growth: one entry per cycle, briefing capped at ~3,000 tokens.
pub cycle_briefings: Vec<CycleBriefing>,
}
/// Cumulative usage statistics for a session.
#[derive(Debug, Clone, Default)]
#[allow(clippy::struct_field_names)]
pub struct SessionUsage {
pub input_tokens: u64,
pub output_tokens: u64,
#[allow(dead_code)]
pub cache_creation_input_tokens: u64,
#[allow(dead_code)]
pub cache_read_input_tokens: u64,
}
impl SessionUsage {
/// Add usage from a turn
pub fn add(&mut self, usage: &Usage) {
self.input_tokens += u64::from(usage.input_tokens);
self.output_tokens += u64::from(usage.output_tokens);
if let Some(tokens) = usage.prompt_cache_miss_tokens {
self.cache_creation_input_tokens += u64::from(tokens);
}
if let Some(tokens) = usage.prompt_cache_hit_tokens {
self.cache_read_input_tokens += u64::from(tokens);
}
}
}
impl Session {
/// Create a new session
pub fn new(
model: String,
workspace: PathBuf,
allow_shell: bool,
trust_mode: bool,
notes_path: PathBuf,
mcp_config_path: PathBuf,
) -> Self {
// Load project context from AGENTS.md, CLAUDE.md, etc.
let project_context = load_project_context_with_parents(&workspace);
let has_context = project_context.has_instructions();
Self {
model,
reasoning_effort: None,
reasoning_effort_auto: false,
auto_model: false,
workspace,
system_prompt: None,
compaction_summary_prompt: None,
messages: Vec::new(),
total_usage: SessionUsage::default(),
allow_shell,
trust_mode,
auto_approve: false,
approval_mode: ApprovalMode::Suggest,
notes_path,
mcp_config_path,
id: uuid::Uuid::new_v4().to_string(),
project_context: if has_context {
Some(project_context)
} else {
None
},
last_system_prompt_hash: None,
working_set: WorkingSet::default(),
cycle_count: 0,
current_cycle_started: Utc::now(),
cycle_briefings: Vec::new(),
}
}
/// Add a message to the conversation
pub fn add_message(&mut self, message: Message) {
self.messages.push(message);
}
/// Rebuild the working set from current messages (best effort).
pub fn rebuild_working_set(&mut self) {
self.working_set
.rebuild_from_messages(&self.messages, &self.workspace);
}
}