1use crate::api::provider::TokenUsage;
2use crate::app::conversation::Message;
3use crate::app::domain::action::{ApprovalDecision, ApprovalMemory, McpServerState};
4use crate::app::domain::types::{
5 CompactionRecord, MessageId, OpId, RequestId, SessionId, ToolCallId,
6};
7use crate::config::model::ModelId;
8use crate::session::state::SessionConfig;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use steer_tools::ToolCall;
12use steer_tools::result::ToolResult;
13
14pub use crate::app::domain::state::OperationKind;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub enum SessionEvent {
18 SessionCreated {
21 config: Box<SessionConfig>,
22 metadata: HashMap<String, String>,
23 #[serde(default, skip_serializing_if = "Option::is_none")]
25 parent_session_id: Option<SessionId>,
26 },
27
28 SessionConfigUpdated {
29 config: Box<SessionConfig>,
30 primary_agent_id: String,
31 },
32
33 AssistantMessageAdded {
35 message: Message,
36 model: ModelId,
37 },
38
39 UserMessageAdded {
41 message: Message,
42 },
43
44 ToolMessageAdded {
46 message: Message,
47 },
48
49 LlmUsageUpdated {
51 op_id: OpId,
52 model: ModelId,
53 usage: TokenUsage,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
55 context_window: Option<ContextWindowUsage>,
56 },
57
58 MessageUpdated {
59 message: Message,
60 },
61
62 ToolCallStarted {
63 id: ToolCallId,
64 name: String,
65 parameters: serde_json::Value,
66 model: ModelId,
67 },
68
69 ToolCallCompleted {
70 id: ToolCallId,
71 name: String,
72 result: ToolResult,
73 model: ModelId,
74 },
75
76 ToolCallFailed {
77 id: ToolCallId,
78 name: String,
79 error: String,
80 model: ModelId,
81 },
82
83 ApprovalRequested {
84 request_id: RequestId,
85 tool_call: ToolCall,
86 },
87
88 ApprovalDecided {
89 request_id: RequestId,
90 decision: ApprovalDecision,
91 remember: Option<ApprovalMemory>,
92 },
93
94 OperationStarted {
95 op_id: OpId,
96 kind: OperationKind,
97 },
98
99 OperationCompleted {
100 op_id: OpId,
101 },
102
103 OperationCancelled {
104 op_id: OpId,
105 info: CancellationInfo,
106 },
107
108 CompactResult {
109 result: CompactResult,
110 trigger: CompactTrigger,
111 },
112
113 ConversationCompacted {
114 record: CompactionRecord,
115 },
116
117 WorkspaceChanged,
118
119 QueueUpdated {
120 queue: Vec<QueuedWorkItemSnapshot>,
121 },
122
123 Error {
124 message: String,
125 },
126
127 McpServerStateChanged {
128 server_name: String,
129 state: McpServerState,
130 },
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
134pub enum CompactTrigger {
135 Manual,
136 Auto,
137}
138
139#[derive(Debug, Clone, PartialEq)]
140pub enum CompactResult {
141 Success(String),
142 Failed(String),
143 Cancelled,
144 InsufficientMessages,
145}
146
147impl Serialize for CompactResult {
148 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
149 where
150 S: serde::Serializer,
151 {
152 use serde::ser::SerializeStruct;
153
154 match self {
155 CompactResult::Success(summary) => {
156 let mut state = serializer.serialize_struct("CompactResult", 2)?;
157 state.serialize_field("result_type", "success")?;
158 state.serialize_field("summary", summary)?;
159 state.end()
160 }
161 CompactResult::Failed(error) => {
162 let mut state = serializer.serialize_struct("CompactResult", 2)?;
163 state.serialize_field("result_type", "failed")?;
164 state.serialize_field("error", error)?;
165 state.end()
166 }
167 CompactResult::Cancelled => {
168 let mut state = serializer.serialize_struct("CompactResult", 1)?;
169 state.serialize_field("result_type", "cancelled")?;
170 state.end()
171 }
172 CompactResult::InsufficientMessages => {
173 let mut state = serializer.serialize_struct("CompactResult", 1)?;
174 state.serialize_field("result_type", "insufficient_messages")?;
175 state.end()
176 }
177 }
178 }
179}
180
181impl<'de> Deserialize<'de> for CompactResult {
182 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
183 where
184 D: serde::Deserializer<'de>,
185 {
186 #[derive(Deserialize)]
187 struct CompactResultPayload {
188 result_type: String,
189 #[serde(default)]
190 summary: Option<String>,
191 #[serde(default)]
192 success: Option<String>,
193 #[serde(default)]
194 error: Option<String>,
195 }
196
197 let payload = CompactResultPayload::deserialize(deserializer)?;
198 match payload.result_type.as_str() {
199 "success" => {
200 let summary = payload
201 .summary
202 .or(payload.success)
203 .ok_or_else(|| serde::de::Error::missing_field("summary"))?;
204 Ok(CompactResult::Success(summary))
205 }
206 "failed" => {
207 let error = payload
208 .error
209 .ok_or_else(|| serde::de::Error::missing_field("error"))?;
210 Ok(CompactResult::Failed(error))
211 }
212 "cancelled" => Ok(CompactResult::Cancelled),
213 "insufficient_messages" => Ok(CompactResult::InsufficientMessages),
214 other => Err(serde::de::Error::unknown_variant(
215 other,
216 &["success", "failed", "cancelled", "insufficient_messages"],
217 )),
218 }
219 }
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
223pub struct ContextWindowUsage {
224 #[serde(default, skip_serializing_if = "Option::is_none")]
225 pub max_context_tokens: Option<u32>,
226 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub remaining_tokens: Option<u32>,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub utilization_ratio: Option<f64>,
230 #[serde(default)]
231 pub estimated: bool,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct CancellationInfo {
236 pub pending_tool_calls: usize,
237 #[serde(default)]
238 pub popped_queued_item: Option<QueuedWorkItemSnapshot>,
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct QueuedWorkItemSnapshot {
243 pub kind: Option<QueuedWorkKind>,
244 pub content: String,
245 pub queued_at: u64,
246 pub model: Option<ModelId>,
247 pub op_id: OpId,
248 pub message_id: MessageId,
249 #[serde(default)]
250 pub attachment_count: u32,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
254pub enum QueuedWorkKind {
255 UserMessage,
256 DirectBash,
257}
258
259impl SessionEvent {
260 pub fn is_error(&self) -> bool {
261 matches!(
262 self,
263 SessionEvent::Error { .. } | SessionEvent::ToolCallFailed { .. }
264 )
265 }
266
267 pub fn operation_id(&self) -> Option<OpId> {
268 match self {
269 SessionEvent::OperationStarted { op_id, .. }
270 | SessionEvent::OperationCompleted { op_id }
271 | SessionEvent::OperationCancelled { op_id, .. }
272 | SessionEvent::LlmUsageUpdated { op_id, .. } => Some(*op_id),
273 _ => None,
274 }
275 }
276
277 pub fn tool_call_id(&self) -> Option<&ToolCallId> {
278 match self {
279 SessionEvent::ToolCallStarted { id, .. }
280 | SessionEvent::ToolCallCompleted { id, .. }
281 | SessionEvent::ToolCallFailed { id, .. } => Some(id),
282 _ => None,
283 }
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use crate::config::model::builtin;
291
292 #[test]
293 fn llm_usage_event_reports_operation_id() {
294 let op_id = OpId::new();
295 let event = SessionEvent::LlmUsageUpdated {
296 op_id,
297 model: builtin::claude_sonnet_4_5(),
298 usage: TokenUsage::new(5, 8, 13),
299 context_window: None,
300 };
301
302 assert_eq!(event.operation_id(), Some(op_id));
303 assert!(!event.is_error());
304 }
305}