Skip to main content

aether_core/events/
agent_event.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4use super::{ContextEvent, MessageEvent, ModelEvent, 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")]
9pub enum AgentEvent {
10    Message(MessageEvent),
11    Tool(ToolEvent),
12    Turn(TurnEvent),
13    Context(ContextEvent),
14    Model(ModelEvent),
15}
16
17impl AgentEvent {
18    pub fn text(message_id: &str, chunk: &str, is_complete: bool) -> Self {
19        Self::Message(MessageEvent::Text { message_id: message_id.into(), chunk: chunk.into(), is_complete })
20    }
21
22    pub fn thought(message_id: &str, chunk: &str, is_complete: bool) -> Self {
23        Self::Message(MessageEvent::Thought { message_id: message_id.into(), chunk: chunk.into(), is_complete })
24    }
25
26    pub fn turn_ended(outcome: TurnOutcome) -> Self {
27        Self::Turn(TurnEvent::Ended { outcome })
28    }
29
30    /// Human-readable text content of this event, if any.
31    pub fn content(&self) -> Option<String> {
32        match self {
33            Self::Message(MessageEvent::Text { chunk, .. } | MessageEvent::Thought { chunk, .. }) => {
34                Some(chunk.clone())
35            }
36            Self::Tool(ToolEvent::Result { result, .. }) => Some(result.result.clone()),
37            Self::Tool(ToolEvent::Error { error }) => Some(error.error.clone()),
38            Self::Context(ContextEvent::CompactionResult { summary, .. }) => Some(summary.clone()),
39            _ => None,
40        }
41    }
42
43    /// The turn's terminal outcome, if this event ends a turn.
44    pub fn turn_outcome(&self) -> Option<&TurnOutcome> {
45        match self {
46            Self::Turn(TurnEvent::Ended { outcome }) => Some(outcome),
47            _ => None,
48        }
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use crate::events::{CompactionOutcome, ContextUsage, LlmCallOutcome, LlmCallPurpose};
56
57    #[test]
58    fn serializes_nested_event_contract() {
59        let event = AgentEvent::text("m1", "hello", true);
60        assert_eq!(
61            serde_json::to_value(event).unwrap(),
62            serde_json::json!({"category":"message","event":{"type":"text","message_id":"m1","chunk":"hello","is_complete":true}})
63        );
64    }
65
66    #[test]
67    fn nested_events_roundtrip() {
68        let events = [
69            AgentEvent::text("m", "text", true),
70            AgentEvent::Tool(ToolEvent::DefinitionsUpdated { tools: vec![] }),
71            AgentEvent::Turn(TurnEvent::LlmCallEnded {
72                purpose: LlmCallPurpose::Chat,
73                outcome: LlmCallOutcome::Cancelled,
74            }),
75            AgentEvent::Context(ContextEvent::UsageUpdated { usage: ContextUsage::default() }),
76            AgentEvent::Context(ContextEvent::CompactionEnded { outcome: CompactionOutcome::Completed }),
77            AgentEvent::Model(ModelEvent::Switched { previous: "a".into(), new: "b".into() }),
78        ];
79        for event in events {
80            let json = serde_json::to_string(&event).unwrap();
81            assert_eq!(serde_json::from_str::<AgentEvent>(&json).unwrap(), event);
82        }
83    }
84
85    #[test]
86    fn turn_outcome_returns_outcome_only_for_turn_end() {
87        assert_eq!(AgentEvent::turn_ended(TurnOutcome::Completed).turn_outcome(), Some(&TurnOutcome::Completed));
88        assert_eq!(AgentEvent::text("m", "text", true).turn_outcome(), None);
89        assert_eq!(AgentEvent::Turn(TurnEvent::Started { content: vec![] }).turn_outcome(), None);
90    }
91}