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