Skip to main content

aether_core/events/
agent_event.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4use super::{ContextEvent, MessageEvent, ModelEvent, StreamState, ToolEvent, TurnEvent, TurnOutcome};
5
6/// A canonical event on the agent's output stream.
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
8#[serde(tag = "category", content = "event", rename_all = "snake_case")]
9#[allow(clippy::large_enum_variant)]
10pub enum AgentEvent {
11    Message(MessageEvent),
12    Tool(ToolEvent),
13    Turn(TurnEvent),
14    Context(ContextEvent),
15    Model(ModelEvent),
16}
17
18impl AgentEvent {
19    pub fn text(message_id: &str, chunk: &str, state: StreamState) -> Self {
20        Self::Message(MessageEvent::Text {
21            message_id: message_id.into(),
22            chunk: chunk.into(),
23            is_complete: state.is_complete(),
24        })
25    }
26
27    pub fn thought(message_id: &str, chunk: &str, state: StreamState) -> Self {
28        Self::Message(MessageEvent::Thought {
29            message_id: message_id.into(),
30            chunk: chunk.into(),
31            is_complete: state.is_complete(),
32        })
33    }
34
35    pub fn turn_ended(outcome: TurnOutcome) -> Self {
36        Self::Turn(TurnEvent::Ended { outcome })
37    }
38
39    /// Human-readable text content of this event, if any.
40    pub fn content(&self) -> Option<String> {
41        match self {
42            Self::Message(MessageEvent::Text { chunk, .. } | MessageEvent::Thought { chunk, .. }) => {
43                Some(chunk.clone())
44            }
45            Self::Tool(ToolEvent::Result { result, .. } | ToolEvent::TaskCompleted { result, .. }) => {
46                Some(result.result.clone())
47            }
48            Self::Tool(ToolEvent::Error { error } | ToolEvent::TaskFailed { error, .. }) => Some(error.error.clone()),
49            Self::Tool(ToolEvent::TaskCreated { task_id, .. }) => Some(task_id.clone()),
50            Self::Tool(ToolEvent::TaskStatus { task_id, status, status_message, .. }) => {
51                Some(status_message.as_ref().map_or_else(
52                    || format!("{task_id}: {status}"),
53                    |message| format!("{task_id}: {status} - {message}"),
54                ))
55            }
56            Self::Tool(ToolEvent::TaskCancelled { task_id, .. }) => Some(format!("{task_id}: cancelled")),
57            Self::Context(ContextEvent::CompactionResult { summary, .. }) => Some(summary.clone()),
58            _ => None,
59        }
60    }
61
62    /// The turn's terminal outcome, if this event ends a turn.
63    pub fn turn_outcome(&self) -> Option<&TurnOutcome> {
64        match self {
65            Self::Turn(TurnEvent::Ended { outcome }) => Some(outcome),
66            _ => None,
67        }
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::events::{CompactionOutcome, ContextUsage, LlmCallOutcome, LlmCallPurpose};
75
76    #[test]
77    fn serializes_nested_event_contract() {
78        let event = AgentEvent::text("m1", "hello", StreamState::Complete);
79        assert_eq!(
80            serde_json::to_value(event).unwrap(),
81            serde_json::json!({"category":"message","event":{"type":"text","message_id":"m1","chunk":"hello","is_complete":true}})
82        );
83    }
84
85    #[test]
86    fn nested_events_roundtrip() {
87        let events = [
88            AgentEvent::text("m", "text", StreamState::Complete),
89            AgentEvent::Tool(ToolEvent::DefinitionsUpdated { tools: vec![] }),
90            AgentEvent::Turn(TurnEvent::LlmCallEnded {
91                purpose: LlmCallPurpose::Chat,
92                outcome: LlmCallOutcome::Cancelled,
93            }),
94            AgentEvent::Context(ContextEvent::UsageUpdated { usage: ContextUsage::default() }),
95            AgentEvent::Context(ContextEvent::CompactionEnded { outcome: CompactionOutcome::Completed }),
96            AgentEvent::Model(ModelEvent::Switched { previous: "a".into(), new: "b".into() }),
97        ];
98        for event in events {
99            let json = serde_json::to_string(&event).unwrap();
100            assert_eq!(serde_json::from_str::<AgentEvent>(&json).unwrap(), event);
101        }
102    }
103
104    #[test]
105    fn turn_outcome_returns_outcome_only_for_turn_end() {
106        assert_eq!(AgentEvent::turn_ended(TurnOutcome::Completed).turn_outcome(), Some(&TurnOutcome::Completed));
107        assert_eq!(AgentEvent::text("m", "text", StreamState::Complete).turn_outcome(), None);
108        assert_eq!(AgentEvent::Turn(TurnEvent::Started { content: vec![] }).turn_outcome(), None);
109    }
110}