Skip to main content

saorsa_agent/
event.rs

1//! Agent events for UI integration.
2//!
3//! The agent emits events as it processes turns, allowing the UI
4//! to display streaming text, tool calls, and status updates.
5
6/// An event emitted by the agent during execution.
7#[derive(Clone, Debug)]
8pub enum AgentEvent {
9    /// A new agent turn has started.
10    TurnStart {
11        /// The turn number (1-indexed).
12        turn: u32,
13    },
14
15    /// Streaming text delta from the assistant.
16    TextDelta {
17        /// The incremental text content.
18        text: String,
19    },
20
21    /// Streaming thinking/reasoning delta from the assistant.
22    ThinkingDelta {
23        /// The incremental thinking text.
24        text: String,
25    },
26
27    /// The assistant is requesting a tool call.
28    ToolCall {
29        /// The tool use ID.
30        id: String,
31        /// The tool name.
32        name: String,
33        /// The tool input as JSON.
34        input: serde_json::Value,
35    },
36
37    /// A tool has returned a result.
38    ToolResult {
39        /// The tool use ID this result corresponds to.
40        id: String,
41        /// The tool name.
42        name: String,
43        /// The tool output.
44        output: String,
45        /// Whether the tool succeeded.
46        success: bool,
47    },
48
49    /// The assistant's text response is complete for this turn.
50    TextComplete {
51        /// The full text of the assistant's response.
52        text: String,
53    },
54
55    /// A turn has ended.
56    TurnEnd {
57        /// The turn number.
58        turn: u32,
59        /// Why the turn ended.
60        reason: TurnEndReason,
61    },
62
63    /// An error occurred during agent execution.
64    Error {
65        /// The error message.
66        message: String,
67    },
68}
69
70/// Why an agent turn ended.
71#[derive(Clone, Debug, PartialEq, Eq)]
72pub enum TurnEndReason {
73    /// The model finished responding naturally.
74    EndTurn,
75    /// The model wants to use a tool (another turn will follow).
76    ToolUse,
77    /// The maximum turn limit was reached.
78    MaxTurns,
79    /// The model hit the max_tokens limit.
80    MaxTokens,
81    /// An error occurred.
82    Error,
83}
84
85/// Sender for agent events.
86pub type EventSender = tokio::sync::mpsc::Sender<AgentEvent>;
87
88/// Receiver for agent events.
89pub type EventReceiver = tokio::sync::mpsc::Receiver<AgentEvent>;
90
91/// Create a new event channel with the given buffer size.
92pub fn event_channel(buffer: usize) -> (EventSender, EventReceiver) {
93    tokio::sync::mpsc::channel(buffer)
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn turn_end_reason_equality() {
102        assert_eq!(TurnEndReason::EndTurn, TurnEndReason::EndTurn);
103        assert_ne!(TurnEndReason::EndTurn, TurnEndReason::ToolUse);
104    }
105
106    #[tokio::test]
107    async fn event_channel_send_receive() {
108        let (tx, mut rx) = event_channel(8);
109        let send_result = tx.send(AgentEvent::TurnStart { turn: 1 }).await;
110        assert!(send_result.is_ok());
111
112        let event = rx.recv().await;
113        match event {
114            Some(AgentEvent::TurnStart { turn }) => {
115                assert_eq!(turn, 1);
116            }
117            _ => panic!("Expected TurnStart event"),
118        }
119    }
120
121    #[tokio::test]
122    async fn event_channel_text_delta() {
123        let (tx, mut rx) = event_channel(8);
124        let _ = tx
125            .send(AgentEvent::TextDelta {
126                text: "Hello".into(),
127            })
128            .await;
129
130        match rx.recv().await {
131            Some(AgentEvent::TextDelta { text }) => {
132                assert_eq!(text, "Hello");
133            }
134            _ => panic!("Expected TextDelta event"),
135        }
136    }
137
138    #[tokio::test]
139    async fn event_channel_tool_result() {
140        let (tx, mut rx) = event_channel(8);
141        let _ = tx
142            .send(AgentEvent::ToolResult {
143                id: "tool_1".into(),
144                name: "bash".into(),
145                output: "done".into(),
146                success: true,
147            })
148            .await;
149
150        match rx.recv().await {
151            Some(AgentEvent::ToolResult {
152                id,
153                name,
154                output,
155                success,
156            }) => {
157                assert_eq!(id, "tool_1");
158                assert_eq!(name, "bash");
159                assert_eq!(output, "done");
160                assert!(success);
161            }
162            _ => panic!("Expected ToolResult event"),
163        }
164    }
165
166    #[test]
167    fn agent_event_debug() {
168        let event = AgentEvent::Error {
169            message: "test error".into(),
170        };
171        let debug_str = format!("{event:?}");
172        assert!(debug_str.contains("test error"));
173    }
174}