1use aether_core::events::{AgentEvent, ContextEvent, LlmCallOutcome, MessageEvent, ModelEvent, ToolEvent, TurnEvent};
2use llm::ContentBlock;
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7#[serde(rename_all = "camelCase")]
8pub struct SessionMeta {
9 pub session_id: String,
10 pub cwd: PathBuf,
11 pub model: String,
12 #[serde(default)]
13 pub selected_mode: Option<String>,
14 pub created_at: String,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
18#[serde(tag = "type", rename_all = "camelCase")]
19pub enum UserEvent {
20 Message { content: Vec<ContentBlock> },
21 ClearContext,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
25#[serde(tag = "type", rename_all = "camelCase")]
26pub enum SessionControlEvent {
27 AgentSwitched { from: Option<String>, to: Option<String> },
28}
29
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31#[serde(tag = "kind", content = "data", rename_all = "camelCase")]
32#[allow(clippy::large_enum_variant)]
33pub enum SessionEvent {
34 User(UserEvent),
35 Agent(AgentEvent),
36 Control(SessionControlEvent),
37}
38
39impl SessionEvent {
40 pub fn content(&self) -> Option<String> {
41 self.user_content().or_else(|| match self {
42 Self::Agent(event) => event.content(),
43 Self::User(_) | Self::Control(_) => None,
44 })
45 }
46
47 pub fn user_content(&self) -> Option<String> {
48 match self {
49 Self::User(UserEvent::Message { content }) => {
50 let text = ContentBlock::join_text(content);
51 (!text.is_empty()).then_some(text)
52 }
53 Self::User(UserEvent::ClearContext) | Self::Agent(_) | Self::Control(_) => None,
54 }
55 }
56
57 pub fn is_persisted(&self) -> bool {
58 match self {
59 Self::User(_) | Self::Control(_) => true,
60 Self::Agent(event) => match event {
61 AgentEvent::Message(
62 MessageEvent::Text { is_complete, .. } | MessageEvent::Thought { is_complete, .. },
63 ) => *is_complete,
64 AgentEvent::Tool(
65 ToolEvent::Call { .. }
66 | ToolEvent::Result { .. }
67 | ToolEvent::Error { .. }
68 | ToolEvent::TaskCreated { .. }
69 | ToolEvent::TaskCompleted { .. }
70 | ToolEvent::TaskFailed { .. }
71 | ToolEvent::TaskCancelled { .. },
72 )
73 | AgentEvent::Turn(
74 TurnEvent::RetryScheduled { .. }
75 | TurnEvent::AutoContinue { .. }
76 | TurnEvent::Ended { .. }
77 | TurnEvent::LlmCallEnded { outcome: LlmCallOutcome::Failed { .. }, .. },
78 )
79 | AgentEvent::Context(
80 ContextEvent::CompactionStarted { .. }
81 | ContextEvent::CompactionEnded { .. }
82 | ContextEvent::CompactionResult { .. }
83 | ContextEvent::UsageUpdated { .. }
84 | ContextEvent::Cleared,
85 )
86 | AgentEvent::Model(ModelEvent::Switched { .. }) => true,
87 AgentEvent::Tool(
88 ToolEvent::CallUpdate { .. }
89 | ToolEvent::ExecutionStarted { .. }
90 | ToolEvent::Progress { .. }
91 | ToolEvent::TaskStatus { .. }
92 | ToolEvent::DefinitionsUpdated { .. },
93 )
94 | AgentEvent::Turn(
95 TurnEvent::Started { .. }
96 | TurnEvent::LlmCallStarted { .. }
97 | TurnEvent::LlmCallEnded {
98 outcome: LlmCallOutcome::Completed { .. } | LlmCallOutcome::Cancelled,
99 ..
100 },
101 ) => false,
102 },
103 }
104 }
105}
106
107pub fn last_agent_from_events(initial: Option<String>, events: &[SessionEvent]) -> Option<String> {
108 events
109 .iter()
110 .rev()
111 .find_map(|event| match event {
112 SessionEvent::Control(SessionControlEvent::AgentSwitched { to, .. }) => Some(to.clone()),
113 _ => None,
114 })
115 .unwrap_or(initial)
116}