Skip to main content

steer_core/app/domain/
event.rs

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    /// Session was created. For sub-agent sessions, `parent_session_id` links
19    /// to the parent session for auditability.
20    SessionCreated {
21        config: Box<SessionConfig>,
22        metadata: HashMap<String, String>,
23        /// If this is a sub-agent session, the parent session ID
24        #[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    /// Assistant-authored message; includes the model that produced it.
34    AssistantMessageAdded {
35        message: Message,
36        model: ModelId,
37    },
38
39    /// User-authored message; no model attribution.
40    UserMessageAdded {
41        message: Message,
42    },
43
44    /// Tool result message; no model attribution.
45    ToolMessageAdded {
46        message: Message,
47    },
48
49    /// Final normalized usage snapshot for a completed model call.
50    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}