aether_core/events/
agent_event.rs1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4use super::{ContextEvent, MessageEvent, ModelEvent, StreamState, ToolEvent, TurnEvent, TurnOutcome};
5
6#[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 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 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}